From 5f3b7eaa459e75f8a6b52e777a95c6ca025a9c3b Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Wed, 22 Mar 2017 23:28:55 +0100 Subject: [PATCH 0001/3000] Discord integration --- pkg/metrics/metrics.go | 2 + pkg/services/alerting/notifiers/discord.go | 114 ++++++++++++++++++ .../alerting/notifiers/discord_test.go | 52 ++++++++ 3 files changed, 168 insertions(+) create mode 100644 pkg/services/alerting/notifiers/discord.go create mode 100644 pkg/services/alerting/notifiers/discord_test.go diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index c23a53009a9..e00aafa54fd 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -52,6 +52,7 @@ var ( M_Alerting_Notification_Sent_Threema Counter M_Alerting_Notification_Sent_Sensu Counter M_Alerting_Notification_Sent_Pushover Counter + M_Alerting_Notification_Sent_Discord Counter M_Aws_CloudWatch_GetMetricStatistics Counter M_Aws_CloudWatch_ListMetrics Counter @@ -124,6 +125,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu") M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE") M_Alerting_Notification_Sent_Pushover = RegCounter("alerting.notifications_sent", "type", "pushover") + M_Alerting_Notification_Sent_Discord = RegCounter("alerting.notifications_sent", "type", "discord") M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics") M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics") diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go new file mode 100644 index 00000000000..ce9849bbe6c --- /dev/null +++ b/pkg/services/alerting/notifiers/discord.go @@ -0,0 +1,114 @@ +package notifiers + +import ( + "strconv" + "strings" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/setting" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "discord", + Name: "Discord", + Description: "Sends notifications to Discord", + Factory: NewDiscordNotifier, + OptionsTemplate: ` +

Discord settings

+
+ Webhook URL + +
+ `, + }) +} + +func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + url := model.Settings.Get("url").MustString() + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"} + } + + return &DiscordNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + WebhookURL: url, + log: log.New("alerting.notifier.discord"), + }, nil +} + +type DiscordNotifier struct { + NotifierBase + WebhookURL string + log log.Logger +} + +func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL) + metrics.M_Alerting_Notification_Sent_Discord.Inc(1) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + + bodyJSON := simplejson.New() + bodyJSON.Set("username", "Grafana") + + fields := make([]map[string]interface{}, 0) + + for _, evt := range evalContext.EvalMatches { + fields = append(fields, map[string]interface{}{ + "name": evt.Metric, + "value": evt.Value, + "inline": true, + }) + } + + footer := map[string]interface{}{ + "text": "Grafana v" + setting.BuildVersion, + "icon_url": "https://grafana.com/assets/img/fav32.png", + } + + color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0) + + image := map[string]interface{}{ + "url": evalContext.ImagePublicUrl, + } + + embed := simplejson.New() + embed.Set("title", evalContext.GetNotificationTitle()) + //Discord takes integer for color + embed.Set("color", color) + embed.Set("url", ruleUrl) + embed.Set("description", evalContext.Rule.Message) + embed.Set("type", "rich") + embed.Set("fields", fields) + embed.Set("footer", footer) + embed.Set("image", image) + + bodyJSON.Set("embeds", []interface{}{embed}) + + body, _ := bodyJSON.MarshalJSON() + + this.log.Info("Message", string(body)) + + cmd := &m.SendWebhookSync{ + Url: this.WebhookURL, + Body: string(body), + HttpMethod: "POST", + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send notification to Discord", "error", err, "body", string(body)) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/discord_test.go b/pkg/services/alerting/notifiers/discord_test.go new file mode 100644 index 00000000000..fe925aab362 --- /dev/null +++ b/pkg/services/alerting/notifiers/discord_test.go @@ -0,0 +1,52 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestDiscordNotifier(t *testing.T) { + Convey("Telegram notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "discord_testing", + Type: "discord", + Settings: settingsJSON, + } + + _, err := NewDiscordNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("settings should trigger incident", func() { + json := ` + { + "url": "https://web.hook/" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "discord_testing", + Type: "discord", + Settings: settingsJSON, + } + + not, err := NewDiscordNotifier(model) + discordNotifier := not.(*DiscordNotifier) + + So(err, ShouldBeNil) + So(discordNotifier.Name, ShouldEqual, "discord_testing") + So(discordNotifier.Type, ShouldEqual, "discord") + So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/") + }) + }) + }) +} From 1bf982439b3a729f5930936a3be4fb2ab9a8f01a Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Thu, 23 Mar 2017 21:53:54 +0100 Subject: [PATCH 0002/3000] Sending image --- pkg/components/null/float.go | 9 +++ pkg/models/notifications.go | 13 ++-- pkg/services/alerting/notifiers/discord.go | 83 ++++++++++++++++++--- pkg/services/notifications/notifications.go | 13 ++-- pkg/services/notifications/webhook.go | 21 ++++-- 5 files changed, 107 insertions(+), 32 deletions(-) diff --git a/pkg/components/null/float.go b/pkg/components/null/float.go index 1e78946e878..b7b2f44a741 100644 --- a/pkg/components/null/float.go +++ b/pkg/components/null/float.go @@ -106,6 +106,15 @@ func (f Float) String() string { return fmt.Sprintf("%1.3f", f.Float64) } +// FullString returns float as string in full precision +func (f Float) FullString() string { + if !f.Valid { + return "null" + } + + return fmt.Sprintf("%f", f.Float64) +} + // SetValid changes this Float's value and also sets it to be non-null. func (f *Float) SetValid(n float64) { f.Float64 = n diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index ad7aed3bc50..a3fc917f0f4 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -18,12 +18,13 @@ type SendEmailCommandSync struct { } type SendWebhookSync struct { - Url string - User string - Password string - Body string - HttpMethod string - HttpHeader map[string]string + Url string + User string + Password string + Body string + HttpMethod string + HttpHeader map[string]string + ContentType string } type SendResetPasswordEmailCommand struct { diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index ce9849bbe6c..17080511656 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -1,6 +1,10 @@ package notifiers import ( + "bytes" + "io" + "mime/multipart" + "os" "strconv" "strings" @@ -64,9 +68,10 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { fields := make([]map[string]interface{}, 0) for _, evt := range evalContext.EvalMatches { + fields = append(fields, map[string]interface{}{ "name": evt.Metric, - "value": evt.Value, + "value": evt.Value.FullString(), "inline": true, }) } @@ -78,10 +83,6 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0) - image := map[string]interface{}{ - "url": evalContext.ImagePublicUrl, - } - embed := simplejson.New() embed.Set("title", evalContext.GetNotificationTitle()) //Discord takes integer for color @@ -91,22 +92,80 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { embed.Set("type", "rich") embed.Set("fields", fields) embed.Set("footer", footer) - embed.Set("image", image) + + var image = make(map[string]interface{}) + + var embeddedImage = false + + if evalContext.ImagePublicUrl != "" { + image = map[string]interface{}{ + "url": evalContext.ImagePublicUrl, + } + embed.Set("image", image) + } else { + image = map[string]interface{}{ + "url": "attachment://graph.png", + } + embed.Set("image", image) + embeddedImage = true + } bodyJSON.Set("embeds", []interface{}{embed}) - body, _ := bodyJSON.MarshalJSON() + json, _ := bodyJSON.MarshalJSON() - this.log.Info("Message", string(body)) + content_type := "application/json" + + var body []byte + + if embeddedImage { + + var b bytes.Buffer + + w := multipart.NewWriter(&b) + + f, err := os.Open(evalContext.ImageOnDiskPath) + + if err != nil { + this.log.Error("Can't open graph file", err) + return err + } + + defer f.Close() + + fw, err := w.CreateFormField("payload_json") + if err != nil { + return err + } + + if _, err = fw.Write([]byte(string(json))); err != nil { + return err + } + + fw, err = w.CreateFormFile("file", "graph.png") + + if _, err = io.Copy(fw, f); err != nil { + return err + } + + w.Close() + + body = b.Bytes() + content_type = w.FormDataContentType() + + } else { + body = json + } cmd := &m.SendWebhookSync{ - Url: this.WebhookURL, - Body: string(body), - HttpMethod: "POST", + Url: this.WebhookURL, + Body: string(body), + HttpMethod: "POST", + ContentType: content_type, } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { - this.log.Error("Failed to send notification to Discord", "error", err, "body", string(body)) + this.log.Error("Failed to send notification to Discord", "error", err) return err } diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index c765774d062..decb74e8be2 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -60,12 +60,13 @@ func Init() error { func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { return sendWebRequestSync(ctx, &Webhook{ - Url: cmd.Url, - User: cmd.User, - Password: cmd.Password, - Body: cmd.Body, - HttpMethod: cmd.HttpMethod, - HttpHeader: cmd.HttpHeader, + Url: cmd.Url, + User: cmd.User, + Password: cmd.Password, + Body: cmd.Body, + HttpMethod: cmd.HttpMethod, + HttpHeader: cmd.HttpHeader, + ContentType: cmd.ContentType, }) } diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index c74804ab828..f532ffc2f9e 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -16,12 +16,13 @@ import ( ) type Webhook struct { - Url string - User string - Password string - Body string - HttpMethod string - HttpHeader map[string]string + Url string + User string + Password string + Body string + HttpMethod string + HttpHeader map[string]string + ContentType string } var netTransport = &http.Transport{ @@ -61,7 +62,7 @@ func processWebhookQueue() { } func sendWebRequestSync(ctx context.Context, webhook *Webhook) error { - webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod) + webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod, "content type", webhook.ContentType) if webhook.HttpMethod == "" { webhook.HttpMethod = http.MethodPost @@ -72,7 +73,11 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error { return err } - request.Header.Add("Content-Type", "application/json") + if webhook.ContentType == "" { + webhook.ContentType = "application/json" + } + + request.Header.Add("Content-Type", webhook.ContentType) request.Header.Add("User-Agent", "Grafana") if webhook.User != "" && webhook.Password != "" { request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password)) From c346aca26d9183fe3245db5be1c81fcf2c340f8e Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 11 Apr 2017 16:30:20 -0700 Subject: [PATCH 0003/3000] allow setting the database --- .../plugins/datasource/influxdb/datasource.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 98c7ba87bd2..ddce4d8e8f9 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -157,7 +157,7 @@ export default class InfluxDatasource { return false; }; - metricFindQuery(query) { + metricFindQuery(query: string, db?: string) { var interpolated = this.templateSrv.replace(query, null, 'regex'); return this._seriesQuery(interpolated) @@ -176,10 +176,10 @@ export default class InfluxDatasource { return this.metricFindQuery(query); } - _seriesQuery(query) { + _seriesQuery(query: string, db?: string) { if (!query) { return this.$q.when({results: []}); } - return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}); + return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}, db); } serializeParams(params) { @@ -207,19 +207,19 @@ export default class InfluxDatasource { }); } - _influxRequest(method, url, data) { - var self = this; - - var currentUrl = self.urls.shift(); - self.urls.push(currentUrl); + _influxRequest(method: string, url: string, data: any, db?: string) { + var currentUrl = this.urls.shift(); + this.urls.push(currentUrl); var params: any = { - u: self.username, - p: self.password, + u: this.username, + p: this.password, }; - if (self.database) { - params.db = self.database; + if (db) { + params.db = db; + } else if (this.database) { + params.db = this.database; } if (method === 'GET') { @@ -241,8 +241,8 @@ export default class InfluxDatasource { if (this.basicAuth || this.withCredentials) { options.withCredentials = true; } - if (self.basicAuth) { - options.headers.Authorization = self.basicAuth; + if (this.basicAuth) { + options.headers.Authorization = this.basicAuth; } return this.backendSrv.datasourceRequest(options).then(result => { From 7009184d6eec4f1612ca036ad32f68b33cdafa3d Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 12 Apr 2017 09:02:06 -0700 Subject: [PATCH 0004/3000] pass database parameter in the options --- .../plugins/datasource/influxdb/datasource.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index ddce4d8e8f9..37a28d45142 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -157,10 +157,10 @@ export default class InfluxDatasource { return false; }; - metricFindQuery(query: string, db?: string) { + metricFindQuery(query: string, options?: any) { var interpolated = this.templateSrv.replace(query, null, 'regex'); - return this._seriesQuery(interpolated) + return this._seriesQuery(interpolated, options) .then(_.curry(this.responseParser.parse)(query)); } @@ -176,10 +176,10 @@ export default class InfluxDatasource { return this.metricFindQuery(query); } - _seriesQuery(query: string, db?: string) { + _seriesQuery(query: string, options?: any) { if (!query) { return this.$q.when({results: []}); } - return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}, db); + return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'}, options); } serializeParams(params) { @@ -207,7 +207,7 @@ export default class InfluxDatasource { }); } - _influxRequest(method: string, url: string, data: any, db?: string) { + _influxRequest(method: string, url: string, data: any, options?: any) { var currentUrl = this.urls.shift(); this.urls.push(currentUrl); @@ -216,8 +216,8 @@ export default class InfluxDatasource { p: this.password, }; - if (db) { - params.db = db; + if (options && options.database) { + params.db = options.database; } else if (this.database) { params.db = this.database; } @@ -227,7 +227,7 @@ export default class InfluxDatasource { data = null; } - var options: any = { + var req: any = { method: method, url: currentUrl + url, params: params, @@ -237,15 +237,15 @@ export default class InfluxDatasource { paramSerializer: this.serializeParams, }; - options.headers = options.headers || {}; + req.headers = req.headers || {}; if (this.basicAuth || this.withCredentials) { - options.withCredentials = true; + req.withCredentials = true; } if (this.basicAuth) { - options.headers.Authorization = this.basicAuth; + req.headers.Authorization = this.basicAuth; } - return this.backendSrv.datasourceRequest(options).then(result => { + return this.backendSrv.datasourceRequest(req).then(result => { return result.data; }, function(err) { if (err.status !== 0 || err.status >= 300) { From 16819da08d9a353fc6599d72836d36019d8ca5ef Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 18 Apr 2017 09:27:25 -0700 Subject: [PATCH 0005/3000] pass the options along with a _seriesQuery --- public/app/plugins/datasource/influxdb/datasource.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 37a28d45142..929c673433b 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -85,7 +85,7 @@ export default class InfluxDatasource { // replace templated variables allQueries = this.templateSrv.replace(allQueries, scopedVars); - return this._seriesQuery(allQueries).then((data): any => { + return this._seriesQuery(allQueries, options).then((data): any => { if (!data || !data.results) { return []; } @@ -131,7 +131,7 @@ export default class InfluxDatasource { var query = options.annotation.query.replace('$timeFilter', timeFilter); query = this.templateSrv.replace(query, null, 'regex'); - return this._seriesQuery(query).then(data => { + return this._seriesQuery(query, options).then(data => { if (!data || !data.results || !data.results[0]) { throw { message: 'No results in response from InfluxDB' }; } @@ -167,13 +167,13 @@ export default class InfluxDatasource { getTagKeys(options) { var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database); var query = queryBuilder.buildExploreQuery('TAG_KEYS'); - return this.metricFindQuery(query); + return this.metricFindQuery(query, options); } getTagValues(options) { var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database); var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key); - return this.metricFindQuery(query); + return this.metricFindQuery(query, options); } _seriesQuery(query: string, options?: any) { From 15e84a1c692f5daf4984da15aa2dad696b257fad Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 26 Apr 2017 15:41:10 -0700 Subject: [PATCH 0006/3000] use targets[0] as the options --- public/app/plugins/datasource/influxdb/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index ec472898583..ac18992b16d 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -85,7 +85,7 @@ export default class InfluxDatasource { // replace templated variables allQueries = this.templateSrv.replace(allQueries, scopedVars); - return this._seriesQuery(allQueries, options).then((data): any => { + return this._seriesQuery(allQueries, targets[0]).then((data): any => { if (!data || !data.results) { return []; } From 26e62b4d0690b19962e23a56840ad1cad6d226e3 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 1 May 2017 20:28:56 -0700 Subject: [PATCH 0007/3000] use the original options parameter --- public/app/plugins/datasource/influxdb/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index ac18992b16d..ec472898583 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -85,7 +85,7 @@ export default class InfluxDatasource { // replace templated variables allQueries = this.templateSrv.replace(allQueries, scopedVars); - return this._seriesQuery(allQueries, targets[0]).then((data): any => { + return this._seriesQuery(allQueries, options).then((data): any => { if (!data || !data.results) { return []; } From 967efea520d61bc6a9cf2d0cfa29c90ce018a658 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 22 Sep 2017 13:28:53 +0200 Subject: [PATCH 0008/3000] fix merge issue --- public/app/plugins/datasource/influxdb/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 9e568866579..bab2d597a67 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -211,7 +211,7 @@ export default class InfluxDatasource { if (this.username) { params.u = this.username; - params.u = this.password; + params.p = this.password; } if (options && options.database) { From 4440133f4dd9b4ca19ea540815683ff0428a7944 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 18 Oct 2017 23:33:10 +0200 Subject: [PATCH 0009/3000] Add a setting to allow DB queries --- pkg/api/pluginproxy/ds_proxy.go | 4 +++- public/app/plugins/datasource/influxdb/datasource.ts | 5 +++++ .../app/plugins/datasource/influxdb/partials/config.html | 8 +++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index faac8c03c62..72309c5c855 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -166,7 +166,9 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { func (proxy *DataSourceProxy) validateRequest() error { if proxy.ds.Type == m.DS_INFLUXDB { if proxy.ctx.Query("db") != proxy.ds.Database { - return errors.New("Datasource is not configured to allow this database") + if(!proxy.ds.JsonData.Get("allowDatabaseQuery").MustBool(false)) { + return errors.New("Datasource is not configured to allow this database"); + } } } diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 7b53d0e1d97..80294396395 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -17,6 +17,7 @@ export default class InfluxDatasource { basicAuth: any; withCredentials: any; interval: any; + allowDatabaseQuery: boolean; supportAnnotations: boolean; supportMetrics: boolean; responseParser: any; @@ -35,6 +36,7 @@ export default class InfluxDatasource { this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; this.interval = (instanceSettings.jsonData || {}).timeInterval; + this.allowDatabaseQuery = (instanceSettings.jsonData || {}).allowDatabaseQuery === true; this.supportAnnotations = true; this.supportMetrics = true; this.responseParser = new ResponseParser(); @@ -214,6 +216,9 @@ export default class InfluxDatasource { if (options && options.database) { params.db = options.database; + if (params.db !== this.database && !this.allowDatabaseQuery) { + return this.$q.reject( { message: 'This datasource does not allow changing database' } ); + } } else if (this.database) { params.db = this.database; } diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 9cb6f5ba749..4b861083928 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -23,10 +23,16 @@ + +
- Min time interval + Min time interval A lower limit for the auto group by time interval. Recommended to be set to write frequency, From fb9c714a9a0e68b8572b94852d880afd7b7919a2 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 18 Oct 2017 23:49:33 +0200 Subject: [PATCH 0010/3000] run go fmt --- pkg/api/pluginproxy/ds_proxy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 72309c5c855..200a1a02bcc 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -166,8 +166,8 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { func (proxy *DataSourceProxy) validateRequest() error { if proxy.ds.Type == m.DS_INFLUXDB { if proxy.ctx.Query("db") != proxy.ds.Database { - if(!proxy.ds.JsonData.Get("allowDatabaseQuery").MustBool(false)) { - return errors.New("Datasource is not configured to allow this database"); + if !proxy.ds.JsonData.Get("allowDatabaseQuery").MustBool(false) { + return errors.New("Datasource is not configured to allow this database") } } } From 5b926cc10273f2acf69aaf9cc1e74233160e8b65 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Fri, 17 Nov 2017 12:19:18 +0100 Subject: [PATCH 0011/3000] Adding a user in a specified organisation uses the admin API --- docs/sources/http_api/org.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md index 4c1dff904c8..2afac49a7ce 100644 --- a/docs/sources/http_api/org.md +++ b/docs/sources/http_api/org.md @@ -380,6 +380,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "role":"Viewer" } ``` +Note: The api will only work when you pass the admin name and password +to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users **Example Response**: @@ -436,4 +438,4 @@ HTTP/1.1 200 Content-Type: application/json {"message":"User removed from organization"} -``` \ No newline at end of file +``` From 3a3272e225c986cdeb762197a82f84b84b9e769f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 12 Dec 2017 09:35:57 +0100 Subject: [PATCH 0012/3000] annotations: allows template variables to be used in tag filter When filtering built in annotations by tag, interpolates the tag with template variables. Fixes #9587 --- .../plugins/datasource/grafana/datasource.ts | 7 +- .../grafana/specs/datasource.jest.ts | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/grafana/specs/datasource.jest.ts diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 5ca3c433476..9eb9862094a 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; class GrafanaDatasource { /** @ngInject */ - constructor(private backendSrv, private $q) {} + constructor(private backendSrv, private $q, private templateSrv) {} query(options) { return this.backendSrv @@ -58,6 +58,11 @@ class GrafanaDatasource { if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) { return this.$q.when([]); } + const tags = []; + for (let t of params.tags) { + tags.push(this.templateSrv.replace(t)); + } + params.tags = tags; } return this.backendSrv.get('/api/annotations', params); diff --git a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts new file mode 100644 index 00000000000..544b04056ac --- /dev/null +++ b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts @@ -0,0 +1,65 @@ +import {GrafanaDatasource} from "../datasource"; +import q from 'q'; +import moment from 'moment'; + +describe('grafana data source', () => { + describe('when executing an annotations query', () => { + let calledBackendSrvParams; + const backendSrvStub = { + get: (url, options) => { + calledBackendSrvParams = options; + return q.resolve([]); + } + }; + + const templateSrvStub = { + replace: val => val.replace('$var', 'replaced') + }; + + const ds = new GrafanaDatasource(backendSrvStub, q, templateSrvStub); + + describe('with tags that have template variables', () => { + const options = setupAnnotationQueryOptions( + {tags: ['tag1:$var']} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should interpolate template variables in tags in query options', () => { + expect(calledBackendSrvParams.tags[0]).toBe('tag1:replaced'); + }); + }); + + describe('with type dashboard', () => { + const options = setupAnnotationQueryOptions( + { + type: 'dashboard', + tags: ['tag1'] + }, + {id: 1} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should remove tags from query options', () => { + expect(calledBackendSrvParams.tags).toBe(undefined); + }); + }); + }); +}); + +function setupAnnotationQueryOptions(annotation, dashboard?) { + return { + annotation: annotation, + dashboard: dashboard, + range: { + from: moment(1432288354), + to: moment(1432288401) + }, + rangeRaw: {from: "now-24h", to: "now"} + }; +} From 13efc529ecbd4b68dcab6c76ed1b5c48be801afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 2 Jan 2018 14:52:30 +0100 Subject: [PATCH 0013/3000] poc: began react panel experiments --- .../dashboard/dashgrid/DashboardPanel.tsx | 78 ++++++++++++++++--- .../app/features/plugins/built_in_plugins.ts | 2 + public/app/plugins/panel/text2/README.md | 5 ++ .../panel/text2/img/icn-text-panel.svg | 26 +++++++ public/app/plugins/panel/text2/module.tsx | 13 ++++ public/app/plugins/panel/text2/plugin.json | 17 ++++ 6 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 public/app/plugins/panel/text2/README.md create mode 100644 public/app/plugins/panel/text2/img/icn-text-panel.svg create mode 100644 public/app/plugins/panel/text2/module.tsx create mode 100644 public/app/plugins/panel/text2/plugin.json diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 27fe64d4660..562b79a859e 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import {PanelModel} from '../panel_model'; -import {PanelContainer} from './PanelContainer'; -import {AttachedPanel} from './PanelLoader'; -import {DashboardRow} from './DashboardRow'; -import {AddPanelPanel} from './AddPanelPanel'; +import config from 'app/core/config'; +import classNames from 'classnames'; +import { PanelModel } from '../panel_model'; +import { PanelContainer } from './PanelContainer'; +import { AttachedPanel } from './PanelLoader'; +import { DashboardRow } from './DashboardRow'; +import { AddPanelPanel } from './AddPanelPanel'; +import { importPluginModule } from 'app/features/plugins/plugin_loader'; export interface DashboardPanelProps { panel: PanelModel; @@ -13,10 +16,26 @@ export interface DashboardPanelProps { export class DashboardPanel extends React.Component { element: any; attachedPanel: AttachedPanel; + pluginInfo: any; + pluginExports: any; + specialPanels = {}; constructor(props) { super(props); this.state = {}; + + this.specialPanels['row'] = this.renderRow.bind(this); + this.specialPanels['add-panel'] = this.renderAddPanel.bind(this); + + if (!this.isSpecial()) { + this.pluginInfo = config.panels[this.props.panel.type]; + + // load panel plugin + importPluginModule(this.pluginInfo.module).then(pluginExports => { + this.pluginExports = pluginExports; + this.forceUpdate(); + }); + } } componentDidMount() { @@ -36,19 +55,54 @@ export class DashboardPanel extends React.Component { } } + isSpecial() { + return this.specialPanels[this.props.panel.type]; + } + + renderRow() { + return ; + } + + renderAddPanel() { + return ; + } + render() { - // special handling for rows - if (this.props.panel.type === 'row') { - return ; + if (this.isSpecial()) { + return this.specialPanels[this.props.panel.type](); } - if (this.props.panel.type === 'add-panel') { - return ; + let isFullscreen = false; + let isLoading = false; + let panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + let PanelComponent = null; + + if (this.pluginExports && this.pluginExports.PanelComponent) { + PanelComponent = this.pluginExports.PanelComponent; } return ( -
this.element = element} className="panel-height-helper" /> +
+
+ + + + + + {isLoading && ( + + + + )} +
{this.props.panel.title}
+
+ +
{PanelComponent && }
+
); + + // return ( + //
this.element = element} className="panel-height-helper" /> + // ); } } - diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index c86efc4f695..a4657f84aa1 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -10,6 +10,7 @@ import * as postgresPlugin from 'app/plugins/datasource/postgres/module'; import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module'; import * as textPanel from 'app/plugins/panel/text/module'; +import * as text2Panel from 'app/plugins/panel/text2/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'; @@ -37,6 +38,7 @@ const builtInPlugins = { 'app/plugins/app/testdata/datasource/module': testDataDSPlugin, 'app/plugins/panel/text/module': textPanel, + 'app/plugins/panel/text2/module': text2Panel, 'app/plugins/panel/graph/module': graphPanel, 'app/plugins/panel/dashlist/module': dashListPanel, 'app/plugins/panel/pluginlist/module': pluginsListPanel, diff --git a/public/app/plugins/panel/text2/README.md b/public/app/plugins/panel/text2/README.md new file mode 100644 index 00000000000..667ab51784a --- /dev/null +++ b/public/app/plugins/panel/text2/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. diff --git a/public/app/plugins/panel/text2/img/icn-text-panel.svg b/public/app/plugins/panel/text2/img/icn-text-panel.svg new file mode 100644 index 00000000000..a9d0a1d2c4a --- /dev/null +++ b/public/app/plugins/panel/text2/img/icn-text-panel.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx new file mode 100644 index 00000000000..7f5e363891c --- /dev/null +++ b/public/app/plugins/panel/text2/module.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export class ReactTestPanel extends React.Component { + constructor(props) { + super(props); + } + + render() { + return

Panel content

; + } +} + +export { ReactTestPanel as PanelComponent }; diff --git a/public/app/plugins/panel/text2/plugin.json b/public/app/plugins/panel/text2/plugin.json new file mode 100644 index 00000000000..95d821bfd50 --- /dev/null +++ b/public/app/plugins/panel/text2/plugin.json @@ -0,0 +1,17 @@ +{ + "type": "panel", + "name": "Text2", + "id": "text2", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-text-panel.svg", + "large": "img/icn-text-panel.svg" + } + } +} + From 3eb5f232094e20caf03d428d1f7031c8b6e15459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 3 Jan 2018 13:03:26 +0100 Subject: [PATCH 0014/3000] poc: began react panel experiments, step2 --- .../dashboard/dashgrid/DashboardPanel.tsx | 77 +++++++++++++++---- public/app/features/panel/panel_header.ts | 15 ---- public/sass/pages/_dashboard.scss | 22 +++--- 3 files changed, 71 insertions(+), 43 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 562b79a859e..7a0a9bb2e86 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,6 +1,7 @@ import React from 'react'; import config from 'app/core/config'; import classNames from 'classnames'; +import appEvents from 'app/core/app_events'; import { PanelModel } from '../panel_model'; import { PanelContainer } from './PanelContainer'; import { AttachedPanel } from './PanelLoader'; @@ -72,9 +73,6 @@ export class DashboardPanel extends React.Component { return this.specialPanels[this.props.panel.type](); } - let isFullscreen = false; - let isLoading = false; - let panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); let PanelComponent = null; if (this.pluginExports && this.pluginExports.PanelComponent) { @@ -83,20 +81,7 @@ export class DashboardPanel extends React.Component { return (
-
- - - - - - {isLoading && ( - - - - )} -
{this.props.panel.title}
-
- +
{PanelComponent && }
); @@ -106,3 +91,61 @@ export class DashboardPanel extends React.Component { // ); } } + +interface PanelHeaderProps { + panel: any; +} + +export class PanelHeader extends React.Component { + onEditPanel = () => { + appEvents.emit('panel-change-view', { + fullscreen: true, + edit: true, + panelId: this.props.panel.id, + }); + }; + + render() { + let isFullscreen = false; + let isLoading = false; + let panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + + return ( +
+ + + + + + {isLoading && ( + + + + )} + +
+ + + {this.props.panel.title} + + + + + + 4m + + +
+
+ ); + } +} diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index ca6ed68b648..cb9f2d5c563 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -10,21 +10,6 @@ var template = ` {{ctrl.timeInfo}} diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index af9a02caa2a..6d4e94a5175 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -40,6 +40,14 @@ div.flot-text { background-color: transparent; border: none; } + + &:hover { + .panel-menu-toggle { + visibility: visible; + transition: opacity 0.1s ease-in 0.2s; + opacity: 1; + } + } } .panel-content { @@ -159,7 +167,7 @@ div.flot-text { display: block; @include panel-corner-color(lighten($panel-bg, 4%)); .fa:before { - content: "\f129"; + content: '\f129'; } } @@ -170,7 +178,7 @@ div.flot-text { left: -5px; } .fa:before { - content: "\f08e"; + content: '\f08e'; } } @@ -179,19 +187,11 @@ div.flot-text { color: $text-color; @include panel-corner-color($popover-error-bg); .fa:before { - content: "\f12a"; + content: '\f12a'; } } } -.panel-hover-highlight { - .panel-menu-toggle { - visibility: visible; - transition: opacity 0.1s ease-in 0.2s; - opacity: 1; - } -} - .panel-time-info { font-weight: bold; float: right; From 456b4d2a66add0f15313dbbe6c8cdfb666ad9c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 3 Jan 2018 13:33:54 +0100 Subject: [PATCH 0015/3000] poc: began react panel experiments, step2 --- public/app/core/directives/dash_class.js | 40 ------------------- public/app/core/directives/dash_class.ts | 34 ++++++++++++++++ .../dashboard/dashgrid/AddPanelPanel.tsx | 7 ++-- .../dashboard/dashgrid/DashboardGrid.tsx | 10 +++-- .../dashboard/dashgrid/DashboardPanel.tsx | 36 ++++------------- .../dashboard/dashgrid/DashboardRow.tsx | 20 +++------- .../dashboard/specs/DashboardRow.jest.tsx | 14 ++----- 7 files changed, 60 insertions(+), 101 deletions(-) delete mode 100644 public/app/core/directives/dash_class.js create mode 100644 public/app/core/directives/dash_class.ts diff --git a/public/app/core/directives/dash_class.js b/public/app/core/directives/dash_class.js deleted file mode 100644 index 9df53bdbd48..00000000000 --- a/public/app/core/directives/dash_class.js +++ /dev/null @@ -1,40 +0,0 @@ -define([ - 'lodash', - 'jquery', - '../core_module', -], -function (_, $, coreModule) { - 'use strict'; - - coreModule.default.directive('dashClass', function() { - return { - link: function($scope, elem) { - - $scope.onAppEvent('panel-fullscreen-enter', function() { - elem.toggleClass('panel-in-fullscreen', true); - }); - - $scope.onAppEvent('panel-fullscreen-exit', function() { - elem.toggleClass('panel-in-fullscreen', false); - }); - - $scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) { - elem.toggleClass('playlist-active', newValue === true); - }); - - $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) { - if (newValue) { - elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue)); - setTimeout(function() { - elem.toggleClass('dashboard-page--settings-open', _.isString(newValue)); - }, 10); - } else { - elem.removeClass('dashboard-page--settings-opening'); - elem.removeClass('dashboard-page--settings-open'); - } - }); - } - }; - }); - -}); diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts new file mode 100644 index 00000000000..f0723f4fec7 --- /dev/null +++ b/public/app/core/directives/dash_class.ts @@ -0,0 +1,34 @@ +import _ from 'lodash'; +import coreModule from '../core_module'; + +coreModule.directive('dashClass', function($timeout) { + return { + link: function($scope, elem) { + $scope.ctrl.dashboard.events.on('view-mode-changed', function(panel) { + $timeout(() => { + elem.toggleClass('panel-in-fullscreen', panel.fullscreen === true); + }); + }); + + $scope.onAppEvent('panel-fullscreen-exit', function() { + elem.toggleClass('panel-in-fullscreen', false); + }); + + $scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) { + elem.toggleClass('playlist-active', newValue === true); + }); + + $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) { + if (newValue) { + elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue)); + setTimeout(function() { + elem.toggleClass('dashboard-page--settings-open', _.isString(newValue)); + }, 10); + } else { + elem.removeClass('dashboard-page--settings-opening'); + elem.removeClass('dashboard-page--settings-open'); + } + }); + }, + }; +}); diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 1f143f3d7f7..483ccb52bda 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -3,14 +3,14 @@ import _ from 'lodash'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; import store from 'app/core/store'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; export interface AddPanelPanelProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export interface AddPanelPanelState { @@ -55,8 +55,7 @@ export class AddPanelPanel extends React.Component { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); + const dashboard = this.props.dashboard; const { gridPos } = this.props.panel; var newPanel: any = { diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 3f65c33c90d..0bb75c54963 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -50,7 +50,8 @@ function GridWrapper({ onResize={onResize} onResizeStop={onResizeStop} onDragStop={onDragStop} - onLayoutChange={onLayoutChange}> + onLayoutChange={onLayoutChange} + > {children} ); @@ -177,8 +178,8 @@ export class DashboardGrid extends React.Component { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
- -
, + +
); } @@ -196,7 +197,8 @@ export class DashboardGrid extends React.Component { onWidthChange={this.onWidthChange} onDragStop={this.onDragStop} onResize={this.onResize} - onResizeStop={this.onResizeStop}> + onResizeStop={this.onResizeStop} + > {this.renderPanels()} ); diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 7a0a9bb2e86..eecf532922d 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -3,7 +3,7 @@ import config from 'app/core/config'; import classNames from 'classnames'; import appEvents from 'app/core/app_events'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import { AttachedPanel } from './PanelLoader'; import { DashboardRow } from './DashboardRow'; import { AddPanelPanel } from './AddPanelPanel'; @@ -11,7 +11,7 @@ import { importPluginModule } from 'app/features/plugins/plugin_loader'; export interface DashboardPanelProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export class DashboardPanel extends React.Component { @@ -39,33 +39,16 @@ export class DashboardPanel extends React.Component { } } - componentDidMount() { - if (!this.element) { - return; - } - - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - const loader = panelContainer.getPanelLoader(); - this.attachedPanel = loader.load(this.element, this.props.panel, dashboard); - } - - componentWillUnmount() { - if (this.attachedPanel) { - this.attachedPanel.destroy(); - } - } - isSpecial() { return this.specialPanels[this.props.panel.type]; } renderRow() { - return ; + return ; } renderAddPanel() { - return ; + return ; } render() { @@ -81,7 +64,7 @@ export class DashboardPanel extends React.Component { return (
- +
{PanelComponent && }
); @@ -93,16 +76,13 @@ export class DashboardPanel extends React.Component { } interface PanelHeaderProps { - panel: any; + panel: PanelModel; + dashboard: DashboardModel; } export class PanelHeader extends React.Component { onEditPanel = () => { - appEvents.emit('panel-change-view', { - fullscreen: true, - edit: true, - panelId: this.props.panel.id, - }); + this.props.dashboard.setViewMode(this.props.panel, true, true); }; render() { diff --git a/public/app/features/dashboard/dashgrid/DashboardRow.tsx b/public/app/features/dashboard/dashgrid/DashboardRow.tsx index fad7c120f65..17dd1681c66 100644 --- a/public/app/features/dashboard/dashgrid/DashboardRow.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardRow.tsx @@ -1,19 +1,16 @@ import React from 'react'; import classNames from 'classnames'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import templateSrv from 'app/features/templating/template_srv'; import appEvents from 'app/core/app_events'; export interface DashboardRowProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export class DashboardRow extends React.Component { - dashboard: any; - panelContainer: any; - constructor(props) { super(props); @@ -21,16 +18,13 @@ export class DashboardRow extends React.Component { collapsed: this.props.panel.collapsed, }; - this.panelContainer = this.props.getPanelContainer(); - this.dashboard = this.panelContainer.getDashboard(); - this.toggle = this.toggle.bind(this); this.openSettings = this.openSettings.bind(this); this.delete = this.delete.bind(this); } toggle() { - this.dashboard.toggleRow(this.props.panel); + this.props.dashboard.toggleRow(this.props.panel); this.setState(prevState => { return { collapsed: !prevState.collapsed }; @@ -55,14 +49,10 @@ export class DashboardRow extends React.Component { altActionText: 'Delete row only', icon: 'fa-trash', onConfirm: () => { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - dashboard.removeRow(this.props.panel, true); + this.props.dashboard.removeRow(this.props.panel, true); }, onAltAction: () => { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - dashboard.removeRow(this.props.panel, false); + this.props.dashboard.removeRow(this.props.panel, false); }, }); } diff --git a/public/app/features/dashboard/specs/DashboardRow.jest.tsx b/public/app/features/dashboard/specs/DashboardRow.jest.tsx index 2d44f2e0e74..3270abc9de9 100644 --- a/public/app/features/dashboard/specs/DashboardRow.jest.tsx +++ b/public/app/features/dashboard/specs/DashboardRow.jest.tsx @@ -4,18 +4,13 @@ import { DashboardRow } from '../dashgrid/DashboardRow'; import { PanelModel } from '../panel_model'; describe('DashboardRow', () => { - let wrapper, panel, getPanelContainer, dashboardMock; + let wrapper, panel, dashboardMock; beforeEach(() => { - dashboardMock = {toggleRow: jest.fn()}; + dashboardMock = { toggleRow: jest.fn() }; - getPanelContainer = jest.fn().mockReturnValue({ - getDashboard: jest.fn().mockReturnValue(dashboardMock), - getPanelLoader: jest.fn() - }); - - panel = new PanelModel({collapsed: false}); - wrapper = shallow(); + panel = new PanelModel({ collapsed: false }); + wrapper = shallow(); }); it('Should not have collapsed class when collaped is false', () => { @@ -29,5 +24,4 @@ describe('DashboardRow', () => { expect(wrapper.find('.dashboard-row--collapsed')).toHaveLength(1); expect(dashboardMock.toggleRow.mock.calls).toHaveLength(1); }); - }); From 61e6f63b32dfa127c11572440f3dd5e5a67f2580 Mon Sep 17 00:00:00 2001 From: Craig Miskell Date: Fri, 5 Jan 2018 09:08:40 +1300 Subject: [PATCH 0016/3000] Align queries to prometheus with the step to ensure 'rate' type expressions get consistent results --- public/app/plugins/datasource/prometheus/datasource.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 122fe9601a1..0782170fbd9 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -173,6 +173,9 @@ export class PrometheusDatasource { throw { message: 'Invalid time range' }; } + start = start - (start % query.step); + end = end - (end % query.step) + query.step; + var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + From f9fb315dbd8c08ee11f80468b17b8d6a0eada89b Mon Sep 17 00:00:00 2001 From: Craig Miskell Date: Fri, 5 Jan 2018 16:20:54 +1300 Subject: [PATCH 0017/3000] Update tests to match new reality, and rejig the implementation a bit to truly work as desired --- .../datasource/prometheus/datasource.ts | 22 ++++++---- .../prometheus/specs/datasource_specs.ts | 41 ++++++++++--------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 0782170fbd9..d0fd44c322a 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -72,6 +72,13 @@ export class PrometheusDatasource { return this.templateSrv.variableExists(target.expr); } + clampRange(start, end, step) { + return { + start: start - start % step, + end: end - end % step + step, + }; + } + query(options) { var self = this; var start = this.getPrometheusTime(options.range.from, false); @@ -99,7 +106,8 @@ export class PrometheusDatasource { var allQueryPromise = _.map(queries, query => { if (!query.instant) { - return this.performTimeSeriesQuery(query, start, end); + let range = this.clampRange(start, end, query.step); + return this.performTimeSeriesQuery(query, range.start, range.end); } else { return this.performInstantQuery(query, end); } @@ -118,7 +126,9 @@ export class PrometheusDatasource { } else { for (let metricData of response.data.data.result) { if (response.data.data.resultType === 'matrix') { - result.push(self.transformMetricData(metricData, activeTargets[index], start, end, queries[index].step)); + let step = queries[index].step; + let range = this.clampRange(start, end, step); + result.push(self.transformMetricData(metricData, activeTargets[index], range.start, range.end, step)); } else if (response.data.data.resultType === 'vector') { result.push(self.transformInstantMetricData(metricData, activeTargets[index])); } @@ -173,9 +183,6 @@ export class PrometheusDatasource { throw { message: 'Invalid time range' }; } - start = start - (start % query.step); - end = end - (end % query.step) + query.step; - var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + @@ -247,11 +254,12 @@ export class PrometheusDatasource { var end = this.getPrometheusTime(options.range.to, true); var query = { expr: interpolated, - step: this.adjustInterval(kbn.interval_to_seconds(step), 0, Math.ceil(end - start), 1) + 's', + step: this.adjustInterval(kbn.interval_to_seconds(step), 0, Math.ceil(end - start), 1), }; + let range = this.clampRange(start, end, query.step); var self = this; - return this.performTimeSeriesQuery(query, start, end).then(function(results) { + return this.performTimeSeriesQuery(query, range.start, range.end).then(function(results) { var eventList = []; tagKeys = tagKeys.split(','); diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index 043bfcf25e0..c3231c1dacb 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -34,7 +34,7 @@ describe('PrometheusDatasource', function() { var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + - '&start=1443438675&end=1443460275&step=60'; + '&start=1443438660&end=1443460320&step=60'; var query = { range: { from: moment(1443438674760), to: moment(1443460274760) }, targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], @@ -69,8 +69,8 @@ describe('PrometheusDatasource', function() { }); describe('When querying prometheus with one target which return multiple series', function() { var results; - var start = 1443438675; - var end = 1443460275; + var start = 1443438660; + var end = 1443460320; var step = 60; var urlExpected = 'proxied/api/v1/query_range?query=' + @@ -102,6 +102,7 @@ describe('PrometheusDatasource', function() { ], }, }; + // console.log(util.inspect(response, {depth: null})); beforeEach(function() { ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query).then(function(data) { @@ -115,6 +116,7 @@ describe('PrometheusDatasource', function() { expect(results.data[1].datapoints.length).to.be((end - start) / step + 1); }); it('should fill null until first datapoint in response', function() { + //console.log(util.inspect(results, {depth: null})); expect(results.data[0].datapoints[0][1]).to.be(start * 1000); expect(results.data[0].datapoints[0][0]).to.be(null); expect(results.data[0].datapoints[1][1]).to.be((start + step * 1) * 1000); @@ -128,6 +130,7 @@ describe('PrometheusDatasource', function() { expect(results.data[0].datapoints[length - 1][0]).to.be(null); }); it('should fill null at gap between series', function() { + //console.log(util.inspect(results, {depth: null})); expect(results.data[0].datapoints[2][1]).to.be((start + step * 2) * 1000); expect(results.data[0].datapoints[2][0]).to.be(null); expect(results.data[1].datapoints[1][1]).to.be((start + step * 1) * 1000); @@ -176,7 +179,7 @@ describe('PrometheusDatasource', function() { var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('ALERTS{alertstate="firing"}') + - '&start=1443438675&end=1443460275&step=60s'; + '&start=1443438660&end=1443460320&step=60'; var options = { annotation: { expr: 'ALERTS{alertstate="firing"}', @@ -327,7 +330,7 @@ describe('PrometheusDatasource', function() { ], interval: '5s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=10'; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438670&end=1443460280&step=10'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -340,7 +343,7 @@ describe('PrometheusDatasource', function() { targets: [{ expr: 'test' }], interval: '100ms', }; - var urlExpected = 'proxied/api/v1/query_range?query=test&start=1508318769&end=1508318771&step=1'; + var urlExpected = 'proxied/api/v1/query_range?query=test&start=1508318769&end=1508318772&step=1'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -358,7 +361,7 @@ describe('PrometheusDatasource', function() { ], interval: '10s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=10'; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438670&end=1443460280&step=10'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -370,7 +373,7 @@ describe('PrometheusDatasource', function() { targets: [{ expr: 'test' }], interval: '1s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=2'; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438674&end=1443460276&step=2'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -388,7 +391,7 @@ describe('PrometheusDatasource', function() { ], interval: '5s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=50'; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438650&end=1443460300&step=50'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -406,7 +409,7 @@ describe('PrometheusDatasource', function() { ], interval: '5s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=15'; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460290&step=15'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -424,7 +427,7 @@ describe('PrometheusDatasource', function() { ], interval: '10s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1443460275&step=100'; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438600&end=1443460300&step=100'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -441,7 +444,7 @@ describe('PrometheusDatasource', function() { ], interval: '10s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1444043475&step=100'; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438600&end=1444043500&step=100'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -458,7 +461,7 @@ describe('PrometheusDatasource', function() { ], interval: '5s', }; - var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438675&end=1444043475&step=60'; + var urlExpected = 'proxied/api/v1/query_range?query=test' + '&start=1443438660&end=1444043520&step=60'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -492,7 +495,7 @@ describe('PrometheusDatasource', function() { var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + - '&start=1443438675&end=1443460275&step=10'; + '&start=1443438670&end=1443460280&step=10'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -521,7 +524,7 @@ describe('PrometheusDatasource', function() { var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[10s])') + - '&start=1443438675&end=1443460275&step=10'; + '&start=1443438670&end=1443460280&step=10'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -551,7 +554,7 @@ describe('PrometheusDatasource', function() { var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[100s])') + - '&start=1443438675&end=1443460275&step=100'; + '&start=1443438600&end=1443460300&step=100'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -581,7 +584,7 @@ describe('PrometheusDatasource', function() { var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[50s])') + - '&start=1443438675&end=1443460275&step=50'; + '&start=1443438650&end=1443460300&step=50'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -611,7 +614,7 @@ describe('PrometheusDatasource', function() { var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[15s])') + - '&start=1443438675&end=1443460275&step=15'; + '&start=1443438675&end=1443460290&step=15'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); @@ -640,7 +643,7 @@ describe('PrometheusDatasource', function() { var urlExpected = 'proxied/api/v1/query_range?query=' + encodeURIComponent('rate(test[60s])') + - '&start=1443438675&end=1444043475&step=60'; + '&start=1443438660&end=1444043520&step=60'; ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query); ctx.$httpBackend.verifyNoOutstandingExpectation(); From 2e86985d44402c19b54ffb8e31ab764a1b032e21 Mon Sep 17 00:00:00 2001 From: Craig Miskell Date: Fri, 5 Jan 2018 16:22:49 +1300 Subject: [PATCH 0018/3000] Remove silly noise --- .../plugins/datasource/prometheus/specs/datasource_specs.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index c3231c1dacb..209b98efd23 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -102,7 +102,6 @@ describe('PrometheusDatasource', function() { ], }, }; - // console.log(util.inspect(response, {depth: null})); beforeEach(function() { ctx.$httpBackend.expect('GET', urlExpected).respond(response); ctx.ds.query(query).then(function(data) { @@ -116,7 +115,6 @@ describe('PrometheusDatasource', function() { expect(results.data[1].datapoints.length).to.be((end - start) / step + 1); }); it('should fill null until first datapoint in response', function() { - //console.log(util.inspect(results, {depth: null})); expect(results.data[0].datapoints[0][1]).to.be(start * 1000); expect(results.data[0].datapoints[0][0]).to.be(null); expect(results.data[0].datapoints[1][1]).to.be((start + step * 1) * 1000); @@ -130,7 +128,6 @@ describe('PrometheusDatasource', function() { expect(results.data[0].datapoints[length - 1][0]).to.be(null); }); it('should fill null at gap between series', function() { - //console.log(util.inspect(results, {depth: null})); expect(results.data[0].datapoints[2][1]).to.be((start + step * 2) * 1000); expect(results.data[0].datapoints[2][0]).to.be(null); expect(results.data[1].datapoints[1][1]).to.be((start + step * 1) * 1000); From 33ac22bfdb53eeb7655966a1aed469a8a6f62a7b Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 21 Jan 2018 22:08:18 +0100 Subject: [PATCH 0019/3000] start query builder ui --- .../postgres/partials/query.editor.html | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 163970a9ad5..635c3e6f222 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -1,10 +1,23 @@ - -
-
- - -
-
+ + +
+
+
+ + +
+
+
+ +
+
+
+ + + +
+
+
From 17be31e2167ef92af57884a2bb9975a33f7968fb Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 28 Jan 2018 22:16:51 +0100 Subject: [PATCH 0020/3000] call render in query --- .../app/plugins/datasource/postgres/datasource.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 8eee389d1a5..3a4bd27bb4c 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import ResponseParser from './response_parser'; +import PostgresQuery from 'app/plugins/datasource/postgres/postgres_query'; export class PostgresDatasource { id: any; @@ -33,16 +34,18 @@ export class PostgresDatasource { } query(options) { - var queries = _.filter(options.targets, item => { - return item.hide !== true; - }).map(item => { + var queries = _.filter(options.targets, target => { + return target.hide !== true; + }).map(target => { + var queryModel = new PostgresQuery(target, this.templateSrv, options.scopedVars); + return { - refId: item.refId, + refId: target.refId, intervalMs: options.intervalMs, maxDataPoints: options.maxDataPoints, datasourceId: this.id, - rawSql: this.templateSrv.replace(item.rawSql, options.scopedVars, this.interpolateVariable), - format: item.format, + rawSql: queryModel.render(this.interpolateVariable), + format: target.format, }; }); From a59e052a0f9ac9aab7467d49dd5586de0d08e124 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 30 Jan 2018 14:08:10 +0100 Subject: [PATCH 0021/3000] more query builder components --- .../postgres/partials/query.editor.html | 94 +++++++++++++------ 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 635c3e6f222..400855cf82f 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -1,12 +1,12 @@
-
-
- - -
-
+
+
+ + +
+
@@ -15,42 +15,76 @@ +
+ +
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+
+
+ +
+
+ + + +
+
+
- -
- -
-
-
+ +
+ +
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
{{ctrl.lastQueryMeta.sql}}
-
+
+
{{ctrl.lastQueryMeta.sql}}
+
-
-
Time series:
+  
+
Time series:
 - return column named time (UTC in seconds or timestamp)
 - return column(s) with numeric datatype as values
 - (Optional: return column named metric to represent the series name. If no column named metric is found the column name of the value column is used as series name)
@@ -78,13 +112,13 @@ Or build your own conditionals using these macros which just return the values:
 - $__timeTo() ->  to_timestamp(1492750877)
 - $__unixEpochFrom() ->  1492750877
 - $__unixEpochTo() ->  1492750877
-		
-
+
+
-
+
-
-
{{ctrl.lastQueryError}}
-
+
+
{{ctrl.lastQueryError}}
+
From 438b10bcd68abd115dbe86acad1a70a53a791c79 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 30 Jan 2018 14:10:38 +0100 Subject: [PATCH 0022/3000] query builder changes --- .../plugins/datasource/postgres/query_ctrl.ts | 179 ++++++++++++++++-- 1 file changed, 168 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 7afd0cf7253..4e39105370b 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -1,12 +1,7 @@ import _ from 'lodash'; import { QueryCtrl } from 'app/plugins/sdk'; - -export interface PostgresQuery { - refId: string; - format: string; - alias: string; - rawSql: string; -} +import queryPart from './query_part'; +import PostgresQuery from './postgres_query'; export interface QueryMeta { sql: string; @@ -26,17 +21,21 @@ export class PostgresQueryCtrl extends QueryCtrl { showLastQuerySQL: boolean; formats: any[]; - target: PostgresQuery; + queryModel: PostgresQuery; lastQueryMeta: QueryMeta; lastQueryError: string; showHelp: boolean; + schemaSegment: any; + tableSegment: any; + timeColumnSegment: any; + selectMenu: any; /** @ngInject **/ - constructor($scope, $injector) { + constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { super($scope, $injector); + this.target = this.target; + this.queryModel = new PostgresQuery(this.target, templateSrv, this.panel.scopedVars); - this.target.format = this.target.format || 'time_series'; - this.target.alias = ''; this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; if (!this.target.rawSql) { @@ -49,10 +48,104 @@ export class PostgresQueryCtrl extends QueryCtrl { } } + this.schemaSegment= uiSegmentSrv.newSegment(this.target.schema); + + if (!this.target.table) { + this.tableSegment = uiSegmentSrv.newSegment({value: 'select table',fake: true}); + } else { + this.tableSegment= uiSegmentSrv.newSegment(this.target.table); + } + + this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); + + this.buildSelectMenu(); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } + buildSelectMenu() { + var categories = queryPart.getCategories(); + this.selectMenu = _.reduce( + categories, + function(memo, cat, key) { + var menu = { + text: key, + submenu: cat.map(item => { + return { text: item.type, value: item.type }; + }), + }; + memo.push(menu); + return memo; + }, + [] + ); + } + + toggleEditorMode() { + try { +// this.target.query = this.queryModel.render(false); + } catch (err) { + console.log('query render error'); + } + this.target.rawQuery = !this.target.rawQuery; + } + + getSchemaSegments() { + var schemaQuery = "SELECT schema_name FROM information_schema.schemata WHERE"; + schemaQuery += " schema_name NOT LIKE 'pg_%' AND schema_name <> 'information_schema';"; + return this.datasource + .metricFindQuery(schemaQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + + getTableSegments() { + var tableQuery = "SELECT table_name FROM information_schema.tables WHERE table_schema = '" + this.target.schema + "';"; + return this.datasource + .metricFindQuery(tableQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + + getTimeColumnSegments() { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + columnQuery += " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real');"; + + return this.datasource + .metricFindQuery(columnQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + + getColumnSegments() { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + columnQuery += " AND data_type IN ('bigint','integer','double precision','real');"; + + return this.datasource + .metricFindQuery(columnQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + + tableChanged() { + this.target.table = this.tableSegment.value; + this.panelCtrl.refresh(); + } + + schemaChanged() { + this.target.schema = this.schemaSegment.value; + this.panelCtrl.refresh(); + } + + timeColumnChanged() { + this.target.time = this.timeColumnSegment.value; + this.panelCtrl.refresh(); + } + onDataReceived(dataList) { this.lastQueryMeta = null; this.lastQueryError = null; @@ -72,4 +165,68 @@ export class PostgresQueryCtrl extends QueryCtrl { } } } + + transformToSegments(addTemplateVars) { + return results => { + var segments = _.map(results, segment => { + return this.uiSegmentSrv.newSegment({ + value: segment.text, + expandable: segment.expandable, + }); + }); + + if (addTemplateVars) { + for (let variable of this.templateSrv.variables) { + segments.unshift( + this.uiSegmentSrv.newSegment({ + type: 'template', + value: '/^$' + variable.name + '$/', + expandable: true, + }) + ); + } + } + + return segments; + }; + } + + addSelectPart(selectParts, cat, subitem) { + this.queryModel.addSelectPart(selectParts, subitem.value); + this.panelCtrl.refresh(); + } + + handleSelectPartEvent(selectParts, part, evt) { + switch (evt.name) { + case 'get-param-options': { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + columnQuery += " AND data_type IN ('bigint','integer','double precision','real');"; + + return this.datasource + .metricFindQuery(columnQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + case 'part-param-changed': { + this.panelCtrl.refresh(); + break; + } + case 'action': { + this.queryModel.removeSelectPart(selectParts, part); + this.panelCtrl.refresh(); + break; + } + case 'get-part-actions': { + return this.$q.when([{ text: 'Remove', value: 'remove-part' }]); + } + } + } + + handleQueryError(err) { + this.error = err.message || 'Failed to issue metric query'; + return []; + } + } From 443504517a1bcd58c533feef202530248a6d7050 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 30 Jan 2018 22:13:55 +0100 Subject: [PATCH 0023/3000] add postgres_query.ts --- .../datasource/postgres/postgres_query.ts | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 public/app/plugins/datasource/postgres/postgres_query.ts diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts new file mode 100644 index 00000000000..3424ae24c78 --- /dev/null +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -0,0 +1,239 @@ +import _ from 'lodash'; +import queryPart from './query_part'; +import kbn from 'app/core/utils/kbn'; + +export default class PostgresQuery { + target: any; + selectModels: any[]; + queryBuilder: any; + groupByParts: any; + templateSrv: any; + scopedVars: any; + + /** @ngInject */ + constructor(target, templateSrv?, scopedVars?) { + this.target = target; + this.templateSrv = templateSrv; + this.scopedVars = scopedVars; + + target.schema = target.schema || 'public'; + target.format = target.format || 'time_series'; + target.timeColumn = target.timeColumn || 'time'; + target.alias = ''; + + target.orderByTime = target.orderByTime || 'ASC'; +// target.groupBy = target.groupBy || [{ type: 'time', params: ['$__interval'] }, { type: 'fill', params: ['null'] }]; + target.select = target.select || [[{ type: 'field', params: ['value'] }]]; + + this.updateProjection(); + } + + updateProjection() { + this.selectModels = _.map(this.target.select, function(parts: any) { + return _.map(parts, queryPart.create); + }); + this.groupByParts = _.map(this.target.groupBy, queryPart.create); + } + + updatePersistedParts() { + this.target.select = _.map(this.selectModels, function(selectParts) { + return _.map(selectParts, function(part: any) { + return { type: part.def.type, params: part.params }; + }); + }); + } + + hasGroupByTime() { + return _.find(this.target.groupBy, (g: any) => g.type === 'time'); + } + + hasFill() { + return _.find(this.target.groupBy, (g: any) => g.type === 'fill'); + } + + addGroupBy(value) { + var stringParts = value.match(/^(\w+)\((.*)\)$/); + var typePart = stringParts[1]; + var arg = stringParts[2]; + var partModel = queryPart.create({ type: typePart, params: [arg] }); + var partCount = this.target.groupBy.length; + + if (partCount === 0) { + this.target.groupBy.push(partModel.part); + } else if (typePart === 'time') { + this.target.groupBy.splice(0, 0, partModel.part); + } else if (typePart === 'tag') { + if (this.target.groupBy[partCount - 1].type === 'fill') { + this.target.groupBy.splice(partCount - 1, 0, partModel.part); + } else { + this.target.groupBy.push(partModel.part); + } + } else { + this.target.groupBy.push(partModel.part); + } + + this.updateProjection(); + } + + removeGroupByPart(part, index) { + var categories = queryPart.getCategories(); + + if (part.def.type === 'time') { + // remove fill + this.target.groupBy = _.filter(this.target.groupBy, (g: any) => g.type !== 'fill'); + // remove aggregations + this.target.select = _.map(this.target.select, (s: any) => { + return _.filter(s, (part: any) => { + var partModel = queryPart.create(part); + if (partModel.def.category === categories.Aggregations) { + return false; + } + if (partModel.def.category === categories.Selectors) { + return false; + } + return true; + }); + }); + } + + this.target.groupBy.splice(index, 1); + this.updateProjection(); + } + + removeSelect(index: number) { + this.target.select.splice(index, 1); + this.updateProjection(); + } + + removeSelectPart(selectParts, part) { + // if we remove the field remove the whole statement + if (part.def.type === 'field') { + if (this.selectModels.length > 1) { + var modelsIndex = _.indexOf(this.selectModels, selectParts); + this.selectModels.splice(modelsIndex, 1); + } + } else { + var partIndex = _.indexOf(selectParts, part); + selectParts.splice(partIndex, 1); + } + + this.updatePersistedParts(); + } + + addSelectPart(selectParts, type) { + var partModel = queryPart.create({ type: type }); + partModel.def.addStrategy(selectParts, partModel, this); + this.updatePersistedParts(); + } + + private renderTagCondition(tag, index, interpolate) { + var str = ''; + var operator = tag.operator; + var value = tag.value; + if (index > 0) { + str = (tag.condition || 'AND') + ' '; + } + + if (!operator) { + if (/^\/.*\/$/.test(value)) { + operator = '=~'; + } else { + operator = '='; + } + } + + // quote value unless regex + if (operator !== '=~' && operator !== '!~') { + if (interpolate) { + value = this.templateSrv.replace(value, this.scopedVars); + } + if (operator !== '>' && operator !== '<') { + value = "'" + value.replace(/\\/g, '\\\\') + "'"; + } + } else if (interpolate) { + value = this.templateSrv.replace(value, this.scopedVars, 'regex'); + } + + return str + '"' + tag.key + '" ' + operator + ' ' + value; + } + + interpolateQueryStr(value, variable, defaultFormatFn) { + // if no multi or include all do not regexEscape + if (!variable.multi && !variable.includeAll) { + return value; + } + + if (typeof value === 'string') { + return kbn.regexEscape(value); + } + + var escapedValues = _.map(value, kbn.regexEscape); + return '(' + escapedValues.join('|') + ')'; + } + + render(interpolate?) { + var target = this.target; + + if (target.rawQuery) { + if (interpolate) { + return this.templateSrv.replace(target.rawSql, this.scopedVars, this.interpolateQueryStr); + } else { + return target.rawSql; + } + } + + var query = 'SELECT '; + query += target.timeColumn + ' AS time,'; + + var i, y; + for (i = 0; i < this.selectModels.length; i++) { + let parts = this.selectModels[i]; + var selectText = ''; + for (y = 0; y < parts.length; y++) { + let part = parts[y]; + selectText = part.render(selectText); + } + + if (i > 0) { + query += ', '; + } + query += selectText; + } + + query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; + var conditions = _.map(target.tags, (tag, index) => { + return this.renderTagCondition(tag, index, interpolate); + }); + + if (conditions.length > 0) { + query += '(' + conditions.join(' ') + ') AND '; + } + + query += '$__timeFilter(time)'; + + var groupBySection = ''; + for (i = 0; i < this.groupByParts.length; i++) { + var part = this.groupByParts[i]; + if (i > 0) { + // for some reason fill has no seperator + groupBySection += part.def.type === 'fill' ? ' ' : ', '; + } + groupBySection += part.render(''); + } + + if (groupBySection.length) { + query += ' GROUP BY ' + groupBySection; + } + + query += ' ORDER BY time'; + + return query; + } + + renderAdhocFilters(filters) { + var conditions = _.map(filters, (tag, index) => { + return this.renderTagCondition(tag, index, false); + }); + return conditions.join(' '); + } +} From 571ecdc740b87aad130ce05bd827135fd2e570d4 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 30 Jan 2018 22:57:38 +0100 Subject: [PATCH 0024/3000] enhance render function --- .../datasource/postgres/postgres_query.ts | 8 +- .../plugins/datasource/postgres/query_part.ts | 380 ++++++++++++++++++ 2 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 public/app/plugins/datasource/postgres/query_part.ts diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 3424ae24c78..e6d84306a9b 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -28,6 +28,10 @@ export default class PostgresQuery { this.updateProjection(); } + quoteIdentifier(field) { + return '"' + field + '"'; + } + updateProjection() { this.selectModels = _.map(this.target.select, function(parts: any) { return _.map(parts, queryPart.create); @@ -183,7 +187,7 @@ export default class PostgresQuery { } var query = 'SELECT '; - query += target.timeColumn + ' AS time,'; + query += this.quoteIdentifier(target.timeColumn) + ' AS time,'; var i, y; for (i = 0; i < this.selectModels.length; i++) { @@ -209,7 +213,7 @@ export default class PostgresQuery { query += '(' + conditions.join(' ') + ') AND '; } - query += '$__timeFilter(time)'; + query += '$__timeFilter(' + this.quoteIdentifier(target.timeColumn) + ')'; var groupBySection = ''; for (i = 0; i < this.groupByParts.length; i++) { diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts new file mode 100644 index 00000000000..dffea15dbd1 --- /dev/null +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -0,0 +1,380 @@ +import _ from 'lodash'; +import { QueryPartDef, QueryPart, functionRenderer, suffixRenderer } from 'app/core/components/query_part/query_part'; + +var index = []; +var categories = { + Aggregations: [], + Selectors: [], + Transformations: [], + Predictors: [], + Math: [], + Aliasing: [], + Fields: [], +}; + +function createPart(part): any { + var def = index[part.type]; + if (!def) { + throw { message: 'Could not find query part ' + part.type }; + } + + return new QueryPart(part, def); +} + +function register(options: any) { + index[options.type] = new QueryPartDef(options); + options.category.push(index[options.type]); +} + +var groupByTimeFunctions = []; + +function aliasRenderer(part, innerExpr) { + return innerExpr + ' AS ' + '"' + part.params[0] + '"'; +} + +function fieldRenderer(part, innerExpr) { + return '"' + part.params[0] + '"'; +} + +function replaceAggregationAddStrategy(selectParts, partModel) { + // look for existing aggregation + for (var i = 0; i < selectParts.length; i++) { + var part = selectParts[i]; + if (part.def.category === categories.Aggregations) { + selectParts[i] = partModel; + return; + } + if (part.def.category === categories.Selectors) { + selectParts[i] = partModel; + return; + } + } + + selectParts.splice(1, 0, partModel); +} + +function addTransformationStrategy(selectParts, partModel) { + var i; + // look for index to add transformation + for (i = 0; i < selectParts.length; i++) { + var part = selectParts[i]; + if (part.def.category === categories.Math || part.def.category === categories.Aliasing) { + break; + } + } + + selectParts.splice(i, 0, partModel); +} + +function addMathStrategy(selectParts, partModel) { + var partCount = selectParts.length; + if (partCount > 0) { + // if last is math, replace it + if (selectParts[partCount - 1].def.type === 'math') { + selectParts[partCount - 1] = partModel; + return; + } + // if next to last is math, replace it + if (partCount > 1 && selectParts[partCount - 2].def.type === 'math') { + selectParts[partCount - 2] = partModel; + return; + } else if (selectParts[partCount - 1].def.type === 'alias') { + // if last is alias add it before + selectParts.splice(partCount - 1, 0, partModel); + return; + } + } + selectParts.push(partModel); +} + +function addAliasStrategy(selectParts, partModel) { + var partCount = selectParts.length; + if (partCount > 0) { + // if last is alias, replace it + if (selectParts[partCount - 1].def.type === 'alias') { + selectParts[partCount - 1] = partModel; + return; + } + } + selectParts.push(partModel); +} + +function addFieldStrategy(selectParts, partModel, query) { + // copy all parts + var parts = _.map(selectParts, function(part: any) { + return createPart({ type: part.def.type, params: _.clone(part.params) }); + }); + + query.selectModels.push(parts); +} + +register({ + type: 'field', + addStrategy: addFieldStrategy, + category: categories.Fields, + params: [{ type: 'field', dynamicLookup: true }], + defaultParams: ['value'], + renderer: fieldRenderer, +}); + +// Aggregations +register({ + type: 'avg', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'count', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'sum', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +// transformations + +register({ + type: 'derivative', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [ + { + name: 'duration', + type: 'interval', + options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + ], + defaultParams: ['10s'], + renderer: functionRenderer, +}); + +register({ + type: 'spread', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'non_negative_derivative', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [ + { + name: 'duration', + type: 'interval', + options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + ], + defaultParams: ['10s'], + renderer: functionRenderer, +}); + +register({ + type: 'difference', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'non_negative_difference', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'moving_average', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [{ name: 'window', type: 'int', options: [5, 10, 20, 30, 40] }], + defaultParams: [10], + renderer: functionRenderer, +}); + +register({ + type: 'cumulative_sum', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'stddev', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'time', + category: groupByTimeFunctions, + params: [ + { + name: 'interval', + type: 'time', + options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + ], + defaultParams: ['$__interval'], + renderer: functionRenderer, +}); + +register({ + type: 'fill', + category: groupByTimeFunctions, + params: [ + { + name: 'fill', + type: 'string', + options: ['none', 'null', '0', 'previous', 'linear'], + }, + ], + defaultParams: ['null'], + renderer: functionRenderer, +}); + +register({ + type: 'elapsed', + addStrategy: addTransformationStrategy, + category: categories.Transformations, + params: [ + { + name: 'duration', + type: 'interval', + options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + ], + defaultParams: ['10s'], + renderer: functionRenderer, +}); + +// predictions +register({ + type: 'holt_winters', + addStrategy: addTransformationStrategy, + category: categories.Predictors, + params: [ + { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] }, + { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] }, + ], + defaultParams: [10, 2], + renderer: functionRenderer, +}); + +register({ + type: 'holt_winters_with_fit', + addStrategy: addTransformationStrategy, + category: categories.Predictors, + params: [ + { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] }, + { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] }, + ], + defaultParams: [10, 2], + renderer: functionRenderer, +}); + +// Selectors +register({ + type: 'bottom', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{ name: 'count', type: 'int' }], + defaultParams: [3], + renderer: functionRenderer, +}); + +register({ + type: 'max', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'min', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'percentile', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{ name: 'nth', type: 'int' }], + defaultParams: [95], + renderer: functionRenderer, +}); + +register({ + type: 'top', + addStrategy: replaceAggregationAddStrategy, + category: categories.Selectors, + params: [{ name: 'count', type: 'int' }], + defaultParams: [3], + renderer: functionRenderer, +}); + +register({ + type: 'tag', + category: groupByTimeFunctions, + params: [{ name: 'tag', type: 'string', dynamicLookup: true }], + defaultParams: ['tag'], + renderer: fieldRenderer, +}); + +register({ + type: 'math', + addStrategy: addMathStrategy, + category: categories.Math, + params: [{ name: 'expr', type: 'string' }], + defaultParams: [' / 100'], + renderer: suffixRenderer, +}); + +register({ + type: 'alias', + addStrategy: addAliasStrategy, + category: categories.Aliasing, + params: [{ name: 'name', type: 'string', quote: 'double' }], + defaultParams: ['alias'], + renderMode: 'suffix', + renderer: aliasRenderer, +}); + +export default { + create: createPart, + getCategories: function() { + return categories; + }, +}; From 4dbd83fac18a3cc5e59208d396a32538f2ab1a5e Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 31 Jan 2018 15:32:32 +0100 Subject: [PATCH 0025/3000] add groupby to querybuilder remove unused aggregations --- .../postgres/partials/query.editor.html | 21 +++ .../datasource/postgres/postgres_query.ts | 5 +- .../plugins/datasource/postgres/query_ctrl.ts | 84 ++++++++++ .../plugins/datasource/postgres/query_part.ts | 150 +----------------- 4 files changed, 110 insertions(+), 150 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 400855cf82f..3e921e1c631 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -51,6 +51,27 @@ +
+
+ + + + +
+ +
+ +
+ +
+
+
+
+
diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index e6d84306a9b..389ce85aae9 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -22,7 +22,7 @@ export default class PostgresQuery { target.alias = ''; target.orderByTime = target.orderByTime || 'ASC'; -// target.groupBy = target.groupBy || [{ type: 'time', params: ['$__interval'] }, { type: 'fill', params: ['null'] }]; + target.groupBy = target.groupBy || [{ type: 'time', params: ['$__interval'] }, { type: 'fill', params: ['null'] }]; target.select = target.select || [[{ type: 'field', params: ['value'] }]]; this.updateProjection(); @@ -92,9 +92,6 @@ export default class PostgresQuery { if (partModel.def.category === categories.Aggregations) { return false; } - if (partModel.def.category === categories.Selectors) { - return false; - } return true; }); }); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 4e39105370b..020d8007789 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -29,6 +29,7 @@ export class PostgresQueryCtrl extends QueryCtrl { tableSegment: any; timeColumnSegment: any; selectMenu: any; + groupBySegment: any; /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { @@ -59,6 +60,8 @@ export class PostgresQueryCtrl extends QueryCtrl { this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); this.buildSelectMenu(); + this.groupBySegment = this.uiSegmentSrv.newPlusButton(); + this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } @@ -224,6 +227,87 @@ export class PostgresQueryCtrl extends QueryCtrl { } } + handleGroupByPartEvent(part, index, evt) { + switch (evt.name) { + case 'get-param-options': { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + + return this.datasource + .metricFindQuery(columnQuery) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + case 'part-param-changed': { + this.panelCtrl.refresh(); + break; + } + case 'action': { + this.queryModel.removeGroupByPart(part, index); + this.panelCtrl.refresh(); + break; + } + case 'get-part-actions': { + return this.$q.when([{ text: 'Remove', value: 'remove-part' }]); + } + } + } + + getGroupByOptions() { + var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; + columnQuery += " table_schema = '" + this.target.schema + "'"; + columnQuery += " AND table_name = '" + this.target.table + "'"; + + + return this.datasource + .metricFindQuery(columnQuery) + .then(tags => { + var options = []; + if (!this.queryModel.hasFill()) { + options.push(this.uiSegmentSrv.newSegment({ value: 'fill(null)' })); + } + if (!this.target.limit) { + options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); + } + if (!this.target.slimit) { + options.push(this.uiSegmentSrv.newSegment({ value: 'SLIMIT' })); + } + if (this.target.orderByTime === 'ASC') { + options.push(this.uiSegmentSrv.newSegment({ value: 'ORDER BY time DESC' })); + } + if (!this.queryModel.hasGroupByTime()) { + options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); + } + for (let tag of tags) { + options.push(this.uiSegmentSrv.newSegment({ value: 'tag(' + tag.text + ')' })); + } + return options; + }) + .catch(this.handleQueryError.bind(this)); + } + + groupByAction() { + switch (this.groupBySegment.value) { + case 'LIMIT': { + this.target.limit = 10; + break; + } + case 'ORDER BY time DESC': { + this.target.orderByTime = 'DESC'; + break; + } + default: { + this.queryModel.addGroupBy(this.groupBySegment.value); + } + } + + var plusButton = this.uiSegmentSrv.newPlusButton(); + this.groupBySegment.value = plusButton.value; + this.groupBySegment.html = plusButton.html; + this.panelCtrl.refresh(); + } + handleQueryError(err) { this.error = err.message || 'Failed to issue metric query'; return []; diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index dffea15dbd1..5828515ec06 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -4,9 +4,6 @@ import { QueryPartDef, QueryPart, functionRenderer, suffixRenderer } from 'app/c var index = []; var categories = { Aggregations: [], - Selectors: [], - Transformations: [], - Predictors: [], Math: [], Aliasing: [], Fields: [], @@ -44,10 +41,6 @@ function replaceAggregationAddStrategy(selectParts, partModel) { selectParts[i] = partModel; return; } - if (part.def.category === categories.Selectors) { - selectParts[i] = partModel; - return; - } } selectParts.splice(1, 0, partModel); @@ -147,34 +140,10 @@ register({ // transformations -register({ - type: 'derivative', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [ - { - name: 'duration', - type: 'interval', - options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - ], - defaultParams: ['10s'], - renderer: functionRenderer, -}); - -register({ - type: 'spread', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - register({ type: 'non_negative_derivative', addStrategy: addTransformationStrategy, - category: categories.Transformations, + category: categories.Aggregations, params: [ { name: 'duration', @@ -186,46 +155,10 @@ register({ renderer: functionRenderer, }); -register({ - type: 'difference', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'non_negative_difference', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'moving_average', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [{ name: 'window', type: 'int', options: [5, 10, 20, 30, 40] }], - defaultParams: [10], - renderer: functionRenderer, -}); - -register({ - type: 'cumulative_sum', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - register({ type: 'stddev', addStrategy: addTransformationStrategy, - category: categories.Transformations, + category: categories.Aggregations, params: [], defaultParams: [], renderer: functionRenderer, @@ -259,60 +192,11 @@ register({ renderer: functionRenderer, }); -register({ - type: 'elapsed', - addStrategy: addTransformationStrategy, - category: categories.Transformations, - params: [ - { - name: 'duration', - type: 'interval', - options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - ], - defaultParams: ['10s'], - renderer: functionRenderer, -}); - -// predictions -register({ - type: 'holt_winters', - addStrategy: addTransformationStrategy, - category: categories.Predictors, - params: [ - { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] }, - { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] }, - ], - defaultParams: [10, 2], - renderer: functionRenderer, -}); - -register({ - type: 'holt_winters_with_fit', - addStrategy: addTransformationStrategy, - category: categories.Predictors, - params: [ - { name: 'number', type: 'int', options: [5, 10, 20, 30, 40] }, - { name: 'season', type: 'int', options: [0, 1, 2, 5, 10] }, - ], - defaultParams: [10, 2], - renderer: functionRenderer, -}); - // Selectors -register({ - type: 'bottom', - addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, - params: [{ name: 'count', type: 'int' }], - defaultParams: [3], - renderer: functionRenderer, -}); - register({ type: 'max', addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, + category: categories.Aggregations, params: [], defaultParams: [], renderer: functionRenderer, @@ -321,38 +205,12 @@ register({ register({ type: 'min', addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, + category: categories.Aggregations, params: [], defaultParams: [], renderer: functionRenderer, }); -register({ - type: 'percentile', - addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, - params: [{ name: 'nth', type: 'int' }], - defaultParams: [95], - renderer: functionRenderer, -}); - -register({ - type: 'top', - addStrategy: replaceAggregationAddStrategy, - category: categories.Selectors, - params: [{ name: 'count', type: 'int' }], - defaultParams: [3], - renderer: functionRenderer, -}); - -register({ - type: 'tag', - category: groupByTimeFunctions, - params: [{ name: 'tag', type: 'string', dynamicLookup: true }], - defaultParams: ['tag'], - renderer: fieldRenderer, -}); - register({ type: 'math', addStrategy: addMathStrategy, From 636cca83320ebce7b82b2bb65b97f67581f9986e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 2 Feb 2018 16:01:01 +0100 Subject: [PATCH 0026/3000] poc: merge sync --- public/app/features/dashboard/dashgrid/AddPanelPanel.tsx | 4 +--- public/app/features/dashboard/dashgrid/DashboardPanel.tsx | 1 - public/app/features/dashboard/dashgrid/DashboardRow.tsx | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index eb6a4de00ae..beae2650d3e 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -86,9 +86,7 @@ export class AddPanelPanel extends React.Component { } update() { - this.dashboard.processRepeats(); + this.props.dashboard.processRepeats(); this.forceUpdate(); } From c65a964cdda6e4cabcc570f0199dcd5378331781 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 3 Feb 2018 18:03:00 +0100 Subject: [PATCH 0027/3000] add metric column selector --- .../postgres/partials/query.editor.html | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 3e921e1c631..e266050dff1 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -17,6 +17,15 @@
+ +
+ +
+ +
+
+
+
@@ -46,9 +55,15 @@
- + +
+ +
+
+
+
From 382a5254772d29b67658dc1ed8e38a34b7df0a11 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 3 Feb 2018 18:26:57 +0100 Subject: [PATCH 0028/3000] make metricColumn functional --- .../app/plugins/datasource/postgres/query_ctrl.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 020d8007789..0b2bce7fb05 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -27,7 +27,9 @@ export class PostgresQueryCtrl extends QueryCtrl { showHelp: boolean; schemaSegment: any; tableSegment: any; + whereSegment: any; timeColumnSegment: any; + metricColumnSegment: any; selectMenu: any; groupBySegment: any; @@ -58,6 +60,7 @@ export class PostgresQueryCtrl extends QueryCtrl { } this.timeColumnSegment = uiSegmentSrv.newSegment(this.target.timeColumn); + this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); this.buildSelectMenu(); this.groupBySegment = this.uiSegmentSrv.newPlusButton(); @@ -122,11 +125,11 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } - getColumnSegments() { + getMetricColumnSegments() { var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; columnQuery += " table_schema = '" + this.target.schema + "'"; columnQuery += " AND table_name = '" + this.target.table + "'"; - columnQuery += " AND data_type IN ('bigint','integer','double precision','real');"; + columnQuery += " AND data_type IN ('text','char','varchar');"; return this.datasource .metricFindQuery(columnQuery) @@ -145,7 +148,12 @@ export class PostgresQueryCtrl extends QueryCtrl { } timeColumnChanged() { - this.target.time = this.timeColumnSegment.value; + this.target.timeColumn = this.timeColumnSegment.value; + this.panelCtrl.refresh(); + } + + metricColumnChanged() { + this.target.metricColumn = this.metricColumnSegment.value; this.panelCtrl.refresh(); } From 3bce45d8a66abf984644f0cad508e73e60b595bc Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 8 Feb 2018 10:19:43 +0100 Subject: [PATCH 0029/3000] add query_builder --- .../datasource/postgres/query_builder.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 public/app/plugins/datasource/postgres/query_builder.ts diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts new file mode 100644 index 00000000000..7a227f33b93 --- /dev/null +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -0,0 +1,50 @@ + +export class PostgresQueryBuilder { + constructor(private target, private queryModel) {} + + buildSchemaQuery() { + var query = "SELECT schema_name FROM information_schema.schemata WHERE"; + query += " schema_name NOT LIKE 'pg_%' AND schema_name NOT LIKE '\\_%' AND schema_name <> 'information_schema';"; + + return query; + } + + buildTableQuery() { + var query = "SELECT table_name FROM information_schema.tables WHERE "; + query += "table_schema = " + this.queryModel.quoteLiteral(this.target.schema); + return query; + } + + buildColumnQuery(type?: string) { + var query = "SELECT column_name FROM information_schema.columns WHERE "; + query += "table_schema = " + this.queryModel.quoteLiteral(this.target.schema); + query += " AND table_name = " + this.queryModel.quoteLiteral(this.target.table); + + switch (type) { + case "time": { + query += " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')"; + break; + } + case "metric": { + query += " AND data_type IN ('text','char','varchar')"; + break; + } + case "value": { + query += " AND data_type IN ('bigint','integer','double precision','real')"; + break; + } + } + + return query; + } + + buildValueQuery(column: string) { + var query = "SELECT DISTINCT " + this.queryModel.quoteIdentifier(column) + "::text"; + query += " FROM " + this.queryModel.quoteIdentifier(this.target.schema); + query += "." + this.queryModel.quoteIdentifier(this.target.table); + query += " ORDER BY " + this.queryModel.quoteIdentifier(column); + query += " LIMIT 100"; + return query; + } + +} From ef18eb7fcb0449c7bc9709b8837ff8fa202ccc93 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 8 Feb 2018 10:25:32 +0100 Subject: [PATCH 0030/3000] add where constraint handling --- .../postgres/partials/query.editor.html | 2 +- .../datasource/postgres/postgres_query.ts | 15 +- .../plugins/datasource/postgres/query_ctrl.ts | 192 ++++++++++++++---- 3 files changed, 163 insertions(+), 46 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index e266050dff1..e25a41b11c2 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -19,7 +19,7 @@
- +
diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 389ce85aae9..9a5a14c2b6a 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -19,17 +19,22 @@ export default class PostgresQuery { target.schema = target.schema || 'public'; target.format = target.format || 'time_series'; target.timeColumn = target.timeColumn || 'time'; - target.alias = ''; + target.metricColumn = target.metricColumn || 'None'; target.orderByTime = target.orderByTime || 'ASC'; - target.groupBy = target.groupBy || [{ type: 'time', params: ['$__interval'] }, { type: 'fill', params: ['null'] }]; + target.groupBy = target.groupBy || []; + target.where = target.where || []; target.select = target.select || [[{ type: 'field', params: ['value'] }]]; this.updateProjection(); } - quoteIdentifier(field) { - return '"' + field + '"'; + quoteIdentifier(value) { + return '"' + value + '"'; + } + + quoteLiteral(value) { + return "'" + value + "'"; } updateProjection() { @@ -202,7 +207,7 @@ export default class PostgresQuery { } query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; - var conditions = _.map(target.tags, (tag, index) => { + var conditions = _.map(target.where, (tag, index) => { return this.renderTagCondition(tag, index, interpolate); }); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 0b2bce7fb05..f6f6641eff0 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -1,4 +1,6 @@ +import angular from 'angular'; import _ from 'lodash'; +import { PostgresQueryBuilder } from './query_builder'; import { QueryCtrl } from 'app/plugins/sdk'; import queryPart from './query_part'; import PostgresQuery from './postgres_query'; @@ -22,22 +24,25 @@ export class PostgresQueryCtrl extends QueryCtrl { showLastQuerySQL: boolean; formats: any[]; queryModel: PostgresQuery; + queryBuilder: PostgresQueryBuilder; lastQueryMeta: QueryMeta; lastQueryError: string; showHelp: boolean; schemaSegment: any; tableSegment: any; - whereSegment: any; + whereSegments: any; timeColumnSegment: any; metricColumnSegment: any; selectMenu: any; groupBySegment: any; + removeWhereFilterSegment: any; /** @ngInject **/ constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) { super($scope, $injector); this.target = this.target; this.queryModel = new PostgresQuery(this.target, templateSrv, this.panel.scopedVars); + this.queryBuilder = new PostgresQueryBuilder(this.target, this.queryModel); this.formats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; @@ -63,8 +68,32 @@ export class PostgresQueryCtrl extends QueryCtrl { this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); this.buildSelectMenu(); + this.whereSegments = []; + for (let tag of this.target.where) { + if (!tag.operator) { + if (/^\/.*\/$/.test(tag.value)) { + tag.operator = '=~'; + } else { + tag.operator = '='; + } + } + + if (tag.condition) { + this.whereSegments.push(uiSegmentSrv.newCondition(tag.condition)); + } + + this.whereSegments.push(uiSegmentSrv.newKey(tag.key)); + this.whereSegments.push(uiSegmentSrv.newOperator(tag.operator)); + this.whereSegments.push(uiSegmentSrv.newKeyValue(tag.value)); + } + + this.fixWhereSegments(); this.groupBySegment = this.uiSegmentSrv.newPlusButton(); + this.removeWhereFilterSegment = uiSegmentSrv.newSegment({ + fake: true, + value: '-- remove tag filter --', + }); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); } @@ -97,42 +126,29 @@ export class PostgresQueryCtrl extends QueryCtrl { } getSchemaSegments() { - var schemaQuery = "SELECT schema_name FROM information_schema.schemata WHERE"; - schemaQuery += " schema_name NOT LIKE 'pg_%' AND schema_name <> 'information_schema';"; return this.datasource - .metricFindQuery(schemaQuery) + .metricFindQuery(this.queryBuilder.buildSchemaQuery()) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } getTableSegments() { - var tableQuery = "SELECT table_name FROM information_schema.tables WHERE table_schema = '" + this.target.schema + "';"; return this.datasource - .metricFindQuery(tableQuery) + .metricFindQuery(this.queryBuilder.buildTableQuery()) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } getTimeColumnSegments() { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; - columnQuery += " AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real');"; - return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(this.queryBuilder.buildColumnQuery("time")) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } getMetricColumnSegments() { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; - columnQuery += " AND data_type IN ('text','char','varchar');"; - return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(this.queryBuilder.buildColumnQuery("metric")) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } @@ -210,13 +226,8 @@ export class PostgresQueryCtrl extends QueryCtrl { handleSelectPartEvent(selectParts, part, evt) { switch (evt.name) { case 'get-param-options': { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; - columnQuery += " AND data_type IN ('bigint','integer','double precision','real');"; - return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(this.queryBuilder.buildColumnQuery("value")) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } @@ -238,12 +249,8 @@ export class PostgresQueryCtrl extends QueryCtrl { handleGroupByPartEvent(part, index, evt) { switch (evt.name) { case 'get-param-options': { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; - return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(this.queryBuilder.buildColumnQuery()) .then(this.transformToSegments(true)) .catch(this.handleQueryError.bind(this)); } @@ -262,14 +269,125 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - getGroupByOptions() { - var columnQuery = "SELECT column_name FROM information_schema.columns WHERE "; - columnQuery += " table_schema = '" + this.target.schema + "'"; - columnQuery += " AND table_name = '" + this.target.table + "'"; + fixWhereSegments() { + var count = this.whereSegments.length; + var lastSegment = this.whereSegments[Math.max(count - 1, 0)]; + if (!lastSegment || lastSegment.type !== 'plus-button') { + this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + } + } + + getTagsOrValues(segment, index) { + if (segment.type === 'condition') { + return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); + } + if (segment.type === 'operator') { + var nextValue = this.whereSegments[index + 1].value; + if (/^\/.*\/$/.test(nextValue)) { + return this.$q.when(this.uiSegmentSrv.newOperators(['=~', '!~'])); + } else { + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>'])); + } + } + + var query, addTemplateVars; + if (segment.type === 'key' || segment.type === 'plus-button') { + query = this.queryBuilder.buildColumnQuery(); + + addTemplateVars = false; + } else if (segment.type === 'value') { + query = this.queryBuilder.buildValueQuery(this.whereSegments[index -2].value); + addTemplateVars = true; + } return this.datasource - .metricFindQuery(columnQuery) + .metricFindQuery(query) + .then(this.transformToSegments(addTemplateVars)) + .then(results => { + if (segment.type === 'key') { + results.splice(0, 0, angular.copy(this.removeWhereFilterSegment)); + } + return results; + }) + .catch(this.handleQueryError.bind(this)); + } + + getTagValueOperator(tagValue, tagOperator): string { + if (tagOperator !== '=~' && tagOperator !== '!~' && /^\/.*\/$/.test(tagValue)) { + return '=~'; + } else if ((tagOperator === '=~' || tagOperator === '!~') && /^(?!\/.*\/$)/.test(tagValue)) { + return '='; + } + return null; + } + + whereSegmentUpdated(segment, index) { + this.whereSegments[index] = segment; + + // handle remove where condition + if (segment.value === this.removeWhereFilterSegment.value) { + this.whereSegments.splice(index, 3); + if (this.whereSegments.length === 0) { + this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + } else if (this.whereSegments.length > 2) { + this.whereSegments.splice(Math.max(index - 1, 0), 1); + if (this.whereSegments[this.whereSegments.length - 1].type !== 'plus-button') { + this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + } + } + } else { + if (segment.type === 'plus-button') { + if (index > 2) { + this.whereSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND')); + } + this.whereSegments.push(this.uiSegmentSrv.newOperator('=')); + this.whereSegments.push(this.uiSegmentSrv.newFake('select value', 'value', 'query-segment-value')); + segment.type = 'key'; + segment.cssClass = 'query-segment-key'; + } + + if (index + 1 === this.whereSegments.length) { + this.whereSegments.push(this.uiSegmentSrv.newPlusButton()); + } + } + + this.rebuildTargetWhereConditions(); + } + + rebuildTargetWhereConditions() { + var where = []; + var tagIndex = 0; + var tagOperator = ''; + + _.each(this.whereSegments, (segment2, index) => { + if (segment2.type === 'key') { + if (where.length === 0) { + where.push({}); + } + where[tagIndex].key = segment2.value; + } else if (segment2.type === 'value') { + tagOperator = this.getTagValueOperator(segment2.value, where[tagIndex].operator); + if (tagOperator) { + this.whereSegments[index - 1] = this.uiSegmentSrv.newOperator(tagOperator); + where[tagIndex].operator = tagOperator; + } + where[tagIndex].value = segment2.value; + } else if (segment2.type === 'condition') { + where.push({ condition: segment2.value }); + tagIndex += 1; + } else if (segment2.type === 'operator') { + where[tagIndex].operator = segment2.value; + } + }); + + this.target.where = where; + this.panelCtrl.refresh(); + } + + getGroupByOptions() { + return this.datasource + .metricFindQuery(this.queryBuilder.buildColumnQuery()) .then(tags => { var options = []; if (!this.queryModel.hasFill()) { @@ -278,12 +396,6 @@ export class PostgresQueryCtrl extends QueryCtrl { if (!this.target.limit) { options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); } - if (!this.target.slimit) { - options.push(this.uiSegmentSrv.newSegment({ value: 'SLIMIT' })); - } - if (this.target.orderByTime === 'ASC') { - options.push(this.uiSegmentSrv.newSegment({ value: 'ORDER BY time DESC' })); - } if (!this.queryModel.hasGroupByTime()) { options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); } From db89ac4134088d1558c80284735043c211d75009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Feb 2018 11:50:58 +0100 Subject: [PATCH 0031/3000] initial fixes for dashboard permission acl list query, fixes #10864 --- pkg/services/sqlstore/dashboard_acl.go | 62 +++++++-------------- pkg/services/sqlstore/dashboard_acl_test.go | 17 ++++++ pkg/services/sqlstore/org_test.go | 15 ++++- 3 files changed, 49 insertions(+), 45 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 829182a8195..a1a308d6497 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -1,7 +1,6 @@ package sqlstore import ( - "fmt" "time" "github.com/grafana/grafana/pkg/bus" @@ -40,7 +39,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { // Update dashboard HasAcl flag dashboard := m.Dashboard{HasAcl: true} - if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil { + if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil { return err } return nil @@ -134,6 +133,8 @@ func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error { func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { var err error + falseStr := dialect.BooleanStr(false) + if query.DashboardId == 0 { sql := `SELECT da.id, @@ -151,18 +152,13 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { '' as title, '' as slug, '' as uid,` + - dialect.BooleanStr(false) + ` AS is_folder + falseStr + ` AS is_folder FROM dashboard_acl as da WHERE da.dashboard_id = -1` query.Result = make([]*m.DashboardAclInfoDTO, 0) err = x.SQL(sql).Find(&query.Result) } else { - dashboardFilter := fmt.Sprintf(`IN ( - SELECT %d - UNION - SELECT folder_id from dashboard where id = %d - )`, query.DashboardId, query.DashboardId) rawSQL := ` -- get permissions for the dashboard and its parent folder @@ -183,41 +179,21 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { d.slug, d.uid, d.is_folder - FROM` + dialect.Quote("dashboard_acl") + ` as da - LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id - LEFT OUTER JOIN team ug on ug.id = da.team_id - LEFT OUTER JOIN dashboard d on da.dashboard_id = d.id - WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ? - - -- Also include default permissions if folder or dashboard field "has_acl" is false - - UNION - SELECT - da.id, - da.org_id, - da.dashboard_id, - da.user_id, - da.team_id, - da.permission, - da.role, - da.created, - da.updated, - '' as user_login, - '' as user_email, - '' as team, - folder.title, - folder.slug, - folder.uid, - folder.is_folder - FROM dashboard_acl as da, - dashboard as dash - LEFT OUTER JOIN dashboard folder on dash.folder_id = folder.id - WHERE - dash.id = ? AND ( - dash.has_acl = ` + dialect.BooleanStr(false) + ` or - folder.has_acl = ` + dialect.BooleanStr(false) + ` - ) AND - da.dashboard_id = -1 + FROM dashboard as d + LEFT JOIN dashboard folder on folder.id = d.folder_id + LEFT JOIN dashboard_acl AS da ON + da.dashboard_id = d.id OR + da.dashboard_id = d.folder_id OR + ( + -- include default permissions --> + da.org_id = -1 AND ( + (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + ) + ) + LEFT JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id + LEFT JOIN team ug on ug.id = da.team_id + WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL ORDER BY 1 ASC ` diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index 8b712c73ece..8d4af9544d9 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -41,6 +41,23 @@ func TestDashboardAclDataAccess(t *testing.T) { }) }) + Convey("Given dashboard folder with removed default permissions", func() { + err := UpdateDashboardAcl(&m.UpdateDashboardAclCommand{ + DashboardId: savedFolder.Id, + Items: []*m.DashboardAcl{}, + }) + So(err, ShouldBeNil) + + Convey("When reading dashboard acl should return no acl items", func() { + query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1} + + err := GetDashboardAclInfoList(&query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 0) + }) + }) + Convey("Given dashboard folder permission", func() { err := SetDashboardAcl(&m.SetDashboardAclCommand{ OrgId: 1, diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 5322dfd4748..c57d15a48d5 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -199,10 +199,13 @@ func TestAccountDataAccess(t *testing.T) { So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 3) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) + dash1 := insertTestDashboard("1 test dash", ac1.OrgId, 0, false, "prod", "webapp") + dash2 := insertTestDashboard("2 test dash", ac3.OrgId, 0, false, "prod", "webapp") + + err = testHelperUpdateDashboardAcl(dash1.Id, m.DashboardAcl{DashboardId: dash1.Id, OrgId: ac1.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) So(err, ShouldBeNil) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 2, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) + err = testHelperUpdateDashboardAcl(dash2.Id, m.DashboardAcl{DashboardId: dash2.Id, OrgId: ac3.OrgId, UserId: ac3.Id, Permission: m.PERMISSION_EDIT}) So(err, ShouldBeNil) Convey("When org user is deleted", func() { @@ -234,3 +237,11 @@ func TestAccountDataAccess(t *testing.T) { }) }) } + +func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error { + cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId} + for _, item := range items { + cmd.Items = append(cmd.Items, &item) + } + return UpdateDashboardAcl(&cmd) +} From ec6f0f94b80c10e30226d2bdccb8d9db5c885b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Feb 2018 14:31:20 +0100 Subject: [PATCH 0032/3000] permissions: refactoring of acl api and query --- pkg/api/api.go | 1 - pkg/api/dashboard_acl.go | 30 ----- pkg/api/dashboard_acl_test.go | 104 ++---------------- pkg/api/dashboard_test.go | 8 +- pkg/models/dashboard_acl.go | 16 --- pkg/services/guardian/guardian.go | 20 ---- pkg/services/sqlstore/dashboard.go | 32 +----- pkg/services/sqlstore/dashboard_acl.go | 85 +------------- pkg/services/sqlstore/dashboard_acl_test.go | 74 ++----------- .../sqlstore/dashboard_folder_test.go | 29 ++--- pkg/services/sqlstore/dashboard_test.go | 19 ---- pkg/services/sqlstore/team_test.go | 2 +- pkg/services/sqlstore/user_test.go | 2 +- .../PermissionsStore/PermissionsStoreItem.ts | 3 +- 14 files changed, 40 insertions(+), 385 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index c03bf7963b8..1320663f630 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -269,7 +269,6 @@ func (hs *HttpServer) registerRoutes() { dashIdRoute.Group("/acl", func(aclRoute RouteRegister) { aclRoute.Get("/", wrap(GetDashboardAclList)) aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl)) - aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl)) }) }) }) diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go index 45f121dd0d0..32b75e80cc0 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_acl.go @@ -84,33 +84,3 @@ func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCom return ApiSuccess("Dashboard acl updated") } - -func DeleteDashboardAcl(c *middleware.Context) Response { - dashId := c.ParamsInt64(":dashboardId") - aclId := c.ParamsInt64(":aclId") - - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") - if rsp != nil { - return rsp - } - - guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) - if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { - return dashboardGuardianResponse(err) - } - - if okToDelete, err := guardian.CheckPermissionBeforeRemove(m.PERMISSION_ADMIN, aclId); err != nil || !okToDelete { - if err != nil { - return ApiError(500, "Error while checking dashboard permissions", err) - } - - return ApiError(403, "Cannot remove own admin permission for a folder", nil) - } - - cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId} - if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to delete permission for user", err) - } - - return Json(200, "") -} diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_acl_test.go index e43e57ed5c0..d6b7e305daf 100644 --- a/pkg/api/dashboard_acl_test.go +++ b/pkg/api/dashboard_acl_test.go @@ -15,11 +15,11 @@ import ( func TestDashboardAclApiEndpoint(t *testing.T) { Convey("Given a dashboard acl", t, func() { mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW}, - {Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, - {Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, - {Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, - {Id: 5, OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT}, + {OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN}, } dtoRes := transformDashboardAclsToDTOs(mockResult) @@ -92,21 +92,11 @@ func TestDashboardAclApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 404) }) }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/2/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_ADMIN, func(sc *scenarioContext) { - getDashboardNotFoundError = m.ErrDashboardNotFound - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - Convey("Should not be able to delete non-existing dashboard", func() { - So(sc.resp.Code, ShouldEqual, 404) - }) - }) }) Convey("When user is org editor and has admin permission in the ACL", func() { loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) + mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) Convey("Should be able to access ACL", func() { sc.handlerFunc = GetDashboardAclList @@ -116,36 +106,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) { }) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) - - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should be able to delete permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 200) - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) - - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should not be able to delete their own Admin permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 403) - }) - }) - Convey("Should not be able to downgrade their own Admin permission", func() { cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -154,7 +114,7 @@ func TestDashboardAclApiEndpoint(t *testing.T) { } postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) + mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) CallPostAcl(sc) So(sc.resp.Code, ShouldEqual, 403) @@ -170,34 +130,18 @@ func TestDashboardAclApiEndpoint(t *testing.T) { } postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, cmd, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 6, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) + mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN}) CallPostAcl(sc) So(sc.resp.Code, ShouldEqual, 200) }) }) - Convey("When user is a member of a team in the ACL with admin permission", func() { - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) { - teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"}) - - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should be able to delete permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 200) - }) - }) - }) }) Convey("When user is org viewer and has edit permission in the ACL", func() { loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_VIEWER, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) + mockResult = append(mockResult, &m.DashboardAclInfoDTO{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) // Getting the permissions is an Admin permission Convey("Should not be able to get list of permissions from ACL", func() { @@ -207,21 +151,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_VIEWER, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT}) - - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should be not be able to delete permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 403) - }) - }) }) Convey("When user is org editor and not in the ACL", func() { @@ -234,20 +163,6 @@ func TestDashboardAclApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) { - mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW}) - bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error { - return nil - }) - - Convey("Should be not be able to delete permission", func() { - sc.handlerFunc = DeleteDashboardAcl - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 403) - }) - }) }) }) } @@ -257,7 +172,6 @@ func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardA for _, acl := range acls { dto := &m.DashboardAclInfoDTO{ - Id: acl.Id, OrgId: acl.OrgId, DashboardId: acl.DashboardId, Permission: acl.Permission, diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e80b3cad4dc..4a45c561d57 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -431,7 +431,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_VIEWER mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT}, + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT}, } bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { @@ -505,7 +505,7 @@ func TestDashboardApiEndpoint(t *testing.T) { setting.ViewersCanEdit = true mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, } bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { @@ -564,7 +564,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_VIEWER mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN}, } bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { @@ -637,7 +637,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_EDITOR mockResult := []*m.DashboardAclInfoDTO{ - {Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW}, } bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 933487650e3..202b519207d 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -44,7 +44,6 @@ type DashboardAcl struct { } type DashboardAclInfoDTO struct { - Id int64 `json:"id"` OrgId int64 `json:"-"` DashboardId int64 `json:"dashboardId"` @@ -75,21 +74,6 @@ type UpdateDashboardAclCommand struct { Items []*DashboardAcl } -type SetDashboardAclCommand struct { - DashboardId int64 - OrgId int64 - UserId int64 - TeamId int64 - Permission PermissionType - - Result DashboardAcl -} - -type RemoveDashboardAclCommand struct { - AclId int64 - OrgId int64 -} - // // QUERIES // diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index b448561494d..05795b7f2df 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -106,26 +106,6 @@ func (g *DashboardGuardian) checkAcl(permission m.PermissionType, acl []*m.Dashb return false, nil } -func (g *DashboardGuardian) CheckPermissionBeforeRemove(permission m.PermissionType, aclIdToRemove int64) (bool, error) { - if g.user.OrgRole == m.ROLE_ADMIN { - return true, nil - } - - acl, err := g.GetAcl() - if err != nil { - return false, err - } - - for i, p := range acl { - if p.Id == aclIdToRemove { - acl = append(acl[:i], acl[i+1:]...) - break - } - } - - return g.checkAcl(permission, acl) -} - func (g *DashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) { if g.user.OrgRole == m.ROLE_ADMIN { return true, nil diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index f3fd81ebbe2..42c83da8810 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -79,11 +79,6 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { dash.Data.Set("uid", uid) } - err = setHasAcl(sess, dash) - if err != nil { - return err - } - parentVersion := dash.Version affectedRows := int64(0) @@ -100,7 +95,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { dash.Updated = cmd.UpdatedAt } - affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash) + affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash) } if err != nil { @@ -233,31 +228,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { return "", m.ErrDashboardFailedGenerateUniqueUid } -func setHasAcl(sess *DBSession, dash *m.Dashboard) error { - // check if parent has acl - if dash.FolderId > 0 { - var parent m.Dashboard - if hasParent, err := sess.Where("folder_id=?", dash.FolderId).Get(&parent); err != nil { - return err - } else if hasParent && parent.HasAcl { - dash.HasAcl = true - } - } - - // check if dash has its own acl - if dash.Id > 0 { - if res, err := sess.Query("SELECT 1 from dashboard_acl WHERE dashboard_id =?", dash.Id); err != nil { - return err - } else { - if len(res) > 0 { - dash.HasAcl = true - } - } - } - - return nil -} - func GetDashboard(query *m.GetDashboardQuery) error { dashboard := m.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid} has, err := x.Get(&dashboard) diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index a1a308d6497..ae91d1d41f3 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -1,16 +1,12 @@ package sqlstore import ( - "time" - "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) func init() { - bus.AddHandler("sql", SetDashboardAcl) bus.AddHandler("sql", UpdateDashboardAcl) - bus.AddHandler("sql", RemoveDashboardAcl) bus.AddHandler("sql", GetDashboardAclInfoList) } @@ -23,7 +19,7 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { } for _, item := range cmd.Items { - if item.UserId == 0 && item.TeamId == 0 && !item.Role.IsValid() { + if item.UserId == 0 && item.TeamId == 0 && (item.Role == nil || !item.Role.IsValid()) { return m.ErrDashboardAclInfoMissing } @@ -46,85 +42,6 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error { }) } -func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error { - return inTransaction(func(sess *DBSession) error { - if cmd.UserId == 0 && cmd.TeamId == 0 { - return m.ErrDashboardAclInfoMissing - } - - if cmd.DashboardId == 0 { - return m.ErrDashboardPermissionDashboardEmpty - } - - if res, err := sess.Query("SELECT 1 from "+dialect.Quote("dashboard_acl")+" WHERE dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId); err != nil { - return err - } else if len(res) == 1 { - - entity := m.DashboardAcl{ - Permission: cmd.Permission, - Updated: time.Now(), - } - - if _, err := sess.Cols("updated", "permission").Where("dashboard_id =? and (team_id=? or user_id=?)", cmd.DashboardId, cmd.TeamId, cmd.UserId).Update(&entity); err != nil { - return err - } - - return nil - } - - entity := m.DashboardAcl{ - OrgId: cmd.OrgId, - TeamId: cmd.TeamId, - UserId: cmd.UserId, - Created: time.Now(), - Updated: time.Now(), - DashboardId: cmd.DashboardId, - Permission: cmd.Permission, - } - - cols := []string{"org_id", "created", "updated", "dashboard_id", "permission"} - - if cmd.UserId != 0 { - cols = append(cols, "user_id") - } - - if cmd.TeamId != 0 { - cols = append(cols, "team_id") - } - - _, err := sess.Cols(cols...).Insert(&entity) - if err != nil { - return err - } - - cmd.Result = entity - - // Update dashboard HasAcl flag - dashboard := m.Dashboard{ - HasAcl: true, - } - - if _, err := sess.Cols("has_acl").Where("id=? OR folder_id=?", cmd.DashboardId, cmd.DashboardId).Update(&dashboard); err != nil { - return err - } - - return nil - }) -} - -// RemoveDashboardAcl removes a specified permission from the dashboard acl -func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error { - return inTransaction(func(sess *DBSession) error { - var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?" - _, err := sess.Exec(rawSQL, cmd.OrgId, cmd.AclId) - if err != nil { - return err - } - - return err - }) -} - // GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three // different places. // 1) Permissions for the dashboard diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index 8d4af9544d9..8fbb9c0d813 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -17,7 +17,7 @@ func TestDashboardAclDataAccess(t *testing.T) { childDash := insertTestDashboard("2 test dash", 1, savedFolder.Id, false, "prod", "webapp") Convey("When adding dashboard permission with userId and teamId set to 0", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, DashboardId: savedFolder.Id, Permission: m.PERMISSION_EDIT, @@ -59,7 +59,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given dashboard folder permission", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, UserId: currentUser.Id, DashboardId: savedFolder.Id, @@ -78,7 +78,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given child dashboard permission", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{ OrgId: 1, UserId: currentUser.Id, DashboardId: childDash.Id, @@ -100,7 +100,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given child dashboard permission in folder with no permissions", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{ OrgId: 1, UserId: currentUser.Id, DashboardId: childDash.Id, @@ -125,17 +125,12 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Should be able to add dashboard permission", func() { - setDashAclCmd := m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, UserId: currentUser.Id, DashboardId: savedFolder.Id, Permission: m.PERMISSION_EDIT, - } - - err := SetDashboardAcl(&setDashAclCmd) - So(err, ShouldBeNil) - - So(setDashAclCmd.Result.Id, ShouldEqual, 3) + }) q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} err = GetDashboardAclInfoList(q1) @@ -147,42 +142,9 @@ func TestDashboardAclDataAccess(t *testing.T) { So(q1.Result[0].UserId, ShouldEqual, currentUser.Id) So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login) So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email) - So(q1.Result[0].Id, ShouldEqual, setDashAclCmd.Result.Id) - - Convey("Should update hasAcl field to true for dashboard folder and its children", func() { - q2 := &m.GetDashboardsQuery{DashboardIds: []int64{savedFolder.Id, childDash.Id}} - err := GetDashboards(q2) - So(err, ShouldBeNil) - So(q2.Result[0].HasAcl, ShouldBeTrue) - So(q2.Result[1].HasAcl, ShouldBeTrue) - }) - - Convey("Should be able to update an existing permission", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ - OrgId: 1, - UserId: 1, - DashboardId: savedFolder.Id, - Permission: m.PERMISSION_ADMIN, - }) - - So(err, ShouldBeNil) - - q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} - err = GetDashboardAclInfoList(q3) - So(err, ShouldBeNil) - So(len(q3.Result), ShouldEqual, 1) - So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id) - So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN) - So(q3.Result[0].UserId, ShouldEqual, 1) - - }) Convey("Should be able to delete an existing permission", func() { - err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{ - OrgId: 1, - AclId: setDashAclCmd.Result.Id, - }) - + err := testHelperUpdateDashboardAcl(savedFolder.Id) So(err, ShouldBeNil) q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} @@ -198,14 +160,12 @@ func TestDashboardAclDataAccess(t *testing.T) { So(err, ShouldBeNil) Convey("Should be able to add a user permission for a team", func() { - setDashAclCmd := m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, TeamId: group1.Result.Id, DashboardId: savedFolder.Id, Permission: m.PERMISSION_EDIT, - } - - err := SetDashboardAcl(&setDashAclCmd) + }) So(err, ShouldBeNil) q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} @@ -214,23 +174,10 @@ func TestDashboardAclDataAccess(t *testing.T) { So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) So(q1.Result[0].Permission, ShouldEqual, m.PERMISSION_EDIT) So(q1.Result[0].TeamId, ShouldEqual, group1.Result.Id) - - Convey("Should be able to delete an existing permission for a team", func() { - err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{ - OrgId: 1, - AclId: setDashAclCmd.Result.Id, - }) - - So(err, ShouldBeNil) - q3 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} - err = GetDashboardAclInfoList(q3) - So(err, ShouldBeNil) - So(len(q3.Result), ShouldEqual, 0) - }) }) Convey("Should be able to update an existing permission for a team", func() { - err := SetDashboardAcl(&m.SetDashboardAclCommand{ + err := testHelperUpdateDashboardAcl(savedFolder.Id, m.DashboardAcl{ OrgId: 1, TeamId: group1.Result.Id, DashboardId: savedFolder.Id, @@ -246,7 +193,6 @@ func TestDashboardAclDataAccess(t *testing.T) { So(q3.Result[0].Permission, ShouldEqual, m.PERMISSION_ADMIN) So(q3.Result[0].TeamId, ShouldEqual, group1.Result.Id) }) - }) }) diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index b32a4dfed1d..40d6cf5bcb2 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -41,7 +41,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and acl is set for dashboard folder", func() { var otherUser int64 = 999 - updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT}) Convey("should not return folder", func() { query := &search.FindPersistedDashboardsQuery{ @@ -55,7 +55,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("when the user is given permission", func() { - updateTestDashboardWithAcl(folder.Id, currentUser.Id, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: currentUser.Id, Permission: m.PERMISSION_EDIT}) Convey("should be able to access folder", func() { query := &search.FindPersistedDashboardsQuery{ @@ -93,9 +93,8 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and acl is set for dashboard child and folder has all permissions removed", func() { var otherUser int64 = 999 - aclId := updateTestDashboardWithAcl(folder.Id, otherUser, m.PERMISSION_EDIT) - removeAcl(aclId) - updateTestDashboardWithAcl(childDash.Id, otherUser, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder.Id) + testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: folder.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT}) Convey("should not return folder or child", func() { query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} @@ -106,7 +105,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("when the user is given permission to child", func() { - updateTestDashboardWithAcl(childDash.Id, currentUser.Id, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(childDash.Id, m.DashboardAcl{DashboardId: childDash.Id, OrgId: 1, UserId: currentUser.Id, Permission: m.PERMISSION_EDIT}) Convey("should be able to search for child dashboard but not folder", func() { query := &search.FindPersistedDashboardsQuery{SignedInUser: &m.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, OrgId: 1, DashboardIds: []int64{folder.Id, childDash.Id, dashInRoot.Id}} @@ -165,11 +164,10 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and acl is set for one dashboard folder", func() { var otherUser int64 = 999 - updateTestDashboardWithAcl(folder1.Id, otherUser, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT}) Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() { - movedDash := moveDashboard(1, childDash2.Data, folder1.Id) - So(movedDash.HasAcl, ShouldBeTrue) + moveDashboard(1, childDash2.Data, folder1.Id) Convey("should not return folder with acl or its children", func() { query := &search.FindPersistedDashboardsQuery{ @@ -184,9 +182,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) }) Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() { - - movedDash := moveDashboard(1, childDash1.Data, folder2.Id) - So(movedDash.HasAcl, ShouldBeFalse) + moveDashboard(1, childDash1.Data, folder2.Id) Convey("should return folder without acl and its children", func() { query := &search.FindPersistedDashboardsQuery{ @@ -205,9 +201,8 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("and a dashboard with an acl is moved to the folder without an acl", func() { - updateTestDashboardWithAcl(childDash1.Id, otherUser, m.PERMISSION_EDIT) - movedDash := moveDashboard(1, childDash1.Data, folder2.Id) - So(movedDash.HasAcl, ShouldBeTrue) + testHelperUpdateDashboardAcl(childDash1.Id, m.DashboardAcl{DashboardId: childDash1.Id, OrgId: 1, UserId: otherUser, Permission: m.PERMISSION_EDIT}) + moveDashboard(1, childDash1.Data, folder2.Id) Convey("should return folder without acl but not the dashboard with acl", func() { query := &search.FindPersistedDashboardsQuery{ @@ -308,7 +303,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() { - updateTestDashboardWithAcl(folder1.Id, editorUser.Id, m.PERMISSION_VIEW) + testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: editorUser.Id, Permission: m.PERMISSION_VIEW}) err := SearchDashboards(&query) So(err, ShouldBeNil) @@ -352,7 +347,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() { - updateTestDashboardWithAcl(folder1.Id, viewerUser.Id, m.PERMISSION_EDIT) + testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_EDIT}) err := SearchDashboards(&query) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index de7cdf19927..7de4c5f5701 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -663,25 +663,6 @@ func createUser(name string, role string, isAdmin bool) m.User { return currentUserCmd.Result } -func updateTestDashboardWithAcl(dashId int64, userId int64, permissions m.PermissionType) int64 { - cmd := &m.SetDashboardAclCommand{ - OrgId: 1, - UserId: userId, - DashboardId: dashId, - Permission: permissions, - } - - err := SetDashboardAcl(cmd) - So(err, ShouldBeNil) - - return cmd.Result.Id -} - -func removeAcl(aclId int64) { - err := RemoveDashboardAcl(&m.RemoveDashboardAclCommand{AclId: aclId, OrgId: 1}) - So(err, ShouldBeNil) -} - func moveDashboard(orgId int64, dashboard *simplejson.Json, newFolderId int64) *m.Dashboard { cmd := m.SaveDashboardCommand{ OrgId: orgId, diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index bebe59f4238..fb76c3fa9d6 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -99,7 +99,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]}) So(err, ShouldBeNil) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId}) + err = testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId}) err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId}) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index a65b7226eb6..2830733c96a 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -99,7 +99,7 @@ func TestUserDataAccess(t *testing.T) { err = AddOrgUser(&m.AddOrgUserCommand{LoginOrEmail: users[0].Login, Role: m.ROLE_VIEWER, OrgId: users[0].OrgId}) So(err, ShouldBeNil) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT}) + testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: users[0].OrgId, UserId: users[0].Id, Permission: m.PERMISSION_EDIT}) So(err, ShouldBeNil) err = SavePreferences(&m.SavePreferencesCommand{UserId: users[0].Id, OrgId: users[0].OrgId, HomeDashboardId: 1, Theme: "dark"}) diff --git a/public/app/stores/PermissionsStore/PermissionsStoreItem.ts b/public/app/stores/PermissionsStore/PermissionsStoreItem.ts index 74769891256..92dca0220ca 100644 --- a/public/app/stores/PermissionsStore/PermissionsStoreItem.ts +++ b/public/app/stores/PermissionsStore/PermissionsStoreItem.ts @@ -1,9 +1,8 @@ -import { types } from 'mobx-state-tree'; +import { types } from 'mobx-state-tree'; export const PermissionsStoreItem = types .model('PermissionsStoreItem', { dashboardId: types.optional(types.number, -1), - id: types.maybe(types.number), permission: types.number, permissionName: types.maybe(types.string), role: types.maybe(types.string), From 73eaba076e4de50289c2403e8ab87a1a4485b213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Feb 2018 15:02:42 +0100 Subject: [PATCH 0033/3000] wip: dashboard acl ux2, #10747 --- pkg/api/dashboard_acl.go | 2 ++ pkg/models/dashboard_acl.go | 3 +++ pkg/services/sqlstore/dashboard_acl.go | 1 + .../DisabledPermissionsListItem.tsx | 6 +++--- .../components/Permissions/Permissions.tsx | 3 +-- .../Permissions/PermissionsList.tsx | 4 ++-- .../Permissions/PermissionsListItem.tsx | 19 +++++++++++++++---- .../PermissionsStore/PermissionsStore.ts | 11 +++-------- .../PermissionsStore/PermissionsStoreItem.ts | 5 +++-- 9 files changed, 33 insertions(+), 21 deletions(-) diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go index 32b75e80cc0..d15a575a05e 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_acl.go @@ -30,6 +30,8 @@ func GetDashboardAclList(c *middleware.Context) Response { } for _, perm := range acl { + perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail) + perm.TeamAvatarUrl = dtos.GetGravatarUrl(perm.TeamEmail) if perm.Slug != "" { perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug) } diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 202b519207d..0e14a3bfd71 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -53,7 +53,10 @@ type DashboardAclInfoDTO struct { UserId int64 `json:"userId"` UserLogin string `json:"userLogin"` UserEmail string `json:"userEmail"` + UserAvatarUrl string `json:"userAvatarUrl"` TeamId int64 `json:"teamId"` + TeamEmail string `json:"teamEmail"` + TeamAvatarUrl string `json:"teamAvatarUrl"` Team string `json:"team"` Role *RoleType `json:"role,omitempty"` Permission PermissionType `json:"permission"` diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index ae91d1d41f3..6e7175335f3 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -92,6 +92,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error { u.login AS user_login, u.email AS user_email, ug.name AS team, + ug.email AS team_email, d.title, d.slug, d.uid, diff --git a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx index db45714136e..e3f3ee56d75 100644 --- a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx +++ b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react'; import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; @@ -12,10 +12,10 @@ export default class DisabledPermissionListItem extends Component { return ( - + - + {item.name} Can diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx index 0a0572ed86e..dbdc1682f6b 100644 --- a/public/app/core/components/Permissions/Permissions.tsx +++ b/public/app/core/components/Permissions/Permissions.tsx @@ -15,9 +15,8 @@ export interface DashboardAcl { permissionName?: string; role?: string; icon?: string; - nameHtml?: string; + name?: string; inherited?: boolean; - sortName?: string; sortRank?: number; } diff --git a/public/app/core/components/Permissions/PermissionsList.tsx b/public/app/core/components/Permissions/PermissionsList.tsx index b215dad2391..a77235ecc30 100644 --- a/public/app/core/components/Permissions/PermissionsList.tsx +++ b/public/app/core/components/Permissions/PermissionsList.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react'; import PermissionsListItem from './PermissionsListItem'; import DisabledPermissionsListItem from './DisabledPermissionsListItem'; import { observer } from 'mobx-react'; @@ -23,7 +23,7 @@ class PermissionsList extends Component { Admin Role', + name: 'Admin', permission: 4, icon: 'fa fa-fw fa-street-view', }} diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx index 3140b8fcc0c..2ab5b948440 100644 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ b/public/app/core/components/Permissions/PermissionsListItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from 'react'; import { observer } from 'mobx-react'; import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; @@ -7,6 +7,16 @@ const setClassNameHelper = inherited => { return inherited ? 'gf-form-disabled' : ''; }; +function ItemAvatar({ item }) { + if (item.userAvatarUrl) { + return ; + } + if (item.teamAvatarUrl) { + return ; + } + return ; +} + export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => { const handleRemoveItem = evt => { evt.preventDefault(); @@ -18,13 +28,14 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde }; const inheritedFromRoot = item.dashboardId === -1 && folderInfo && folderInfo.id === 0; + console.log(item.name); return ( - - - + + + {item.name} {item.inherited && folderInfo && ( diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts index a7c90d13da0..7838744c541 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.ts @@ -231,19 +231,14 @@ const prepareItem = (item, dashboardId: number, isFolder: boolean, isInRoot: boo item.sortRank = 0; if (item.userId > 0) { - item.icon = 'fa fa-fw fa-user'; - item.nameHtml = item.userLogin; - item.sortName = item.userLogin; + item.name = item.userLogin; item.sortRank = 10; } else if (item.teamId > 0) { - item.icon = 'fa fa-fw fa-users'; - item.nameHtml = item.team; - item.sortName = item.team; + item.name = item.team; item.sortRank = 20; } else if (item.role) { item.icon = 'fa fa-fw fa-street-view'; - item.nameHtml = `Everyone with ${item.role} Role`; - item.sortName = item.role; + item.name = item.role; item.sortRank = 30; if (item.role === 'Viewer') { item.sortRank += 1; diff --git a/public/app/stores/PermissionsStore/PermissionsStoreItem.ts b/public/app/stores/PermissionsStore/PermissionsStoreItem.ts index 92dca0220ca..c4873cb9c01 100644 --- a/public/app/stores/PermissionsStore/PermissionsStoreItem.ts +++ b/public/app/stores/PermissionsStore/PermissionsStoreItem.ts @@ -14,8 +14,9 @@ export const PermissionsStoreItem = types inherited: types.maybe(types.boolean), sortRank: types.maybe(types.number), icon: types.maybe(types.string), - nameHtml: types.maybe(types.string), - sortName: types.maybe(types.string), + name: types.maybe(types.string), + teamAvatarUrl: types.maybe(types.string), + userAvatarUrl: types.maybe(types.string), }) .actions(self => ({ updateRole: role => { From 258a0d276cc61c5a391a1fe3e037ad5c7db66458 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Sun, 18 Feb 2018 00:51:32 +0500 Subject: [PATCH 0034/3000] Add hook processRange to flot plugin. --- public/vendor/flot/jquery.flot.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index ec35fb87bd8..040eb808f48 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -632,6 +632,7 @@ Licensed under the MIT license. processRawData: [], processDatapoints: [], processOffset: [], + processRange: [], drawBackground: [], drawSeries: [], draw: [], @@ -1613,6 +1614,8 @@ Licensed under the MIT license. setRange(axis); }); + executeHooks(hooks.processRange, []); + if (showGrid) { var allocatedAxes = $.grep(axes, function (axis) { From 57013d2228cd2421ca819fbe22d307046158077b Mon Sep 17 00:00:00 2001 From: ilgizar Date: Sun, 18 Feb 2018 00:54:35 +0500 Subject: [PATCH 0035/3000] Share zero between Y axis. --- .../app/plugins/panel/graph/axes_editor.html | 1 + public/app/plugins/panel/graph/graph.ts | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 6160ef01fec..ee3654a9bbd 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -29,6 +29,7 @@
+
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 3ed8cbc1836..ade2fb80960 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -155,6 +155,116 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { } } + function processRangeHook(plot) { + var yaxis = plot.getYAxes(); + if (yaxis.length > 1 && panel.yaxes[1].shareZero) { + shareYLevel(yaxis[0].min, yaxis[0].max, yaxis[1].min, yaxis[1].max, 0); + } + } + + function shareYLevel(minLeft, maxLeft, minRight, maxRight, shareLevel) { + if (shareLevel !== 0) { + minLeft -= shareLevel; + maxLeft -= shareLevel; + minRight -= shareLevel; + maxRight -= shareLevel; + } + + // wide Y min and max using increased wideFactor + var deltaLeft = maxLeft - minLeft; + var deltaRight = maxRight - minRight; + var wideFactor = 0.25; + if (deltaLeft === 0) { + minLeft -= wideFactor; + maxLeft += wideFactor; + } + if (deltaRight === 0) { + minRight -= wideFactor; + maxRight += wideFactor; + } + + // on the opposite sides with respect to zero + if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { + if (minLeft >= 0) { + minLeft = -maxLeft; + maxRight = -minRight; + } else { + maxLeft = -minLeft; + minRight = -maxRight; + } + } else { + var limitTop = Infinity; + var limitBottom = -Infinity; + var absLeftMin = Math.abs(minLeft); + var absLeftMax = Math.abs(maxLeft); + var absRightMin = Math.abs(minRight); + var absRightMax = Math.abs(maxRight); + var upLeft = _.max([absLeftMin, absLeftMax]); + var downLeft = _.min([absLeftMin, absLeftMax]); + var upRight = _.max([absRightMin, absRightMax]); + var downRight = _.min([absRightMin, absRightMax]); + var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); + var rateLeft, rateRight, rate; + + // on the one hand with respect to zero + if (oneSide) { + rateLeft = downLeft ? upLeft / downLeft : downLeft >= 0 ? limitTop : limitBottom; + rateRight = downRight ? upRight / downRight : downRight >= 0 ? limitTop : limitBottom; + rate = _.max([rateLeft, rateRight]); + + if (rate === limitTop) { + if (maxLeft > 0) { + minLeft = 0; + minRight = 0; + } else { + maxLeft = 0; + maxRight = 0; + } + } else { + var coef = deltaLeft / deltaRight; + if ((rate === rateLeft && minLeft > 0) || (rate === rateRight && maxRight < 0)) { + maxLeft = maxRight * coef; + minRight = minLeft / coef; + } else { + minLeft = minRight * coef; + maxRight = maxLeft / coef; + } + } + } else { + rateLeft = + minLeft && maxLeft + ? minLeft < 0 ? maxLeft / minLeft : limitBottom + : minLeft < 0 || maxRight >= 0 ? limitBottom : limitTop; + rateRight = + minRight && maxRight + ? minRight < 0 ? maxRight / minRight : limitBottom + : minRight < 0 || maxLeft >= 0 ? limitBottom : limitTop; + rate = _.max([rateLeft, rateRight]); + + if (rate === rateLeft) { + minRight = + upRight === absRightMin && (absRightMin !== absRightMax || upLeft !== absLeftMin) + ? -upRight + : upRight / rate; + maxRight = upRight === absRightMax ? upRight : -upRight * rate; + } else { + minLeft = + upLeft === absLeftMin && (absLeftMin !== absLeftMax || upRight !== absRightMin) + ? -upLeft + : upLeft / rate; + maxLeft = upLeft === absLeftMax ? upLeft : -upLeft * rate; + } + } + } + + if (shareLevel !== 0) { + minLeft += shareLevel; + maxLeft += shareLevel; + minRight += shareLevel; + maxRight += shareLevel; + } + } + // Series could have different timeSteps, // let's find the smallest one so that bars are correctly rendered. // In addition, only take series which are rendered as bars for this. @@ -296,6 +406,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { hooks: { draw: [drawHook], processOffset: [processOffsetHook], + processRange: [processRangeHook], }, legend: { show: false }, series: { From be2fa54459bd6aa9305ec6ee83be8599370f9ffe Mon Sep 17 00:00:00 2001 From: Martin Molnar Date: Tue, 20 Feb 2018 11:15:31 +0100 Subject: [PATCH 0036/3000] feat(ldap): Allow use of DN in user attribute filter (#3132) --- pkg/login/ldap.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index be3babac02e..12e10557ffc 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -408,6 +408,10 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { if a.server.GroupSearchFilterUserAttribute == "" { filter_replace = getLdapAttr(a.server.Attr.Username, searchResult) } + if a.server.GroupSearchFilterUserAttribute == "dn" { + filter_replace = searchResult.Entries[0].DN + } + filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1) a.log.Info("Searching for user's groups", "filter", filter) @@ -430,7 +434,11 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { if len(groupSearchResult.Entries) > 0 { for i := range groupSearchResult.Entries { - memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i)) + if a.server.Attr.MemberOf == "dn" { + memberOf = append(memberOf, groupSearchResult.Entries[i].DN) + } else { + memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i)) + } } break } From 7eeb68b59088686ad3b350d0a8e839d3f41d3116 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Feb 2018 16:58:49 +0500 Subject: [PATCH 0037/3000] Refactoring code. Change Y-Zero to Y-Level. --- .../app/plugins/panel/graph/axes_editor.html | 8 +- public/app/plugins/panel/graph/graph.ts | 148 ++++++++++-------- public/app/plugins/panel/graph/module.ts | 2 + public/vendor/flot/jquery.flot.js | 37 +++-- 4 files changed, 117 insertions(+), 78 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index ee3654a9bbd..a80ebd3036c 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -29,7 +29,6 @@
-
@@ -40,6 +39,13 @@
+
+ +
+ + +
+
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index ade2fb80960..95790222cac 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -157,12 +157,17 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && panel.yaxes[1].shareZero) { - shareYLevel(yaxis[0].min, yaxis[0].max, yaxis[1].min, yaxis[1].max, 0); + if (yaxis.length > 1 && panel.yaxes[1].shareLevel) { + shareYLevel(yaxis, parseFloat(panel.yaxes[1].shareY || 0)); } } - function shareYLevel(minLeft, maxLeft, minRight, maxRight, shareLevel) { + function shareYLevel(yaxis, shareLevel) { + var minLeft = yaxis[0].min; + var maxLeft = yaxis[0].max; + var minRight = yaxis[1].min; + var maxRight = yaxis[1].max; + if (shareLevel !== 0) { minLeft -= shareLevel; maxLeft -= shareLevel; @@ -183,76 +188,80 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { maxRight += wideFactor; } - // on the opposite sides with respect to zero - if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { - if (minLeft >= 0) { - minLeft = -maxLeft; - maxRight = -minRight; - } else { - maxLeft = -minLeft; - minRight = -maxRight; - } + // one of graphs on zero + var zero = minLeft === 0 || minRight === 0 || maxLeft === 0 || maxRight === 0; + + // on the one hand with respect to zero + var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); + + if (zero && oneSide) { + minLeft = maxLeft > 0 ? 0 : minLeft; + maxLeft = maxLeft > 0 ? maxLeft : 0; + minRight = maxRight > 0 ? 0 : minRight; + maxRight = maxRight > 0 ? maxRight : 0; } else { - var limitTop = Infinity; - var limitBottom = -Infinity; - var absLeftMin = Math.abs(minLeft); - var absLeftMax = Math.abs(maxLeft); - var absRightMin = Math.abs(minRight); - var absRightMax = Math.abs(maxRight); - var upLeft = _.max([absLeftMin, absLeftMax]); - var downLeft = _.min([absLeftMin, absLeftMax]); - var upRight = _.max([absRightMin, absRightMax]); - var downRight = _.min([absRightMin, absRightMax]); - var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); - var rateLeft, rateRight, rate; - - // on the one hand with respect to zero - if (oneSide) { - rateLeft = downLeft ? upLeft / downLeft : downLeft >= 0 ? limitTop : limitBottom; - rateRight = downRight ? upRight / downRight : downRight >= 0 ? limitTop : limitBottom; - rate = _.max([rateLeft, rateRight]); - - if (rate === limitTop) { - if (maxLeft > 0) { - minLeft = 0; - minRight = 0; - } else { - maxLeft = 0; - maxRight = 0; - } + // on the opposite sides with respect to zero + if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { + if (minLeft >= 0) { + minLeft = -maxLeft; + maxRight = -minRight; } else { - var coef = deltaLeft / deltaRight; - if ((rate === rateLeft && minLeft > 0) || (rate === rateRight && maxRight < 0)) { - maxLeft = maxRight * coef; - minRight = minLeft / coef; - } else { - minLeft = minRight * coef; - maxRight = maxLeft / coef; - } + maxLeft = -minLeft; + minRight = -maxRight; } } else { - rateLeft = - minLeft && maxLeft - ? minLeft < 0 ? maxLeft / minLeft : limitBottom - : minLeft < 0 || maxRight >= 0 ? limitBottom : limitTop; - rateRight = - minRight && maxRight - ? minRight < 0 ? maxRight / minRight : limitBottom - : minRight < 0 || maxLeft >= 0 ? limitBottom : limitTop; - rate = _.max([rateLeft, rateRight]); + // both across zero + var twoCross = minLeft <= 0 && maxLeft >= 0 && minRight <= 0 && maxRight >= 0; - if (rate === rateLeft) { - minRight = - upRight === absRightMin && (absRightMin !== absRightMax || upLeft !== absLeftMin) - ? -upRight - : upRight / rate; - maxRight = upRight === absRightMax ? upRight : -upRight * rate; + var rateLeft, rateRight, rate; + if (twoCross) { + rateLeft = minRight ? minLeft / minRight : 0; + rateRight = maxRight ? maxLeft / maxRight : 0; } else { - minLeft = - upLeft === absLeftMin && (absLeftMin !== absLeftMax || upRight !== absRightMin) - ? -upLeft - : upLeft / rate; - maxLeft = upLeft === absLeftMax ? upLeft : -upLeft * rate; + if (oneSide) { + var absLeftMin = Math.abs(minLeft); + var absLeftMax = Math.abs(maxLeft); + var absRightMin = Math.abs(minRight); + var absRightMax = Math.abs(maxRight); + var upLeft = _.max([absLeftMin, absLeftMax]); + var downLeft = _.min([absLeftMin, absLeftMax]); + var upRight = _.max([absRightMin, absRightMax]); + var downRight = _.min([absRightMin, absRightMax]); + + rateLeft = downLeft ? upLeft / downLeft : upLeft; + rateRight = downRight ? upRight / downRight : upRight; + } else { + if (minLeft > 0 || minRight > 0) { + rateLeft = maxLeft / maxRight; + rateRight = 0; + } else { + rateLeft = 0; + rateRight = minLeft / minRight; + } + } + } + rate = rateLeft > rateRight ? rateLeft : rateRight; + + if (oneSide) { + if (minLeft > 0) { + minLeft = maxLeft / rate; + minRight = maxRight / rate; + } else { + maxLeft = minLeft / rate; + maxRight = minRight / rate; + } + } else { + if (twoCross) { + minLeft = minRight ? minRight * rate : minLeft; + minRight = minLeft ? minLeft / rate : minRight; + maxLeft = maxRight ? maxRight * rate : maxLeft; + maxRight = maxLeft ? maxLeft / rate : maxRight; + } else { + minLeft = minLeft > 0 ? minRight * rate : minLeft; + minRight = minRight > 0 ? minLeft / rate : minRight; + maxLeft = maxLeft < 0 ? maxRight * rate : maxLeft; + maxRight = maxRight < 0 ? maxLeft / rate : maxRight; + } } } } @@ -263,6 +272,11 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { minRight += shareLevel; maxRight += shareLevel; } + + yaxis[0].min = minLeft; + yaxis[0].max = maxLeft; + yaxis[1].min = minRight; + yaxis[1].max = maxRight; } // Series could have different timeSteps, diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 59e72124c74..67b59997278 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -46,6 +46,8 @@ class GraphCtrl extends MetricsPanelCtrl { min: null, max: null, format: 'short', + shareLevel: false, + shareY: 0, }, ], xaxis: { diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 040eb808f48..401198b712d 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1622,14 +1622,24 @@ Licensed under the MIT license. return axis.show || axis.reserveSpace; }); - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); + var snaped = false; + for (var i = 0; i < 2; i++) { + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snaped = snapRangeToTicks(axis, axis.ticks) || snaped; + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + if (snaped) { + executeHooks(hooks.processRange, []); + snaped = false; + } else { + break; + } + } // with all dimensions calculated, we can compute the // axis bounding boxes, start from the outside @@ -1646,6 +1656,7 @@ Licensed under the MIT license. }); } + plotWidth = surface.width - plotOffset.left - plotOffset.right; plotHeight = surface.height - plotOffset.bottom - plotOffset.top; @@ -1879,13 +1890,19 @@ Licensed under the MIT license. } function snapRangeToTicks(axis, ticks) { + var changed = false; if (axis.options.autoscaleMargin && ticks.length > 0) { // snap to ticks - if (axis.options.min == null) + if (axis.options.min == null) { axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) + changed = true; + } + if (axis.options.max == null && ticks.length > 1) { axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + changed = true; + } } + return changed; } function draw() { From 9ac82f3d0ec213a4936d78c50943ee82d1937f70 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 21 Feb 2018 14:51:28 +0100 Subject: [PATCH 0038/3000] added tabs and searchfilter to addpanel, fixes#10427 --- .../dashboard/dashgrid/AddPanelPanel.tsx | 98 +++++++++++++++++-- public/sass/components/_panel_add_panel.scss | 19 +++- public/sass/components/_tabs.scss | 12 +-- 3 files changed, 110 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index aeb840c317a..8d4ebfb3a10 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -16,6 +16,8 @@ export interface AddPanelPanelProps { export interface AddPanelPanelState { filter: string; panelPlugins: any[]; + copiedPanelPlugins: any[]; + tab: string; } export class AddPanelPanel extends React.Component { @@ -25,12 +27,14 @@ export class AddPanelPanel extends React.Component item) @@ -39,6 +43,19 @@ export class AddPanelPanel extends React.Component item) + .value(); + let copiedPanels = []; + let copiedPanelJson = store.get(LS_PANEL_COPY_KEY); if (copiedPanelJson) { let copiedPanel = JSON.parse(copiedPanelJson); @@ -48,12 +65,13 @@ export class AddPanelPanel extends React.Component { @@ -101,19 +119,85 @@ export class AddPanelPanel extends React.Component { + return regex.test(panel.name); + }); + } + + openCopy() { + this.setState({ tab: 'Copy' }); + this.setState({ filter: '' }); + this.setState({ panelPlugins: this.getPanelPlugins('') }); + this.setState({ copiedPanelPlugins: this.getCopiedPanelPlugins('') }); + } + + openAdd() { + this.setState({ tab: 'Add' }); + this.setState({ filter: '' }); + this.setState({ panelPlugins: this.getPanelPlugins('') }); + this.setState({ copiedPanelPlugins: this.getCopiedPanelPlugins('') }); + } + render() { + let addClass; + let copyClass; + let panelTab; + + if (this.state.tab === 'Add') { + addClass = 'active active--panel'; + copyClass = ''; + panelTab = this.state.panelPlugins.map(this.renderPanelItem); + } else if (this.state.tab === 'Copy') { + addClass = ''; + copyClass = 'active active--panel'; + panelTab = this.state.copiedPanelPlugins.map(this.renderPanelItem); + } + return (
New Panel - Select a visualization +
    +
  • +
    + Add +
    +
  • +
  • +
    + Copy +
    +
  • +
- {this.state.panelPlugins.map(this.renderPanelItem)} + +
+ +
+ {panelTab} +
); diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index 51754a54d92..70aff32a945 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -3,9 +3,12 @@ } .add-panel__header { - padding: 5px 15px; + padding: 0 15px; display: flex; align-items: center; + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; .gicon { font-size: 30px; @@ -23,7 +26,7 @@ .add-panel__title { font-size: $font-size-md; - margin-right: $spacer/2; + margin-right: $spacer*2; } .add-panel__sub-title { @@ -39,9 +42,9 @@ flex-direction: row; flex-wrap: wrap; overflow: auto; - height: calc(100% - 43px); + height: calc(100% - 50px); align-content: flex-start; - justify-content: space-around; + justify-content: space-between; position: relative; } @@ -51,7 +54,7 @@ border-radius: 3px; padding: $spacer/3 $spacer; - width: 31%; + width: 32%; height: 60px; text-align: center; margin: $gf-form-margin; @@ -77,3 +80,9 @@ .add-panel__item-icon { padding: 2px; } + +.add-panel__searchbar { + width: 100%; + margin-bottom: 10px; + margin-top: 7px; +} diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index 197d5892652..eb3c8ce13f5 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -44,18 +44,16 @@ &::before { display: block; - content: " "; + content: ' '; position: absolute; left: 0; right: 0; height: 2px; top: 0; - background-image: linear-gradient( - to right, - #ffd500 0%, - #ff4400 99%, - #ff4400 100% - ); + background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%); } } + &.active--panel { + background: $panel-bg !important; + } } From 5e5a4cf1b0f8391b85521182c44b995d7c6eee3d Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 21 Feb 2018 15:39:15 +0100 Subject: [PATCH 0039/3000] added highlighter, fixed setState and changed back flex to spacea around --- .../dashboard/dashgrid/AddPanelPanel.tsx | 39 +++++++++++++------ public/sass/components/_panel_add_panel.scss | 4 +- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 8d4ebfb3a10..1c2eeb8fcc6 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -7,6 +7,7 @@ import { PanelContainer } from './PanelContainer'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; import store from 'app/core/store'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; +import Highlighter from 'react-highlight-words'; export interface AddPanelPanelProps { panel: PanelModel; @@ -110,19 +111,29 @@ export class AddPanelPanel extends React.Component; + //} + //return text; + } + renderPanelItem(panel, index) { return (
this.onAddPanel(panel)} title={panel.name}> -
{panel.name}
+
{this.renderText(panel.name)}
); } filterChange(evt) { - this.setState({ filter: evt.target.value }); - this.setState({ panelPlugins: this.getPanelPlugins(evt.target.value) }); - this.setState({ copiedPanelPlugins: this.getCopiedPanelPlugins(evt.target.value) }); + this.setState({ + filter: evt.target.value, + panelPlugins: this.getPanelPlugins(evt.target.value), + copiedPanelPlugins: this.getCopiedPanelPlugins(evt.target.value), + }); } filterPanels(panels, filter) { @@ -133,17 +144,21 @@ export class AddPanelPanel extends React.Component Date: Thu, 22 Feb 2018 09:58:52 +0100 Subject: [PATCH 0040/3000] added no copies div --- .../features/dashboard/dashgrid/AddPanelPanel.tsx | 14 ++++++++++---- public/sass/components/_panel_add_panel.scss | 7 +++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 1c2eeb8fcc6..23042285754 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -112,11 +112,8 @@ export class AddPanelPanel extends React.Component; - //} - //return text; } renderPanelItem(panel, index) { @@ -128,6 +125,10 @@ export class AddPanelPanel extends React.ComponentNo copied panels yet.
; + } + filterChange(evt) { this.setState({ filter: evt.target.value, @@ -173,7 +174,12 @@ export class AddPanelPanel extends React.Component 0) { + panelTab = this.state.copiedPanelPlugins.map(this.renderPanelItem); + } else { + panelTab = this.noCopiedPanelPlugins(); + } } return ( diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index 6dd609ee544..5322d8fcea0 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -86,3 +86,10 @@ margin-bottom: 10px; margin-top: 7px; } + +.add-panel__no-panels { + color: $text-color-weak; + font-style: italic; + width: 100%; + padding: 3px 8px; +} From 07c3fb7e0f1a86009c9497698fa79d731d88dff7 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 22 Feb 2018 10:38:22 +0100 Subject: [PATCH 0041/3000] changed name of copy tab to paste --- public/app/features/dashboard/dashgrid/AddPanelPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 23042285754..d5b301a9ea1 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -196,7 +196,7 @@ export class AddPanelPanel extends React.Component
  • - Copy + Paste
  • From 9e68cbea514380998d4ab5d4b9efe7926935cb05 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Thu, 22 Feb 2018 15:38:32 +0500 Subject: [PATCH 0042/3000] Refactoring code --- public/app/plugins/panel/graph/align_yaxes.ts | 123 ++++++++++++++++++ .../app/plugins/panel/graph/axes_editor.html | 19 +-- public/app/plugins/panel/graph/graph.ts | 122 +---------------- public/app/plugins/panel/graph/module.ts | 3 +- 4 files changed, 137 insertions(+), 130 deletions(-) create mode 100644 public/app/plugins/panel/graph/align_yaxes.ts diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts new file mode 100644 index 00000000000..74fc9a063c7 --- /dev/null +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -0,0 +1,123 @@ +import _ from 'lodash'; + +/** + * To align two Y axes by Y level + * @param yaxis data [{min: min_y1, min: max_y1}, {min: min_y2, max: max_y2}] + * @param align Y level + */ +export function alignYLevel(yaxis, alignLevel) { + var minLeft = yaxis[0].min; + var maxLeft = yaxis[0].max; + var minRight = yaxis[1].min; + var maxRight = yaxis[1].max; + + if (alignLevel !== 0) { + minLeft -= alignLevel; + maxLeft -= alignLevel; + minRight -= alignLevel; + maxRight -= alignLevel; + } + + // wide Y min and max using increased wideFactor + var deltaLeft = maxLeft - minLeft; + var deltaRight = maxRight - minRight; + var wideFactor = 0.25; + if (deltaLeft === 0) { + minLeft -= wideFactor; + maxLeft += wideFactor; + } + if (deltaRight === 0) { + minRight -= wideFactor; + maxRight += wideFactor; + } + + // one of graphs on zero + var zero = minLeft === 0 || minRight === 0 || maxLeft === 0 || maxRight === 0; + + // on the one hand with respect to zero + var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); + + if (zero && oneSide) { + minLeft = maxLeft > 0 ? 0 : minLeft; + maxLeft = maxLeft > 0 ? maxLeft : 0; + minRight = maxRight > 0 ? 0 : minRight; + maxRight = maxRight > 0 ? maxRight : 0; + } else { + // on the opposite sides with respect to zero + if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { + if (minLeft >= 0) { + minLeft = -maxLeft; + maxRight = -minRight; + } else { + maxLeft = -minLeft; + minRight = -maxRight; + } + } else { + // both across zero + var twoCross = minLeft <= 0 && maxLeft >= 0 && minRight <= 0 && maxRight >= 0; + + var rateLeft, rateRight, rate; + if (twoCross) { + rateLeft = minRight ? minLeft / minRight : 0; + rateRight = maxRight ? maxLeft / maxRight : 0; + } else { + if (oneSide) { + var absLeftMin = Math.abs(minLeft); + var absLeftMax = Math.abs(maxLeft); + var absRightMin = Math.abs(minRight); + var absRightMax = Math.abs(maxRight); + var upLeft = _.max([absLeftMin, absLeftMax]); + var downLeft = _.min([absLeftMin, absLeftMax]); + var upRight = _.max([absRightMin, absRightMax]); + var downRight = _.min([absRightMin, absRightMax]); + + rateLeft = downLeft ? upLeft / downLeft : upLeft; + rateRight = downRight ? upRight / downRight : upRight; + } else { + if (minLeft > 0 || minRight > 0) { + rateLeft = maxLeft / maxRight; + rateRight = 0; + } else { + rateLeft = 0; + rateRight = minLeft / minRight; + } + } + } + rate = rateLeft > rateRight ? rateLeft : rateRight; + + if (oneSide) { + if (minLeft > 0) { + minLeft = maxLeft / rate; + minRight = maxRight / rate; + } else { + maxLeft = minLeft / rate; + maxRight = minRight / rate; + } + } else { + if (twoCross) { + minLeft = minRight ? minRight * rate : minLeft; + minRight = minLeft ? minLeft / rate : minRight; + maxLeft = maxRight ? maxRight * rate : maxLeft; + maxRight = maxLeft ? maxLeft / rate : maxRight; + } else { + minLeft = minLeft > 0 ? minRight * rate : minLeft; + minRight = minRight > 0 ? minLeft / rate : minRight; + maxLeft = maxLeft < 0 ? maxRight * rate : maxLeft; + maxRight = maxRight < 0 ? maxLeft / rate : maxRight; + } + } + } + } + + if (alignLevel !== 0) { + minLeft += alignLevel; + maxLeft += alignLevel; + minRight += alignLevel; + maxRight += alignLevel; + } + + yaxis[0].min = minLeft; + yaxis[0].max = maxLeft; + yaxis[1].min = minRight; + yaxis[1].max = maxRight; +} diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index a80ebd3036c..7bf6756a7df 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -11,6 +11,7 @@
    +
    @@ -28,8 +29,15 @@
    - -
    +
    + +
    + + +
    + +
    +
    @@ -39,13 +47,6 @@
    -
    - -
    - - -
    -
    diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 95790222cac..3f7d1bee33c 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -18,6 +18,7 @@ import GraphTooltip from './graph_tooltip'; import { ThresholdManager } from './threshold_manager'; import { EventManager } from 'app/features/annotations/all'; import { convertValuesToHistogram, getSeriesValues } from './histogram'; +import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; /** @ngInject **/ @@ -157,128 +158,11 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && panel.yaxes[1].shareLevel) { - shareYLevel(yaxis, parseFloat(panel.yaxes[1].shareY || 0)); + if (yaxis.length > 1 && panel.yaxes[1].align !== null) { + alignYLevel(yaxis, parseFloat(panel.yaxes[1].align)); } } - function shareYLevel(yaxis, shareLevel) { - var minLeft = yaxis[0].min; - var maxLeft = yaxis[0].max; - var minRight = yaxis[1].min; - var maxRight = yaxis[1].max; - - if (shareLevel !== 0) { - minLeft -= shareLevel; - maxLeft -= shareLevel; - minRight -= shareLevel; - maxRight -= shareLevel; - } - - // wide Y min and max using increased wideFactor - var deltaLeft = maxLeft - minLeft; - var deltaRight = maxRight - minRight; - var wideFactor = 0.25; - if (deltaLeft === 0) { - minLeft -= wideFactor; - maxLeft += wideFactor; - } - if (deltaRight === 0) { - minRight -= wideFactor; - maxRight += wideFactor; - } - - // one of graphs on zero - var zero = minLeft === 0 || minRight === 0 || maxLeft === 0 || maxRight === 0; - - // on the one hand with respect to zero - var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); - - if (zero && oneSide) { - minLeft = maxLeft > 0 ? 0 : minLeft; - maxLeft = maxLeft > 0 ? maxLeft : 0; - minRight = maxRight > 0 ? 0 : minRight; - maxRight = maxRight > 0 ? maxRight : 0; - } else { - // on the opposite sides with respect to zero - if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { - if (minLeft >= 0) { - minLeft = -maxLeft; - maxRight = -minRight; - } else { - maxLeft = -minLeft; - minRight = -maxRight; - } - } else { - // both across zero - var twoCross = minLeft <= 0 && maxLeft >= 0 && minRight <= 0 && maxRight >= 0; - - var rateLeft, rateRight, rate; - if (twoCross) { - rateLeft = minRight ? minLeft / minRight : 0; - rateRight = maxRight ? maxLeft / maxRight : 0; - } else { - if (oneSide) { - var absLeftMin = Math.abs(minLeft); - var absLeftMax = Math.abs(maxLeft); - var absRightMin = Math.abs(minRight); - var absRightMax = Math.abs(maxRight); - var upLeft = _.max([absLeftMin, absLeftMax]); - var downLeft = _.min([absLeftMin, absLeftMax]); - var upRight = _.max([absRightMin, absRightMax]); - var downRight = _.min([absRightMin, absRightMax]); - - rateLeft = downLeft ? upLeft / downLeft : upLeft; - rateRight = downRight ? upRight / downRight : upRight; - } else { - if (minLeft > 0 || minRight > 0) { - rateLeft = maxLeft / maxRight; - rateRight = 0; - } else { - rateLeft = 0; - rateRight = minLeft / minRight; - } - } - } - rate = rateLeft > rateRight ? rateLeft : rateRight; - - if (oneSide) { - if (minLeft > 0) { - minLeft = maxLeft / rate; - minRight = maxRight / rate; - } else { - maxLeft = minLeft / rate; - maxRight = minRight / rate; - } - } else { - if (twoCross) { - minLeft = minRight ? minRight * rate : minLeft; - minRight = minLeft ? minLeft / rate : minRight; - maxLeft = maxRight ? maxRight * rate : maxLeft; - maxRight = maxLeft ? maxLeft / rate : maxRight; - } else { - minLeft = minLeft > 0 ? minRight * rate : minLeft; - minRight = minRight > 0 ? minLeft / rate : minRight; - maxLeft = maxLeft < 0 ? maxRight * rate : maxLeft; - maxRight = maxRight < 0 ? maxLeft / rate : maxRight; - } - } - } - } - - if (shareLevel !== 0) { - minLeft += shareLevel; - maxLeft += shareLevel; - minRight += shareLevel; - maxRight += shareLevel; - } - - yaxis[0].min = minLeft; - yaxis[0].max = maxLeft; - yaxis[1].min = minRight; - yaxis[1].max = maxRight; - } - // Series could have different timeSteps, // let's find the smallest one so that bars are correctly rendered. // In addition, only take series which are rendered as bars for this. diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 67b59997278..c198d118115 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -46,8 +46,7 @@ class GraphCtrl extends MetricsPanelCtrl { min: null, max: null, format: 'short', - shareLevel: false, - shareY: 0, + align: null, }, ], xaxis: { From 66590042a6b91dc3ce4e197724d0c1ef96b1bfea Mon Sep 17 00:00:00 2001 From: ilgizar Date: Thu, 22 Feb 2018 15:41:11 +0500 Subject: [PATCH 0043/3000] Add unit tests. --- .../plugins/panel/graph/specs/align_y.jest.ts | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 public/app/plugins/panel/graph/specs/align_y.jest.ts diff --git a/public/app/plugins/panel/graph/specs/align_y.jest.ts b/public/app/plugins/panel/graph/specs/align_y.jest.ts new file mode 100644 index 00000000000..046d02ee787 --- /dev/null +++ b/public/app/plugins/panel/graph/specs/align_y.jest.ts @@ -0,0 +1,167 @@ +import { alignYLevel } from '../align_yaxes'; + +describe('Graph Y axes aligner', function() { + let yaxes, expected; + let alignY = 0; + + describe('on the one hand with respect to zero', () => { + it('Should shrink Y axis', () => { + yaxes = [{ min: 5, max: 10 }, { min: 2, max: 3 }]; + expected = [{ min: 5, max: 10 }, { min: 1.5, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axis', () => { + yaxes = [{ min: 2, max: 3 }, { min: 5, max: 10 }]; + expected = [{ min: 1.5, max: 3 }, { min: 5, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axis', () => { + yaxes = [{ min: -10, max: -5 }, { min: -3, max: -2 }]; + expected = [{ min: -10, max: -5 }, { min: -3, max: -1.5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axis', () => { + yaxes = [{ min: -3, max: -2 }, { min: -10, max: -5 }]; + expected = [{ min: -3, max: -1.5 }, { min: -10, max: -5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('on the opposite sides with respect to zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: -3, max: -1 }, { min: 5, max: 10 }]; + expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: 1, max: 3 }, { min: -10, max: -5 }]; + expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('both across zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: -10, max: 5 }, { min: -2, max: 3 }]; + expected = [{ min: -10, max: 15 }, { min: -2, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -5, max: 10 }, { min: -3, max: 2 }]; + expected = [{ min: -15, max: 10 }, { min: -3, max: 2 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('one of graphs on zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: 0, max: 3 }, { min: 5, max: 10 }]; + expected = [{ min: 0, max: 3 }, { min: 0, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: 5, max: 10 }, { min: 0, max: 3 }]; + expected = [{ min: 0, max: 10 }, { min: 0, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -3, max: 0 }, { min: -10, max: -5 }]; + expected = [{ min: -3, max: 0 }, { min: -10, max: 0 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -10, max: -5 }, { min: -3, max: 0 }]; + expected = [{ min: -10, max: 0 }, { min: -3, max: 0 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('both graphs on zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: 0, max: 3 }, { min: -10, max: 0 }]; + expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -3, max: 0 }, { min: 0, max: 10 }]; + expected = [{ min: -3, max: 3 }, { min: -10, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); + + describe('mixed placement of graphs relative to zero', () => { + it('Should shrink Y axes', () => { + yaxes = [{ min: -10, max: 5 }, { min: 1, max: 3 }]; + expected = [{ min: -10, max: 5 }, { min: -6, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: 1, max: 3 }, { min: -10, max: 5 }]; + expected = [{ min: -6, max: 3 }, { min: -10, max: 5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -10, max: 5 }, { min: -3, max: -1 }]; + expected = [{ min: -10, max: 5 }, { min: -3, max: 1.5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + yaxes = [{ min: -3, max: -1 }, { min: -10, max: 5 }]; + expected = [{ min: -3, max: 1.5 }, { min: -10, max: 5 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); +}); From e037ef21f790f6ea4aea3c3e0ee29e66375e636c Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 26 Feb 2018 10:21:24 +0100 Subject: [PATCH 0044/3000] added admin icon and permission member definitions(role,team,user) --- .../Permissions/DisabledPermissionsListItem.tsx | 7 +++++-- .../Permissions/PermissionsListItem.tsx | 16 ++++++++++++++-- public/sass/components/_filter-table.scss | 4 ++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx index e3f3ee56d75..adc2bec3d81 100644 --- a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx +++ b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx @@ -13,9 +13,12 @@ export default class DisabledPermissionListItem extends Component { return ( - + + + + {item.name} + (Role) - {item.name} Can diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx index 2ab5b948440..1bec7003f1f 100644 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ b/public/app/core/components/Permissions/PermissionsListItem.tsx @@ -14,7 +14,17 @@ function ItemAvatar({ item }) { if (item.teamAvatarUrl) { return ; } - return ; + return ; +} + +function ItemDescription({ item }) { + if (item.userId) { + return (User); + } + if (item.teamId) { + return (Team); + } + return (Role); } export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => { @@ -35,7 +45,9 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde - {item.name} + + {item.name} + {item.inherited && folderInfo && ( diff --git a/public/sass/components/_filter-table.scss b/public/sass/components/_filter-table.scss index 00f9b93dcfd..bfa9fbbbc5a 100644 --- a/public/sass/components/_filter-table.scss +++ b/public/sass/components/_filter-table.scss @@ -85,3 +85,7 @@ } } } +.filter-table__weak-italic { + font-style: italic; + color: $text-color-weak; +} From fd518846b1865385eb9775332ffd04fc5388dcc9 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 3 Mar 2018 20:57:00 +0100 Subject: [PATCH 0045/3000] rename field to column --- .../datasource/postgres/postgres_query.ts | 22 ++++++++----- .../plugins/datasource/postgres/query_ctrl.ts | 7 ++--- .../plugins/datasource/postgres/query_part.ts | 31 +++++++------------ 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 9a5a14c2b6a..78e9c9c1a3f 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -24,7 +24,7 @@ export default class PostgresQuery { target.orderByTime = target.orderByTime || 'ASC'; target.groupBy = target.groupBy || []; target.where = target.where || []; - target.select = target.select || [[{ type: 'field', params: ['value'] }]]; + target.select = target.select || [[{ type: 'column', params: ['value'] }]]; this.updateProjection(); } @@ -88,8 +88,6 @@ export default class PostgresQuery { var categories = queryPart.getCategories(); if (part.def.type === 'time') { - // remove fill - this.target.groupBy = _.filter(this.target.groupBy, (g: any) => g.type !== 'fill'); // remove aggregations this.target.select = _.map(this.target.select, (s: any) => { return _.filter(s, (part: any) => { @@ -113,7 +111,7 @@ export default class PostgresQuery { removeSelectPart(selectParts, part) { // if we remove the field remove the whole statement - if (part.def.type === 'field') { + if (part.def.type === 'column') { if (this.selectModels.length > 1) { var modelsIndex = _.indexOf(this.selectModels, selectParts); this.selectModels.splice(modelsIndex, 1); @@ -189,7 +187,12 @@ export default class PostgresQuery { } var query = 'SELECT '; - query += this.quoteIdentifier(target.timeColumn) + ' AS time,'; + + if (this.hasGroupByTime()) { + query += '$__timeGroup(' + this.quoteIdentifier(target.timeColumn) + ',1m),'; + } else { + query += this.quoteIdentifier(target.timeColumn) + ' AS time,'; + } var i, y; for (i = 0; i < this.selectModels.length; i++) { @@ -221,10 +224,13 @@ export default class PostgresQuery { for (i = 0; i < this.groupByParts.length; i++) { var part = this.groupByParts[i]; if (i > 0) { - // for some reason fill has no seperator - groupBySection += part.def.type === 'fill' ? ' ' : ', '; + groupBySection += ', '; + } + if (part.def.type === 'time') { + groupBySection += 'time'; + } else { + groupBySection += part.render(''); } - groupBySection += part.render(''); } if (groupBySection.length) { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index f6f6641eff0..0c30e2c51ff 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -92,7 +92,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.removeWhereFilterSegment = uiSegmentSrv.newSegment({ fake: true, - value: '-- remove tag filter --', + value: '-- remove filter --', }); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); @@ -390,9 +390,6 @@ export class PostgresQueryCtrl extends QueryCtrl { .metricFindQuery(this.queryBuilder.buildColumnQuery()) .then(tags => { var options = []; - if (!this.queryModel.hasFill()) { - options.push(this.uiSegmentSrv.newSegment({ value: 'fill(null)' })); - } if (!this.target.limit) { options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); } @@ -400,7 +397,7 @@ export class PostgresQueryCtrl extends QueryCtrl { options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); } for (let tag of tags) { - options.push(this.uiSegmentSrv.newSegment({ value: 'tag(' + tag.text + ')' })); + options.push(this.uiSegmentSrv.newSegment({ value: tag.text })); } return options; }) diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 5828515ec06..0086b45b848 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -6,7 +6,7 @@ var categories = { Aggregations: [], Math: [], Aliasing: [], - Fields: [], + Columns: [], }; function createPart(part): any { @@ -29,7 +29,7 @@ function aliasRenderer(part, innerExpr) { return innerExpr + ' AS ' + '"' + part.params[0] + '"'; } -function fieldRenderer(part, innerExpr) { +function columnRenderer(part, innerExpr) { return '"' + part.params[0] + '"'; } @@ -92,7 +92,7 @@ function addAliasStrategy(selectParts, partModel) { selectParts.push(partModel); } -function addFieldStrategy(selectParts, partModel, query) { +function addColumnStrategy(selectParts, partModel, query) { // copy all parts var parts = _.map(selectParts, function(part: any) { return createPart({ type: part.def.type, params: _.clone(part.params) }); @@ -102,12 +102,12 @@ function addFieldStrategy(selectParts, partModel, query) { } register({ - type: 'field', - addStrategy: addFieldStrategy, - category: categories.Fields, - params: [{ type: 'field', dynamicLookup: true }], + type: 'column', + addStrategy: addColumnStrategy, + category: categories.Columns, + params: [{ type: 'column', dynamicLookup: true }], defaultParams: ['value'], - renderer: fieldRenderer, + renderer: columnRenderer, }); // Aggregations @@ -170,25 +170,16 @@ register({ params: [ { name: 'interval', - type: 'time', + type: 'interval', options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], }, - ], - defaultParams: ['$__interval'], - renderer: functionRenderer, -}); - -register({ - type: 'fill', - category: groupByTimeFunctions, - params: [ { name: 'fill', type: 'string', - options: ['none', 'null', '0', 'previous', 'linear'], + options: ['none', 'null', '0'], }, ], - defaultParams: ['null'], + defaultParams: ['$__interval','none'], renderer: functionRenderer, }); From 7104e6f9f8dccd5bbba53519daaa40a7cce6a329 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 3 Mar 2018 22:11:51 +0100 Subject: [PATCH 0046/3000] fix variable interpolation --- public/app/plugins/datasource/postgres/postgres_query.ts | 3 +++ public/app/plugins/datasource/postgres/query_ctrl.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 78e9c9c1a3f..2f8b3911ebc 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -239,6 +239,9 @@ export default class PostgresQuery { query += ' ORDER BY time'; + if (interpolate) { + query = this.templateSrv.replace(query, this.scopedVars, this.interpolateQueryStr); + } return query; } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 0c30e2c51ff..bde4010e226 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -207,7 +207,7 @@ export class PostgresQueryCtrl extends QueryCtrl { segments.unshift( this.uiSegmentSrv.newSegment({ type: 'template', - value: '/^$' + variable.name + '$/', + value: '$' + variable.name, expandable: true, }) ); From e8c6341fed3811b3d6339dc5e2fc2a8815caa285 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 4 Mar 2018 10:18:11 +0100 Subject: [PATCH 0047/3000] clean up aggregation functions --- .../plugins/datasource/postgres/query_part.ts | 87 ++++++------------- 1 file changed, 28 insertions(+), 59 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 0086b45b848..600723be00d 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -46,19 +46,6 @@ function replaceAggregationAddStrategy(selectParts, partModel) { selectParts.splice(1, 0, partModel); } -function addTransformationStrategy(selectParts, partModel) { - var i; - // look for index to add transformation - for (i = 0; i < selectParts.length; i++) { - var part = selectParts[i]; - if (part.def.category === categories.Math || part.def.category === categories.Aliasing) { - break; - } - } - - selectParts.splice(i, 0, partModel); -} - function addMathStrategy(selectParts, partModel) { var partCount = selectParts.length; if (partCount > 0) { @@ -138,54 +125,8 @@ register({ renderer: functionRenderer, }); -// transformations - -register({ - type: 'non_negative_derivative', - addStrategy: addTransformationStrategy, - category: categories.Aggregations, - params: [ - { - name: 'duration', - type: 'interval', - options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - ], - defaultParams: ['10s'], - renderer: functionRenderer, -}); - register({ type: 'stddev', - addStrategy: addTransformationStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'time', - category: groupByTimeFunctions, - params: [ - { - name: 'interval', - type: 'interval', - options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], - }, - { - name: 'fill', - type: 'string', - options: ['none', 'null', '0'], - }, - ], - defaultParams: ['$__interval','none'], - renderer: functionRenderer, -}); - -// Selectors -register({ - type: 'max', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, params: [], @@ -202,6 +143,15 @@ register({ renderer: functionRenderer, }); +register({ + type: 'max', + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + register({ type: 'math', addStrategy: addMathStrategy, @@ -221,6 +171,25 @@ register({ renderer: aliasRenderer, }); +register({ + type: 'time', + category: groupByTimeFunctions, + params: [ + { + name: 'interval', + type: 'interval', + options: ['$__interval', '1s', '10s', '1m', '5m', '10m', '15m', '1h'], + }, + { + name: 'fill', + type: 'string', + options: ['none', 'NULL', '0'], + }, + ], + defaultParams: ['$__interval','none'], + renderer: functionRenderer, +}); + export default { create: createPart, getCategories: function() { From 26e09b598c809e93a120dfbc30214af8ff9ac690 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 4 Mar 2018 19:35:43 +0100 Subject: [PATCH 0048/3000] fix group by column --- .../datasource/postgres/postgres_query.ts | 24 ++++++++++++------- .../plugins/datasource/postgres/query_ctrl.ts | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 2f8b3911ebc..a236fa91b18 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -61,17 +61,17 @@ export default class PostgresQuery { } addGroupBy(value) { - var stringParts = value.match(/^(\w+)\((.*)\)$/); + var stringParts = value.match(/^(\w+)(\((.*)\))?$/); var typePart = stringParts[1]; - var arg = stringParts[2]; - var partModel = queryPart.create({ type: typePart, params: [arg] }); + var args = stringParts[3].split(","); + var partModel = queryPart.create({ type: typePart, params: args }); var partCount = this.target.groupBy.length; if (partCount === 0) { this.target.groupBy.push(partModel.part); } else if (typePart === 'time') { this.target.groupBy.splice(0, 0, partModel.part); - } else if (typePart === 'tag') { + } else if (typePart === 'column') { if (this.target.groupBy[partCount - 1].type === 'fill') { this.target.groupBy.splice(partCount - 1, 0, partModel.part); } else { @@ -188,8 +188,16 @@ export default class PostgresQuery { var query = 'SELECT '; - if (this.hasGroupByTime()) { - query += '$__timeGroup(' + this.quoteIdentifier(target.timeColumn) + ',1m),'; + var timeGroup = this.hasGroupByTime(); + + if (timeGroup) { + var args; + if (timeGroup.params.length > 1 && timeGroup.params[1] !== "none") { + args = timeGroup.params.join(","); + } else { + args = timeGroup.params[0]; + } + query += '$__timeGroup(' + this.quoteIdentifier(target.timeColumn) + ',' + args + '),'; } else { query += this.quoteIdentifier(target.timeColumn) + ' AS time,'; } @@ -227,7 +235,7 @@ export default class PostgresQuery { groupBySection += ', '; } if (part.def.type === 'time') { - groupBySection += 'time'; + groupBySection += '1'; } else { groupBySection += part.render(''); } @@ -237,7 +245,7 @@ export default class PostgresQuery { query += ' GROUP BY ' + groupBySection; } - query += ' ORDER BY time'; + query += ' ORDER BY 1'; if (interpolate) { query = this.templateSrv.replace(query, this.scopedVars, this.interpolateQueryStr); diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index bde4010e226..b0d5d7ab9ca 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -394,10 +394,10 @@ export class PostgresQueryCtrl extends QueryCtrl { options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); } if (!this.queryModel.hasGroupByTime()) { - options.push(this.uiSegmentSrv.newSegment({ value: 'time($interval)' })); + options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time(1m,none)' })); } for (let tag of tags) { - options.push(this.uiSegmentSrv.newSegment({ value: tag.text })); + options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: 'column(' + tag.text + ')' })); } return options; }) From bf4a30d30f93aa7ac1cf01c319666242022272fe Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 4 Mar 2018 19:46:11 +0100 Subject: [PATCH 0049/3000] set rawSQL when rendering query builder query --- public/app/plugins/datasource/postgres/postgres_query.ts | 1 + public/app/plugins/datasource/postgres/query_ctrl.ts | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index a236fa91b18..a708f384972 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -247,6 +247,7 @@ export default class PostgresQuery { query += ' ORDER BY 1'; + this.target.rawSql = query; if (interpolate) { query = this.templateSrv.replace(query, this.scopedVars, this.interpolateQueryStr); } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index b0d5d7ab9ca..4e36e8699ae 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -117,11 +117,6 @@ export class PostgresQueryCtrl extends QueryCtrl { } toggleEditorMode() { - try { -// this.target.query = this.queryModel.render(false); - } catch (err) { - console.log('query render error'); - } this.target.rawQuery = !this.target.rawQuery; } From 83200c289a454e978ac4c0ade432ac7ccb31dbc4 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 4 Mar 2018 23:32:23 +0100 Subject: [PATCH 0050/3000] use metricColumn in query builder --- public/app/plugins/datasource/postgres/postgres_query.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index a708f384972..4541ebae7de 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -217,6 +217,10 @@ export default class PostgresQuery { query += selectText; } + if (this.target.metricColumn !== 'None') { + query += "," + this.quoteIdentifier(this.target.metricColumn) + " AS metric"; + } + query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { return this.renderTagCondition(tag, index, interpolate); From 8a1bd2ee223410b09fe0833f9de2749bb74c9776 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 6 Mar 2018 09:24:36 +0100 Subject: [PATCH 0051/3000] docs: fill for mysql/postgres ref #10138 --- docs/sources/features/datasources/mysql.md | 14 ++++++++++++++ docs/sources/features/datasources/postgres.md | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 7fae7441b6d..6c15006949e 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -50,6 +50,7 @@ Macro example | Description *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *FROM_UNIXTIME(1494410783)* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *FROM_UNIXTIME(1494497183)* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed) as time_sec,* +*$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* @@ -119,6 +120,19 @@ GROUP BY 1, metric_name ORDER BY 1 ``` +Example using the fill parameter in the $__timeGroup macro to convert null values to be zero instead: + +```sql +SELECT + $__timeGroup(atimestamp,'24h',0) as time_sec, + avg(afloat) as value, + avarchar as metric +FROM testdata.grafana_metrics +WHERE $__timeFilter(atimestamp) +GROUP BY 1, avarchar +ORDER BY 1 +``` + Currently, there is no support for a dynamic group by time based on time range & panel width. This is something we plan to add. diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 7d52df2fd3e..270640a93dc 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -49,6 +49,7 @@ Macro example | Description *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time* +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* @@ -103,6 +104,20 @@ GROUP BY time ORDER BY time ``` +Example using the fill parameter in the $__timeGroup macro to convert null values to be zero instead: + +```sql +SELECT + $__timeGroup("createdAt",'5m',0), + sum(value) as value, + measurement +FROM public.grafana_metric +WHERE + $__timeFilter("createdAt") +GROUP BY time, measurement +ORDER BY time +``` + Example with multiple columns: ```sql From 7cddc543068c954124201f1906d0dc576a891499 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Wed, 7 Mar 2018 12:12:39 +0500 Subject: [PATCH 0052/3000] Add bs-tooltip to Y-Align element. --- public/app/plugins/panel/graph/axes_editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 7bf6756a7df..2c08755c17a 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -33,7 +33,7 @@
    - +
    From 916539fad9ebd9cb5a278796a2c54e2310efd755 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Wed, 7 Mar 2018 14:21:10 +0500 Subject: [PATCH 0053/3000] Append test to check not zero level. --- .../plugins/panel/graph/specs/align_y.jest.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/public/app/plugins/panel/graph/specs/align_y.jest.ts b/public/app/plugins/panel/graph/specs/align_y.jest.ts index 046d02ee787..ff540fd223f 100644 --- a/public/app/plugins/panel/graph/specs/align_y.jest.ts +++ b/public/app/plugins/panel/graph/specs/align_y.jest.ts @@ -158,8 +158,41 @@ describe('Graph Y axes aligner', function() { alignYLevel(yaxes, alignY); expect(yaxes).toMatchObject(expected); }); + }); + + describe('on level not zero', () => { + it('Should shrink Y axis', () => { + alignY = 1; + yaxes = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; + expected = [{ min: 4, max: 10 }, { min: 2, max: 4 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); it('Should shrink Y axes', () => { + alignY = 2; + yaxes = [{ min: -3, max: 1 }, { min: 5, max: 10 }]; + expected = [{ min: -3, max: 7 }, { min: -6, max: 10 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + alignY = -1; + yaxes = [{ min: -5, max: 5 }, { min: -2, max: 3 }]; + expected = [{ min: -5, max: 15 }, { min: -2, max: 3 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + + it('Should shrink Y axes', () => { + alignY = -2; + yaxes = [{ min: -2, max: 3 }, { min: 5, max: 10 }]; + expected = [{ min: -2, max: 3 }, { min: -2, max: 10 }]; + alignYLevel(yaxes, alignY); expect(yaxes).toMatchObject(expected); }); From 8d4c439eebeaa07af8eeab17395a31d26466cbb6 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 7 Mar 2018 12:46:27 +0100 Subject: [PATCH 0054/3000] add panel to list now copy, started on jest --- public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx | 4 ++++ public/app/features/panel/panel_ctrl.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx new file mode 100644 index 00000000000..e68d84ad8bb --- /dev/null +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import { AddPanelPanel } from './AddPanelPanel'; + +describe('AddPanelPanel', () => {}); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index d8757f49be6..11dac549aea 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -193,7 +193,7 @@ export class PanelCtrl { }); menu.push({ - text: 'Add to Panel List', + text: 'Copy', click: 'ctrl.addToPanelList()', role: 'Editor', }); From 3c9f31a0bb502d8c2f6ad85a85cb9b39d88c54e4 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 7 Mar 2018 13:10:44 +0100 Subject: [PATCH 0055/3000] added media breakpoint to legend-right --- public/sass/components/_panel_graph.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 716778096d6..c00af05140a 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -4,7 +4,9 @@ height: 100%; &--legend-right { - flex-direction: row; + @include media-breakpoint-up(sm) { + flex-direction: row; + } .graph-legend { flex: 0 1 10px; From 834c42194321920e969f0e1155cbf476fe69a8df Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 7 Mar 2018 15:01:50 +0100 Subject: [PATCH 0056/3000] replaced if with classNames --- .../dashboard/dashgrid/AddPanelPanel.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index d5b301a9ea1..eb677b4b4b2 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -1,6 +1,6 @@ import React from 'react'; import _ from 'lodash'; - +import classNames from 'classnames'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { PanelContainer } from './PanelContainer'; @@ -163,18 +163,21 @@ export class AddPanelPanel extends React.Component 0) { panelTab = this.state.copiedPanelPlugins.map(this.renderPanelItem); } else { From 380aa26ea37000adc1bf5a92bf49d4496f0a8320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BERNARD?= Date: Wed, 7 Mar 2018 18:14:18 +0100 Subject: [PATCH 0057/3000] Fix the code to match the documentation. Permit for LDAP groups to be groupofuniquenames composed of uniquename (DN). For this, propose DN as group_search_filter_user_attribute and DN also for the member_of in the server.attributes section. DN is processed as a special attribute name which returns the LdapSearchResult.DN field instead of a member of attr array. --- pkg/login/ldap.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index be3babac02e..3bb63a2c28e 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -404,9 +404,11 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { var groupSearchResult *ldap.SearchResult for _, groupSearchBase := range a.server.GroupSearchBaseDNs { var filter_replace string - filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult) + if a.server.GroupSearchFilterUserAttribute == "" { filter_replace = getLdapAttr(a.server.Attr.Username, searchResult) + } else { + filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult) } filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1) @@ -448,6 +450,9 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { } func getLdapAttrN(name string, result *ldap.SearchResult, n int) string { + if name == "DN" { + return result.Entries[0].DN + } for _, attr := range result.Entries[n].Attributes { if attr.Name == name { if len(attr.Values) > 0 { From abef722265b0199133d64ccb683a0be00ab87a0a Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 7 Mar 2018 14:41:05 -0500 Subject: [PATCH 0058/3000] Fix indent --- pkg/login/ldap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 3bb63a2c28e..bc5fe13dba3 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -450,7 +450,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { } func getLdapAttrN(name string, result *ldap.SearchResult, n int) string { - if name == "DN" { + if name == "DN" { return result.Entries[0].DN } for _, attr := range result.Entries[n].Attributes { From 340f679d0f7c531eaa332c036a3141eb40119bff Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 9 Mar 2018 18:09:59 +0100 Subject: [PATCH 0059/3000] quote schema and table --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 4541ebae7de..bd66ab5eca9 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -221,7 +221,7 @@ export default class PostgresQuery { query += "," + this.quoteIdentifier(this.target.metricColumn) + " AS metric"; } - query += ' FROM ' + target.schema + '.' + target.table + ' WHERE '; + query += ' FROM ' + this.quoteIdentifier(target.schema) + '.' + this.quoteIdentifier(target.table) + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { return this.renderTagCondition(tag, index, interpolate); }); From 1d8540ac69d37b84689eaf0016b8d155d8e899a3 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 9 Mar 2018 18:18:12 +0100 Subject: [PATCH 0060/3000] properly quote where constraint parts --- .../datasource/postgres/postgres_query.ts | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index bd66ab5eca9..9e682d2f186 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -130,7 +130,7 @@ export default class PostgresQuery { this.updatePersistedParts(); } - private renderTagCondition(tag, index, interpolate) { + private renderWhereConstraint(tag, index, interpolate) { var str = ''; var operator = tag.operator; var value = tag.value; @@ -138,27 +138,11 @@ export default class PostgresQuery { str = (tag.condition || 'AND') + ' '; } - if (!operator) { - if (/^\/.*\/$/.test(value)) { - operator = '=~'; - } else { - operator = '='; - } + if (interpolate) { + value = this.templateSrv.replace(value, this.scopedVars); } - // quote value unless regex - if (operator !== '=~' && operator !== '!~') { - if (interpolate) { - value = this.templateSrv.replace(value, this.scopedVars); - } - if (operator !== '>' && operator !== '<') { - value = "'" + value.replace(/\\/g, '\\\\') + "'"; - } - } else if (interpolate) { - value = this.templateSrv.replace(value, this.scopedVars, 'regex'); - } - - return str + '"' + tag.key + '" ' + operator + ' ' + value; + return str + this.quoteIdentifier(tag.key) + ' ' + operator + ' ' + this.quoteLiteral(value); } interpolateQueryStr(value, variable, defaultFormatFn) { @@ -223,7 +207,7 @@ export default class PostgresQuery { query += ' FROM ' + this.quoteIdentifier(target.schema) + '.' + this.quoteIdentifier(target.table) + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { - return this.renderTagCondition(tag, index, interpolate); + return this.renderWhereConstraint(tag, index, interpolate); }); if (conditions.length > 0) { @@ -260,7 +244,7 @@ export default class PostgresQuery { renderAdhocFilters(filters) { var conditions = _.map(filters, (tag, index) => { - return this.renderTagCondition(tag, index, false); + return this.renderWhereConstraint(tag, index, false); }); return conditions.join(' '); } From cb5278d413e85ec7fbd21490ca4edc467f691518 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 10 Mar 2018 20:18:03 +0100 Subject: [PATCH 0061/3000] handle variables in where constraints --- .../datasource/postgres/postgres_query.ts | 16 ++++++---------- .../plugins/datasource/postgres/query_ctrl.ts | 8 ++------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 9e682d2f186..9c48a94862f 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -56,10 +56,6 @@ export default class PostgresQuery { return _.find(this.target.groupBy, (g: any) => g.type === 'time'); } - hasFill() { - return _.find(this.target.groupBy, (g: any) => g.type === 'fill'); - } - addGroupBy(value) { var stringParts = value.match(/^(\w+)(\((.*)\))?$/); var typePart = stringParts[1]; @@ -130,19 +126,19 @@ export default class PostgresQuery { this.updatePersistedParts(); } - private renderWhereConstraint(tag, index, interpolate) { + private renderWhereConstraint(constraint, index, interpolate) { var str = ''; - var operator = tag.operator; - var value = tag.value; + var operator = constraint.operator; + var value = constraint.value; if (index > 0) { - str = (tag.condition || 'AND') + ' '; + str = (constraint.condition || 'AND') + ' '; } if (interpolate) { value = this.templateSrv.replace(value, this.scopedVars); } - return str + this.quoteIdentifier(tag.key) + ' ' + operator + ' ' + this.quoteLiteral(value); + return str + this.quoteIdentifier(constraint.key) + ' ' + operator + ' ' + this.quoteLiteral(value); } interpolateQueryStr(value, variable, defaultFormatFn) { @@ -207,7 +203,7 @@ export default class PostgresQuery { query += ' FROM ' + this.quoteIdentifier(target.schema) + '.' + this.quoteIdentifier(target.table) + ' WHERE '; var conditions = _.map(target.where, (tag, index) => { - return this.renderWhereConstraint(tag, index, interpolate); + return this.renderWhereConstraint(tag, index, false); }); if (conditions.length > 0) { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 4e36e8699ae..da8c28eab9c 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -353,7 +353,6 @@ export class PostgresQueryCtrl extends QueryCtrl { rebuildTargetWhereConditions() { var where = []; var tagIndex = 0; - var tagOperator = ''; _.each(this.whereSegments, (segment2, index) => { if (segment2.type === 'key') { @@ -362,11 +361,8 @@ export class PostgresQueryCtrl extends QueryCtrl { } where[tagIndex].key = segment2.value; } else if (segment2.type === 'value') { - tagOperator = this.getTagValueOperator(segment2.value, where[tagIndex].operator); - if (tagOperator) { - this.whereSegments[index - 1] = this.uiSegmentSrv.newOperator(tagOperator); - where[tagIndex].operator = tagOperator; - } + where[tagIndex].value = segment2.value; + } else if (segment2.type === 'template') { where[tagIndex].value = segment2.value; } else if (segment2.type === 'condition') { where.push({ condition: segment2.value }); From 0b358ff5f30930401a3cfa9ca35699d2903786dc Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 10 Mar 2018 22:39:42 +0100 Subject: [PATCH 0062/3000] remove limit --- .../postgres/partials/query.editor.html | 2 +- .../plugins/datasource/postgres/query_ctrl.ts | 31 ++----------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index e25a41b11c2..0706cf3a6cc 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -19,7 +19,7 @@
    - +
    diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index da8c28eab9c..101d52ee096 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -273,17 +273,12 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - getTagsOrValues(segment, index) { + getWhereSegments(segment, index) { if (segment.type === 'condition') { return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); } if (segment.type === 'operator') { - var nextValue = this.whereSegments[index + 1].value; - if (/^\/.*\/$/.test(nextValue)) { - return this.$q.when(this.uiSegmentSrv.newOperators(['=~', '!~'])); - } else { - return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>'])); - } + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>'])); } var query, addTemplateVars; @@ -308,15 +303,6 @@ export class PostgresQueryCtrl extends QueryCtrl { .catch(this.handleQueryError.bind(this)); } - getTagValueOperator(tagValue, tagOperator): string { - if (tagOperator !== '=~' && tagOperator !== '!~' && /^\/.*\/$/.test(tagValue)) { - return '=~'; - } else if ((tagOperator === '=~' || tagOperator === '!~') && /^(?!\/.*\/$)/.test(tagValue)) { - return '='; - } - return null; - } - whereSegmentUpdated(segment, index) { this.whereSegments[index] = segment; @@ -381,14 +367,11 @@ export class PostgresQueryCtrl extends QueryCtrl { .metricFindQuery(this.queryBuilder.buildColumnQuery()) .then(tags => { var options = []; - if (!this.target.limit) { - options.push(this.uiSegmentSrv.newSegment({ value: 'LIMIT' })); - } if (!this.queryModel.hasGroupByTime()) { options.push(this.uiSegmentSrv.newSegment({ type: 'time', value: 'time(1m,none)' })); } for (let tag of tags) { - options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: 'column(' + tag.text + ')' })); + options.push(this.uiSegmentSrv.newSegment({ type: 'column', value: tag.text })); } return options; }) @@ -397,14 +380,6 @@ export class PostgresQueryCtrl extends QueryCtrl { groupByAction() { switch (this.groupBySegment.value) { - case 'LIMIT': { - this.target.limit = 10; - break; - } - case 'ORDER BY time DESC': { - this.target.orderByTime = 'DESC'; - break; - } default: { this.queryModel.addGroupBy(this.groupBySegment.value); } From e780b1bce5140a6c74fcc4782090e15035c5268a Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sun, 11 Mar 2018 12:06:54 +0100 Subject: [PATCH 0063/3000] cleanup where segment handling --- .../plugins/datasource/postgres/query_ctrl.ts | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 101d52ee096..afde342474b 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -68,26 +68,7 @@ export class PostgresQueryCtrl extends QueryCtrl { this.metricColumnSegment = uiSegmentSrv.newSegment(this.target.metricColumn); this.buildSelectMenu(); - this.whereSegments = []; - for (let tag of this.target.where) { - if (!tag.operator) { - if (/^\/.*\/$/.test(tag.value)) { - tag.operator = '=~'; - } else { - tag.operator = '='; - } - } - - if (tag.condition) { - this.whereSegments.push(uiSegmentSrv.newCondition(tag.condition)); - } - - this.whereSegments.push(uiSegmentSrv.newKey(tag.key)); - this.whereSegments.push(uiSegmentSrv.newOperator(tag.operator)); - this.whereSegments.push(uiSegmentSrv.newKeyValue(tag.value)); - } - - this.fixWhereSegments(); + this.buildWhereSegments(); this.groupBySegment = this.uiSegmentSrv.newPlusButton(); this.removeWhereFilterSegment = uiSegmentSrv.newSegment({ @@ -264,7 +245,18 @@ export class PostgresQueryCtrl extends QueryCtrl { } } - fixWhereSegments() { + buildWhereSegments() { + this.whereSegments = []; + for (let constraint of this.target.where) { + + if (constraint.condition) { + this.whereSegments.push(this.uiSegmentSrv.newCondition(constraint.condition)); + } + this.whereSegments.push(this.uiSegmentSrv.newKey(constraint.key)); + this.whereSegments.push(this.uiSegmentSrv.newOperator(constraint.operator)); + this.whereSegments.push(this.uiSegmentSrv.newKeyValue(constraint.value)); + } + var count = this.whereSegments.length; var lastSegment = this.whereSegments[Math.max(count - 1, 0)]; From 8152b9d6fe750a3f662130973141a6a71a752694 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Mon, 12 Mar 2018 10:54:03 +0500 Subject: [PATCH 0064/3000] Refactoring --- public/app/plugins/panel/graph/align_yaxes.ts | 191 ++++++++++-------- 1 file changed, 102 insertions(+), 89 deletions(-) diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts index 74fc9a063c7..4884cd12441 100644 --- a/public/app/plugins/panel/graph/align_yaxes.ts +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -6,118 +6,131 @@ import _ from 'lodash'; * @param align Y level */ export function alignYLevel(yaxis, alignLevel) { - var minLeft = yaxis[0].min; - var maxLeft = yaxis[0].max; - var minRight = yaxis[1].min; - var maxRight = yaxis[1].max; + moveLevelToZero(yaxis, alignLevel); - if (alignLevel !== 0) { - minLeft -= alignLevel; - maxLeft -= alignLevel; - minRight -= alignLevel; - maxRight -= alignLevel; - } - - // wide Y min and max using increased wideFactor - var deltaLeft = maxLeft - minLeft; - var deltaRight = maxRight - minRight; - var wideFactor = 0.25; - if (deltaLeft === 0) { - minLeft -= wideFactor; - maxLeft += wideFactor; - } - if (deltaRight === 0) { - minRight -= wideFactor; - maxRight += wideFactor; - } + expandStuckValues(yaxis); // one of graphs on zero - var zero = minLeft === 0 || minRight === 0 || maxLeft === 0 || maxRight === 0; + var zero = yaxis[0].min === 0 || yaxis[1].min === 0 || yaxis[0].max === 0 || yaxis[1].max === 0; - // on the one hand with respect to zero - var oneSide = (minLeft >= 0 && minRight >= 0) || (maxLeft <= 0 && maxRight <= 0); + var oneSide = checkOneSide(yaxis); if (zero && oneSide) { - minLeft = maxLeft > 0 ? 0 : minLeft; - maxLeft = maxLeft > 0 ? maxLeft : 0; - minRight = maxRight > 0 ? 0 : minRight; - maxRight = maxRight > 0 ? maxRight : 0; + yaxis[0].min = yaxis[0].max > 0 ? 0 : yaxis[0].min; + yaxis[0].max = yaxis[0].max > 0 ? yaxis[0].max : 0; + yaxis[1].min = yaxis[1].max > 0 ? 0 : yaxis[1].min; + yaxis[1].max = yaxis[1].max > 0 ? yaxis[1].max : 0; } else { // on the opposite sides with respect to zero - if ((minLeft >= 0 && maxRight <= 0) || (maxLeft <= 0 && minRight >= 0)) { - if (minLeft >= 0) { - minLeft = -maxLeft; - maxRight = -minRight; + if ((yaxis[0].min >= 0 && yaxis[1].max <= 0) || (yaxis[0].max <= 0 && yaxis[1].min >= 0)) { + if (yaxis[0].min >= 0) { + yaxis[0].min = -yaxis[0].max; + yaxis[1].max = -yaxis[1].min; } else { - maxLeft = -minLeft; - minRight = -maxRight; + yaxis[0].max = -yaxis[0].min; + yaxis[1].min = -yaxis[1].max; } } else { - // both across zero - var twoCross = minLeft <= 0 && maxLeft >= 0 && minRight <= 0 && maxRight >= 0; - - var rateLeft, rateRight, rate; - if (twoCross) { - rateLeft = minRight ? minLeft / minRight : 0; - rateRight = maxRight ? maxLeft / maxRight : 0; - } else { - if (oneSide) { - var absLeftMin = Math.abs(minLeft); - var absLeftMax = Math.abs(maxLeft); - var absRightMin = Math.abs(minRight); - var absRightMax = Math.abs(maxRight); - var upLeft = _.max([absLeftMin, absLeftMax]); - var downLeft = _.min([absLeftMin, absLeftMax]); - var upRight = _.max([absRightMin, absRightMax]); - var downRight = _.min([absRightMin, absRightMax]); - - rateLeft = downLeft ? upLeft / downLeft : upLeft; - rateRight = downRight ? upRight / downRight : upRight; - } else { - if (minLeft > 0 || minRight > 0) { - rateLeft = maxLeft / maxRight; - rateRight = 0; - } else { - rateLeft = 0; - rateRight = minLeft / minRight; - } - } - } - rate = rateLeft > rateRight ? rateLeft : rateRight; + var rate = getRate(yaxis); if (oneSide) { - if (minLeft > 0) { - minLeft = maxLeft / rate; - minRight = maxRight / rate; + if (yaxis[0].min > 0) { + yaxis[0].min = yaxis[0].max / rate; + yaxis[1].min = yaxis[1].max / rate; } else { - maxLeft = minLeft / rate; - maxRight = minRight / rate; + yaxis[0].max = yaxis[0].min / rate; + yaxis[1].max = yaxis[1].min / rate; } } else { - if (twoCross) { - minLeft = minRight ? minRight * rate : minLeft; - minRight = minLeft ? minLeft / rate : minRight; - maxLeft = maxRight ? maxRight * rate : maxLeft; - maxRight = maxLeft ? maxLeft / rate : maxRight; + if (checkTwoCross(yaxis)) { + yaxis[0].min = yaxis[1].min ? yaxis[1].min * rate : yaxis[0].min; + yaxis[1].min = yaxis[0].min ? yaxis[0].min / rate : yaxis[1].min; + yaxis[0].max = yaxis[1].max ? yaxis[1].max * rate : yaxis[0].max; + yaxis[1].max = yaxis[0].max ? yaxis[0].max / rate : yaxis[1].max; } else { - minLeft = minLeft > 0 ? minRight * rate : minLeft; - minRight = minRight > 0 ? minLeft / rate : minRight; - maxLeft = maxLeft < 0 ? maxRight * rate : maxLeft; - maxRight = maxRight < 0 ? maxLeft / rate : maxRight; + yaxis[0].min = yaxis[0].min > 0 ? yaxis[1].min * rate : yaxis[0].min; + yaxis[1].min = yaxis[1].min > 0 ? yaxis[0].min / rate : yaxis[1].min; + yaxis[0].max = yaxis[0].max < 0 ? yaxis[1].max * rate : yaxis[0].max; + yaxis[1].max = yaxis[1].max < 0 ? yaxis[0].max / rate : yaxis[1].max; } } } } + restoreLevelFromZero(yaxis, alignLevel); +} + +function expandStuckValues(yaxis) { + // wide Y min and max using increased wideFactor + var wideFactor = 0.25; + if (yaxis[0].max === yaxis[0].min) { + yaxis[0].min -= wideFactor; + yaxis[0].max += wideFactor; + } + if (yaxis[1].max === yaxis[1].min) { + yaxis[1].min -= wideFactor; + yaxis[1].max += wideFactor; + } +} + +function moveLevelToZero(yaxis, alignLevel) { if (alignLevel !== 0) { - minLeft += alignLevel; - maxLeft += alignLevel; - minRight += alignLevel; - maxRight += alignLevel; + yaxis[0].min -= alignLevel; + yaxis[0].max -= alignLevel; + yaxis[1].min -= alignLevel; + yaxis[1].max -= alignLevel; + } +} + +function restoreLevelFromZero(yaxis, alignLevel) { + if (alignLevel !== 0) { + yaxis[0].min += alignLevel; + yaxis[0].max += alignLevel; + yaxis[1].min += alignLevel; + yaxis[1].max += alignLevel; + } +} + +function checkOneSide(yaxis) { + // on the one hand with respect to zero + return (yaxis[0].min >= 0 && yaxis[1].min >= 0) || (yaxis[0].max <= 0 && yaxis[1].max <= 0); +} + +function checkTwoCross(yaxis) { + // both across zero + return yaxis[0].min <= 0 && yaxis[0].max >= 0 && yaxis[1].min <= 0 && yaxis[1].max >= 0; +} + +function getRate(yaxis) { + var rateLeft, rateRight, rate; + if (checkTwoCross(yaxis)) { + rateLeft = yaxis[1].min ? yaxis[0].min / yaxis[1].min : 0; + rateRight = yaxis[1].max ? yaxis[0].max / yaxis[1].max : 0; + } else { + if (checkOneSide(yaxis)) { + var absLeftMin = Math.abs(yaxis[0].min); + var absLeftMax = Math.abs(yaxis[0].max); + var absRightMin = Math.abs(yaxis[1].min); + var absRightMax = Math.abs(yaxis[1].max); + var upLeft = _.max([absLeftMin, absLeftMax]); + var downLeft = _.min([absLeftMin, absLeftMax]); + var upRight = _.max([absRightMin, absRightMax]); + var downRight = _.min([absRightMin, absRightMax]); + + rateLeft = downLeft ? upLeft / downLeft : upLeft; + rateRight = downRight ? upRight / downRight : upRight; + } else { + if (yaxis[0].min > 0 || yaxis[1].min > 0) { + rateLeft = yaxis[0].max / yaxis[1].max; + rateRight = 0; + } else { + rateLeft = 0; + rateRight = yaxis[0].min / yaxis[1].min; + } + } } - yaxis[0].min = minLeft; - yaxis[0].max = maxLeft; - yaxis[1].min = minRight; - yaxis[1].max = maxRight; + rate = rateLeft > rateRight ? rateLeft : rateRight; + + return rate; } From 1d190de91800223c732a0ebc57e6c71921f2e0c4 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 12 Mar 2018 11:58:47 +0100 Subject: [PATCH 0065/3000] added test for sorting and filtering --- .../dashboard/dashgrid/AddPanelPanel.jest.tsx | 100 +++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx index e68d84ad8bb..be7659ae030 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.jest.tsx @@ -1,4 +1,102 @@ import React from 'react'; import { AddPanelPanel } from './AddPanelPanel'; +import { PanelModel } from '../panel_model'; +import { shallow } from 'enzyme'; +import config from '../../../core/config'; -describe('AddPanelPanel', () => {}); +jest.mock('app/core/store', () => ({ + get: key => { + return null; + }, + delete: key => { + return null; + }, +})); + +describe('AddPanelPanel', () => { + let wrapper, dashboardMock, getPanelContainer, panel; + + beforeEach(() => { + config.panels = [ + { + id: 'singlestat', + hideFromList: false, + name: 'Singlestat', + sort: 2, + info: { + logos: { + small: '', + }, + }, + }, + { + id: 'hidden', + hideFromList: true, + name: 'Hidden', + sort: 100, + info: { + logos: { + small: '', + }, + }, + }, + { + id: 'graph', + hideFromList: false, + name: 'Graph', + sort: 1, + info: { + logos: { + small: '', + }, + }, + }, + { + id: 'alexander_zabbix', + hideFromList: false, + name: 'Zabbix', + sort: 100, + info: { + logos: { + small: '', + }, + }, + }, + { + id: 'piechart', + hideFromList: false, + name: 'Piechart', + sort: 100, + info: { + logos: { + small: '', + }, + }, + }, + ]; + + dashboardMock = { toggleRow: jest.fn() }; + + getPanelContainer = jest.fn().mockReturnValue({ + getDashboard: jest.fn().mockReturnValue(dashboardMock), + getPanelLoader: jest.fn(), + }); + + panel = new PanelModel({ collapsed: false }); + wrapper = shallow(); + }); + + it('should fetch all panels sorted with core plugins first', () => { + //console.log(wrapper.debug()); + //console.log(wrapper.find('.add-panel__item').get(0).props.title); + expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Singlestat'); + expect(wrapper.find('.add-panel__item').get(4).props.title).toBe('Piechart'); + }); + + it('should filter', () => { + wrapper.find('input').simulate('change', { target: { value: 'p' } }); + + expect(wrapper.find('.add-panel__item').get(1).props.title).toBe('Piechart'); + expect(wrapper.find('.add-panel__item').get(0).props.title).toBe('Graph'); + }); +}); From 11ae926388ff80e3caefdc6812a7e651ad3b7ed5 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Mon, 12 Mar 2018 23:11:11 +0500 Subject: [PATCH 0066/3000] Rename test file according module name. --- .../panel/graph/specs/{align_y.jest.ts => align_yaxes.jest.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename public/app/plugins/panel/graph/specs/{align_y.jest.ts => align_yaxes.jest.ts} (100%) diff --git a/public/app/plugins/panel/graph/specs/align_y.jest.ts b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts similarity index 100% rename from public/app/plugins/panel/graph/specs/align_y.jest.ts rename to public/app/plugins/panel/graph/specs/align_yaxes.jest.ts From 8c82e5701c41f828f7d7770fb8177b258afd231f Mon Sep 17 00:00:00 2001 From: ilgizar Date: Mon, 12 Mar 2018 23:12:45 +0500 Subject: [PATCH 0067/3000] Replaced array values to variables yLeft and yRight for easy reading code. --- public/app/plugins/panel/graph/align_yaxes.ts | 134 +++++++++--------- 1 file changed, 70 insertions(+), 64 deletions(-) diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts index 4884cd12441..b60d75e7b66 100644 --- a/public/app/plugins/panel/graph/align_yaxes.ts +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -6,112 +6,118 @@ import _ from 'lodash'; * @param align Y level */ export function alignYLevel(yaxis, alignLevel) { - moveLevelToZero(yaxis, alignLevel); + var [yLeft, yRight] = yaxis; + moveLevelToZero(yLeft, yRight, alignLevel); - expandStuckValues(yaxis); + expandStuckValues(yLeft, yRight); // one of graphs on zero - var zero = yaxis[0].min === 0 || yaxis[1].min === 0 || yaxis[0].max === 0 || yaxis[1].max === 0; + var zero = yLeft.min === 0 || yRight.min === 0 || yLeft.max === 0 || yRight.max === 0; - var oneSide = checkOneSide(yaxis); + var oneSide = checkOneSide(yLeft, yRight); if (zero && oneSide) { - yaxis[0].min = yaxis[0].max > 0 ? 0 : yaxis[0].min; - yaxis[0].max = yaxis[0].max > 0 ? yaxis[0].max : 0; - yaxis[1].min = yaxis[1].max > 0 ? 0 : yaxis[1].min; - yaxis[1].max = yaxis[1].max > 0 ? yaxis[1].max : 0; + yLeft.min = yLeft.max > 0 ? 0 : yLeft.min; + yLeft.max = yLeft.max > 0 ? yLeft.max : 0; + yRight.min = yRight.max > 0 ? 0 : yRight.min; + yRight.max = yRight.max > 0 ? yRight.max : 0; } else { - // on the opposite sides with respect to zero - if ((yaxis[0].min >= 0 && yaxis[1].max <= 0) || (yaxis[0].max <= 0 && yaxis[1].min >= 0)) { - if (yaxis[0].min >= 0) { - yaxis[0].min = -yaxis[0].max; - yaxis[1].max = -yaxis[1].min; + if (checkOppositeSides(yLeft, yRight)) { + if (yLeft.min >= 0) { + yLeft.min = -yLeft.max; + yRight.max = -yRight.min; } else { - yaxis[0].max = -yaxis[0].min; - yaxis[1].min = -yaxis[1].max; + yLeft.max = -yLeft.min; + yRight.min = -yRight.max; } } else { - var rate = getRate(yaxis); + var rate = getRate(yLeft, yRight); if (oneSide) { - if (yaxis[0].min > 0) { - yaxis[0].min = yaxis[0].max / rate; - yaxis[1].min = yaxis[1].max / rate; + // all graphs above the Y level + if (yLeft.min > 0) { + yLeft.min = yLeft.max / rate; + yRight.min = yRight.max / rate; } else { - yaxis[0].max = yaxis[0].min / rate; - yaxis[1].max = yaxis[1].min / rate; + yLeft.max = yLeft.min / rate; + yRight.max = yRight.min / rate; } } else { - if (checkTwoCross(yaxis)) { - yaxis[0].min = yaxis[1].min ? yaxis[1].min * rate : yaxis[0].min; - yaxis[1].min = yaxis[0].min ? yaxis[0].min / rate : yaxis[1].min; - yaxis[0].max = yaxis[1].max ? yaxis[1].max * rate : yaxis[0].max; - yaxis[1].max = yaxis[0].max ? yaxis[0].max / rate : yaxis[1].max; + if (checkTwoCross(yLeft, yRight)) { + yLeft.min = yRight.min ? yRight.min * rate : yLeft.min; + yRight.min = yLeft.min ? yLeft.min / rate : yRight.min; + yLeft.max = yRight.max ? yRight.max * rate : yLeft.max; + yRight.max = yLeft.max ? yLeft.max / rate : yRight.max; } else { - yaxis[0].min = yaxis[0].min > 0 ? yaxis[1].min * rate : yaxis[0].min; - yaxis[1].min = yaxis[1].min > 0 ? yaxis[0].min / rate : yaxis[1].min; - yaxis[0].max = yaxis[0].max < 0 ? yaxis[1].max * rate : yaxis[0].max; - yaxis[1].max = yaxis[1].max < 0 ? yaxis[0].max / rate : yaxis[1].max; + yLeft.min = yLeft.min > 0 ? yRight.min * rate : yLeft.min; + yRight.min = yRight.min > 0 ? yLeft.min / rate : yRight.min; + yLeft.max = yLeft.max < 0 ? yRight.max * rate : yLeft.max; + yRight.max = yRight.max < 0 ? yLeft.max / rate : yRight.max; } } } } - restoreLevelFromZero(yaxis, alignLevel); + restoreLevelFromZero(yLeft, yRight, alignLevel); } -function expandStuckValues(yaxis) { +function expandStuckValues(yLeft, yRight) { // wide Y min and max using increased wideFactor var wideFactor = 0.25; - if (yaxis[0].max === yaxis[0].min) { - yaxis[0].min -= wideFactor; - yaxis[0].max += wideFactor; + if (yLeft.max === yLeft.min) { + yLeft.min -= wideFactor; + yLeft.max += wideFactor; } - if (yaxis[1].max === yaxis[1].min) { - yaxis[1].min -= wideFactor; - yaxis[1].max += wideFactor; + if (yRight.max === yRight.min) { + yRight.min -= wideFactor; + yRight.max += wideFactor; } } -function moveLevelToZero(yaxis, alignLevel) { +function moveLevelToZero(yLeft, yRight, alignLevel) { if (alignLevel !== 0) { - yaxis[0].min -= alignLevel; - yaxis[0].max -= alignLevel; - yaxis[1].min -= alignLevel; - yaxis[1].max -= alignLevel; + yLeft.min -= alignLevel; + yLeft.max -= alignLevel; + yRight.min -= alignLevel; + yRight.max -= alignLevel; } } -function restoreLevelFromZero(yaxis, alignLevel) { +function restoreLevelFromZero(yLeft, yRight, alignLevel) { if (alignLevel !== 0) { - yaxis[0].min += alignLevel; - yaxis[0].max += alignLevel; - yaxis[1].min += alignLevel; - yaxis[1].max += alignLevel; + yLeft.min += alignLevel; + yLeft.max += alignLevel; + yRight.min += alignLevel; + yRight.max += alignLevel; } } -function checkOneSide(yaxis) { +function checkOneSide(yLeft, yRight) { // on the one hand with respect to zero - return (yaxis[0].min >= 0 && yaxis[1].min >= 0) || (yaxis[0].max <= 0 && yaxis[1].max <= 0); + return (yLeft.min >= 0 && yRight.min >= 0) || (yLeft.max <= 0 && yRight.max <= 0); } -function checkTwoCross(yaxis) { +function checkTwoCross(yLeft, yRight) { // both across zero - return yaxis[0].min <= 0 && yaxis[0].max >= 0 && yaxis[1].min <= 0 && yaxis[1].max >= 0; + return yLeft.min <= 0 && yLeft.max >= 0 && yRight.min <= 0 && yRight.max >= 0; } -function getRate(yaxis) { +function checkOppositeSides(yLeft, yRight) { + // on the opposite sides with respect to zero + return (yLeft.min >= 0 && yRight.max <= 0) || (yLeft.max <= 0 && yRight.min >= 0); +} + +function getRate(yLeft, yRight) { var rateLeft, rateRight, rate; - if (checkTwoCross(yaxis)) { - rateLeft = yaxis[1].min ? yaxis[0].min / yaxis[1].min : 0; - rateRight = yaxis[1].max ? yaxis[0].max / yaxis[1].max : 0; + if (checkTwoCross(yLeft, yRight)) { + rateLeft = yRight.min ? yLeft.min / yRight.min : 0; + rateRight = yRight.max ? yLeft.max / yRight.max : 0; } else { - if (checkOneSide(yaxis)) { - var absLeftMin = Math.abs(yaxis[0].min); - var absLeftMax = Math.abs(yaxis[0].max); - var absRightMin = Math.abs(yaxis[1].min); - var absRightMax = Math.abs(yaxis[1].max); + if (checkOneSide(yLeft, yRight)) { + var absLeftMin = Math.abs(yLeft.min); + var absLeftMax = Math.abs(yLeft.max); + var absRightMin = Math.abs(yRight.min); + var absRightMax = Math.abs(yRight.max); var upLeft = _.max([absLeftMin, absLeftMax]); var downLeft = _.min([absLeftMin, absLeftMax]); var upRight = _.max([absRightMin, absRightMax]); @@ -120,12 +126,12 @@ function getRate(yaxis) { rateLeft = downLeft ? upLeft / downLeft : upLeft; rateRight = downRight ? upRight / downRight : upRight; } else { - if (yaxis[0].min > 0 || yaxis[1].min > 0) { - rateLeft = yaxis[0].max / yaxis[1].max; + if (yLeft.min > 0 || yRight.min > 0) { + rateLeft = yLeft.max / yRight.max; rateRight = 0; } else { rateLeft = 0; - rateRight = yaxis[0].min / yaxis[1].min; + rateRight = yLeft.min / yRight.min; } } } From 7dd66450adc6dea4ac499c1027937160997f64b4 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Mon, 12 Mar 2018 23:43:13 +0500 Subject: [PATCH 0068/3000] Corrected work for graphs created before this feature. --- public/app/plugins/panel/graph/graph.ts | 2 +- public/vendor/flot/jquery.flot.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 3f7d1bee33c..231b1ecaf42 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -158,7 +158,7 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && panel.yaxes[1].align !== null) { + if (yaxis.length > 1 && 'align' in panel.yaxes[1] && panel.yaxes[1].align !== null) { alignYLevel(yaxis, parseFloat(panel.yaxes[1].align)); } } diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 401198b712d..8ee09e25c41 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1633,7 +1633,7 @@ Licensed under the MIT license. measureTickLabels(axis); }); - if (snaped) { + if (snaped && hooks.processRange.length > 0) { executeHooks(hooks.processRange, []); snaped = false; } else { From cdb4e2ba0b44741ed9efc09fbf060a7dd5d0c6ac Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 21:31:07 +0100 Subject: [PATCH 0069/3000] remove unused setting --- public/app/plugins/datasource/postgres/postgres_query.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 9c48a94862f..f9b87e89541 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -21,7 +21,6 @@ export default class PostgresQuery { target.timeColumn = target.timeColumn || 'time'; target.metricColumn = target.metricColumn || 'None'; - target.orderByTime = target.orderByTime || 'ASC'; target.groupBy = target.groupBy || []; target.where = target.where || []; target.select = target.select || [[{ type: 'column', params: ['value'] }]]; From af63a26be0a16acbada306fa053386f93e342af7 Mon Sep 17 00:00:00 2001 From: flopp999 <21694965+flopp999@users.noreply.github.com> Date: Tue, 13 Mar 2018 22:11:58 +0100 Subject: [PATCH 0070/3000] Added W/m2(energy) and l/h(flow) both as .fixedUnit --- public/app/core/utils/kbn.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 3b78ccfc001..3f2f0ad9419 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -496,6 +496,7 @@ kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W'); kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1); kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1); kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1); +kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m2'); kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA'); kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1); kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var'); @@ -576,6 +577,7 @@ kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm'); kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms'); kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); +kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h'); // Angle kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°'); @@ -1007,6 +1009,7 @@ kbn.getUnitFormats = function() { { text: 'Watt (W)', value: 'watt' }, { text: 'Kilowatt (kW)', value: 'kwatt' }, { text: 'Milliwatt (mW)', value: 'mwatt' }, + { text: 'Watt per square metre (W/m2)', value: 'Wm2' }, { text: 'Volt-ampere (VA)', value: 'voltamp' }, { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' }, { text: 'Volt-ampere reactive (var)', value: 'voltampreact' }, @@ -1062,6 +1065,7 @@ kbn.getUnitFormats = function() { { text: 'Cubic meters/sec (cms)', value: 'flowcms' }, { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' }, { text: 'Cubic feet/min (cfm)', value: 'flowcfm' }, + { text: 'Litre/hour', value: 'litreh' }, ], }, { From 08461408a279afa9af2bd8b746d474de373fd6fa Mon Sep 17 00:00:00 2001 From: flopp999 <21694965+flopp999@users.noreply.github.com> Date: Tue, 13 Mar 2018 22:17:56 +0100 Subject: [PATCH 0071/3000] Added Kilopascals(kPa) under pressure --- public/app/core/utils/kbn.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 3f2f0ad9419..87075e0de2e 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -526,6 +526,7 @@ kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar'); kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1); kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1); kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa'); +kbn.valueFormats.pressurekpa = kbn.formatBuilders.fixedUnit('kPa'); kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg'); kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']); @@ -1045,6 +1046,7 @@ kbn.getUnitFormats = function() { { text: 'Bars', value: 'pressurebar' }, { text: 'Kilobars', value: 'pressurekbar' }, { text: 'Hectopascals', value: 'pressurehpa' }, + { text: 'Kilopascals', value: 'pressurekpa' }, { text: 'Inches of mercury', value: 'pressurehg' }, { text: 'PSI', value: 'pressurepsi' }, ], From b7c7030a4681e3c5c8d3e565588a02e9e8a2e9db Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:06:39 +0100 Subject: [PATCH 0072/3000] add regex operators --- .../datasource/postgres/query_builder.ts | 8 ++++++++ .../plugins/datasource/postgres/query_ctrl.ts | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts index 7a227f33b93..23691830b17 100644 --- a/public/app/plugins/datasource/postgres/query_builder.ts +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -47,4 +47,12 @@ export class PostgresQueryBuilder { return query; } + buildDatatypeQuery(column: string) { + var query = "SELECT data_type FROM information_schema.columns WHERE "; + query += " table_schema = " + this.queryModel.quoteLiteral(this.target.schema); + query += " AND table_name = " + this.queryModel.quoteLiteral(this.target.table); + query += " AND column_name = " + this.queryModel.quoteLiteral(column); + return query; + } + } diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index afde342474b..b84b0ce0750 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -266,14 +266,28 @@ export class PostgresQueryCtrl extends QueryCtrl { } getWhereSegments(segment, index) { + var query, addTemplateVars; + if (segment.type === 'condition') { return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]); } if (segment.type === 'operator') { - return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>'])); + var columnName = this.whereSegments[index - 1].value; + query = this.queryBuilder.buildDatatypeQuery(columnName); + return this.datasource.metricFindQuery(query) + .then(results => { + var datatype = results[0].text; + switch (datatype) { + case "text": + case "character varying": + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '~', '~*','!~','!~*','IN'])); + default: + return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<', '<=', '>', '>='])); + } + }) + .catch(this.handleQueryError.bind(this)); } - var query, addTemplateVars; if (segment.type === 'key' || segment.type === 'plus-button') { query = this.queryBuilder.buildColumnQuery(); From 958646d976ad64a3b32707c2a7b686a906cd9fc1 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:15:25 +0100 Subject: [PATCH 0073/3000] dont quote where constraints --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index f9b87e89541..48f7efae760 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -137,7 +137,7 @@ export default class PostgresQuery { value = this.templateSrv.replace(value, this.scopedVars); } - return str + this.quoteIdentifier(constraint.key) + ' ' + operator + ' ' + this.quoteLiteral(value); + return str + constraint.key + ' ' + operator + ' ' + value; } interpolateQueryStr(value, variable, defaultFormatFn) { From 5e9a66de5f36c8e29ebeb5370f503529ac2dd7c7 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:19:56 +0100 Subject: [PATCH 0074/3000] put values for IN in parens --- public/app/plugins/datasource/postgres/postgres_query.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 48f7efae760..e06cb3f2126 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -137,7 +137,11 @@ export default class PostgresQuery { value = this.templateSrv.replace(value, this.scopedVars); } - return str + constraint.key + ' ' + operator + ' ' + value; + if (operator === "IN") { + return str + constraint.key + ' ' + operator + ' (' + value + ')'; + } else { + return str + constraint.key + ' ' + operator + ' ' + value; + } } interpolateQueryStr(value, variable, defaultFormatFn) { From e6501f0f0ecec19ccafd9191bcfcdef4a1e19c6f Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:24:26 +0100 Subject: [PATCH 0075/3000] revert special handling for IN --- public/app/plugins/datasource/postgres/postgres_query.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index e06cb3f2126..48f7efae760 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -137,11 +137,7 @@ export default class PostgresQuery { value = this.templateSrv.replace(value, this.scopedVars); } - if (operator === "IN") { - return str + constraint.key + ' ' + operator + ' (' + value + ')'; - } else { - return str + constraint.key + ' ' + operator + ' ' + value; - } + return str + constraint.key + ' ' + operator + ' ' + value; } interpolateQueryStr(value, variable, defaultFormatFn) { From 6793fa5e549dfe31c04dd3dacfa476a9091d3f3a Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Tue, 13 Mar 2018 23:33:40 +0100 Subject: [PATCH 0076/3000] join multivalue variables with , --- public/app/plugins/datasource/postgres/postgres_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 48f7efae760..3a11c344f3e 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -151,7 +151,7 @@ export default class PostgresQuery { } var escapedValues = _.map(value, kbn.regexEscape); - return '(' + escapedValues.join('|') + ')'; + return '(' + escapedValues.join(',') + ')'; } render(interpolate?) { From 64fa1ce8a0a3bfe6b7bb4e3c7cbc4227d0c42af4 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 14 Mar 2018 18:09:47 +0100 Subject: [PATCH 0077/3000] properly handle IN queries --- .../app/plugins/datasource/postgres/postgres_query.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 3a11c344f3e..8f9f1261340 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -1,6 +1,5 @@ import _ from 'lodash'; import queryPart from './query_part'; -import kbn from 'app/core/utils/kbn'; export default class PostgresQuery { target: any; @@ -26,14 +25,16 @@ export default class PostgresQuery { target.select = target.select || [[{ type: 'column', params: ['value'] }]]; this.updateProjection(); + // give interpolateQueryStr access to this + this.interpolateQueryStr = this.interpolateQueryStr.bind(this); } quoteIdentifier(value) { - return '"' + value + '"'; + return '"' + value.replace('"','""') + '"'; } quoteLiteral(value) { - return "'" + value + "'"; + return "'" + value.replace("'","''") + "'"; } updateProjection() { @@ -147,10 +148,10 @@ export default class PostgresQuery { } if (typeof value === 'string') { - return kbn.regexEscape(value); + return this.quoteLiteral(value); } - var escapedValues = _.map(value, kbn.regexEscape); + var escapedValues = _.map(value, this.quoteLiteral); return '(' + escapedValues.join(',') + ')'; } From 9d7ab78d9f0c38c143f8cdf1fda0644897493e12 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Wed, 14 Mar 2018 23:39:05 +0500 Subject: [PATCH 0078/3000] Resolved conflict --- public/app/plugins/panel/graph/graph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 231b1ecaf42..bbfe63a87b2 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -17,7 +17,7 @@ import { appEvents, coreModule, updateLegendValues } from 'app/core/core'; import GraphTooltip from './graph_tooltip'; import { ThresholdManager } from './threshold_manager'; import { EventManager } from 'app/features/annotations/all'; -import { convertValuesToHistogram, getSeriesValues } from './histogram'; +import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; import config from 'app/core/config'; From 0c3afd0e9c098c83f04f44f048f72468496aec5c Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 14 Mar 2018 22:59:48 +0100 Subject: [PATCH 0079/3000] add buildAggregateQuery --- public/app/plugins/datasource/postgres/query_builder.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/app/plugins/datasource/postgres/query_builder.ts b/public/app/plugins/datasource/postgres/query_builder.ts index 23691830b17..275d15492fe 100644 --- a/public/app/plugins/datasource/postgres/query_builder.ts +++ b/public/app/plugins/datasource/postgres/query_builder.ts @@ -55,4 +55,12 @@ export class PostgresQueryBuilder { return query; } + buildAggregateQuery() { + var query = "SELECT DISTINCT proname FROM pg_aggregate "; + query += "INNER JOIN pg_proc ON pg_aggregate.aggfnoid = pg_proc.oid "; + query += "INNER JOIN pg_type ON pg_type.oid=pg_proc.prorettype "; + query += "WHERE pronargs=1 AND typname IN ('int8','float8') AND aggkind='n' ORDER BY 1"; + return query; + } + } From 46c229188e985f74ed35d7505c21d9ec723d7b99 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 14 Mar 2018 23:03:32 +0100 Subject: [PATCH 0080/3000] read aggregate functions from database --- .../datasource/postgres/postgres_query.ts | 3 ++- .../plugins/datasource/postgres/query_ctrl.ts | 11 +++++++++++ .../plugins/datasource/postgres/query_part.ts | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 8f9f1261340..4516bf4a4be 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -24,9 +24,10 @@ export default class PostgresQuery { target.where = target.where || []; target.select = target.select || [[{ type: 'column', params: ['value'] }]]; - this.updateProjection(); // give interpolateQueryStr access to this this.interpolateQueryStr = this.interpolateQueryStr.bind(this); + + this.updateProjection(); } quoteIdentifier(value) { diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index b84b0ce0750..4a2a0ce6c1c 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -77,9 +77,19 @@ export class PostgresQueryCtrl extends QueryCtrl { }); this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope); this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope); + } buildSelectMenu() { + + if (!queryPart.hasAggregates()) { + this.datasource.metricFindQuery(this.queryBuilder.buildAggregateQuery()) + .then(results => { + queryPart.clearAggregates(); + _.map(results, segment => { queryPart.registerAggregate(segment.text); }); + }) + .catch(this.handleQueryError.bind(this)); + } var categories = queryPart.getCategories(); this.selectMenu = _.reduce( categories, @@ -279,6 +289,7 @@ export class PostgresQueryCtrl extends QueryCtrl { var datatype = results[0].text; switch (datatype) { case "text": + case "character": case "character varying": return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '~', '~*','!~','!~*','IN'])); default: diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 600723be00d..b63ebfae0c1 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -23,6 +23,17 @@ function register(options: any) { options.category.push(index[options.type]); } +function registerAggregate(name: string) { + register({ + type: name, + addStrategy: replaceAggregationAddStrategy, + category: categories.Aggregations, + params: [], + defaultParams: [], + renderer: functionRenderer, + }); +} + var groupByTimeFunctions = []; function aliasRenderer(part, innerExpr) { @@ -192,6 +203,12 @@ register({ export default { create: createPart, + registerAggregate: registerAggregate, + clearAggregates: function() { categories.Aggregations = []; }, + hasAggregates: function() { + // FIXME + return categories.Aggregations.length > 6; + }, getCategories: function() { return categories; }, From 7a7e40739349edb8a65719cd2fa2f1bba4592587 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 15 Mar 2018 00:02:38 +0100 Subject: [PATCH 0081/3000] fix lint problems From cae9c28f7031ff686a78b5c0a910c9532d375b8b Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 15 Mar 2018 00:03:47 +0100 Subject: [PATCH 0082/3000] fix lint problems --- public/sass/_variables.scss | 29 +- public/sass/base/_forms.scss | 44 +- public/sass/base/_grafana_icons.scss | 123 +- public/sass/base/_normalize.scss | 20 +- public/sass/base/_reboot.scss | 8 +- public/sass/base/font-awesome/_core.scss | 3 +- public/sass/base/font-awesome/_mixins.scss | 7 +- public/sass/base/font-awesome/_path.scss | 19 +- public/sass/base/font-awesome/_variables.scss | 1576 ++++++++--------- public/sass/components/_drop.scss | 12 +- public/sass/components/_filter-list.scss | 6 +- public/sass/components/_footer.scss | 4 +- public/sass/components/_json_explorer.scss | 6 +- public/sass/components/_jsontree.scss | 4 +- .../components/_panel_gettingstarted.scss | 4 +- public/sass/components/_row.scss | 2 +- public/sass/components/_shortcuts.scss | 2 +- public/sass/components/_switch.scss | 8 +- public/sass/components/_tabs.scss | 9 +- public/sass/components/_timepicker.scss | 4 +- public/sass/grafana.dark.scss | 6 +- public/sass/mixins/_drop_element.scss | 52 +- public/sass/mixins/_forms.scss | 3 +- public/sass/mixins/_mixins.scss | 63 +- public/sass/pages/_login.scss | 2 +- public/sass/pages/_playlist.scss | 4 +- public/sass/utils/_validation.scss | 2 +- public/test/core/utils/version_specs.ts | 36 +- public/test/jest-shim.ts | 3 +- public/test/mocks/backend_srv.ts | 5 +- public/test/specs/helpers.ts | 22 +- .../bradfitz/gomemcache/memcache/memcache.go | 2 +- .../github.com/hashicorp/go-plugin/client.go | 2 +- .../hashicorp/go-plugin/rpc_client.go | 2 +- .../hashicorp/go-plugin/rpc_server.go | 2 +- .../sergi/go-diff/diffmatchpatch/diff.go | 26 +- .../sergi/go-diff/diffmatchpatch/patch.go | 4 +- vendor/golang.org/x/net/http2/transport.go | 4 +- vendor/golang.org/x/text/language/gen.go | 2 +- vendor/golang.org/x/text/language/lookup.go | 56 +- vendor/golang.org/x/text/language/tables.go | 6 +- vendor/golang.org/x/text/unicode/cldr/cldr.go | 2 +- .../golang.org/x/text/unicode/cldr/resolve.go | 4 +- .../golang.org/x/text/unicode/cldr/slice.go | 2 +- .../x/text/unicode/norm/maketables.go | 2 +- vendor/gopkg.in/macaron.v1/context.go | 2 +- 46 files changed, 1144 insertions(+), 1062 deletions(-) diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 5a5f495470d..f46cacb0dd1 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -53,9 +53,9 @@ $enable-flex: true; // Typography // ------------------------- -$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif; -$font-family-serif: Georgia, 'Times New Roman', Times, serif; -$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace; +$font-family-sans-serif: "Roboto", Helvetica, Arial, sans-serif; +$font-family-serif: Georgia, "Times New Roman", Times, serif; +$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; $font-family-base: $font-family-sans-serif !default; $font-size-root: 14px !default; @@ -90,7 +90,7 @@ $lead-font-size: 1.25rem !default; $lead-font-weight: 300 !default; $headings-margin-bottom: ($spacer / 2) !default; -$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; +$headings-font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; $headings-font-weight: 400 !default; $headings-line-height: 1.1 !default; @@ -152,9 +152,16 @@ $input-padding-y-sm: 4px !default; $input-padding-x-lg: 20px !default; $input-padding-y-lg: 10px !default; -$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default; -$input-height-lg: ( ($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2)) !default; -$input-height-sm: ( ($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2)) !default; +$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) + !default; +$input-height-lg: ( + ($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2) + ) + !default; +$input-height-sm: ( + ($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2) + ) + !default; $form-group-margin-bottom: $spacer-y !default; $gf-form-margin: 0.2rem; @@ -214,9 +221,9 @@ $panel-padding: 0px 10px 5px 10px; $tabs-padding: 10px 15px 9px; $external-services: ( - github: (bgColor: #464646, borderColor: #393939, icon: ''), - google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''), - grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''), - oauth: (bgColor: inherit, borderColor: #393939, icon: '') + github: (bgColor: #464646, borderColor: #393939, icon: ""), + google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ""), + grafanacom: (bgColor: inherit, borderColor: #393939, icon: ""), + oauth: (bgColor: inherit, borderColor: #393939, icon: "") ) !default; diff --git a/public/sass/base/_forms.scss b/public/sass/base/_forms.scss index 239bb8d4e1e..3197eb57991 100644 --- a/public/sass/base/_forms.scss +++ b/public/sass/base/_forms.scss @@ -58,19 +58,19 @@ textarea { } // Reset width of input images, buttons, radios, checkboxes -input[type='file'], -input[type='image'], -input[type='submit'], -input[type='reset'], -input[type='button'], -input[type='radio'], -input[type='checkbox'] { +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { width: auto; // Override of generic input selector } // Set the height of select and file controls to match text inputs select, -input[type='file'] { +input[type="file"] { height: $input-height; /* In IE7, the height of the select element cannot be changed by height, only font-size */ line-height: $input-height; } @@ -90,19 +90,19 @@ select[size] { // Focus for select, file, radio, and checkbox select:focus, -input[type='file']:focus, -input[type='radio']:focus, -input[type='checkbox']:focus { +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { @include tab-focus(); } // not a big fan of number fields -input[type='number']::-webkit-outer-spin-button, -input[type='number']::-webkit-inner-spin-button { +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } -input[type='number'] { +input[type="number"] { -moz-appearance: textfield; } // Placeholder @@ -155,15 +155,15 @@ textarea[readonly] { } // Explicitly reset the colors here -input[type='radio'][disabled], -input[type='checkbox'][disabled], -input[type='radio'][readonly], -input[type='checkbox'][readonly] { +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { cursor: $cursor-disabled; background-color: transparent; } -input[type='text'].input-fluid { +input[type="text"].input-fluid { width: 100%; box-sizing: border-box; padding: 10px; @@ -172,7 +172,7 @@ input[type='text'].input-fluid { height: 100%; } -input[type='checkbox'].cr1 { +input[type="checkbox"].cr1 { display: none; } @@ -194,7 +194,7 @@ label.cr1 { cursor: pointer; } -input[type='checkbox'].cr1:checked + label { +input[type="checkbox"].cr1:checked + label { background: url($checkboxImageUrl) 0px -18px no-repeat; } @@ -203,7 +203,7 @@ input[type='checkbox'].cr1:checked + label { display: block; overflow: hidden; padding-right: 10px; - input[type='text'] { + input[type="text"] { width: 100%; padding: 5px 6px; height: 100%; diff --git a/public/sass/base/_grafana_icons.scss b/public/sass/base/_grafana_icons.scss index 73574ff9efc..55e57f6d087 100644 --- a/public/sass/base/_grafana_icons.scss +++ b/public/sass/base/_grafana_icons.scss @@ -1,17 +1,18 @@ @font-face { - font-family: 'grafana-icons'; - src: url('../fonts/grafana-icons.eot?okx5td'); - src: url('../fonts/grafana-icons.eot?okx5td#iefix') format('embedded-opentype'), - url('../fonts/grafana-icons.ttf?okx5td') format('truetype'), - url('../fonts/grafana-icons.woff?okx5td') format('woff'), - url('../fonts/grafana-icons.svg?okx5td#grafana-icons') format('svg'); + font-family: "grafana-icons"; + src: url("../fonts/grafana-icons.eot?okx5td"); + src: url("../fonts/grafana-icons.eot?okx5td#iefix") + format("embedded-opentype"), + url("../fonts/grafana-icons.ttf?okx5td") format("truetype"), + url("../fonts/grafana-icons.woff?okx5td") format("woff"), + url("../fonts/grafana-icons.svg?okx5td#grafana-icons") format("svg"); font-weight: normal; font-style: normal; } .icon-gf { /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'grafana-icons' !important; + font-family: "grafana-icons" !important; speak: none; font-style: normal; font-weight: normal; @@ -36,165 +37,165 @@ } .icon-gf-raintank_wordmark:before { - content: '\e600'; + content: "\e600"; } .micon-gf-raintank_icn:before { - content: '\e601'; + content: "\e601"; } .icon-gf-raintank_r-icn:before { - content: '\e905'; + content: "\e905"; } .icon-gf-check-alt:before { - content: '\e603'; + content: "\e603"; } .icon-gf-check:before { - content: '\e604'; + content: "\e604"; } .icon-gf-collector:before { - content: '\e605'; + content: "\e605"; } .icon-gf-dashboard:before { - content: '\e606'; + content: "\e606"; } .icon-gf-panel:before { - content: '\e904'; + content: "\e904"; } .icon-gf-datasources:before { - content: '\e607'; + content: "\e607"; } .icon-gf-endpoint-tiny:before { - content: '\e608'; + content: "\e608"; } .icon-gf-endpoint:before { - content: '\e609'; + content: "\e609"; } .icon-gf-page:before { - content: '\e908'; + content: "\e908"; } .icon-gf-filter:before { - content: '\e60a'; + content: "\e60a"; } .icon-gf-status:before { - content: '\e60b'; + content: "\e60b"; } .icon-gf-monitoring:before { - content: '\e60c'; + content: "\e60c"; } .icon-gf-monitoring-tiny:before { - content: '\e620'; + content: "\e620"; } .icon-gf-jump-to-dashboard:before { - content: '\e60d'; + content: "\e60d"; } .icon-gf-warn, .icon-gf-warning:before { - content: '\e60e'; + content: "\e60e"; } .icon-gf-nodata:before { - content: '\e60f'; + content: "\e60f"; } .icon-gf-critical:before { - content: '\e610'; + content: "\e610"; } .icon-gf-crit:before { - content: '\e610'; + content: "\e610"; } .icon-gf-online:before { - content: '\e611'; + content: "\e611"; } .icon-gf-event-error:before { - content: '\e623'; + content: "\e623"; } .icon-gf-event:before { - content: '\e624'; + content: "\e624"; } .icon-gf-sadface:before { - content: '\e907'; + content: "\e907"; } .icon-gf-private-collector:before { - content: '\e612'; + content: "\e612"; } .icon-gf-alert:before { - content: '\e61f'; + content: "\e61f"; } .icon-gf-alert-disabled:before { - content: '\e621'; + content: "\e621"; } .icon-gf-refresh:before { - content: '\e613'; + content: "\e613"; } .icon-gf-save:before { - content: '\e614'; + content: "\e614"; } .icon-gf-share:before { - content: '\e616'; + content: "\e616"; } .icon-gf-star:before { - content: '\e617'; + content: "\e617"; } .icon-gf-search:before { - content: '\e618'; + content: "\e618"; } .icon-gf-settings:before { - content: '\e615'; + content: "\e615"; } .icon-gf-add:before { - content: '\e619'; + content: "\e619"; } .icon-gf-remove:before { - content: '\e61a'; + content: "\e61a"; } .icon-gf-video:before { - content: '\e61b'; + content: "\e61b"; } .icon-gf-bulk_action:before { - content: '\e61c'; + content: "\e61c"; } .icon-gf-grabber:before { - content: '\e90b'; + content: "\e90b"; } .icon-gf-users:before { - content: '\e622'; + content: "\e622"; } .icon-gf-globe:before { - content: '\e61d'; + content: "\e61d"; } .icon-gf-snapshot:before { - content: '\e61e'; + content: "\e61e"; } .icon-gf-play-grafana-icon:before { - content: '\e629'; + content: "\e629"; } .icon-gf-grafana-icon:before { - content: '\e625'; + content: "\e625"; } .icon-gf-email:before { - content: '\e628'; + content: "\e628"; } .icon-gf-stopwatch:before { - content: '\e626'; + content: "\e626"; } .icon-gf-skull:before { - content: '\e900'; + content: "\e900"; } .icon-gf-probe:before { - content: '\e901'; + content: "\e901"; } .icon-gf-apps:before { - content: '\e902'; + content: "\e902"; } .icon-gf-scale:before { - content: '\e906'; + content: "\e906"; } .icon-gf-pending:before { - content: '\e909'; + content: "\e909"; } .icon-gf-verified:before { - content: '\e90a'; + content: "\e90a"; } .icon-gf-worldping:before { - content: '\e627'; + content: "\e627"; } .icon-gf-grafana_wordmark:before { - content: '\e903'; + content: "\e903"; } diff --git a/public/sass/base/_normalize.scss b/public/sass/base/_normalize.scss index fff6ad95d7c..057f5c63602 100644 --- a/public/sass/base/_normalize.scss +++ b/public/sass/base/_normalize.scss @@ -291,9 +291,9 @@ select { // button, -html input[type='button'], -// 1 input[type='reset'], -input[type='submit'] { +html input[type="button"], +// 1 input[type="reset"], +input[type="submit"] { -webkit-appearance: button; // 2 cursor: pointer; // 3 } @@ -334,8 +334,8 @@ input { // 2. Remove excess padding in IE 8/9/10. // -input[type='checkbox'], -input[type='radio'] { +input[type="checkbox"], +input[type="radio"] { box-sizing: border-box; // 1 padding: 0; // 2 } @@ -346,8 +346,8 @@ input[type='radio'] { // decrement button to change from `default` to `text`. // -input[type='number']::-webkit-inner-spin-button, -input[type='number']::-webkit-outer-spin-button { +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { height: auto; } @@ -355,7 +355,7 @@ input[type='number']::-webkit-outer-spin-button { // Address `appearance` set to `searchfield` in Safari and Chrome. // -input[type='search'] { +input[type="search"] { -webkit-appearance: textfield; } @@ -365,8 +365,8 @@ input[type='search'] { // padding (and `textfield` appearance). // -input[type='search']::-webkit-search-cancel-button, -input[type='search']::-webkit-search-decoration { +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } diff --git a/public/sass/base/_reboot.scss b/public/sass/base/_reboot.scss index ccbb17a07b6..e34bdfdb9ed 100644 --- a/public/sass/base/_reboot.scss +++ b/public/sass/base/_reboot.scss @@ -87,7 +87,7 @@ body { // might still respond to pointer events. // // Credit: https://github.com/suitcss/base -[tabindex='-1']:focus { +[tabindex="-1"]:focus { outline: none !important; } @@ -214,7 +214,7 @@ img { // for traditionally non-focusable elements with role="button" // see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile -[role='button'] { +[role="button"] { cursor: pointer; } @@ -231,7 +231,7 @@ img { a, area, button, -[role='button'], +[role="button"], input, label, select, @@ -320,7 +320,7 @@ legend { // border: 0; } -input[type='search'] { +input[type="search"] { // This overrides the extra rounded corners on search inputs in iOS so that our // `.form-control` class can properly style them. Note that this cannot simply // be added to `.form-control` as it's not specific enough. For details, see diff --git a/public/sass/base/font-awesome/_core.scss b/public/sass/base/font-awesome/_core.scss index a5411f108af..9940b63463d 100644 --- a/public/sass/base/font-awesome/_core.scss +++ b/public/sass/base/font-awesome/_core.scss @@ -3,7 +3,8 @@ .#{$fa-css-prefix} { display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} + FontAwesome; // shortening font declaration font-size: inherit; // can't have font-size inherit on line above, so need to override text-rendering: auto; // optimizelegibility throws things off #1094 -webkit-font-smoothing: antialiased; diff --git a/public/sass/base/font-awesome/_mixins.scss b/public/sass/base/font-awesome/_mixins.scss index f2a92c1e922..19771009107 100644 --- a/public/sass/base/font-awesome/_mixins.scss +++ b/public/sass/base/font-awesome/_mixins.scss @@ -3,7 +3,8 @@ @mixin fa-icon() { display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} + FontAwesome; // shortening font declaration font-size: inherit; // can't have font-size inherit on line above, so need to override text-rendering: auto; // optimizelegibility throws things off #1094 -webkit-font-smoothing: antialiased; @@ -11,14 +12,14 @@ } @mixin fa-icon-rotate($degrees, $rotation) { - -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})'; + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; -webkit-transform: rotate($degrees); -ms-transform: rotate($degrees); transform: rotate($degrees); } @mixin fa-icon-flip($horiz, $vert, $rotation) { - -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)'; + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; -webkit-transform: scale($horiz, $vert); -ms-transform: scale($horiz, $vert); transform: scale($horiz, $vert); diff --git a/public/sass/base/font-awesome/_path.scss b/public/sass/base/font-awesome/_path.scss index d452de2170a..0316afa161d 100644 --- a/public/sass/base/font-awesome/_path.scss +++ b/public/sass/base/font-awesome/_path.scss @@ -2,13 +2,18 @@ * -------------------------- */ @font-face { - font-family: 'FontAwesome'; - src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); - src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), - url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), - url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), - url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), - url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); + font-family: "FontAwesome"; + src: url("#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}"); + src: url("#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}") + format("embedded-opentype"), + url("#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}") + format("woff2"), + url("#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}") + format("woff"), + url("#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}") + format("truetype"), + url("#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular") + format("svg"); // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts font-weight: normal; font-style: normal; diff --git a/public/sass/base/font-awesome/_variables.scss b/public/sass/base/font-awesome/_variables.scss index 1e91e831e66..a4bd3cd87b9 100644 --- a/public/sass/base/font-awesome/_variables.scss +++ b/public/sass/base/font-awesome/_variables.scss @@ -1,799 +1,799 @@ // Variables // -------------------------- -$fa-font-path: '../fonts' !default; +$fa-font-path: "../fonts" !default; $fa-font-size-base: 14px !default; $fa-line-height-base: 1 !default; //$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly $fa-css-prefix: fa !default; -$fa-version: '4.7.0' !default; +$fa-version: "4.7.0" !default; $fa-border-color: #eee !default; $fa-inverse: #fff !default; $fa-li-width: (30em / 14) !default; -$fa-var-500px: '\f26e'; -$fa-var-address-book: '\f2b9'; -$fa-var-address-book-o: '\f2ba'; -$fa-var-address-card: '\f2bb'; -$fa-var-address-card-o: '\f2bc'; -$fa-var-adjust: '\f042'; -$fa-var-adn: '\f170'; -$fa-var-align-center: '\f037'; -$fa-var-align-justify: '\f039'; -$fa-var-align-left: '\f036'; -$fa-var-align-right: '\f038'; -$fa-var-amazon: '\f270'; -$fa-var-ambulance: '\f0f9'; -$fa-var-american-sign-language-interpreting: '\f2a3'; -$fa-var-anchor: '\f13d'; -$fa-var-android: '\f17b'; -$fa-var-angellist: '\f209'; -$fa-var-angle-double-down: '\f103'; -$fa-var-angle-double-left: '\f100'; -$fa-var-angle-double-right: '\f101'; -$fa-var-angle-double-up: '\f102'; -$fa-var-angle-down: '\f107'; -$fa-var-angle-left: '\f104'; -$fa-var-angle-right: '\f105'; -$fa-var-angle-up: '\f106'; -$fa-var-apple: '\f179'; -$fa-var-archive: '\f187'; -$fa-var-area-chart: '\f1fe'; -$fa-var-arrow-circle-down: '\f0ab'; -$fa-var-arrow-circle-left: '\f0a8'; -$fa-var-arrow-circle-o-down: '\f01a'; -$fa-var-arrow-circle-o-left: '\f190'; -$fa-var-arrow-circle-o-right: '\f18e'; -$fa-var-arrow-circle-o-up: '\f01b'; -$fa-var-arrow-circle-right: '\f0a9'; -$fa-var-arrow-circle-up: '\f0aa'; -$fa-var-arrow-down: '\f063'; -$fa-var-arrow-left: '\f060'; -$fa-var-arrow-right: '\f061'; -$fa-var-arrow-up: '\f062'; -$fa-var-arrows: '\f047'; -$fa-var-arrows-alt: '\f0b2'; -$fa-var-arrows-h: '\f07e'; -$fa-var-arrows-v: '\f07d'; -$fa-var-asl-interpreting: '\f2a3'; -$fa-var-assistive-listening-systems: '\f2a2'; -$fa-var-asterisk: '\f069'; -$fa-var-at: '\f1fa'; -$fa-var-audio-description: '\f29e'; -$fa-var-automobile: '\f1b9'; -$fa-var-backward: '\f04a'; -$fa-var-balance-scale: '\f24e'; -$fa-var-ban: '\f05e'; -$fa-var-bandcamp: '\f2d5'; -$fa-var-bank: '\f19c'; -$fa-var-bar-chart: '\f080'; -$fa-var-bar-chart-o: '\f080'; -$fa-var-barcode: '\f02a'; -$fa-var-bars: '\f0c9'; -$fa-var-bath: '\f2cd'; -$fa-var-bathtub: '\f2cd'; -$fa-var-battery: '\f240'; -$fa-var-battery-0: '\f244'; -$fa-var-battery-1: '\f243'; -$fa-var-battery-2: '\f242'; -$fa-var-battery-3: '\f241'; -$fa-var-battery-4: '\f240'; -$fa-var-battery-empty: '\f244'; -$fa-var-battery-full: '\f240'; -$fa-var-battery-half: '\f242'; -$fa-var-battery-quarter: '\f243'; -$fa-var-battery-three-quarters: '\f241'; -$fa-var-bed: '\f236'; -$fa-var-beer: '\f0fc'; -$fa-var-behance: '\f1b4'; -$fa-var-behance-square: '\f1b5'; -$fa-var-bell: '\f0f3'; -$fa-var-bell-o: '\f0a2'; -$fa-var-bell-slash: '\f1f6'; -$fa-var-bell-slash-o: '\f1f7'; -$fa-var-bicycle: '\f206'; -$fa-var-binoculars: '\f1e5'; -$fa-var-birthday-cake: '\f1fd'; -$fa-var-bitbucket: '\f171'; -$fa-var-bitbucket-square: '\f172'; -$fa-var-bitcoin: '\f15a'; -$fa-var-black-tie: '\f27e'; -$fa-var-blind: '\f29d'; -$fa-var-bluetooth: '\f293'; -$fa-var-bluetooth-b: '\f294'; -$fa-var-bold: '\f032'; -$fa-var-bolt: '\f0e7'; -$fa-var-bomb: '\f1e2'; -$fa-var-book: '\f02d'; -$fa-var-bookmark: '\f02e'; -$fa-var-bookmark-o: '\f097'; -$fa-var-braille: '\f2a1'; -$fa-var-briefcase: '\f0b1'; -$fa-var-btc: '\f15a'; -$fa-var-bug: '\f188'; -$fa-var-building: '\f1ad'; -$fa-var-building-o: '\f0f7'; -$fa-var-bullhorn: '\f0a1'; -$fa-var-bullseye: '\f140'; -$fa-var-bus: '\f207'; -$fa-var-buysellads: '\f20d'; -$fa-var-cab: '\f1ba'; -$fa-var-calculator: '\f1ec'; -$fa-var-calendar: '\f073'; -$fa-var-calendar-check-o: '\f274'; -$fa-var-calendar-minus-o: '\f272'; -$fa-var-calendar-o: '\f133'; -$fa-var-calendar-plus-o: '\f271'; -$fa-var-calendar-times-o: '\f273'; -$fa-var-camera: '\f030'; -$fa-var-camera-retro: '\f083'; -$fa-var-car: '\f1b9'; -$fa-var-caret-down: '\f0d7'; -$fa-var-caret-left: '\f0d9'; -$fa-var-caret-right: '\f0da'; -$fa-var-caret-square-o-down: '\f150'; -$fa-var-caret-square-o-left: '\f191'; -$fa-var-caret-square-o-right: '\f152'; -$fa-var-caret-square-o-up: '\f151'; -$fa-var-caret-up: '\f0d8'; -$fa-var-cart-arrow-down: '\f218'; -$fa-var-cart-plus: '\f217'; -$fa-var-cc: '\f20a'; -$fa-var-cc-amex: '\f1f3'; -$fa-var-cc-diners-club: '\f24c'; -$fa-var-cc-discover: '\f1f2'; -$fa-var-cc-jcb: '\f24b'; -$fa-var-cc-mastercard: '\f1f1'; -$fa-var-cc-paypal: '\f1f4'; -$fa-var-cc-stripe: '\f1f5'; -$fa-var-cc-visa: '\f1f0'; -$fa-var-certificate: '\f0a3'; -$fa-var-chain: '\f0c1'; -$fa-var-chain-broken: '\f127'; -$fa-var-check: '\f00c'; -$fa-var-check-circle: '\f058'; -$fa-var-check-circle-o: '\f05d'; -$fa-var-check-square: '\f14a'; -$fa-var-check-square-o: '\f046'; -$fa-var-chevron-circle-down: '\f13a'; -$fa-var-chevron-circle-left: '\f137'; -$fa-var-chevron-circle-right: '\f138'; -$fa-var-chevron-circle-up: '\f139'; -$fa-var-chevron-down: '\f078'; -$fa-var-chevron-left: '\f053'; -$fa-var-chevron-right: '\f054'; -$fa-var-chevron-up: '\f077'; -$fa-var-child: '\f1ae'; -$fa-var-chrome: '\f268'; -$fa-var-circle: '\f111'; -$fa-var-circle-o: '\f10c'; -$fa-var-circle-o-notch: '\f1ce'; -$fa-var-circle-thin: '\f1db'; -$fa-var-clipboard: '\f0ea'; -$fa-var-clock-o: '\f017'; -$fa-var-clone: '\f24d'; -$fa-var-close: '\f00d'; -$fa-var-cloud: '\f0c2'; -$fa-var-cloud-download: '\f0ed'; -$fa-var-cloud-upload: '\f0ee'; -$fa-var-cny: '\f157'; -$fa-var-code: '\f121'; -$fa-var-code-fork: '\f126'; -$fa-var-codepen: '\f1cb'; -$fa-var-codiepie: '\f284'; -$fa-var-coffee: '\f0f4'; -$fa-var-cog: '\f013'; -$fa-var-cogs: '\f085'; -$fa-var-columns: '\f0db'; -$fa-var-comment: '\f075'; -$fa-var-comment-o: '\f0e5'; -$fa-var-commenting: '\f27a'; -$fa-var-commenting-o: '\f27b'; -$fa-var-comments: '\f086'; -$fa-var-comments-o: '\f0e6'; -$fa-var-compass: '\f14e'; -$fa-var-compress: '\f066'; -$fa-var-connectdevelop: '\f20e'; -$fa-var-contao: '\f26d'; -$fa-var-copy: '\f0c5'; -$fa-var-copyright: '\f1f9'; -$fa-var-creative-commons: '\f25e'; -$fa-var-credit-card: '\f09d'; -$fa-var-credit-card-alt: '\f283'; -$fa-var-crop: '\f125'; -$fa-var-crosshairs: '\f05b'; -$fa-var-css3: '\f13c'; -$fa-var-cube: '\f1b2'; -$fa-var-cubes: '\f1b3'; -$fa-var-cut: '\f0c4'; -$fa-var-cutlery: '\f0f5'; -$fa-var-dashboard: '\f0e4'; -$fa-var-dashcube: '\f210'; -$fa-var-database: '\f1c0'; -$fa-var-deaf: '\f2a4'; -$fa-var-deafness: '\f2a4'; -$fa-var-dedent: '\f03b'; -$fa-var-delicious: '\f1a5'; -$fa-var-desktop: '\f108'; -$fa-var-deviantart: '\f1bd'; -$fa-var-diamond: '\f219'; -$fa-var-digg: '\f1a6'; -$fa-var-dollar: '\f155'; -$fa-var-dot-circle-o: '\f192'; -$fa-var-download: '\f019'; -$fa-var-dribbble: '\f17d'; -$fa-var-drivers-license: '\f2c2'; -$fa-var-drivers-license-o: '\f2c3'; -$fa-var-dropbox: '\f16b'; -$fa-var-drupal: '\f1a9'; -$fa-var-edge: '\f282'; -$fa-var-edit: '\f044'; -$fa-var-eercast: '\f2da'; -$fa-var-eject: '\f052'; -$fa-var-ellipsis-h: '\f141'; -$fa-var-ellipsis-v: '\f142'; -$fa-var-empire: '\f1d1'; -$fa-var-envelope: '\f0e0'; -$fa-var-envelope-o: '\f003'; -$fa-var-envelope-open: '\f2b6'; -$fa-var-envelope-open-o: '\f2b7'; -$fa-var-envelope-square: '\f199'; -$fa-var-envira: '\f299'; -$fa-var-eraser: '\f12d'; -$fa-var-etsy: '\f2d7'; -$fa-var-eur: '\f153'; -$fa-var-euro: '\f153'; -$fa-var-exchange: '\f0ec'; -$fa-var-exclamation: '\f12a'; -$fa-var-exclamation-circle: '\f06a'; -$fa-var-exclamation-triangle: '\f071'; -$fa-var-expand: '\f065'; -$fa-var-expeditedssl: '\f23e'; -$fa-var-external-link: '\f08e'; -$fa-var-external-link-square: '\f14c'; -$fa-var-eye: '\f06e'; -$fa-var-eye-slash: '\f070'; -$fa-var-eyedropper: '\f1fb'; -$fa-var-fa: '\f2b4'; -$fa-var-facebook: '\f09a'; -$fa-var-facebook-f: '\f09a'; -$fa-var-facebook-official: '\f230'; -$fa-var-facebook-square: '\f082'; -$fa-var-fast-backward: '\f049'; -$fa-var-fast-forward: '\f050'; -$fa-var-fax: '\f1ac'; -$fa-var-feed: '\f09e'; -$fa-var-female: '\f182'; -$fa-var-fighter-jet: '\f0fb'; -$fa-var-file: '\f15b'; -$fa-var-file-archive-o: '\f1c6'; -$fa-var-file-audio-o: '\f1c7'; -$fa-var-file-code-o: '\f1c9'; -$fa-var-file-excel-o: '\f1c3'; -$fa-var-file-image-o: '\f1c5'; -$fa-var-file-movie-o: '\f1c8'; -$fa-var-file-o: '\f016'; -$fa-var-file-pdf-o: '\f1c1'; -$fa-var-file-photo-o: '\f1c5'; -$fa-var-file-picture-o: '\f1c5'; -$fa-var-file-powerpoint-o: '\f1c4'; -$fa-var-file-sound-o: '\f1c7'; -$fa-var-file-text: '\f15c'; -$fa-var-file-text-o: '\f0f6'; -$fa-var-file-video-o: '\f1c8'; -$fa-var-file-word-o: '\f1c2'; -$fa-var-file-zip-o: '\f1c6'; -$fa-var-files-o: '\f0c5'; -$fa-var-film: '\f008'; -$fa-var-filter: '\f0b0'; -$fa-var-fire: '\f06d'; -$fa-var-fire-extinguisher: '\f134'; -$fa-var-firefox: '\f269'; -$fa-var-first-order: '\f2b0'; -$fa-var-flag: '\f024'; -$fa-var-flag-checkered: '\f11e'; -$fa-var-flag-o: '\f11d'; -$fa-var-flash: '\f0e7'; -$fa-var-flask: '\f0c3'; -$fa-var-flickr: '\f16e'; -$fa-var-floppy-o: '\f0c7'; -$fa-var-folder: '\f07b'; -$fa-var-folder-o: '\f114'; -$fa-var-folder-open: '\f07c'; -$fa-var-folder-open-o: '\f115'; -$fa-var-font: '\f031'; -$fa-var-font-awesome: '\f2b4'; -$fa-var-fonticons: '\f280'; -$fa-var-fort-awesome: '\f286'; -$fa-var-forumbee: '\f211'; -$fa-var-forward: '\f04e'; -$fa-var-foursquare: '\f180'; -$fa-var-free-code-camp: '\f2c5'; -$fa-var-frown-o: '\f119'; -$fa-var-futbol-o: '\f1e3'; -$fa-var-gamepad: '\f11b'; -$fa-var-gavel: '\f0e3'; -$fa-var-gbp: '\f154'; -$fa-var-ge: '\f1d1'; -$fa-var-gear: '\f013'; -$fa-var-gears: '\f085'; -$fa-var-genderless: '\f22d'; -$fa-var-get-pocket: '\f265'; -$fa-var-gg: '\f260'; -$fa-var-gg-circle: '\f261'; -$fa-var-gift: '\f06b'; -$fa-var-git: '\f1d3'; -$fa-var-git-square: '\f1d2'; -$fa-var-github: '\f09b'; -$fa-var-github-alt: '\f113'; -$fa-var-github-square: '\f092'; -$fa-var-gitlab: '\f296'; -$fa-var-gittip: '\f184'; -$fa-var-glass: '\f000'; -$fa-var-glide: '\f2a5'; -$fa-var-glide-g: '\f2a6'; -$fa-var-globe: '\f0ac'; -$fa-var-google: '\f1a0'; -$fa-var-google-plus: '\f0d5'; -$fa-var-google-plus-circle: '\f2b3'; -$fa-var-google-plus-official: '\f2b3'; -$fa-var-google-plus-square: '\f0d4'; -$fa-var-google-wallet: '\f1ee'; -$fa-var-graduation-cap: '\f19d'; -$fa-var-gratipay: '\f184'; -$fa-var-grav: '\f2d6'; -$fa-var-group: '\f0c0'; -$fa-var-h-square: '\f0fd'; -$fa-var-hacker-news: '\f1d4'; -$fa-var-hand-grab-o: '\f255'; -$fa-var-hand-lizard-o: '\f258'; -$fa-var-hand-o-down: '\f0a7'; -$fa-var-hand-o-left: '\f0a5'; -$fa-var-hand-o-right: '\f0a4'; -$fa-var-hand-o-up: '\f0a6'; -$fa-var-hand-paper-o: '\f256'; -$fa-var-hand-peace-o: '\f25b'; -$fa-var-hand-pointer-o: '\f25a'; -$fa-var-hand-rock-o: '\f255'; -$fa-var-hand-scissors-o: '\f257'; -$fa-var-hand-spock-o: '\f259'; -$fa-var-hand-stop-o: '\f256'; -$fa-var-handshake-o: '\f2b5'; -$fa-var-hard-of-hearing: '\f2a4'; -$fa-var-hashtag: '\f292'; -$fa-var-hdd-o: '\f0a0'; -$fa-var-header: '\f1dc'; -$fa-var-headphones: '\f025'; -$fa-var-heart: '\f004'; -$fa-var-heart-o: '\f08a'; -$fa-var-heartbeat: '\f21e'; -$fa-var-history: '\f1da'; -$fa-var-home: '\f015'; -$fa-var-hospital-o: '\f0f8'; -$fa-var-hotel: '\f236'; -$fa-var-hourglass: '\f254'; -$fa-var-hourglass-1: '\f251'; -$fa-var-hourglass-2: '\f252'; -$fa-var-hourglass-3: '\f253'; -$fa-var-hourglass-end: '\f253'; -$fa-var-hourglass-half: '\f252'; -$fa-var-hourglass-o: '\f250'; -$fa-var-hourglass-start: '\f251'; -$fa-var-houzz: '\f27c'; -$fa-var-html5: '\f13b'; -$fa-var-i-cursor: '\f246'; -$fa-var-id-badge: '\f2c1'; -$fa-var-id-card: '\f2c2'; -$fa-var-id-card-o: '\f2c3'; -$fa-var-ils: '\f20b'; -$fa-var-image: '\f03e'; -$fa-var-imdb: '\f2d8'; -$fa-var-inbox: '\f01c'; -$fa-var-indent: '\f03c'; -$fa-var-industry: '\f275'; -$fa-var-info: '\f129'; -$fa-var-info-circle: '\f05a'; -$fa-var-inr: '\f156'; -$fa-var-instagram: '\f16d'; -$fa-var-institution: '\f19c'; -$fa-var-internet-explorer: '\f26b'; -$fa-var-intersex: '\f224'; -$fa-var-ioxhost: '\f208'; -$fa-var-italic: '\f033'; -$fa-var-joomla: '\f1aa'; -$fa-var-jpy: '\f157'; -$fa-var-jsfiddle: '\f1cc'; -$fa-var-key: '\f084'; -$fa-var-keyboard-o: '\f11c'; -$fa-var-krw: '\f159'; -$fa-var-language: '\f1ab'; -$fa-var-laptop: '\f109'; -$fa-var-lastfm: '\f202'; -$fa-var-lastfm-square: '\f203'; -$fa-var-leaf: '\f06c'; -$fa-var-leanpub: '\f212'; -$fa-var-legal: '\f0e3'; -$fa-var-lemon-o: '\f094'; -$fa-var-level-down: '\f149'; -$fa-var-level-up: '\f148'; -$fa-var-life-bouy: '\f1cd'; -$fa-var-life-buoy: '\f1cd'; -$fa-var-life-ring: '\f1cd'; -$fa-var-life-saver: '\f1cd'; -$fa-var-lightbulb-o: '\f0eb'; -$fa-var-line-chart: '\f201'; -$fa-var-link: '\f0c1'; -$fa-var-linkedin: '\f0e1'; -$fa-var-linkedin-square: '\f08c'; -$fa-var-linode: '\f2b8'; -$fa-var-linux: '\f17c'; -$fa-var-list: '\f03a'; -$fa-var-list-alt: '\f022'; -$fa-var-list-ol: '\f0cb'; -$fa-var-list-ul: '\f0ca'; -$fa-var-location-arrow: '\f124'; -$fa-var-lock: '\f023'; -$fa-var-long-arrow-down: '\f175'; -$fa-var-long-arrow-left: '\f177'; -$fa-var-long-arrow-right: '\f178'; -$fa-var-long-arrow-up: '\f176'; -$fa-var-low-vision: '\f2a8'; -$fa-var-magic: '\f0d0'; -$fa-var-magnet: '\f076'; -$fa-var-mail-forward: '\f064'; -$fa-var-mail-reply: '\f112'; -$fa-var-mail-reply-all: '\f122'; -$fa-var-male: '\f183'; -$fa-var-map: '\f279'; -$fa-var-map-marker: '\f041'; -$fa-var-map-o: '\f278'; -$fa-var-map-pin: '\f276'; -$fa-var-map-signs: '\f277'; -$fa-var-mars: '\f222'; -$fa-var-mars-double: '\f227'; -$fa-var-mars-stroke: '\f229'; -$fa-var-mars-stroke-h: '\f22b'; -$fa-var-mars-stroke-v: '\f22a'; -$fa-var-maxcdn: '\f136'; -$fa-var-meanpath: '\f20c'; -$fa-var-medium: '\f23a'; -$fa-var-medkit: '\f0fa'; -$fa-var-meetup: '\f2e0'; -$fa-var-meh-o: '\f11a'; -$fa-var-mercury: '\f223'; -$fa-var-microchip: '\f2db'; -$fa-var-microphone: '\f130'; -$fa-var-microphone-slash: '\f131'; -$fa-var-minus: '\f068'; -$fa-var-minus-circle: '\f056'; -$fa-var-minus-square: '\f146'; -$fa-var-minus-square-o: '\f147'; -$fa-var-mixcloud: '\f289'; -$fa-var-mobile: '\f10b'; -$fa-var-mobile-phone: '\f10b'; -$fa-var-modx: '\f285'; -$fa-var-money: '\f0d6'; -$fa-var-moon-o: '\f186'; -$fa-var-mortar-board: '\f19d'; -$fa-var-motorcycle: '\f21c'; -$fa-var-mouse-pointer: '\f245'; -$fa-var-music: '\f001'; -$fa-var-navicon: '\f0c9'; -$fa-var-neuter: '\f22c'; -$fa-var-newspaper-o: '\f1ea'; -$fa-var-object-group: '\f247'; -$fa-var-object-ungroup: '\f248'; -$fa-var-odnoklassniki: '\f263'; -$fa-var-odnoklassniki-square: '\f264'; -$fa-var-opencart: '\f23d'; -$fa-var-openid: '\f19b'; -$fa-var-opera: '\f26a'; -$fa-var-optin-monster: '\f23c'; -$fa-var-outdent: '\f03b'; -$fa-var-pagelines: '\f18c'; -$fa-var-paint-brush: '\f1fc'; -$fa-var-paper-plane: '\f1d8'; -$fa-var-paper-plane-o: '\f1d9'; -$fa-var-paperclip: '\f0c6'; -$fa-var-paragraph: '\f1dd'; -$fa-var-paste: '\f0ea'; -$fa-var-pause: '\f04c'; -$fa-var-pause-circle: '\f28b'; -$fa-var-pause-circle-o: '\f28c'; -$fa-var-paw: '\f1b0'; -$fa-var-paypal: '\f1ed'; -$fa-var-pencil: '\f040'; -$fa-var-pencil-square: '\f14b'; -$fa-var-pencil-square-o: '\f044'; -$fa-var-percent: '\f295'; -$fa-var-phone: '\f095'; -$fa-var-phone-square: '\f098'; -$fa-var-photo: '\f03e'; -$fa-var-picture-o: '\f03e'; -$fa-var-pie-chart: '\f200'; -$fa-var-pied-piper: '\f2ae'; -$fa-var-pied-piper-alt: '\f1a8'; -$fa-var-pied-piper-pp: '\f1a7'; -$fa-var-pinterest: '\f0d2'; -$fa-var-pinterest-p: '\f231'; -$fa-var-pinterest-square: '\f0d3'; -$fa-var-plane: '\f072'; -$fa-var-play: '\f04b'; -$fa-var-play-circle: '\f144'; -$fa-var-play-circle-o: '\f01d'; -$fa-var-plug: '\f1e6'; -$fa-var-plus: '\f067'; -$fa-var-plus-circle: '\f055'; -$fa-var-plus-square: '\f0fe'; -$fa-var-plus-square-o: '\f196'; -$fa-var-podcast: '\f2ce'; -$fa-var-power-off: '\f011'; -$fa-var-print: '\f02f'; -$fa-var-product-hunt: '\f288'; -$fa-var-puzzle-piece: '\f12e'; -$fa-var-qq: '\f1d6'; -$fa-var-qrcode: '\f029'; -$fa-var-question: '\f128'; -$fa-var-question-circle: '\f059'; -$fa-var-question-circle-o: '\f29c'; -$fa-var-quora: '\f2c4'; -$fa-var-quote-left: '\f10d'; -$fa-var-quote-right: '\f10e'; -$fa-var-ra: '\f1d0'; -$fa-var-random: '\f074'; -$fa-var-ravelry: '\f2d9'; -$fa-var-rebel: '\f1d0'; -$fa-var-recycle: '\f1b8'; -$fa-var-reddit: '\f1a1'; -$fa-var-reddit-alien: '\f281'; -$fa-var-reddit-square: '\f1a2'; -$fa-var-refresh: '\f021'; -$fa-var-registered: '\f25d'; -$fa-var-remove: '\f00d'; -$fa-var-renren: '\f18b'; -$fa-var-reorder: '\f0c9'; -$fa-var-repeat: '\f01e'; -$fa-var-reply: '\f112'; -$fa-var-reply-all: '\f122'; -$fa-var-resistance: '\f1d0'; -$fa-var-retweet: '\f079'; -$fa-var-rmb: '\f157'; -$fa-var-road: '\f018'; -$fa-var-rocket: '\f135'; -$fa-var-rotate-left: '\f0e2'; -$fa-var-rotate-right: '\f01e'; -$fa-var-rouble: '\f158'; -$fa-var-rss: '\f09e'; -$fa-var-rss-square: '\f143'; -$fa-var-rub: '\f158'; -$fa-var-ruble: '\f158'; -$fa-var-rupee: '\f156'; -$fa-var-s15: '\f2cd'; -$fa-var-safari: '\f267'; -$fa-var-save: '\f0c7'; -$fa-var-scissors: '\f0c4'; -$fa-var-scribd: '\f28a'; -$fa-var-search: '\f002'; -$fa-var-search-minus: '\f010'; -$fa-var-search-plus: '\f00e'; -$fa-var-sellsy: '\f213'; -$fa-var-send: '\f1d8'; -$fa-var-send-o: '\f1d9'; -$fa-var-server: '\f233'; -$fa-var-share: '\f064'; -$fa-var-share-alt: '\f1e0'; -$fa-var-share-alt-square: '\f1e1'; -$fa-var-share-square: '\f14d'; -$fa-var-share-square-o: '\f045'; -$fa-var-shekel: '\f20b'; -$fa-var-sheqel: '\f20b'; -$fa-var-shield: '\f132'; -$fa-var-ship: '\f21a'; -$fa-var-shirtsinbulk: '\f214'; -$fa-var-shopping-bag: '\f290'; -$fa-var-shopping-basket: '\f291'; -$fa-var-shopping-cart: '\f07a'; -$fa-var-shower: '\f2cc'; -$fa-var-sign-in: '\f090'; -$fa-var-sign-language: '\f2a7'; -$fa-var-sign-out: '\f08b'; -$fa-var-signal: '\f012'; -$fa-var-signing: '\f2a7'; -$fa-var-simplybuilt: '\f215'; -$fa-var-sitemap: '\f0e8'; -$fa-var-skyatlas: '\f216'; -$fa-var-skype: '\f17e'; -$fa-var-slack: '\f198'; -$fa-var-sliders: '\f1de'; -$fa-var-slideshare: '\f1e7'; -$fa-var-smile-o: '\f118'; -$fa-var-snapchat: '\f2ab'; -$fa-var-snapchat-ghost: '\f2ac'; -$fa-var-snapchat-square: '\f2ad'; -$fa-var-snowflake-o: '\f2dc'; -$fa-var-soccer-ball-o: '\f1e3'; -$fa-var-sort: '\f0dc'; -$fa-var-sort-alpha-asc: '\f15d'; -$fa-var-sort-alpha-desc: '\f15e'; -$fa-var-sort-amount-asc: '\f160'; -$fa-var-sort-amount-desc: '\f161'; -$fa-var-sort-asc: '\f0de'; -$fa-var-sort-desc: '\f0dd'; -$fa-var-sort-down: '\f0dd'; -$fa-var-sort-numeric-asc: '\f162'; -$fa-var-sort-numeric-desc: '\f163'; -$fa-var-sort-up: '\f0de'; -$fa-var-soundcloud: '\f1be'; -$fa-var-space-shuttle: '\f197'; -$fa-var-spinner: '\f110'; -$fa-var-spoon: '\f1b1'; -$fa-var-spotify: '\f1bc'; -$fa-var-square: '\f0c8'; -$fa-var-square-o: '\f096'; -$fa-var-stack-exchange: '\f18d'; -$fa-var-stack-overflow: '\f16c'; -$fa-var-star: '\f005'; -$fa-var-star-half: '\f089'; -$fa-var-star-half-empty: '\f123'; -$fa-var-star-half-full: '\f123'; -$fa-var-star-half-o: '\f123'; -$fa-var-star-o: '\f006'; -$fa-var-steam: '\f1b6'; -$fa-var-steam-square: '\f1b7'; -$fa-var-step-backward: '\f048'; -$fa-var-step-forward: '\f051'; -$fa-var-stethoscope: '\f0f1'; -$fa-var-sticky-note: '\f249'; -$fa-var-sticky-note-o: '\f24a'; -$fa-var-stop: '\f04d'; -$fa-var-stop-circle: '\f28d'; -$fa-var-stop-circle-o: '\f28e'; -$fa-var-street-view: '\f21d'; -$fa-var-strikethrough: '\f0cc'; -$fa-var-stumbleupon: '\f1a4'; -$fa-var-stumbleupon-circle: '\f1a3'; -$fa-var-subscript: '\f12c'; -$fa-var-subway: '\f239'; -$fa-var-suitcase: '\f0f2'; -$fa-var-sun-o: '\f185'; -$fa-var-superpowers: '\f2dd'; -$fa-var-superscript: '\f12b'; -$fa-var-support: '\f1cd'; -$fa-var-table: '\f0ce'; -$fa-var-tablet: '\f10a'; -$fa-var-tachometer: '\f0e4'; -$fa-var-tag: '\f02b'; -$fa-var-tags: '\f02c'; -$fa-var-tasks: '\f0ae'; -$fa-var-taxi: '\f1ba'; -$fa-var-telegram: '\f2c6'; -$fa-var-television: '\f26c'; -$fa-var-tencent-weibo: '\f1d5'; -$fa-var-terminal: '\f120'; -$fa-var-text-height: '\f034'; -$fa-var-text-width: '\f035'; -$fa-var-th: '\f00a'; -$fa-var-th-large: '\f009'; -$fa-var-th-list: '\f00b'; -$fa-var-themeisle: '\f2b2'; -$fa-var-thermometer: '\f2c7'; -$fa-var-thermometer-0: '\f2cb'; -$fa-var-thermometer-1: '\f2ca'; -$fa-var-thermometer-2: '\f2c9'; -$fa-var-thermometer-3: '\f2c8'; -$fa-var-thermometer-4: '\f2c7'; -$fa-var-thermometer-empty: '\f2cb'; -$fa-var-thermometer-full: '\f2c7'; -$fa-var-thermometer-half: '\f2c9'; -$fa-var-thermometer-quarter: '\f2ca'; -$fa-var-thermometer-three-quarters: '\f2c8'; -$fa-var-thumb-tack: '\f08d'; -$fa-var-thumbs-down: '\f165'; -$fa-var-thumbs-o-down: '\f088'; -$fa-var-thumbs-o-up: '\f087'; -$fa-var-thumbs-up: '\f164'; -$fa-var-ticket: '\f145'; -$fa-var-times: '\f00d'; -$fa-var-times-circle: '\f057'; -$fa-var-times-circle-o: '\f05c'; -$fa-var-times-rectangle: '\f2d3'; -$fa-var-times-rectangle-o: '\f2d4'; -$fa-var-tint: '\f043'; -$fa-var-toggle-down: '\f150'; -$fa-var-toggle-left: '\f191'; -$fa-var-toggle-off: '\f204'; -$fa-var-toggle-on: '\f205'; -$fa-var-toggle-right: '\f152'; -$fa-var-toggle-up: '\f151'; -$fa-var-trademark: '\f25c'; -$fa-var-train: '\f238'; -$fa-var-transgender: '\f224'; -$fa-var-transgender-alt: '\f225'; -$fa-var-trash: '\f1f8'; -$fa-var-trash-o: '\f014'; -$fa-var-tree: '\f1bb'; -$fa-var-trello: '\f181'; -$fa-var-tripadvisor: '\f262'; -$fa-var-trophy: '\f091'; -$fa-var-truck: '\f0d1'; -$fa-var-try: '\f195'; -$fa-var-tty: '\f1e4'; -$fa-var-tumblr: '\f173'; -$fa-var-tumblr-square: '\f174'; -$fa-var-turkish-lira: '\f195'; -$fa-var-tv: '\f26c'; -$fa-var-twitch: '\f1e8'; -$fa-var-twitter: '\f099'; -$fa-var-twitter-square: '\f081'; -$fa-var-umbrella: '\f0e9'; -$fa-var-underline: '\f0cd'; -$fa-var-undo: '\f0e2'; -$fa-var-universal-access: '\f29a'; -$fa-var-university: '\f19c'; -$fa-var-unlink: '\f127'; -$fa-var-unlock: '\f09c'; -$fa-var-unlock-alt: '\f13e'; -$fa-var-unsorted: '\f0dc'; -$fa-var-upload: '\f093'; -$fa-var-usb: '\f287'; -$fa-var-usd: '\f155'; -$fa-var-user: '\f007'; -$fa-var-user-circle: '\f2bd'; -$fa-var-user-circle-o: '\f2be'; -$fa-var-user-md: '\f0f0'; -$fa-var-user-o: '\f2c0'; -$fa-var-user-plus: '\f234'; -$fa-var-user-secret: '\f21b'; -$fa-var-user-times: '\f235'; -$fa-var-users: '\f0c0'; -$fa-var-vcard: '\f2bb'; -$fa-var-vcard-o: '\f2bc'; -$fa-var-venus: '\f221'; -$fa-var-venus-double: '\f226'; -$fa-var-venus-mars: '\f228'; -$fa-var-viacoin: '\f237'; -$fa-var-viadeo: '\f2a9'; -$fa-var-viadeo-square: '\f2aa'; -$fa-var-video-camera: '\f03d'; -$fa-var-vimeo: '\f27d'; -$fa-var-vimeo-square: '\f194'; -$fa-var-vine: '\f1ca'; -$fa-var-vk: '\f189'; -$fa-var-volume-control-phone: '\f2a0'; -$fa-var-volume-down: '\f027'; -$fa-var-volume-off: '\f026'; -$fa-var-volume-up: '\f028'; -$fa-var-warning: '\f071'; -$fa-var-wechat: '\f1d7'; -$fa-var-weibo: '\f18a'; -$fa-var-weixin: '\f1d7'; -$fa-var-whatsapp: '\f232'; -$fa-var-wheelchair: '\f193'; -$fa-var-wheelchair-alt: '\f29b'; -$fa-var-wifi: '\f1eb'; -$fa-var-wikipedia-w: '\f266'; -$fa-var-window-close: '\f2d3'; -$fa-var-window-close-o: '\f2d4'; -$fa-var-window-maximize: '\f2d0'; -$fa-var-window-minimize: '\f2d1'; -$fa-var-window-restore: '\f2d2'; -$fa-var-windows: '\f17a'; -$fa-var-won: '\f159'; -$fa-var-wordpress: '\f19a'; -$fa-var-wpbeginner: '\f297'; -$fa-var-wpexplorer: '\f2de'; -$fa-var-wpforms: '\f298'; -$fa-var-wrench: '\f0ad'; -$fa-var-xing: '\f168'; -$fa-var-xing-square: '\f169'; -$fa-var-y-combinator: '\f23b'; -$fa-var-y-combinator-square: '\f1d4'; -$fa-var-yahoo: '\f19e'; -$fa-var-yc: '\f23b'; -$fa-var-yc-square: '\f1d4'; -$fa-var-yelp: '\f1e9'; -$fa-var-yen: '\f157'; -$fa-var-yoast: '\f2b1'; -$fa-var-youtube: '\f167'; -$fa-var-youtube-play: '\f16a'; -$fa-var-youtube-square: '\f166'; +$fa-var-500px: "\f26e"; +$fa-var-address-book: "\f2b9"; +$fa-var-address-book-o: "\f2ba"; +$fa-var-address-card: "\f2bb"; +$fa-var-address-card-o: "\f2bc"; +$fa-var-adjust: "\f042"; +$fa-var-adn: "\f170"; +$fa-var-align-center: "\f037"; +$fa-var-align-justify: "\f039"; +$fa-var-align-left: "\f036"; +$fa-var-align-right: "\f038"; +$fa-var-amazon: "\f270"; +$fa-var-ambulance: "\f0f9"; +$fa-var-american-sign-language-interpreting: "\f2a3"; +$fa-var-anchor: "\f13d"; +$fa-var-android: "\f17b"; +$fa-var-angellist: "\f209"; +$fa-var-angle-double-down: "\f103"; +$fa-var-angle-double-left: "\f100"; +$fa-var-angle-double-right: "\f101"; +$fa-var-angle-double-up: "\f102"; +$fa-var-angle-down: "\f107"; +$fa-var-angle-left: "\f104"; +$fa-var-angle-right: "\f105"; +$fa-var-angle-up: "\f106"; +$fa-var-apple: "\f179"; +$fa-var-archive: "\f187"; +$fa-var-area-chart: "\f1fe"; +$fa-var-arrow-circle-down: "\f0ab"; +$fa-var-arrow-circle-left: "\f0a8"; +$fa-var-arrow-circle-o-down: "\f01a"; +$fa-var-arrow-circle-o-left: "\f190"; +$fa-var-arrow-circle-o-right: "\f18e"; +$fa-var-arrow-circle-o-up: "\f01b"; +$fa-var-arrow-circle-right: "\f0a9"; +$fa-var-arrow-circle-up: "\f0aa"; +$fa-var-arrow-down: "\f063"; +$fa-var-arrow-left: "\f060"; +$fa-var-arrow-right: "\f061"; +$fa-var-arrow-up: "\f062"; +$fa-var-arrows: "\f047"; +$fa-var-arrows-alt: "\f0b2"; +$fa-var-arrows-h: "\f07e"; +$fa-var-arrows-v: "\f07d"; +$fa-var-asl-interpreting: "\f2a3"; +$fa-var-assistive-listening-systems: "\f2a2"; +$fa-var-asterisk: "\f069"; +$fa-var-at: "\f1fa"; +$fa-var-audio-description: "\f29e"; +$fa-var-automobile: "\f1b9"; +$fa-var-backward: "\f04a"; +$fa-var-balance-scale: "\f24e"; +$fa-var-ban: "\f05e"; +$fa-var-bandcamp: "\f2d5"; +$fa-var-bank: "\f19c"; +$fa-var-bar-chart: "\f080"; +$fa-var-bar-chart-o: "\f080"; +$fa-var-barcode: "\f02a"; +$fa-var-bars: "\f0c9"; +$fa-var-bath: "\f2cd"; +$fa-var-bathtub: "\f2cd"; +$fa-var-battery: "\f240"; +$fa-var-battery-0: "\f244"; +$fa-var-battery-1: "\f243"; +$fa-var-battery-2: "\f242"; +$fa-var-battery-3: "\f241"; +$fa-var-battery-4: "\f240"; +$fa-var-battery-empty: "\f244"; +$fa-var-battery-full: "\f240"; +$fa-var-battery-half: "\f242"; +$fa-var-battery-quarter: "\f243"; +$fa-var-battery-three-quarters: "\f241"; +$fa-var-bed: "\f236"; +$fa-var-beer: "\f0fc"; +$fa-var-behance: "\f1b4"; +$fa-var-behance-square: "\f1b5"; +$fa-var-bell: "\f0f3"; +$fa-var-bell-o: "\f0a2"; +$fa-var-bell-slash: "\f1f6"; +$fa-var-bell-slash-o: "\f1f7"; +$fa-var-bicycle: "\f206"; +$fa-var-binoculars: "\f1e5"; +$fa-var-birthday-cake: "\f1fd"; +$fa-var-bitbucket: "\f171"; +$fa-var-bitbucket-square: "\f172"; +$fa-var-bitcoin: "\f15a"; +$fa-var-black-tie: "\f27e"; +$fa-var-blind: "\f29d"; +$fa-var-bluetooth: "\f293"; +$fa-var-bluetooth-b: "\f294"; +$fa-var-bold: "\f032"; +$fa-var-bolt: "\f0e7"; +$fa-var-bomb: "\f1e2"; +$fa-var-book: "\f02d"; +$fa-var-bookmark: "\f02e"; +$fa-var-bookmark-o: "\f097"; +$fa-var-braille: "\f2a1"; +$fa-var-briefcase: "\f0b1"; +$fa-var-btc: "\f15a"; +$fa-var-bug: "\f188"; +$fa-var-building: "\f1ad"; +$fa-var-building-o: "\f0f7"; +$fa-var-bullhorn: "\f0a1"; +$fa-var-bullseye: "\f140"; +$fa-var-bus: "\f207"; +$fa-var-buysellads: "\f20d"; +$fa-var-cab: "\f1ba"; +$fa-var-calculator: "\f1ec"; +$fa-var-calendar: "\f073"; +$fa-var-calendar-check-o: "\f274"; +$fa-var-calendar-minus-o: "\f272"; +$fa-var-calendar-o: "\f133"; +$fa-var-calendar-plus-o: "\f271"; +$fa-var-calendar-times-o: "\f273"; +$fa-var-camera: "\f030"; +$fa-var-camera-retro: "\f083"; +$fa-var-car: "\f1b9"; +$fa-var-caret-down: "\f0d7"; +$fa-var-caret-left: "\f0d9"; +$fa-var-caret-right: "\f0da"; +$fa-var-caret-square-o-down: "\f150"; +$fa-var-caret-square-o-left: "\f191"; +$fa-var-caret-square-o-right: "\f152"; +$fa-var-caret-square-o-up: "\f151"; +$fa-var-caret-up: "\f0d8"; +$fa-var-cart-arrow-down: "\f218"; +$fa-var-cart-plus: "\f217"; +$fa-var-cc: "\f20a"; +$fa-var-cc-amex: "\f1f3"; +$fa-var-cc-diners-club: "\f24c"; +$fa-var-cc-discover: "\f1f2"; +$fa-var-cc-jcb: "\f24b"; +$fa-var-cc-mastercard: "\f1f1"; +$fa-var-cc-paypal: "\f1f4"; +$fa-var-cc-stripe: "\f1f5"; +$fa-var-cc-visa: "\f1f0"; +$fa-var-certificate: "\f0a3"; +$fa-var-chain: "\f0c1"; +$fa-var-chain-broken: "\f127"; +$fa-var-check: "\f00c"; +$fa-var-check-circle: "\f058"; +$fa-var-check-circle-o: "\f05d"; +$fa-var-check-square: "\f14a"; +$fa-var-check-square-o: "\f046"; +$fa-var-chevron-circle-down: "\f13a"; +$fa-var-chevron-circle-left: "\f137"; +$fa-var-chevron-circle-right: "\f138"; +$fa-var-chevron-circle-up: "\f139"; +$fa-var-chevron-down: "\f078"; +$fa-var-chevron-left: "\f053"; +$fa-var-chevron-right: "\f054"; +$fa-var-chevron-up: "\f077"; +$fa-var-child: "\f1ae"; +$fa-var-chrome: "\f268"; +$fa-var-circle: "\f111"; +$fa-var-circle-o: "\f10c"; +$fa-var-circle-o-notch: "\f1ce"; +$fa-var-circle-thin: "\f1db"; +$fa-var-clipboard: "\f0ea"; +$fa-var-clock-o: "\f017"; +$fa-var-clone: "\f24d"; +$fa-var-close: "\f00d"; +$fa-var-cloud: "\f0c2"; +$fa-var-cloud-download: "\f0ed"; +$fa-var-cloud-upload: "\f0ee"; +$fa-var-cny: "\f157"; +$fa-var-code: "\f121"; +$fa-var-code-fork: "\f126"; +$fa-var-codepen: "\f1cb"; +$fa-var-codiepie: "\f284"; +$fa-var-coffee: "\f0f4"; +$fa-var-cog: "\f013"; +$fa-var-cogs: "\f085"; +$fa-var-columns: "\f0db"; +$fa-var-comment: "\f075"; +$fa-var-comment-o: "\f0e5"; +$fa-var-commenting: "\f27a"; +$fa-var-commenting-o: "\f27b"; +$fa-var-comments: "\f086"; +$fa-var-comments-o: "\f0e6"; +$fa-var-compass: "\f14e"; +$fa-var-compress: "\f066"; +$fa-var-connectdevelop: "\f20e"; +$fa-var-contao: "\f26d"; +$fa-var-copy: "\f0c5"; +$fa-var-copyright: "\f1f9"; +$fa-var-creative-commons: "\f25e"; +$fa-var-credit-card: "\f09d"; +$fa-var-credit-card-alt: "\f283"; +$fa-var-crop: "\f125"; +$fa-var-crosshairs: "\f05b"; +$fa-var-css3: "\f13c"; +$fa-var-cube: "\f1b2"; +$fa-var-cubes: "\f1b3"; +$fa-var-cut: "\f0c4"; +$fa-var-cutlery: "\f0f5"; +$fa-var-dashboard: "\f0e4"; +$fa-var-dashcube: "\f210"; +$fa-var-database: "\f1c0"; +$fa-var-deaf: "\f2a4"; +$fa-var-deafness: "\f2a4"; +$fa-var-dedent: "\f03b"; +$fa-var-delicious: "\f1a5"; +$fa-var-desktop: "\f108"; +$fa-var-deviantart: "\f1bd"; +$fa-var-diamond: "\f219"; +$fa-var-digg: "\f1a6"; +$fa-var-dollar: "\f155"; +$fa-var-dot-circle-o: "\f192"; +$fa-var-download: "\f019"; +$fa-var-dribbble: "\f17d"; +$fa-var-drivers-license: "\f2c2"; +$fa-var-drivers-license-o: "\f2c3"; +$fa-var-dropbox: "\f16b"; +$fa-var-drupal: "\f1a9"; +$fa-var-edge: "\f282"; +$fa-var-edit: "\f044"; +$fa-var-eercast: "\f2da"; +$fa-var-eject: "\f052"; +$fa-var-ellipsis-h: "\f141"; +$fa-var-ellipsis-v: "\f142"; +$fa-var-empire: "\f1d1"; +$fa-var-envelope: "\f0e0"; +$fa-var-envelope-o: "\f003"; +$fa-var-envelope-open: "\f2b6"; +$fa-var-envelope-open-o: "\f2b7"; +$fa-var-envelope-square: "\f199"; +$fa-var-envira: "\f299"; +$fa-var-eraser: "\f12d"; +$fa-var-etsy: "\f2d7"; +$fa-var-eur: "\f153"; +$fa-var-euro: "\f153"; +$fa-var-exchange: "\f0ec"; +$fa-var-exclamation: "\f12a"; +$fa-var-exclamation-circle: "\f06a"; +$fa-var-exclamation-triangle: "\f071"; +$fa-var-expand: "\f065"; +$fa-var-expeditedssl: "\f23e"; +$fa-var-external-link: "\f08e"; +$fa-var-external-link-square: "\f14c"; +$fa-var-eye: "\f06e"; +$fa-var-eye-slash: "\f070"; +$fa-var-eyedropper: "\f1fb"; +$fa-var-fa: "\f2b4"; +$fa-var-facebook: "\f09a"; +$fa-var-facebook-f: "\f09a"; +$fa-var-facebook-official: "\f230"; +$fa-var-facebook-square: "\f082"; +$fa-var-fast-backward: "\f049"; +$fa-var-fast-forward: "\f050"; +$fa-var-fax: "\f1ac"; +$fa-var-feed: "\f09e"; +$fa-var-female: "\f182"; +$fa-var-fighter-jet: "\f0fb"; +$fa-var-file: "\f15b"; +$fa-var-file-archive-o: "\f1c6"; +$fa-var-file-audio-o: "\f1c7"; +$fa-var-file-code-o: "\f1c9"; +$fa-var-file-excel-o: "\f1c3"; +$fa-var-file-image-o: "\f1c5"; +$fa-var-file-movie-o: "\f1c8"; +$fa-var-file-o: "\f016"; +$fa-var-file-pdf-o: "\f1c1"; +$fa-var-file-photo-o: "\f1c5"; +$fa-var-file-picture-o: "\f1c5"; +$fa-var-file-powerpoint-o: "\f1c4"; +$fa-var-file-sound-o: "\f1c7"; +$fa-var-file-text: "\f15c"; +$fa-var-file-text-o: "\f0f6"; +$fa-var-file-video-o: "\f1c8"; +$fa-var-file-word-o: "\f1c2"; +$fa-var-file-zip-o: "\f1c6"; +$fa-var-files-o: "\f0c5"; +$fa-var-film: "\f008"; +$fa-var-filter: "\f0b0"; +$fa-var-fire: "\f06d"; +$fa-var-fire-extinguisher: "\f134"; +$fa-var-firefox: "\f269"; +$fa-var-first-order: "\f2b0"; +$fa-var-flag: "\f024"; +$fa-var-flag-checkered: "\f11e"; +$fa-var-flag-o: "\f11d"; +$fa-var-flash: "\f0e7"; +$fa-var-flask: "\f0c3"; +$fa-var-flickr: "\f16e"; +$fa-var-floppy-o: "\f0c7"; +$fa-var-folder: "\f07b"; +$fa-var-folder-o: "\f114"; +$fa-var-folder-open: "\f07c"; +$fa-var-folder-open-o: "\f115"; +$fa-var-font: "\f031"; +$fa-var-font-awesome: "\f2b4"; +$fa-var-fonticons: "\f280"; +$fa-var-fort-awesome: "\f286"; +$fa-var-forumbee: "\f211"; +$fa-var-forward: "\f04e"; +$fa-var-foursquare: "\f180"; +$fa-var-free-code-camp: "\f2c5"; +$fa-var-frown-o: "\f119"; +$fa-var-futbol-o: "\f1e3"; +$fa-var-gamepad: "\f11b"; +$fa-var-gavel: "\f0e3"; +$fa-var-gbp: "\f154"; +$fa-var-ge: "\f1d1"; +$fa-var-gear: "\f013"; +$fa-var-gears: "\f085"; +$fa-var-genderless: "\f22d"; +$fa-var-get-pocket: "\f265"; +$fa-var-gg: "\f260"; +$fa-var-gg-circle: "\f261"; +$fa-var-gift: "\f06b"; +$fa-var-git: "\f1d3"; +$fa-var-git-square: "\f1d2"; +$fa-var-github: "\f09b"; +$fa-var-github-alt: "\f113"; +$fa-var-github-square: "\f092"; +$fa-var-gitlab: "\f296"; +$fa-var-gittip: "\f184"; +$fa-var-glass: "\f000"; +$fa-var-glide: "\f2a5"; +$fa-var-glide-g: "\f2a6"; +$fa-var-globe: "\f0ac"; +$fa-var-google: "\f1a0"; +$fa-var-google-plus: "\f0d5"; +$fa-var-google-plus-circle: "\f2b3"; +$fa-var-google-plus-official: "\f2b3"; +$fa-var-google-plus-square: "\f0d4"; +$fa-var-google-wallet: "\f1ee"; +$fa-var-graduation-cap: "\f19d"; +$fa-var-gratipay: "\f184"; +$fa-var-grav: "\f2d6"; +$fa-var-group: "\f0c0"; +$fa-var-h-square: "\f0fd"; +$fa-var-hacker-news: "\f1d4"; +$fa-var-hand-grab-o: "\f255"; +$fa-var-hand-lizard-o: "\f258"; +$fa-var-hand-o-down: "\f0a7"; +$fa-var-hand-o-left: "\f0a5"; +$fa-var-hand-o-right: "\f0a4"; +$fa-var-hand-o-up: "\f0a6"; +$fa-var-hand-paper-o: "\f256"; +$fa-var-hand-peace-o: "\f25b"; +$fa-var-hand-pointer-o: "\f25a"; +$fa-var-hand-rock-o: "\f255"; +$fa-var-hand-scissors-o: "\f257"; +$fa-var-hand-spock-o: "\f259"; +$fa-var-hand-stop-o: "\f256"; +$fa-var-handshake-o: "\f2b5"; +$fa-var-hard-of-hearing: "\f2a4"; +$fa-var-hashtag: "\f292"; +$fa-var-hdd-o: "\f0a0"; +$fa-var-header: "\f1dc"; +$fa-var-headphones: "\f025"; +$fa-var-heart: "\f004"; +$fa-var-heart-o: "\f08a"; +$fa-var-heartbeat: "\f21e"; +$fa-var-history: "\f1da"; +$fa-var-home: "\f015"; +$fa-var-hospital-o: "\f0f8"; +$fa-var-hotel: "\f236"; +$fa-var-hourglass: "\f254"; +$fa-var-hourglass-1: "\f251"; +$fa-var-hourglass-2: "\f252"; +$fa-var-hourglass-3: "\f253"; +$fa-var-hourglass-end: "\f253"; +$fa-var-hourglass-half: "\f252"; +$fa-var-hourglass-o: "\f250"; +$fa-var-hourglass-start: "\f251"; +$fa-var-houzz: "\f27c"; +$fa-var-html5: "\f13b"; +$fa-var-i-cursor: "\f246"; +$fa-var-id-badge: "\f2c1"; +$fa-var-id-card: "\f2c2"; +$fa-var-id-card-o: "\f2c3"; +$fa-var-ils: "\f20b"; +$fa-var-image: "\f03e"; +$fa-var-imdb: "\f2d8"; +$fa-var-inbox: "\f01c"; +$fa-var-indent: "\f03c"; +$fa-var-industry: "\f275"; +$fa-var-info: "\f129"; +$fa-var-info-circle: "\f05a"; +$fa-var-inr: "\f156"; +$fa-var-instagram: "\f16d"; +$fa-var-institution: "\f19c"; +$fa-var-internet-explorer: "\f26b"; +$fa-var-intersex: "\f224"; +$fa-var-ioxhost: "\f208"; +$fa-var-italic: "\f033"; +$fa-var-joomla: "\f1aa"; +$fa-var-jpy: "\f157"; +$fa-var-jsfiddle: "\f1cc"; +$fa-var-key: "\f084"; +$fa-var-keyboard-o: "\f11c"; +$fa-var-krw: "\f159"; +$fa-var-language: "\f1ab"; +$fa-var-laptop: "\f109"; +$fa-var-lastfm: "\f202"; +$fa-var-lastfm-square: "\f203"; +$fa-var-leaf: "\f06c"; +$fa-var-leanpub: "\f212"; +$fa-var-legal: "\f0e3"; +$fa-var-lemon-o: "\f094"; +$fa-var-level-down: "\f149"; +$fa-var-level-up: "\f148"; +$fa-var-life-bouy: "\f1cd"; +$fa-var-life-buoy: "\f1cd"; +$fa-var-life-ring: "\f1cd"; +$fa-var-life-saver: "\f1cd"; +$fa-var-lightbulb-o: "\f0eb"; +$fa-var-line-chart: "\f201"; +$fa-var-link: "\f0c1"; +$fa-var-linkedin: "\f0e1"; +$fa-var-linkedin-square: "\f08c"; +$fa-var-linode: "\f2b8"; +$fa-var-linux: "\f17c"; +$fa-var-list: "\f03a"; +$fa-var-list-alt: "\f022"; +$fa-var-list-ol: "\f0cb"; +$fa-var-list-ul: "\f0ca"; +$fa-var-location-arrow: "\f124"; +$fa-var-lock: "\f023"; +$fa-var-long-arrow-down: "\f175"; +$fa-var-long-arrow-left: "\f177"; +$fa-var-long-arrow-right: "\f178"; +$fa-var-long-arrow-up: "\f176"; +$fa-var-low-vision: "\f2a8"; +$fa-var-magic: "\f0d0"; +$fa-var-magnet: "\f076"; +$fa-var-mail-forward: "\f064"; +$fa-var-mail-reply: "\f112"; +$fa-var-mail-reply-all: "\f122"; +$fa-var-male: "\f183"; +$fa-var-map: "\f279"; +$fa-var-map-marker: "\f041"; +$fa-var-map-o: "\f278"; +$fa-var-map-pin: "\f276"; +$fa-var-map-signs: "\f277"; +$fa-var-mars: "\f222"; +$fa-var-mars-double: "\f227"; +$fa-var-mars-stroke: "\f229"; +$fa-var-mars-stroke-h: "\f22b"; +$fa-var-mars-stroke-v: "\f22a"; +$fa-var-maxcdn: "\f136"; +$fa-var-meanpath: "\f20c"; +$fa-var-medium: "\f23a"; +$fa-var-medkit: "\f0fa"; +$fa-var-meetup: "\f2e0"; +$fa-var-meh-o: "\f11a"; +$fa-var-mercury: "\f223"; +$fa-var-microchip: "\f2db"; +$fa-var-microphone: "\f130"; +$fa-var-microphone-slash: "\f131"; +$fa-var-minus: "\f068"; +$fa-var-minus-circle: "\f056"; +$fa-var-minus-square: "\f146"; +$fa-var-minus-square-o: "\f147"; +$fa-var-mixcloud: "\f289"; +$fa-var-mobile: "\f10b"; +$fa-var-mobile-phone: "\f10b"; +$fa-var-modx: "\f285"; +$fa-var-money: "\f0d6"; +$fa-var-moon-o: "\f186"; +$fa-var-mortar-board: "\f19d"; +$fa-var-motorcycle: "\f21c"; +$fa-var-mouse-pointer: "\f245"; +$fa-var-music: "\f001"; +$fa-var-navicon: "\f0c9"; +$fa-var-neuter: "\f22c"; +$fa-var-newspaper-o: "\f1ea"; +$fa-var-object-group: "\f247"; +$fa-var-object-ungroup: "\f248"; +$fa-var-odnoklassniki: "\f263"; +$fa-var-odnoklassniki-square: "\f264"; +$fa-var-opencart: "\f23d"; +$fa-var-openid: "\f19b"; +$fa-var-opera: "\f26a"; +$fa-var-optin-monster: "\f23c"; +$fa-var-outdent: "\f03b"; +$fa-var-pagelines: "\f18c"; +$fa-var-paint-brush: "\f1fc"; +$fa-var-paper-plane: "\f1d8"; +$fa-var-paper-plane-o: "\f1d9"; +$fa-var-paperclip: "\f0c6"; +$fa-var-paragraph: "\f1dd"; +$fa-var-paste: "\f0ea"; +$fa-var-pause: "\f04c"; +$fa-var-pause-circle: "\f28b"; +$fa-var-pause-circle-o: "\f28c"; +$fa-var-paw: "\f1b0"; +$fa-var-paypal: "\f1ed"; +$fa-var-pencil: "\f040"; +$fa-var-pencil-square: "\f14b"; +$fa-var-pencil-square-o: "\f044"; +$fa-var-percent: "\f295"; +$fa-var-phone: "\f095"; +$fa-var-phone-square: "\f098"; +$fa-var-photo: "\f03e"; +$fa-var-picture-o: "\f03e"; +$fa-var-pie-chart: "\f200"; +$fa-var-pied-piper: "\f2ae"; +$fa-var-pied-piper-alt: "\f1a8"; +$fa-var-pied-piper-pp: "\f1a7"; +$fa-var-pinterest: "\f0d2"; +$fa-var-pinterest-p: "\f231"; +$fa-var-pinterest-square: "\f0d3"; +$fa-var-plane: "\f072"; +$fa-var-play: "\f04b"; +$fa-var-play-circle: "\f144"; +$fa-var-play-circle-o: "\f01d"; +$fa-var-plug: "\f1e6"; +$fa-var-plus: "\f067"; +$fa-var-plus-circle: "\f055"; +$fa-var-plus-square: "\f0fe"; +$fa-var-plus-square-o: "\f196"; +$fa-var-podcast: "\f2ce"; +$fa-var-power-off: "\f011"; +$fa-var-print: "\f02f"; +$fa-var-product-hunt: "\f288"; +$fa-var-puzzle-piece: "\f12e"; +$fa-var-qq: "\f1d6"; +$fa-var-qrcode: "\f029"; +$fa-var-question: "\f128"; +$fa-var-question-circle: "\f059"; +$fa-var-question-circle-o: "\f29c"; +$fa-var-quora: "\f2c4"; +$fa-var-quote-left: "\f10d"; +$fa-var-quote-right: "\f10e"; +$fa-var-ra: "\f1d0"; +$fa-var-random: "\f074"; +$fa-var-ravelry: "\f2d9"; +$fa-var-rebel: "\f1d0"; +$fa-var-recycle: "\f1b8"; +$fa-var-reddit: "\f1a1"; +$fa-var-reddit-alien: "\f281"; +$fa-var-reddit-square: "\f1a2"; +$fa-var-refresh: "\f021"; +$fa-var-registered: "\f25d"; +$fa-var-remove: "\f00d"; +$fa-var-renren: "\f18b"; +$fa-var-reorder: "\f0c9"; +$fa-var-repeat: "\f01e"; +$fa-var-reply: "\f112"; +$fa-var-reply-all: "\f122"; +$fa-var-resistance: "\f1d0"; +$fa-var-retweet: "\f079"; +$fa-var-rmb: "\f157"; +$fa-var-road: "\f018"; +$fa-var-rocket: "\f135"; +$fa-var-rotate-left: "\f0e2"; +$fa-var-rotate-right: "\f01e"; +$fa-var-rouble: "\f158"; +$fa-var-rss: "\f09e"; +$fa-var-rss-square: "\f143"; +$fa-var-rub: "\f158"; +$fa-var-ruble: "\f158"; +$fa-var-rupee: "\f156"; +$fa-var-s15: "\f2cd"; +$fa-var-safari: "\f267"; +$fa-var-save: "\f0c7"; +$fa-var-scissors: "\f0c4"; +$fa-var-scribd: "\f28a"; +$fa-var-search: "\f002"; +$fa-var-search-minus: "\f010"; +$fa-var-search-plus: "\f00e"; +$fa-var-sellsy: "\f213"; +$fa-var-send: "\f1d8"; +$fa-var-send-o: "\f1d9"; +$fa-var-server: "\f233"; +$fa-var-share: "\f064"; +$fa-var-share-alt: "\f1e0"; +$fa-var-share-alt-square: "\f1e1"; +$fa-var-share-square: "\f14d"; +$fa-var-share-square-o: "\f045"; +$fa-var-shekel: "\f20b"; +$fa-var-sheqel: "\f20b"; +$fa-var-shield: "\f132"; +$fa-var-ship: "\f21a"; +$fa-var-shirtsinbulk: "\f214"; +$fa-var-shopping-bag: "\f290"; +$fa-var-shopping-basket: "\f291"; +$fa-var-shopping-cart: "\f07a"; +$fa-var-shower: "\f2cc"; +$fa-var-sign-in: "\f090"; +$fa-var-sign-language: "\f2a7"; +$fa-var-sign-out: "\f08b"; +$fa-var-signal: "\f012"; +$fa-var-signing: "\f2a7"; +$fa-var-simplybuilt: "\f215"; +$fa-var-sitemap: "\f0e8"; +$fa-var-skyatlas: "\f216"; +$fa-var-skype: "\f17e"; +$fa-var-slack: "\f198"; +$fa-var-sliders: "\f1de"; +$fa-var-slideshare: "\f1e7"; +$fa-var-smile-o: "\f118"; +$fa-var-snapchat: "\f2ab"; +$fa-var-snapchat-ghost: "\f2ac"; +$fa-var-snapchat-square: "\f2ad"; +$fa-var-snowflake-o: "\f2dc"; +$fa-var-soccer-ball-o: "\f1e3"; +$fa-var-sort: "\f0dc"; +$fa-var-sort-alpha-asc: "\f15d"; +$fa-var-sort-alpha-desc: "\f15e"; +$fa-var-sort-amount-asc: "\f160"; +$fa-var-sort-amount-desc: "\f161"; +$fa-var-sort-asc: "\f0de"; +$fa-var-sort-desc: "\f0dd"; +$fa-var-sort-down: "\f0dd"; +$fa-var-sort-numeric-asc: "\f162"; +$fa-var-sort-numeric-desc: "\f163"; +$fa-var-sort-up: "\f0de"; +$fa-var-soundcloud: "\f1be"; +$fa-var-space-shuttle: "\f197"; +$fa-var-spinner: "\f110"; +$fa-var-spoon: "\f1b1"; +$fa-var-spotify: "\f1bc"; +$fa-var-square: "\f0c8"; +$fa-var-square-o: "\f096"; +$fa-var-stack-exchange: "\f18d"; +$fa-var-stack-overflow: "\f16c"; +$fa-var-star: "\f005"; +$fa-var-star-half: "\f089"; +$fa-var-star-half-empty: "\f123"; +$fa-var-star-half-full: "\f123"; +$fa-var-star-half-o: "\f123"; +$fa-var-star-o: "\f006"; +$fa-var-steam: "\f1b6"; +$fa-var-steam-square: "\f1b7"; +$fa-var-step-backward: "\f048"; +$fa-var-step-forward: "\f051"; +$fa-var-stethoscope: "\f0f1"; +$fa-var-sticky-note: "\f249"; +$fa-var-sticky-note-o: "\f24a"; +$fa-var-stop: "\f04d"; +$fa-var-stop-circle: "\f28d"; +$fa-var-stop-circle-o: "\f28e"; +$fa-var-street-view: "\f21d"; +$fa-var-strikethrough: "\f0cc"; +$fa-var-stumbleupon: "\f1a4"; +$fa-var-stumbleupon-circle: "\f1a3"; +$fa-var-subscript: "\f12c"; +$fa-var-subway: "\f239"; +$fa-var-suitcase: "\f0f2"; +$fa-var-sun-o: "\f185"; +$fa-var-superpowers: "\f2dd"; +$fa-var-superscript: "\f12b"; +$fa-var-support: "\f1cd"; +$fa-var-table: "\f0ce"; +$fa-var-tablet: "\f10a"; +$fa-var-tachometer: "\f0e4"; +$fa-var-tag: "\f02b"; +$fa-var-tags: "\f02c"; +$fa-var-tasks: "\f0ae"; +$fa-var-taxi: "\f1ba"; +$fa-var-telegram: "\f2c6"; +$fa-var-television: "\f26c"; +$fa-var-tencent-weibo: "\f1d5"; +$fa-var-terminal: "\f120"; +$fa-var-text-height: "\f034"; +$fa-var-text-width: "\f035"; +$fa-var-th: "\f00a"; +$fa-var-th-large: "\f009"; +$fa-var-th-list: "\f00b"; +$fa-var-themeisle: "\f2b2"; +$fa-var-thermometer: "\f2c7"; +$fa-var-thermometer-0: "\f2cb"; +$fa-var-thermometer-1: "\f2ca"; +$fa-var-thermometer-2: "\f2c9"; +$fa-var-thermometer-3: "\f2c8"; +$fa-var-thermometer-4: "\f2c7"; +$fa-var-thermometer-empty: "\f2cb"; +$fa-var-thermometer-full: "\f2c7"; +$fa-var-thermometer-half: "\f2c9"; +$fa-var-thermometer-quarter: "\f2ca"; +$fa-var-thermometer-three-quarters: "\f2c8"; +$fa-var-thumb-tack: "\f08d"; +$fa-var-thumbs-down: "\f165"; +$fa-var-thumbs-o-down: "\f088"; +$fa-var-thumbs-o-up: "\f087"; +$fa-var-thumbs-up: "\f164"; +$fa-var-ticket: "\f145"; +$fa-var-times: "\f00d"; +$fa-var-times-circle: "\f057"; +$fa-var-times-circle-o: "\f05c"; +$fa-var-times-rectangle: "\f2d3"; +$fa-var-times-rectangle-o: "\f2d4"; +$fa-var-tint: "\f043"; +$fa-var-toggle-down: "\f150"; +$fa-var-toggle-left: "\f191"; +$fa-var-toggle-off: "\f204"; +$fa-var-toggle-on: "\f205"; +$fa-var-toggle-right: "\f152"; +$fa-var-toggle-up: "\f151"; +$fa-var-trademark: "\f25c"; +$fa-var-train: "\f238"; +$fa-var-transgender: "\f224"; +$fa-var-transgender-alt: "\f225"; +$fa-var-trash: "\f1f8"; +$fa-var-trash-o: "\f014"; +$fa-var-tree: "\f1bb"; +$fa-var-trello: "\f181"; +$fa-var-tripadvisor: "\f262"; +$fa-var-trophy: "\f091"; +$fa-var-truck: "\f0d1"; +$fa-var-try: "\f195"; +$fa-var-tty: "\f1e4"; +$fa-var-tumblr: "\f173"; +$fa-var-tumblr-square: "\f174"; +$fa-var-turkish-lira: "\f195"; +$fa-var-tv: "\f26c"; +$fa-var-twitch: "\f1e8"; +$fa-var-twitter: "\f099"; +$fa-var-twitter-square: "\f081"; +$fa-var-umbrella: "\f0e9"; +$fa-var-underline: "\f0cd"; +$fa-var-undo: "\f0e2"; +$fa-var-universal-access: "\f29a"; +$fa-var-university: "\f19c"; +$fa-var-unlink: "\f127"; +$fa-var-unlock: "\f09c"; +$fa-var-unlock-alt: "\f13e"; +$fa-var-unsorted: "\f0dc"; +$fa-var-upload: "\f093"; +$fa-var-usb: "\f287"; +$fa-var-usd: "\f155"; +$fa-var-user: "\f007"; +$fa-var-user-circle: "\f2bd"; +$fa-var-user-circle-o: "\f2be"; +$fa-var-user-md: "\f0f0"; +$fa-var-user-o: "\f2c0"; +$fa-var-user-plus: "\f234"; +$fa-var-user-secret: "\f21b"; +$fa-var-user-times: "\f235"; +$fa-var-users: "\f0c0"; +$fa-var-vcard: "\f2bb"; +$fa-var-vcard-o: "\f2bc"; +$fa-var-venus: "\f221"; +$fa-var-venus-double: "\f226"; +$fa-var-venus-mars: "\f228"; +$fa-var-viacoin: "\f237"; +$fa-var-viadeo: "\f2a9"; +$fa-var-viadeo-square: "\f2aa"; +$fa-var-video-camera: "\f03d"; +$fa-var-vimeo: "\f27d"; +$fa-var-vimeo-square: "\f194"; +$fa-var-vine: "\f1ca"; +$fa-var-vk: "\f189"; +$fa-var-volume-control-phone: "\f2a0"; +$fa-var-volume-down: "\f027"; +$fa-var-volume-off: "\f026"; +$fa-var-volume-up: "\f028"; +$fa-var-warning: "\f071"; +$fa-var-wechat: "\f1d7"; +$fa-var-weibo: "\f18a"; +$fa-var-weixin: "\f1d7"; +$fa-var-whatsapp: "\f232"; +$fa-var-wheelchair: "\f193"; +$fa-var-wheelchair-alt: "\f29b"; +$fa-var-wifi: "\f1eb"; +$fa-var-wikipedia-w: "\f266"; +$fa-var-window-close: "\f2d3"; +$fa-var-window-close-o: "\f2d4"; +$fa-var-window-maximize: "\f2d0"; +$fa-var-window-minimize: "\f2d1"; +$fa-var-window-restore: "\f2d2"; +$fa-var-windows: "\f17a"; +$fa-var-won: "\f159"; +$fa-var-wordpress: "\f19a"; +$fa-var-wpbeginner: "\f297"; +$fa-var-wpexplorer: "\f2de"; +$fa-var-wpforms: "\f298"; +$fa-var-wrench: "\f0ad"; +$fa-var-xing: "\f168"; +$fa-var-xing-square: "\f169"; +$fa-var-y-combinator: "\f23b"; +$fa-var-y-combinator-square: "\f1d4"; +$fa-var-yahoo: "\f19e"; +$fa-var-yc: "\f23b"; +$fa-var-yc-square: "\f1d4"; +$fa-var-yelp: "\f1e9"; +$fa-var-yen: "\f157"; +$fa-var-yoast: "\f2b1"; +$fa-var-youtube: "\f167"; +$fa-var-youtube-play: "\f16a"; +$fa-var-youtube-square: "\f166"; diff --git a/public/sass/components/_drop.scss b/public/sass/components/_drop.scss index 8d9d4fc6b7d..6568414ed88 100644 --- a/public/sass/components/_drop.scss +++ b/public/sass/components/_drop.scss @@ -5,13 +5,13 @@ $useDropShadow: false; $attachmentOffset: 0%; $easing: cubic-bezier(0, 0, 0.265, 1); -@include drop-theme('error', $popover-error-bg, $popover-color); -@include drop-theme('popover', $popover-bg, $popover-color, $popover-border-color); -@include drop-theme('help', $popover-help-bg, $popover-help-color); +@include drop-theme("error", $popover-error-bg, $popover-color); +@include drop-theme("popover", $popover-bg, $popover-color, $popover-border-color); +@include drop-theme("help", $popover-help-bg, $popover-help-color); -@include drop-animation-scale('drop', 'help', $attachmentOffset: $attachmentOffset, $easing: $easing); -@include drop-animation-scale('drop', 'error', $attachmentOffset: $attachmentOffset, $easing: $easing); -@include drop-animation-scale('drop', 'popover', $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale("drop", "help", $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale("drop", "error", $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale("drop", "popover", $attachmentOffset: $attachmentOffset, $easing: $easing); .drop-element { z-index: 10000; diff --git a/public/sass/components/_filter-list.scss b/public/sass/components/_filter-list.scss index 90d0a21c539..7713aa05ac2 100644 --- a/public/sass/components/_filter-list.scss +++ b/public/sass/components/_filter-list.scss @@ -67,17 +67,17 @@ text-transform: uppercase; &.online { - background-image: url('/img/online.svg'); + background-image: url("/img/online.svg"); color: $online; } &.warn { - background-image: url('/img/warn-tiny.svg'); + background-image: url("/img/warn-tiny.svg"); color: $warn; } &.critical { - background-image: url('/img/critical.svg'); + background-image: url("/img/critical.svg"); color: $critical; } } diff --git a/public/sass/components/_footer.scss b/public/sass/components/_footer.scss index cec6f820118..8b7d64e47fe 100644 --- a/public/sass/components/_footer.scss +++ b/public/sass/components/_footer.scss @@ -25,7 +25,7 @@ display: inline-block; padding-right: 2px; &::after { - content: ' | '; + content: " | "; padding-left: 2px; } } @@ -33,7 +33,7 @@ li:last-child { &::after { padding-left: 0; - content: ''; + content: ""; } } } diff --git a/public/sass/components/_json_explorer.scss b/public/sass/components/_json_explorer.scss index aa212cd2dab..2b1be8bd4f5 100644 --- a/public/sass/components/_json_explorer.scss +++ b/public/sass/components/_json_explorer.scss @@ -21,10 +21,10 @@ display: none; } &.json-formatter-object::after { - content: 'No properties'; + content: "No properties"; } &.json-formatter-array::after { - content: '[]'; + content: "[]"; } } } @@ -87,7 +87,7 @@ &::after { display: inline-block; transition: transform $json-explorer-rotate-time ease-in; - content: '►'; + content: "►"; } } diff --git a/public/sass/components/_jsontree.scss b/public/sass/components/_jsontree.scss index 0a0497a0627..665deda0f12 100644 --- a/public/sass/components/_jsontree.scss +++ b/public/sass/components/_jsontree.scss @@ -35,12 +35,12 @@ json-tree { color: $variable; padding: 5px 10px 5px 15px; &::after { - content: ':'; + content: ":"; } } json-node.expandable { &::before { - content: '\25b6'; + content: "\25b6"; position: absolute; left: 0px; font-size: 8px; diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 7b935e3707d..1fb3eda1834 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -52,7 +52,7 @@ $path-position: $marker-size-half - ($path-height / 2); &::after { right: -50%; - content: ''; + content: ""; display: block; position: absolute; z-index: 1; @@ -105,7 +105,7 @@ $path-position: $marker-size-half - ($path-height / 2); // change icon to check .icon-gf::before { - content: '\e604'; + content: "\e604"; } } .progress-text { diff --git a/public/sass/components/_row.scss b/public/sass/components/_row.scss index 9cd564a4edf..3c1465a30bc 100644 --- a/public/sass/components/_row.scss +++ b/public/sass/components/_row.scss @@ -69,7 +69,7 @@ cursor: move; width: 1rem; height: 100%; - background: url('../img/grab_dark.svg') no-repeat 50% 50%; + background: url("../img/grab_dark.svg") no-repeat 50% 50%; background-size: 8px; visibility: hidden; position: absolute; diff --git a/public/sass/components/_shortcuts.scss b/public/sass/components/_shortcuts.scss index 83e112648cf..b5f61872585 100644 --- a/public/sass/components/_shortcuts.scss +++ b/public/sass/components/_shortcuts.scss @@ -33,7 +33,7 @@ text-align: center; margin-right: 0.3rem; padding: 3px 5px; - font: 11px Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 10px; color: #555; vertical-align: middle; diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 11ab2e3554a..c7eb1914103 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -64,8 +64,8 @@ } input + label::before { - font-family: 'FontAwesome'; - content: '\f096'; // square-o + font-family: "FontAwesome"; + content: "\f096"; // square-o color: $text-color-weak; transition: transform 0.4s; backface-visibility: hidden; @@ -73,11 +73,11 @@ } input + label::after { - content: '\f046'; // check-square-o + content: "\f046"; // check-square-o color: $orange; text-shadow: $text-shadow-strong; - font-family: 'FontAwesome'; + font-family: "FontAwesome"; transition: transform 0.4s; transform: rotateY(180deg); backface-visibility: hidden; diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index 44a116cd0a4..197d5892652 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -44,13 +44,18 @@ &::before { display: block; - content: ' '; + content: " "; position: absolute; left: 0; right: 0; height: 2px; top: 0; - background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%); + background-image: linear-gradient( + to right, + #ffd500 0%, + #ff4400 99%, + #ff4400 100% + ); } } } diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index b9e39ae9e04..2d7a12c3d01 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -103,10 +103,10 @@ } .fa-chevron-left::before { - content: '\f053'; + content: "\f053"; } .fa-chevron-right::before { - content: '\f054'; + content: "\f054"; } .glyphicon-chevron-right { diff --git a/public/sass/grafana.dark.scss b/public/sass/grafana.dark.scss index f7f5163f36f..53193d213e6 100644 --- a/public/sass/grafana.dark.scss +++ b/public/sass/grafana.dark.scss @@ -1,3 +1,3 @@ -@import 'variables'; -@import 'variables.dark'; -@import 'grafana'; +@import "variables"; +@import "variables.dark"; +@import "grafana"; diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss index eb354b219fc..e7e53382e0e 100644 --- a/public/sass/mixins/_drop_element.scss +++ b/public/sass/mixins/_drop_element.scss @@ -15,7 +15,7 @@ border: 1px solid $border-color; &:before { - content: ''; + content: ""; display: block; position: absolute; width: 0; @@ -88,7 +88,8 @@ left: $popover-arrow-size * 2; } - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-middle .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-middle + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -98,7 +99,8 @@ } } - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-middle .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-middle + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -108,7 +110,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-middle .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-middle + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -118,7 +121,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-middle .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-middle + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -129,7 +133,8 @@ } // Top and bottom corners - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -139,7 +144,8 @@ } } - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -149,7 +155,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -159,7 +166,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -170,7 +178,8 @@ } // Side corners - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left + .drop-content { margin-right: $popover-arrow-size; &:before { @@ -180,7 +189,8 @@ } } - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right + .drop-content { margin-left: $popover-arrow-size; &:before { @@ -190,7 +200,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left + .drop-content { margin-right: $popover-arrow-size; &:before { @@ -200,7 +211,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right + .drop-content { margin-left: $popover-arrow-size; &:before { @@ -212,7 +224,7 @@ } } -@mixin drop-animation-scale($themePrefix: 'drop', $themeName: 'default', $attachmentOffset: 0, $easing: 'linear') { +@mixin drop-animation-scale($themePrefix: "drop", $themeName: "default", $attachmentOffset: 0, $easing: "linear") { .#{$themePrefix}-element.#{$themePrefix}-#{$themeName} { transform: translateZ(0); transition: opacity 100ms; @@ -235,16 +247,20 @@ } } // Centers and middles - &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center + .#{$themePrefix}-content { transform-origin: 50% calc(100% + #{$attachmentOffset}); } - &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center + .#{$themePrefix}-content { transform-origin: 50% (-$attachmentOffset); } - &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle + .#{$themePrefix}-content { transform-origin: calc(100% + #{$attachmentOffset}) 50%; } - &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle + .#{$themePrefix}-content { transform-origin: -($attachmentOffset 50%); } // Top and bottom corners diff --git a/public/sass/mixins/_forms.scss b/public/sass/mixins/_forms.scss index 2f163e0f46b..ce488f0f636 100644 --- a/public/sass/mixins/_forms.scss +++ b/public/sass/mixins/_forms.scss @@ -41,7 +41,8 @@ &:focus { border-color: $input-border-focus; outline: none; - $shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px $input-box-shadow-focus; + $shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 5px $input-box-shadow-focus; @include box-shadow($shadow); } } diff --git a/public/sass/mixins/_mixins.scss b/public/sass/mixins/_mixins.scss index 2e3c2fdcdd8..f3be6af56ba 100644 --- a/public/sass/mixins/_mixins.scss +++ b/public/sass/mixins/_mixins.scss @@ -1,6 +1,6 @@ @mixin clearfix() { &::after { - content: ''; + content: ""; display: table; clear: both; } @@ -265,10 +265,20 @@ // Add an alphatransparency value to any background or border color (via Elyse Holladay) #translucent { @mixin background($color: $white, $alpha: 1) { - background-color: hsla(hue($color), saturation($color), lightness($color), $alpha); + background-color: hsla( + hue($color), + saturation($color), + lightness($color), + $alpha + ); } @mixin border($color: $white, $alpha: 1) { - border-color: hsla(hue($color), saturation($color), lightness($color), $alpha); + border-color: hsla( + hue($color), + saturation($color), + lightness($color), + $alpha + ); @include background-clip(padding-box); } } @@ -284,37 +294,66 @@ // Gradients @mixin gradient-horizontal($startColor: #555, $endColor: #333) { background-color: $endColor; - background-image: linear-gradient(to right, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + to right, + $startColor, + $endColor + ); // Standard, IE10 background-repeat: repeat-x; } @mixin gradient-vertical($startColor: #555, $endColor: #333) { background-color: mix($startColor, $endColor, 60%); - background-image: linear-gradient(to bottom, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + to bottom, + $startColor, + $endColor + ); // Standard, IE10 background-repeat: repeat-x; } @mixin gradient-directional($startColor: #555, $endColor: #333, $deg: 45deg) { background-color: $endColor; background-repeat: repeat-x; - background-image: linear-gradient($deg, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + $deg, + $startColor, + $endColor + ); // Standard, IE10 } @mixin gradient-horizontal-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) { background-color: mix($midColor, $endColor, 80%); - background-image: linear-gradient(to right, $startColor, $midColor $colorStop, $endColor); + background-image: linear-gradient( + to right, + $startColor, + $midColor $colorStop, + $endColor + ); background-repeat: no-repeat; } @mixin gradient-vertical-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) { background-color: mix($midColor, $endColor, 80%); - background-image: linear-gradient($startColor, $midColor $colorStop, $endColor); + background-image: linear-gradient( + $startColor, + $midColor $colorStop, + $endColor + ); background-repeat: no-repeat; } @mixin gradient-radial($innerColor: #555, $outerColor: #333) { background-color: $outerColor; - background-image: -webkit-gradient(radial, center center, 0, center center, 460, from($innerColor), to($outerColor)); + background-image: -webkit-gradient( + radial, + center center, + 0, + center center, + 460, + from($innerColor), + to($outerColor) + ); background-image: -webkit-radial-gradient(circle, $innerColor, $outerColor); background-image: -moz-radial-gradient(circle, $innerColor, $outerColor); background-image: -o-radial-gradient(circle, $innerColor, $outerColor); @@ -341,7 +380,11 @@ @mixin left-brand-border-gradient() { border: none; - border-image: linear-gradient(rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); + border-image: linear-gradient( + rgba(255, 213, 0, 1) 0%, + rgba(255, 68, 0, 1) 99%, + rgba(255, 68, 0, 1) 100% + ); border-image-slice: 1; border-style: solid; border-top: 0; diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss index ce1e9eb4ea1..8622eec4e99 100644 --- a/public/sass/pages/_login.scss +++ b/public/sass/pages/_login.scss @@ -371,7 +371,7 @@ select:-webkit-autofill:focus { left: 0; right: 0; height: 100%; - content: ''; + content: ""; display: block; } diff --git a/public/sass/pages/_playlist.scss b/public/sass/pages/_playlist.scss index 37fa7f17e20..5dd1c92cbd2 100644 --- a/public/sass/pages/_playlist.scss +++ b/public/sass/pages/_playlist.scss @@ -84,11 +84,11 @@ background-color: $list-item-bg; margin-bottom: 4px; .search-result-icon:before { - content: '\f009'; + content: "\f009"; } &.search-item-dash-home .search-result-icon:before { - content: '\f015'; + content: "\f015"; } } diff --git a/public/sass/utils/_validation.scss b/public/sass/utils/_validation.scss index c074d847490..86b7c008bfd 100644 --- a/public/sass/utils/_validation.scss +++ b/public/sass/utils/_validation.scss @@ -1,4 +1,4 @@ -input[type='text'].ng-dirty.ng-invalid { +input[type="text"].ng-dirty.ng-invalid { } input.validation-error, diff --git a/public/test/core/utils/version_specs.ts b/public/test/core/utils/version_specs.ts index 9abc4f326fc..a057c8e16bd 100644 --- a/public/test/core/utils/version_specs.ts +++ b/public/test/core/utils/version_specs.ts @@ -1,8 +1,8 @@ -import { describe, beforeEach, it, expect } from 'test/lib/common'; +import {describe, beforeEach, it, expect} from 'test/lib/common'; -import { SemVersion, isVersionGtOrEq } from 'app/core/utils/version'; +import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version'; -describe('SemVersion', () => { +describe("SemVersion", () => { let version = '1.0.0-alpha.1'; describe('parsing', () => { @@ -23,13 +23,13 @@ describe('SemVersion', () => { it('should detect greater version properly', () => { let semver = new SemVersion(version); let cases = [ - { value: '3.4.5', expected: true }, - { value: '3.4.4', expected: true }, - { value: '3.4.6', expected: false }, - { value: '4', expected: false }, - { value: '3.5', expected: false }, + {value: '3.4.5', expected: true}, + {value: '3.4.4', expected: true}, + {value: '3.4.6', expected: false}, + {value: '4', expected: false}, + {value: '3.5', expected: false}, ]; - cases.forEach(testCase => { + cases.forEach((testCase) => { expect(semver.isGtOrEq(testCase.value)).to.be(testCase.expected); }); }); @@ -38,16 +38,16 @@ describe('SemVersion', () => { describe('isVersionGtOrEq', () => { it('should compare versions properly (a >= b)', () => { let cases = [ - { values: ['3.4.5', '3.4.5'], expected: true }, - { values: ['3.4.5', '3.4.4'], expected: true }, - { values: ['3.4.5', '3.4.6'], expected: false }, - { values: ['3.4', '3.4.0'], expected: true }, - { values: ['3', '3.0.0'], expected: true }, - { values: ['3.1.1-beta1', '3.1'], expected: true }, - { values: ['3.4.5', '4'], expected: false }, - { values: ['3.4.5', '3.5'], expected: false }, + {values: ['3.4.5', '3.4.5'], expected: true}, + {values: ['3.4.5', '3.4.4'] , expected: true}, + {values: ['3.4.5', '3.4.6'], expected: false}, + {values: ['3.4', '3.4.0'], expected: true}, + {values: ['3', '3.0.0'], expected: true}, + {values: ['3.1.1-beta1', '3.1'], expected: true}, + {values: ['3.4.5', '4'], expected: false}, + {values: ['3.4.5', '3.5'], expected: false}, ]; - cases.forEach(testCase => { + cases.forEach((testCase) => { expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).to.be(testCase.expected); }); }); diff --git a/public/test/jest-shim.ts b/public/test/jest-shim.ts index f9af0c4a3f3..80c4bb3d21b 100644 --- a/public/test/jest-shim.ts +++ b/public/test/jest-shim.ts @@ -1,5 +1,6 @@ declare var global: NodeJS.Global; -(global).requestAnimationFrame = callback => { +(global).requestAnimationFrame = (callback) => { setTimeout(callback, 0); }; + diff --git a/public/test/mocks/backend_srv.ts b/public/test/mocks/backend_srv.ts index 32c04866395..666de593722 100644 --- a/public/test/mocks/backend_srv.ts +++ b/public/test/mocks/backend_srv.ts @@ -1,5 +1,8 @@ export class BackendSrvMock { search: any; - constructor() {} + constructor() { + } + } + diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index b92e853d8f8..8e83915362f 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; import config from 'app/core/config'; import * as dateMath from 'app/core/utils/datemath'; -import { angularMocks, sinon } from '../lib/common'; -import { PanelModel } from 'app/features/dashboard/panel_model'; +import {angularMocks, sinon} from '../lib/common'; +import {PanelModel} from 'app/features/dashboard/panel_model'; export function ControllerTestContext() { var self = this; @@ -42,8 +42,8 @@ export function ControllerTestContext() { self.$location = $location; self.$browser = $browser; self.$q = $q; - self.panel = new PanelModel({ type: 'test' }); - self.dashboard = { meta: {} }; + self.panel = new PanelModel({type: 'test'}); + self.dashboard = {meta: {}}; $rootScope.appEvent = sinon.spy(); $rootScope.onAppEvent = sinon.spy(); @@ -53,14 +53,14 @@ export function ControllerTestContext() { $rootScope.colors.push('#' + i); } - config.panels['test'] = { info: {} }; + config.panels['test'] = {info: {}}; self.ctrl = $controller( Ctrl, - { $scope: self.scope }, + {$scope: self.scope}, { panel: self.panel, dashboard: self.dashboard, - } + }, ); }); }; @@ -72,7 +72,7 @@ export function ControllerTestContext() { self.$browser = $browser; self.scope.contextSrv = {}; self.scope.panel = {}; - self.scope.dashboard = { meta: {} }; + self.scope.dashboard = {meta: {}}; self.scope.dashboardMeta = {}; self.scope.dashboardViewState = new DashboardViewStateStub(); self.scope.appEvent = sinon.spy(); @@ -131,7 +131,7 @@ export function DashboardViewStateStub() { export function TimeSrvStub() { this.init = sinon.spy(); - this.time = { from: 'now-1h', to: 'now' }; + this.time = {from: 'now-1h', to: 'now'}; this.timeRange = function(parse) { if (parse === false) { return this.time; @@ -159,7 +159,7 @@ export function ContextSrvStub() { export function TemplateSrvStub() { this.variables = []; - this.templateSettings = { interpolate: /\[\[([\s\S]+?)\]\]/g }; + this.templateSettings = {interpolate: /\[\[([\s\S]+?)\]\]/g}; this.data = {}; this.replace = function(text) { return _.template(text, this.templateSettings)(this.data); @@ -188,7 +188,7 @@ var allDeps = { TimeSrvStub: TimeSrvStub, ControllerTestContext: ControllerTestContext, ServiceTestContext: ServiceTestContext, - DashboardViewStateStub: DashboardViewStateStub, + DashboardViewStateStub: DashboardViewStateStub }; // for legacy diff --git a/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go b/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go index 8508063bc35..b98a7653467 100644 --- a/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go +++ b/vendor/github.com/bradfitz/gomemcache/memcache/memcache.go @@ -457,7 +457,7 @@ func (c *Client) GetMulti(keys []string) (map[string]*Item, error) { } var err error - for range keyMap { + for _ = range keyMap { if ge := <-ch; ge != nil { err = ge } diff --git a/vendor/github.com/hashicorp/go-plugin/client.go b/vendor/github.com/hashicorp/go-plugin/client.go index de03690703f..b912826b200 100644 --- a/vendor/github.com/hashicorp/go-plugin/client.go +++ b/vendor/github.com/hashicorp/go-plugin/client.go @@ -567,7 +567,7 @@ func (c *Client) Start() (addr net.Addr, err error) { // so they don't block since it is an io.Pipe defer func() { go func() { - for range linesCh { + for _ = range linesCh { } }() }() diff --git a/vendor/github.com/hashicorp/go-plugin/rpc_client.go b/vendor/github.com/hashicorp/go-plugin/rpc_client.go index 4d99d42c7e1..f30a4b1d387 100644 --- a/vendor/github.com/hashicorp/go-plugin/rpc_client.go +++ b/vendor/github.com/hashicorp/go-plugin/rpc_client.go @@ -75,7 +75,7 @@ func NewRPCClient(conn io.ReadWriteCloser, plugins map[string]Plugin) (*RPCClien // Connect stdout, stderr streams stdstream := make([]net.Conn, 2) - for i := range stdstream { + for i, _ := range stdstream { stdstream[i], err = mux.Open() if err != nil { mux.Close() diff --git a/vendor/github.com/hashicorp/go-plugin/rpc_server.go b/vendor/github.com/hashicorp/go-plugin/rpc_server.go index 168ef7dd944..5bb18dd5db1 100644 --- a/vendor/github.com/hashicorp/go-plugin/rpc_server.go +++ b/vendor/github.com/hashicorp/go-plugin/rpc_server.go @@ -78,7 +78,7 @@ func (s *RPCServer) ServeConn(conn io.ReadWriteCloser) { // Connect the stdstreams (in, out, err) stdstream := make([]net.Conn, 2) - for i := range stdstream { + for i, _ := range stdstream { stdstream[i], err = mux.Accept() if err != nil { mux.Close() diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go index 1857f93f226..82ad7bc8f1c 100644 --- a/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go +++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go @@ -85,7 +85,7 @@ func (dmp *DiffMatchPatch) diffMainRunes(text1, text2 []rune, checklines bool, d // Restore the prefix and suffix. if len(commonprefix) != 0 { - diffs = append([]Diff{{DiffEqual, string(commonprefix)}}, diffs...) + diffs = append([]Diff{Diff{DiffEqual, string(commonprefix)}}, diffs...) } if len(commonsuffix) != 0 { diffs = append(diffs, Diff{DiffEqual, string(commonsuffix)}) @@ -122,16 +122,16 @@ func (dmp *DiffMatchPatch) diffCompute(text1, text2 []rune, checklines bool, dea } // Shorter text is inside the longer text (speedup). return []Diff{ - {op, string(longtext[:i])}, - {DiffEqual, string(shorttext)}, - {op, string(longtext[i+len(shorttext):])}, + Diff{op, string(longtext[:i])}, + Diff{DiffEqual, string(shorttext)}, + Diff{op, string(longtext[i+len(shorttext):])}, } } else if len(shorttext) == 1 { // Single character string. // After the previous speedup, the character can't be an equality. return []Diff{ - {DiffDelete, string(text1)}, - {DiffInsert, string(text2)}, + Diff{DiffDelete, string(text1)}, + Diff{DiffInsert, string(text2)}, } // Check to see if the problem can be split in two. } else if hm := dmp.diffHalfMatch(text1, text2); hm != nil { @@ -145,7 +145,7 @@ func (dmp *DiffMatchPatch) diffCompute(text1, text2 []rune, checklines bool, dea diffsA := dmp.diffMainRunes(text1A, text2A, checklines, deadline) diffsB := dmp.diffMainRunes(text1B, text2B, checklines, deadline) // Merge the results. - return append(diffsA, append([]Diff{{DiffEqual, string(midCommon)}}, diffsB...)...) + return append(diffsA, append([]Diff{Diff{DiffEqual, string(midCommon)}}, diffsB...)...) } else if checklines && len(text1) > 100 && len(text2) > 100 { return dmp.diffLineMode(text1, text2, deadline) } @@ -330,8 +330,8 @@ func (dmp *DiffMatchPatch) diffBisect(runes1, runes2 []rune, deadline time.Time) } // Diff took too long and hit the deadline or number of diffs equals number of characters, no commonality at all. return []Diff{ - {DiffDelete, string(runes1)}, - {DiffInsert, string(runes2)}, + Diff{DiffDelete, string(runes1)}, + Diff{DiffInsert, string(runes2)}, } } @@ -673,7 +673,7 @@ func (dmp *DiffMatchPatch) DiffCleanupSemantic(diffs []Diff) []Diff { insPoint := equalities.data diffs = append( diffs[:insPoint], - append([]Diff{{DiffDelete, lastequality}}, diffs[insPoint:]...)...) + append([]Diff{Diff{DiffDelete, lastequality}}, diffs[insPoint:]...)...) // Change second copy to insert. diffs[insPoint+1].Type = DiffInsert @@ -726,7 +726,7 @@ func (dmp *DiffMatchPatch) DiffCleanupSemantic(diffs []Diff) []Diff { // Overlap found. Insert an equality and trim the surrounding edits. diffs = append( diffs[:pointer], - append([]Diff{{DiffEqual, insertion[:overlapLength1]}}, diffs[pointer:]...)...) + append([]Diff{Diff{DiffEqual, insertion[:overlapLength1]}}, diffs[pointer:]...)...) diffs[pointer-1].Text = deletion[0 : len(deletion)-overlapLength1] @@ -955,7 +955,7 @@ func (dmp *DiffMatchPatch) DiffCleanupEfficiency(diffs []Diff) []Diff { // Duplicate record. diffs = append(diffs[:insPoint], - append([]Diff{{DiffDelete, lastequality}}, diffs[insPoint:]...)...) + append([]Diff{Diff{DiffDelete, lastequality}}, diffs[insPoint:]...)...) // Change second copy to insert. diffs[insPoint+1].Type = DiffInsert @@ -1028,7 +1028,7 @@ func (dmp *DiffMatchPatch) DiffCleanupMerge(diffs []Diff) []Diff { if x > 0 && diffs[x-1].Type == DiffEqual { diffs[x-1].Text += string(textInsert[:commonlength]) } else { - diffs = append([]Diff{{DiffEqual, string(textInsert[:commonlength])}}, diffs...) + diffs = append([]Diff{Diff{DiffEqual, string(textInsert[:commonlength])}}, diffs...) pointer++ } textInsert = textInsert[commonlength:] diff --git a/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go b/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go index 1708a96fbed..223c43c4268 100644 --- a/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go +++ b/vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go @@ -93,7 +93,7 @@ func (dmp *DiffMatchPatch) PatchAddContext(patch Patch, text string) Patch { // Add the prefix. prefix := text[max(0, patch.Start2-padding):patch.Start2] if len(prefix) != 0 { - patch.diffs = append([]Diff{{DiffEqual, prefix}}, patch.diffs...) + patch.diffs = append([]Diff{Diff{DiffEqual, prefix}}, patch.diffs...) } // Add the suffix. suffix := text[patch.Start2+patch.Length1 : min(len(text), patch.Start2+patch.Length1+padding)] @@ -336,7 +336,7 @@ func (dmp *DiffMatchPatch) PatchAddPadding(patches []Patch) string { // Add some padding on start of first diff. if len(patches[0].diffs) == 0 || patches[0].diffs[0].Type != DiffEqual { // Add nullPadding equality. - patches[0].diffs = append([]Diff{{DiffEqual, nullPadding}}, patches[0].diffs...) + patches[0].diffs = append([]Diff{Diff{DiffEqual, nullPadding}}, patches[0].diffs...) patches[0].Start1 -= paddingLength // Should be 0. patches[0].Start2 -= paddingLength // Should be 0. patches[0].Length1 += paddingLength diff --git a/vendor/golang.org/x/net/http2/transport.go b/vendor/golang.org/x/net/http2/transport.go index a3fe975f049..e6b321f4bb6 100644 --- a/vendor/golang.org/x/net/http2/transport.go +++ b/vendor/golang.org/x/net/http2/transport.go @@ -321,9 +321,7 @@ func (noCachedConnError) Error() string { return "http2: no cached c // or its equivalent renamed type in net/http2's h2_bundle.go. Both types // may coexist in the same running program. func isNoCachedConnError(err error) bool { - _, ok := err.(interface { - IsHTTP2NoCachedConnError() - }) + _, ok := err.(interface{ IsHTTP2NoCachedConnError() }) return ok } diff --git a/vendor/golang.org/x/text/language/gen.go b/vendor/golang.org/x/text/language/gen.go index fea288d4621..302f1940aaf 100644 --- a/vendor/golang.org/x/text/language/gen.go +++ b/vendor/golang.org/x/text/language/gen.go @@ -1050,7 +1050,7 @@ func (b *builder) writeRegion() { m49Index := [9]int16{} fromM49 := []uint16{} m49 := []int{} - for k := range fromM49map { + for k, _ := range fromM49map { m49 = append(m49, int(k)) } sort.Ints(m49) diff --git a/vendor/golang.org/x/text/language/lookup.go b/vendor/golang.org/x/text/language/lookup.go index 96d16dac9d3..1d80ac37082 100644 --- a/vendor/golang.org/x/text/language/lookup.go +++ b/vendor/golang.org/x/text/language/lookup.go @@ -344,39 +344,39 @@ var ( // grandfatheredMap holds a mapping from legacy and grandfathered tags to // their base language or index to more elaborate tag. grandfatheredMap = map[[maxLen]byte]int16{ - {'a', 'r', 't', '-', 'l', 'o', 'j', 'b', 'a', 'n'}: _jbo, // art-lojban - {'i', '-', 'a', 'm', 'i'}: _ami, // i-ami - {'i', '-', 'b', 'n', 'n'}: _bnn, // i-bnn - {'i', '-', 'h', 'a', 'k'}: _hak, // i-hak - {'i', '-', 'k', 'l', 'i', 'n', 'g', 'o', 'n'}: _tlh, // i-klingon - {'i', '-', 'l', 'u', 'x'}: _lb, // i-lux - {'i', '-', 'n', 'a', 'v', 'a', 'j', 'o'}: _nv, // i-navajo - {'i', '-', 'p', 'w', 'n'}: _pwn, // i-pwn - {'i', '-', 't', 'a', 'o'}: _tao, // i-tao - {'i', '-', 't', 'a', 'y'}: _tay, // i-tay - {'i', '-', 't', 's', 'u'}: _tsu, // i-tsu - {'n', 'o', '-', 'b', 'o', 'k'}: _nb, // no-bok - {'n', 'o', '-', 'n', 'y', 'n'}: _nn, // no-nyn - {'s', 'g', 'n', '-', 'b', 'e', '-', 'f', 'r'}: _sfb, // sgn-BE-FR - {'s', 'g', 'n', '-', 'b', 'e', '-', 'n', 'l'}: _vgt, // sgn-BE-NL - {'s', 'g', 'n', '-', 'c', 'h', '-', 'd', 'e'}: _sgg, // sgn-CH-DE - {'z', 'h', '-', 'g', 'u', 'o', 'y', 'u'}: _cmn, // zh-guoyu - {'z', 'h', '-', 'h', 'a', 'k', 'k', 'a'}: _hak, // zh-hakka - {'z', 'h', '-', 'm', 'i', 'n', '-', 'n', 'a', 'n'}: _nan, // zh-min-nan - {'z', 'h', '-', 'x', 'i', 'a', 'n', 'g'}: _hsn, // zh-xiang + [maxLen]byte{'a', 'r', 't', '-', 'l', 'o', 'j', 'b', 'a', 'n'}: _jbo, // art-lojban + [maxLen]byte{'i', '-', 'a', 'm', 'i'}: _ami, // i-ami + [maxLen]byte{'i', '-', 'b', 'n', 'n'}: _bnn, // i-bnn + [maxLen]byte{'i', '-', 'h', 'a', 'k'}: _hak, // i-hak + [maxLen]byte{'i', '-', 'k', 'l', 'i', 'n', 'g', 'o', 'n'}: _tlh, // i-klingon + [maxLen]byte{'i', '-', 'l', 'u', 'x'}: _lb, // i-lux + [maxLen]byte{'i', '-', 'n', 'a', 'v', 'a', 'j', 'o'}: _nv, // i-navajo + [maxLen]byte{'i', '-', 'p', 'w', 'n'}: _pwn, // i-pwn + [maxLen]byte{'i', '-', 't', 'a', 'o'}: _tao, // i-tao + [maxLen]byte{'i', '-', 't', 'a', 'y'}: _tay, // i-tay + [maxLen]byte{'i', '-', 't', 's', 'u'}: _tsu, // i-tsu + [maxLen]byte{'n', 'o', '-', 'b', 'o', 'k'}: _nb, // no-bok + [maxLen]byte{'n', 'o', '-', 'n', 'y', 'n'}: _nn, // no-nyn + [maxLen]byte{'s', 'g', 'n', '-', 'b', 'e', '-', 'f', 'r'}: _sfb, // sgn-BE-FR + [maxLen]byte{'s', 'g', 'n', '-', 'b', 'e', '-', 'n', 'l'}: _vgt, // sgn-BE-NL + [maxLen]byte{'s', 'g', 'n', '-', 'c', 'h', '-', 'd', 'e'}: _sgg, // sgn-CH-DE + [maxLen]byte{'z', 'h', '-', 'g', 'u', 'o', 'y', 'u'}: _cmn, // zh-guoyu + [maxLen]byte{'z', 'h', '-', 'h', 'a', 'k', 'k', 'a'}: _hak, // zh-hakka + [maxLen]byte{'z', 'h', '-', 'm', 'i', 'n', '-', 'n', 'a', 'n'}: _nan, // zh-min-nan + [maxLen]byte{'z', 'h', '-', 'x', 'i', 'a', 'n', 'g'}: _hsn, // zh-xiang // Grandfathered tags with no modern replacement will be converted as // follows: - {'c', 'e', 'l', '-', 'g', 'a', 'u', 'l', 'i', 's', 'h'}: -1, // cel-gaulish - {'e', 'n', '-', 'g', 'b', '-', 'o', 'e', 'd'}: -2, // en-GB-oed - {'i', '-', 'd', 'e', 'f', 'a', 'u', 'l', 't'}: -3, // i-default - {'i', '-', 'e', 'n', 'o', 'c', 'h', 'i', 'a', 'n'}: -4, // i-enochian - {'i', '-', 'm', 'i', 'n', 'g', 'o'}: -5, // i-mingo - {'z', 'h', '-', 'm', 'i', 'n'}: -6, // zh-min + [maxLen]byte{'c', 'e', 'l', '-', 'g', 'a', 'u', 'l', 'i', 's', 'h'}: -1, // cel-gaulish + [maxLen]byte{'e', 'n', '-', 'g', 'b', '-', 'o', 'e', 'd'}: -2, // en-GB-oed + [maxLen]byte{'i', '-', 'd', 'e', 'f', 'a', 'u', 'l', 't'}: -3, // i-default + [maxLen]byte{'i', '-', 'e', 'n', 'o', 'c', 'h', 'i', 'a', 'n'}: -4, // i-enochian + [maxLen]byte{'i', '-', 'm', 'i', 'n', 'g', 'o'}: -5, // i-mingo + [maxLen]byte{'z', 'h', '-', 'm', 'i', 'n'}: -6, // zh-min // CLDR-specific tag. - {'r', 'o', 'o', 't'}: 0, // root - {'e', 'n', '-', 'u', 's', '-', 'p', 'o', 's', 'i', 'x'}: -7, // en_US_POSIX" + [maxLen]byte{'r', 'o', 'o', 't'}: 0, // root + [maxLen]byte{'e', 'n', '-', 'u', 's', '-', 'p', 'o', 's', 'i', 'x'}: -7, // en_US_POSIX" } altTagIndex = [...]uint8{0, 17, 31, 45, 61, 74, 86, 102} diff --git a/vendor/golang.org/x/text/language/tables.go b/vendor/golang.org/x/text/language/tables.go index a28524e1d72..b738d457b5d 100644 --- a/vendor/golang.org/x/text/language/tables.go +++ b/vendor/golang.org/x/text/language/tables.go @@ -3348,9 +3348,9 @@ var regionToGroups = [358]uint8{ // Size: 18 bytes, 3 elements var paradigmLocales = [3][3]uint16{ - 0: {0x139, 0x0, 0x7b}, - 1: {0x13e, 0x0, 0x1f}, - 2: {0x3c0, 0x41, 0xee}, + 0: [3]uint16{0x139, 0x0, 0x7b}, + 1: [3]uint16{0x13e, 0x0, 0x1f}, + 2: [3]uint16{0x3c0, 0x41, 0xee}, } type mutualIntelligibility struct { diff --git a/vendor/golang.org/x/text/unicode/cldr/cldr.go b/vendor/golang.org/x/text/unicode/cldr/cldr.go index 19b8cefd706..2197f8ac268 100644 --- a/vendor/golang.org/x/text/unicode/cldr/cldr.go +++ b/vendor/golang.org/x/text/unicode/cldr/cldr.go @@ -110,7 +110,7 @@ func (cldr *CLDR) Supplemental() *SupplementalData { func (cldr *CLDR) Locales() []string { loc := []string{"root"} hasRoot := false - for l := range cldr.locale { + for l, _ := range cldr.locale { if l == "root" { hasRoot = true continue diff --git a/vendor/golang.org/x/text/unicode/cldr/resolve.go b/vendor/golang.org/x/text/unicode/cldr/resolve.go index c6919216b80..691b5903fe4 100644 --- a/vendor/golang.org/x/text/unicode/cldr/resolve.go +++ b/vendor/golang.org/x/text/unicode/cldr/resolve.go @@ -289,7 +289,7 @@ var distinguishing = map[string][]string{ "mzone": nil, "from": nil, "to": nil, - "type": { + "type": []string{ "abbreviationFallback", "default", "mapping", @@ -527,7 +527,7 @@ func (cldr *CLDR) inheritSlice(enc, v, parent reflect.Value) (res reflect.Value, } } keys := make([]string, 0, len(index)) - for k := range index { + for k, _ := range index { keys = append(keys, k) } sort.Strings(keys) diff --git a/vendor/golang.org/x/text/unicode/cldr/slice.go b/vendor/golang.org/x/text/unicode/cldr/slice.go index ea5f31a3903..388c983ff13 100644 --- a/vendor/golang.org/x/text/unicode/cldr/slice.go +++ b/vendor/golang.org/x/text/unicode/cldr/slice.go @@ -83,7 +83,7 @@ func (s Slice) Group(fn func(e Elem) string) []Slice { m[key] = append(m[key], vi) } keys := []string{} - for k := range m { + for k, _ := range m { keys = append(keys, k) } sort.Strings(keys) diff --git a/vendor/golang.org/x/text/unicode/norm/maketables.go b/vendor/golang.org/x/text/unicode/norm/maketables.go index f66778d450a..338c395ee6f 100644 --- a/vendor/golang.org/x/text/unicode/norm/maketables.go +++ b/vendor/golang.org/x/text/unicode/norm/maketables.go @@ -241,7 +241,7 @@ func compactCCC() { m[c.ccc] = 0 } cccs := []int{} - for v := range m { + for v, _ := range m { cccs = append(cccs, int(v)) } sort.Ints(cccs) diff --git a/vendor/gopkg.in/macaron.v1/context.go b/vendor/gopkg.in/macaron.v1/context.go index 0f86e0d41ec..94a8c45d7da 100644 --- a/vendor/gopkg.in/macaron.v1/context.go +++ b/vendor/gopkg.in/macaron.v1/context.go @@ -270,7 +270,7 @@ func (ctx *Context) SetParams(name, val string) { // ReplaceAllParams replace all current params with given params func (ctx *Context) ReplaceAllParams(params Params) { - ctx.params = params + ctx.params = params; } // ParamsEscape returns escapred params result. From 1ce3e49e72d92e44fc6e4fe8ab4446a93d6ae262 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 15 Mar 2018 00:05:15 +0100 Subject: [PATCH 0083/3000] fix lint problems --- .../core/components/Login/LoginBackground.tsx | 16 +++++----- .../app/core/components/PasswordStrength.tsx | 15 +++++---- .../components/colorpicker/ColorPalette.tsx | 4 +-- .../colorpicker/ColorPickerPopover.tsx | 31 ++++++++----------- .../components/colorpicker/SpectrumPicker.tsx | 24 +++++++------- .../core/components/search/SearchResult.tsx | 31 ++++++++++++------- .../app/core/specs/PasswordStrength.jest.tsx | 13 +++++--- public/app/core/utils/kbn.ts | 12 +++---- .../dashboard/dashgrid/DashboardPanel.tsx | 15 +++++---- 9 files changed, 86 insertions(+), 75 deletions(-) diff --git a/public/app/core/components/Login/LoginBackground.tsx b/public/app/core/components/Login/LoginBackground.tsx index fb554845240..83e228ab6e0 100644 --- a/public/app/core/components/Login/LoginBackground.tsx +++ b/public/app/core/components/Login/LoginBackground.tsx @@ -4,14 +4,10 @@ const xCount = 50; const yCount = 50; function Cell({ x, y, flipIndex }) { - const index = y * xCount + x; + const index = (y * xCount) + x; const bgColor1 = getColor(x, y); return ( -
    +
    ); } @@ -35,7 +31,7 @@ export default class LoginBackground extends Component { } flipElements() { - const elementIndexToFlip = getRandomInt(0, xCount * yCount - 1); + const elementIndexToFlip = getRandomInt(0, (xCount * yCount) - 1); this.setState(prevState => { return { ...prevState, @@ -61,7 +57,9 @@ export default class LoginBackground extends Component { return (
    {Array.from(Array(xCount)).map((el2, x) => { - return ; + return ( + + ); })}
    ); @@ -1238,5 +1236,5 @@ function getColor(x, y) { // let randY = getRandomInt(0, y); // let randIndex = randY * xCount + randX; - return colors[(y * xCount + x) % colors.length]; + return colors[(y*xCount + x) % colors.length]; } diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx index 4830ebdb61b..8f92b18445c 100644 --- a/public/app/core/components/PasswordStrength.tsx +++ b/public/app/core/components/PasswordStrength.tsx @@ -5,27 +5,28 @@ export interface IProps { } export class PasswordStrength extends React.Component { + constructor(props) { super(props); } render() { const { password } = this.props; - let strengthText = 'strength: strong like a bull.'; - let strengthClass = 'password-strength-good'; + let strengthText = "strength: strong like a bull."; + let strengthClass = "password-strength-good"; if (!password) { return null; } if (password.length <= 8) { - strengthText = 'strength: you can do better.'; - strengthClass = 'password-strength-ok'; + strengthText = "strength: you can do better."; + strengthClass = "password-strength-ok"; } if (password.length < 4) { - strengthText = 'strength: weak sauce.'; - strengthClass = 'password-strength-bad'; + strengthText = "strength: weak sauce."; + strengthClass = "password-strength-bad"; } return ( @@ -35,3 +36,5 @@ export class PasswordStrength extends React.Component { ); } } + + diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/public/app/core/components/colorpicker/ColorPalette.tsx index 812827996c7..07b25a32046 100644 --- a/public/app/core/components/colorpicker/ColorPalette.tsx +++ b/public/app/core/components/colorpicker/ColorPalette.tsx @@ -29,8 +29,7 @@ export class ColorPalette extends React.Component { key={paletteColor} className={'pointer fa ' + cssClass} style={{ color: paletteColor }} - onClick={this.onColorSelect(paletteColor)} - > + onClick={this.onColorSelect(paletteColor)}>  
    ); @@ -42,3 +41,4 @@ export class ColorPalette extends React.Component { ); } } + diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index 4ac4161a160..360c3fdd5c4 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -19,7 +19,7 @@ export class ColorPickerPopover extends React.Component { this.state = { tab: 'palette', color: this.props.color || DEFAULT_COLOR, - colorString: this.props.color || DEFAULT_COLOR, + colorString: this.props.color || DEFAULT_COLOR }; } @@ -32,7 +32,7 @@ export class ColorPickerPopover extends React.Component { if (newColor.isValid()) { this.setState({ color: newColor.toString(), - colorString: newColor.toString(), + colorString: newColor.toString() }); this.props.onColorSelect(color); } @@ -50,7 +50,7 @@ export class ColorPickerPopover extends React.Component { onColorStringChange(e) { let colorString = e.target.value; this.setState({ - colorString: colorString, + colorString: colorString }); let newColor = tinycolor(colorString); @@ -71,11 +71,11 @@ export class ColorPickerPopover extends React.Component { componentDidMount() { this.pickerNavElem.find('li:first').addClass('active'); - this.pickerNavElem.on('show', e => { + this.pickerNavElem.on('show', (e) => { // use href attr (#name => name) let tab = e.target.hash.slice(1); this.setState({ - tab: tab, + tab: tab }); }); } @@ -97,24 +97,19 @@ export class ColorPickerPopover extends React.Component {
    -
    {currentTab}
    +
    + {currentTab} +
    - + +
    ); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx index 7242c094cde..eef04545308 100644 --- a/public/app/core/components/colorpicker/SpectrumPicker.tsx +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -29,17 +29,14 @@ export class SpectrumPicker extends React.Component { } componentDidMount() { - let spectrumOptions = _.assignIn( - { - flat: true, - showAlpha: true, - showButtons: false, - color: this.props.color, - appendTo: this.elem, - move: this.onSpectrumMove, - }, - this.props.options - ); + let spectrumOptions = _.assignIn({ + flat: true, + showAlpha: true, + showButtons: false, + color: this.props.color, + appendTo: this.elem, + move: this.onSpectrumMove, + }, this.props.options); this.elem.spectrum(spectrumOptions); this.elem.spectrum('show'); @@ -67,6 +64,9 @@ export class SpectrumPicker extends React.Component { } render() { - return
    ; + return ( +
    + ); } } + diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx index 5ab4bba8edb..6d6b001cc1d 100644 --- a/public/app/core/components/search/SearchResult.tsx +++ b/public/app/core/components/search/SearchResult.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import classNames from 'classnames'; -import { observer } from 'mobx-react'; -import { store } from 'app/stores/store'; +import React from "react"; +import classNames from "classnames"; +import { observer } from "mobx-react"; +import { store } from "app/stores/store"; export interface SearchResultProps { search: any; @@ -13,7 +13,7 @@ export class SearchResult extends React.Component { super(props); this.state = { - search: store.search, + search: store.search }; store.search.query(); @@ -56,20 +56,29 @@ export class SearchResultSection extends React.Component { render() { let collapseClassNames = classNames({ fa: true, - 'fa-plus': !this.props.section.expanded, - 'fa-minus': this.props.section.expanded, - 'search-section__header__toggle': true, + "fa-plus": !this.props.section.expanded, + "fa-minus": this.props.section.expanded, + "search-section__header__toggle": true }); return (
    - - {this.props.section.title} + + + {this.props.section.title} +
    {this.props.section.expanded && ( -
    {this.props.section.items.map(this.renderItem)}
    +
    + {this.props.section.items.map(this.renderItem)} +
    )}
    ); diff --git a/public/app/core/specs/PasswordStrength.jest.tsx b/public/app/core/specs/PasswordStrength.jest.tsx index 1bd52ee6d50..a0a2df69029 100644 --- a/public/app/core/specs/PasswordStrength.jest.tsx +++ b/public/app/core/specs/PasswordStrength.jest.tsx @@ -1,21 +1,24 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import {shallow} from 'enzyme'; -import { PasswordStrength } from '../components/PasswordStrength'; +import {PasswordStrength} from '../components/PasswordStrength'; describe('PasswordStrength', () => { + it('should have class bad if length below 4', () => { const wrapper = shallow(); - expect(wrapper.find('.password-strength-bad')).toHaveLength(1); + expect(wrapper.find(".password-strength-bad")).toHaveLength(1); }); it('should have class ok if length below 8', () => { const wrapper = shallow(); - expect(wrapper.find('.password-strength-ok')).toHaveLength(1); + expect(wrapper.find(".password-strength-ok")).toHaveLength(1); }); it('should have class good if length above 8', () => { const wrapper = shallow(); - expect(wrapper.find('.password-strength-good')).toHaveLength(1); + expect(wrapper.find(".password-strength-good")).toHaveLength(1); }); + }); + diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index c2bcf1ba70e..3b78ccfc001 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -133,12 +133,12 @@ kbn.secondsToHms = function(seconds) { kbn.secondsToHhmmss = function(seconds) { var strings = []; - var numhours = Math.floor(seconds / 3600); - var numminutes = Math.floor((seconds % 3600) / 60); - var numseconds = Math.floor((seconds % 3600) % 60); - numhours > 9 ? strings.push('' + numhours) : strings.push('0' + numhours); - numminutes > 9 ? strings.push('' + numminutes) : strings.push('0' + numminutes); - numseconds > 9 ? strings.push('' + numseconds) : strings.push('0' + numseconds); + var numhours = Math.floor(seconds/3600); + var numminutes = Math.floor((seconds%3600)/60); + var numseconds = Math.floor((seconds%3600)%60); + numhours > 9 ? strings.push(''+numhours) : strings.push('0'+numhours); + numminutes > 9 ? strings.push(''+numminutes) : strings.push('0'+numminutes); + numseconds > 9 ? strings.push(''+numseconds) : strings.push('0'+numseconds); return strings.join(':'); }; diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index f135d94431a..27fe64d4660 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; -import { AttachedPanel } from './PanelLoader'; -import { DashboardRow } from './DashboardRow'; -import { AddPanelPanel } from './AddPanelPanel'; +import {PanelModel} from '../panel_model'; +import {PanelContainer} from './PanelContainer'; +import {AttachedPanel} from './PanelLoader'; +import {DashboardRow} from './DashboardRow'; +import {AddPanelPanel} from './AddPanelPanel'; export interface DashboardPanelProps { panel: PanelModel; @@ -46,6 +46,9 @@ export class DashboardPanel extends React.Component { return ; } - return
    (this.element = element)} className="panel-height-helper" />; + return ( +
    this.element = element} className="panel-height-helper" /> + ); } } + From 1080c113f6ea576cee65cf7bd66dd2c3f3e66287 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 15 Mar 2018 00:07:20 +0100 Subject: [PATCH 0084/3000] fix lint problems --- .../components/EmptyListCTA/EmptyListCTA.tsx | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index c32edee156d..1583303dfa1 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -1,37 +1,34 @@ import React, { Component } from 'react'; export interface IProps { - model: any; + model: any; } class EmptyListCTA extends Component { - render() { - const { - title, - buttonIcon, - buttonLink, - buttonTitle, - proTip, - proTipLink, - proTipLinkTitle, - proTipTarget, - } = this.props.model; - return ( -
    -
    {title}
    - - - {buttonTitle} - -
    - ProTip: {proTip} - - {proTipLinkTitle} - -
    -
    - ); - } + render() { + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget + } = this.props.model; + return ( +
    +
    {title}
    + {buttonTitle} +
    + ProTip: {proTip} + {proTipLinkTitle} +
    +
    + ); + } } export default EmptyListCTA; From 3aed867b4ba094d7315db3333096b16b85fb9b51 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 15 Mar 2018 10:35:50 +0100 Subject: [PATCH 0085/3000] fix merge error --- public/app/plugins/datasource/influxdb/datasource.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 19df26b9e63..90337fdc3f2 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -228,10 +228,10 @@ export default class InfluxDatasource { } _influxRequest(method: string, url: string, data: any, options?: any) { - var currentUrl = this.urls.shift(); + const currentUrl = this.urls.shift(); this.urls.push(currentUrl); - var params: any = {}; + let params: any = {}; if (this.username) { params.u = this.username; @@ -252,7 +252,7 @@ export default class InfluxDatasource { data = null; } - var req: any = { + let req: any = { method: method, url: currentUrl + url, params: params, @@ -270,7 +270,7 @@ export default class InfluxDatasource { req.headers.Authorization = this.basicAuth; } - return this.backendSrv.datasourceRequest(options).then( + return this.backendSrv.datasourceRequest(req).then( result => { return result.data; }, From ad88e5398c663feb853e7fb1bc8bb2dacd0b9380 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 15 Mar 2018 12:57:09 +0100 Subject: [PATCH 0086/3000] remove --- pkg/api/pluginproxy/ds_proxy.go | 4 +--- .../plugins/datasource/influxdb/datasource.ts | 5 ----- .../datasource/influxdb/partials/config.html | 20 +++++++++++++------ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 0b0cd4ee79f..b861a344c75 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -180,9 +180,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { func (proxy *DataSourceProxy) validateRequest() error { if proxy.ds.Type == m.DS_INFLUXDB { if proxy.ctx.Query("db") != proxy.ds.Database { - if !proxy.ds.JsonData.Get("allowDatabaseQuery").MustBool(false) { - return errors.New("Datasource is not configured to allow this database") - } + return errors.New("Datasource is not configured to allow this database") } } diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 90337fdc3f2..8b213ca0f36 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -16,7 +16,6 @@ export default class InfluxDatasource { basicAuth: any; withCredentials: any; interval: any; - allowDatabaseQuery: boolean; supportAnnotations: boolean; supportMetrics: boolean; responseParser: any; @@ -35,7 +34,6 @@ export default class InfluxDatasource { this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; this.interval = (instanceSettings.jsonData || {}).timeInterval; - this.allowDatabaseQuery = (instanceSettings.jsonData || {}).allowDatabaseQuery === true; this.supportAnnotations = true; this.supportMetrics = true; this.responseParser = new ResponseParser(); @@ -240,9 +238,6 @@ export default class InfluxDatasource { if (options && options.database) { params.db = options.database; - if (params.db !== this.database && !this.allowDatabaseQuery) { - return this.$q.reject({ message: 'This datasource does not allow changing database' }); - } } else if (this.database) { params.db = this.database; } diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 4b861083928..a70a1de98a4 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -23,16 +23,24 @@
    - + +
    +
    +
    Database Access
    +

    + Setting the database for this datasource does not deny access to other databases. The InfluxDB query syntax allows + switching the database in the query. For example: + SHOW MEASUREMENTS ON _internal or SELECT * FROM "_internal".."database" LIMIT 10 +

    + To support data isolation and security, make sure appropriate permissions are configured in InfluxDB. +

    +
    +
    - Min time interval + Min time interval A lower limit for the auto group by time interval. Recommended to be set to write frequency, From a04c4ba4541e8641b2ef07f37d280f20e7aa4472 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 15 Mar 2018 13:01:17 +0100 Subject: [PATCH 0087/3000] allow any database for influx proxy --- pkg/api/pluginproxy/ds_proxy.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index b861a344c75..4b84c40643c 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -178,12 +178,6 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { } func (proxy *DataSourceProxy) validateRequest() error { - if proxy.ds.Type == m.DS_INFLUXDB { - if proxy.ctx.Query("db") != proxy.ds.Database { - return errors.New("Datasource is not configured to allow this database") - } - } - if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) { return errors.New("Target url is not a valid target") } From 1094dc32bc75bb3dfb4266a21ec53e3aa3b6366b Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 15 Mar 2018 17:22:11 +0100 Subject: [PATCH 0088/3000] made a keyboard shortcut to duplicate panel --- public/app/core/services/keybindingSrv.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 1658d74be0a..0d468b6980f 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -178,6 +178,14 @@ export class KeybindingSrv { } }); + // duplicate panel + this.bind('p d', () => { + if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { + let panelIndex = dashboard.getPanelInfoById(dashboard.meta.focusPanelId).index; + dashboard.duplicatePanel(dashboard.panels[panelIndex]); + } + }); + // share panel this.bind('p s', () => { if (dashboard.meta.focusPanelId) { From 1c20126f8710d9dd13b3d14c86428d88731bc454 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 15 Mar 2018 09:51:29 +0100 Subject: [PATCH 0089/3000] database: update xorm to v0.6.4 and xorm core to v0.5.7 --- Gopkg.lock | 8 +- Gopkg.toml | 6 +- pkg/services/sqlstore/migrator/migrator.go | 2 +- vendor/github.com/go-xorm/core/column.go | 5 +- vendor/github.com/go-xorm/core/dialect.go | 3 + vendor/github.com/go-xorm/core/rows.go | 14 +- vendor/github.com/go-xorm/core/table.go | 1 + vendor/github.com/go-xorm/core/type.go | 5 +- .../xorm/{lru_cacher.go => cache_lru.go} | 34 +- ...{memory_store.go => cache_memory_store.go} | 0 vendor/github.com/go-xorm/xorm/context.go | 26 + vendor/github.com/go-xorm/xorm/convert.go | 105 +++- .../github.com/go-xorm/xorm/dialect_mssql.go | 31 +- .../github.com/go-xorm/xorm/dialect_mysql.go | 14 +- .../github.com/go-xorm/xorm/dialect_oracle.go | 7 + .../go-xorm/xorm/dialect_postgres.go | 73 +-- .../go-xorm/xorm/dialect_sqlite3.go | 21 +- vendor/github.com/go-xorm/xorm/doc.go | 13 +- vendor/github.com/go-xorm/xorm/engine.go | 494 ++++++++++-------- vendor/github.com/go-xorm/xorm/engine_cond.go | 230 ++++++++ .../github.com/go-xorm/xorm/engine_group.go | 194 +++++++ .../go-xorm/xorm/engine_group_policy.go | 116 ++++ .../github.com/go-xorm/xorm/engine_maxlife.go | 22 + vendor/github.com/go-xorm/xorm/error.go | 2 + vendor/github.com/go-xorm/xorm/helpers.go | 257 ++------- .../github.com/go-xorm/xorm/helpler_time.go | 21 + vendor/github.com/go-xorm/xorm/interface.go | 103 ++++ vendor/github.com/go-xorm/xorm/processors.go | 40 +- vendor/github.com/go-xorm/xorm/rows.go | 78 ++- vendor/github.com/go-xorm/xorm/session.go | 373 ++++++------- .../github.com/go-xorm/xorm/session_cols.go | 24 +- .../github.com/go-xorm/xorm/session_cond.go | 18 +- .../go-xorm/xorm/session_convert.go | 210 ++++---- .../github.com/go-xorm/xorm/session_delete.go | 78 +-- .../github.com/go-xorm/xorm/session_exist.go | 77 +++ .../github.com/go-xorm/xorm/session_find.go | 189 +++---- vendor/github.com/go-xorm/xorm/session_get.go | 129 ++--- .../github.com/go-xorm/xorm/session_insert.go | 181 ++++--- .../go-xorm/xorm/session_iterate.go | 54 ++ .../github.com/go-xorm/xorm/session_query.go | 252 +++++++++ vendor/github.com/go-xorm/xorm/session_raw.go | 247 +++++---- .../github.com/go-xorm/xorm/session_schema.go | 251 ++++----- .../github.com/go-xorm/xorm/session_stats.go | 98 ++++ vendor/github.com/go-xorm/xorm/session_sum.go | 137 ----- vendor/github.com/go-xorm/xorm/session_tx.go | 24 +- .../github.com/go-xorm/xorm/session_update.go | 200 ++++--- vendor/github.com/go-xorm/xorm/statement.go | 400 +++++--------- vendor/github.com/go-xorm/xorm/tag.go | 9 + vendor/github.com/go-xorm/xorm/xorm.go | 13 +- 49 files changed, 2995 insertions(+), 1894 deletions(-) rename vendor/github.com/go-xorm/xorm/{lru_cacher.go => cache_lru.go} (90%) rename vendor/github.com/go-xorm/xorm/{memory_store.go => cache_memory_store.go} (100%) create mode 100644 vendor/github.com/go-xorm/xorm/context.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_cond.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_group.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_group_policy.go create mode 100644 vendor/github.com/go-xorm/xorm/engine_maxlife.go create mode 100644 vendor/github.com/go-xorm/xorm/helpler_time.go create mode 100644 vendor/github.com/go-xorm/xorm/interface.go create mode 100644 vendor/github.com/go-xorm/xorm/session_exist.go create mode 100644 vendor/github.com/go-xorm/xorm/session_query.go create mode 100644 vendor/github.com/go-xorm/xorm/session_stats.go delete mode 100644 vendor/github.com/go-xorm/xorm/session_sum.go diff --git a/Gopkg.lock b/Gopkg.lock index 8d82b29d622..20bee94cf8f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -171,12 +171,14 @@ [[projects]] name = "github.com/go-xorm/core" packages = ["."] - revision = "e8409d73255791843585964791443dbad877058c" + revision = "da1adaf7a28ca792961721a34e6e04945200c890" + version = "v0.5.7" [[projects]] name = "github.com/go-xorm/xorm" packages = ["."] - revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e" + revision = "1933dd69e294c0a26c0266637067f24dbb25770c" + version = "v0.6.4" [[projects]] branch = "master" @@ -631,6 +633,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "4de68f1342ba98a637ec8ca7496aeeae2021bf9e4c7c80db7924e14709151a62" + inputs-digest = "112ccff73f668c8c4dbe3d41c37ebee65fd7d839f5a4fa0665c593cae0095dad" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 22c56d29c71..a0d797f0db1 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -85,13 +85,11 @@ ignored = [ [[constraint]] name = "github.com/go-xorm/core" - revision = "e8409d73255791843585964791443dbad877058c" - #version = "0.5.7" //keeping this since we would rather depend on version then commit + version = "0.5.7" [[constraint]] name = "github.com/go-xorm/xorm" - revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e" - #version = "0.6.4" //keeping this since we would rather depend on version then commit + version = "0.6.4" [[constraint]] name = "github.com/gorilla/websocket" diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index a8bd36ac8a3..0fde3f27c01 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -125,7 +125,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { condition := m.GetCondition() if condition != nil { sql, args := condition.Sql(mg.dialect) - results, err := sess.Query(sql, args...) + results, err := sess.SQL(sql).Query(args...) if err != nil || len(results) == 0 { mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id()) return sess.Rollback() diff --git a/vendor/github.com/go-xorm/core/column.go b/vendor/github.com/go-xorm/core/column.go index c59d01021fa..d9362e98578 100644 --- a/vendor/github.com/go-xorm/core/column.go +++ b/vendor/github.com/go-xorm/core/column.go @@ -13,12 +13,13 @@ const ( ONLYFROMDB ) -// database column +// Column defines database column type Column struct { Name string TableName string FieldName string SQLType SQLType + IsJSON bool Length int Length2 int Nullable bool @@ -37,6 +38,7 @@ type Column struct { SetOptions map[string]int DisableTimeZone bool TimeZone *time.Location // column specified time zone + Comment string } func NewColumn(name, fieldName string, sqlType SQLType, len1, len2 int, nullable bool) *Column { @@ -60,6 +62,7 @@ func NewColumn(name, fieldName string, sqlType SQLType, len1, len2 int, nullable IsVersion: false, DefaultIsEmpty: false, EnumOptions: make(map[string]int), + Comment: "", } } diff --git a/vendor/github.com/go-xorm/core/dialect.go b/vendor/github.com/go-xorm/core/dialect.go index 74478301e45..6f2e81d017b 100644 --- a/vendor/github.com/go-xorm/core/dialect.go +++ b/vendor/github.com/go-xorm/core/dialect.go @@ -244,6 +244,9 @@ func (b *Base) CreateTableSql(table *Table, tableName, storeEngine, charset stri sql += col.StringNoPk(b.dialect) } sql = strings.TrimSpace(sql) + if b.DriverName() == MYSQL && len(col.Comment) > 0 { + sql += " COMMENT '" + col.Comment + "'" + } sql += ", " } diff --git a/vendor/github.com/go-xorm/core/rows.go b/vendor/github.com/go-xorm/core/rows.go index c4dec23e0de..1da63837692 100644 --- a/vendor/github.com/go-xorm/core/rows.go +++ b/vendor/github.com/go-xorm/core/rows.go @@ -196,7 +196,7 @@ func (rs *Rows) ScanMap(dest interface{}) error { newDest := make([]interface{}, len(cols)) vvv := vv.Elem() - for i, _ := range cols { + for i := range cols { newDest[i] = ReflectNew(vvv.Type().Elem()).Interface() //v := reflect.New(vvv.Type().Elem()) //newDest[i] = v.Interface() @@ -247,6 +247,18 @@ type Row struct { err error // deferred error for easy chaining } +// ErrorRow return an error row +func ErrorRow(err error) *Row { + return &Row{ + err: err, + } +} + +// NewRow from rows +func NewRow(rows *Rows, err error) *Row { + return &Row{rows, err} +} + func (row *Row) Columns() ([]string, error) { if row.err != nil { return nil, row.err diff --git a/vendor/github.com/go-xorm/core/table.go b/vendor/github.com/go-xorm/core/table.go index e6f6a7518b5..88199bedd61 100644 --- a/vendor/github.com/go-xorm/core/table.go +++ b/vendor/github.com/go-xorm/core/table.go @@ -22,6 +22,7 @@ type Table struct { Cacher Cacher StoreEngine string Charset string + Comment string } func (table *Table) Columns() []*Column { diff --git a/vendor/github.com/go-xorm/core/type.go b/vendor/github.com/go-xorm/core/type.go index 86048225176..8010a2220fc 100644 --- a/vendor/github.com/go-xorm/core/type.go +++ b/vendor/github.com/go-xorm/core/type.go @@ -100,7 +100,8 @@ var ( LongBlob = "LONGBLOB" Bytea = "BYTEA" - Bool = "BOOL" + Bool = "BOOL" + Boolean = "BOOLEAN" Serial = "SERIAL" BigSerial = "BIGSERIAL" @@ -163,7 +164,7 @@ var ( uintTypes = sort.StringSlice{"*uint", "*uint16", "*uint32", "*uint8"} ) -// !nashtsai! treat following var as interal const values, these are used for reflect.TypeOf comparision +// !nashtsai! treat following var as interal const values, these are used for reflect.TypeOf comparison var ( c_EMPTY_STRING string c_BOOL_DEFAULT bool diff --git a/vendor/github.com/go-xorm/xorm/lru_cacher.go b/vendor/github.com/go-xorm/xorm/cache_lru.go similarity index 90% rename from vendor/github.com/go-xorm/xorm/lru_cacher.go rename to vendor/github.com/go-xorm/xorm/cache_lru.go index 4a74504351f..c9672cebe4d 100644 --- a/vendor/github.com/go-xorm/xorm/lru_cacher.go +++ b/vendor/github.com/go-xorm/xorm/cache_lru.go @@ -15,13 +15,12 @@ import ( // LRUCacher implments cache object facilities type LRUCacher struct { - idList *list.List - sqlList *list.List - idIndex map[string]map[string]*list.Element - sqlIndex map[string]map[string]*list.Element - store core.CacheStore - mutex sync.Mutex - // maxSize int + idList *list.List + sqlList *list.List + idIndex map[string]map[string]*list.Element + sqlIndex map[string]map[string]*list.Element + store core.CacheStore + mutex sync.Mutex MaxElementSize int Expired time.Duration GcInterval time.Duration @@ -54,8 +53,6 @@ func (m *LRUCacher) RunGC() { // GC check ids lit and sql list to remove all element expired func (m *LRUCacher) GC() { - //fmt.Println("begin gc ...") - //defer fmt.Println("end gc ...") m.mutex.Lock() defer m.mutex.Unlock() var removedNum int @@ -64,12 +61,10 @@ func (m *LRUCacher) GC() { time.Now().Sub(e.Value.(*idNode).lastVisit) > m.Expired { removedNum++ next := e.Next() - //fmt.Println("removing ...", e.Value) node := e.Value.(*idNode) m.delBean(node.tbName, node.id) e = next } else { - //fmt.Printf("removing %d cache nodes ..., left %d\n", removedNum, m.idList.Len()) break } } @@ -80,12 +75,10 @@ func (m *LRUCacher) GC() { time.Now().Sub(e.Value.(*sqlNode).lastVisit) > m.Expired { removedNum++ next := e.Next() - //fmt.Println("removing ...", e.Value) node := e.Value.(*sqlNode) m.delIds(node.tbName, node.sql) e = next } else { - //fmt.Printf("removing %d cache nodes ..., left %d\n", removedNum, m.sqlList.Len()) break } } @@ -116,7 +109,6 @@ func (m *LRUCacher) GetIds(tableName, sql string) interface{} { } m.delIds(tableName, sql) - return nil } @@ -134,7 +126,6 @@ func (m *LRUCacher) GetBean(tableName string, id string) interface{} { // if expired, remove the node and return nil if time.Now().Sub(lastTime) > m.Expired { m.delBean(tableName, id) - //m.clearIds(tableName) return nil } m.idList.MoveToBack(el) @@ -148,7 +139,6 @@ func (m *LRUCacher) GetBean(tableName string, id string) interface{} { // store bean is not exist, then remove memory's index m.delBean(tableName, id) - //m.clearIds(tableName) return nil } @@ -166,8 +156,8 @@ func (m *LRUCacher) clearIds(tableName string) { // ClearIds clears all sql-ids mapping on table tableName from cache func (m *LRUCacher) ClearIds(tableName string) { m.mutex.Lock() - defer m.mutex.Unlock() m.clearIds(tableName) + m.mutex.Unlock() } func (m *LRUCacher) clearBeans(tableName string) { @@ -184,14 +174,13 @@ func (m *LRUCacher) clearBeans(tableName string) { // ClearBeans clears all beans in some table func (m *LRUCacher) ClearBeans(tableName string) { m.mutex.Lock() - defer m.mutex.Unlock() m.clearBeans(tableName) + m.mutex.Unlock() } // PutIds pus ids into table func (m *LRUCacher) PutIds(tableName, sql string, ids interface{}) { m.mutex.Lock() - defer m.mutex.Unlock() if _, ok := m.sqlIndex[tableName]; !ok { m.sqlIndex[tableName] = make(map[string]*list.Element) } @@ -207,12 +196,12 @@ func (m *LRUCacher) PutIds(tableName, sql string, ids interface{}) { node := e.Value.(*sqlNode) m.delIds(node.tbName, node.sql) } + m.mutex.Unlock() } // PutBean puts beans into table func (m *LRUCacher) PutBean(tableName string, id string, obj interface{}) { m.mutex.Lock() - defer m.mutex.Unlock() var el *list.Element var ok bool @@ -229,6 +218,7 @@ func (m *LRUCacher) PutBean(tableName string, id string, obj interface{}) { node := e.Value.(*idNode) m.delBean(node.tbName, node.id) } + m.mutex.Unlock() } func (m *LRUCacher) delIds(tableName, sql string) { @@ -244,8 +234,8 @@ func (m *LRUCacher) delIds(tableName, sql string) { // DelIds deletes ids func (m *LRUCacher) DelIds(tableName, sql string) { m.mutex.Lock() - defer m.mutex.Unlock() m.delIds(tableName, sql) + m.mutex.Unlock() } func (m *LRUCacher) delBean(tableName string, id string) { @@ -261,8 +251,8 @@ func (m *LRUCacher) delBean(tableName string, id string) { // DelBean deletes beans in some table func (m *LRUCacher) DelBean(tableName string, id string) { m.mutex.Lock() - defer m.mutex.Unlock() m.delBean(tableName, id) + m.mutex.Unlock() } type idNode struct { diff --git a/vendor/github.com/go-xorm/xorm/memory_store.go b/vendor/github.com/go-xorm/xorm/cache_memory_store.go similarity index 100% rename from vendor/github.com/go-xorm/xorm/memory_store.go rename to vendor/github.com/go-xorm/xorm/cache_memory_store.go diff --git a/vendor/github.com/go-xorm/xorm/context.go b/vendor/github.com/go-xorm/xorm/context.go new file mode 100644 index 00000000000..074ba35a80a --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/context.go @@ -0,0 +1,26 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.8 + +package xorm + +import "context" + +// PingContext tests if database is alive +func (engine *Engine) PingContext(ctx context.Context) error { + session := engine.NewSession() + defer session.Close() + return session.PingContext(ctx) +} + +// PingContext test if database is ok +func (session *Session) PingContext(ctx context.Context) error { + if session.isAutoClose { + defer session.Close() + } + + session.engine.logger.Infof("PING DATABASE %v", session.engine.DriverName()) + return session.DB().PingContext(ctx) +} diff --git a/vendor/github.com/go-xorm/xorm/convert.go b/vendor/github.com/go-xorm/xorm/convert.go index 87f0d3f1ec5..2316ca0b4dc 100644 --- a/vendor/github.com/go-xorm/xorm/convert.go +++ b/vendor/github.com/go-xorm/xorm/convert.go @@ -209,10 +209,10 @@ func convertAssign(dest, src interface{}) error { if src == nil { dv.Set(reflect.Zero(dv.Type())) return nil - } else { - dv.Set(reflect.New(dv.Type().Elem())) - return convertAssign(dv.Interface(), src) } + + dv.Set(reflect.New(dv.Type().Elem())) + return convertAssign(dv.Interface(), src) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: s := asString(src) i64, err := strconv.ParseInt(s, 10, dv.Type().Bits()) @@ -247,3 +247,102 @@ func convertAssign(dest, src interface{}) error { return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest) } + +func asKind(vv reflect.Value, tp reflect.Type) (interface{}, error) { + switch tp.Kind() { + case reflect.Int64: + return vv.Int(), nil + case reflect.Int: + return int(vv.Int()), nil + case reflect.Int32: + return int32(vv.Int()), nil + case reflect.Int16: + return int16(vv.Int()), nil + case reflect.Int8: + return int8(vv.Int()), nil + case reflect.Uint64: + return vv.Uint(), nil + case reflect.Uint: + return uint(vv.Uint()), nil + case reflect.Uint32: + return uint32(vv.Uint()), nil + case reflect.Uint16: + return uint16(vv.Uint()), nil + case reflect.Uint8: + return uint8(vv.Uint()), nil + case reflect.String: + return vv.String(), nil + case reflect.Slice: + if tp.Elem().Kind() == reflect.Uint8 { + v, err := strconv.ParseInt(string(vv.Interface().([]byte)), 10, 64) + if err != nil { + return nil, err + } + return v, nil + } + + } + return nil, fmt.Errorf("unsupported primary key type: %v, %v", tp, vv) +} + +func convertFloat(v interface{}) (float64, error) { + switch v.(type) { + case float32: + return float64(v.(float32)), nil + case float64: + return v.(float64), nil + case string: + i, err := strconv.ParseFloat(v.(string), 64) + if err != nil { + return 0, err + } + return i, nil + case []byte: + i, err := strconv.ParseFloat(string(v.([]byte)), 64) + if err != nil { + return 0, err + } + return i, nil + } + return 0, fmt.Errorf("unsupported type: %v", v) +} + +func convertInt(v interface{}) (int64, error) { + switch v.(type) { + case int: + return int64(v.(int)), nil + case int8: + return int64(v.(int8)), nil + case int16: + return int64(v.(int16)), nil + case int32: + return int64(v.(int32)), nil + case int64: + return v.(int64), nil + case []byte: + i, err := strconv.ParseInt(string(v.([]byte)), 10, 64) + if err != nil { + return 0, err + } + return i, nil + case string: + i, err := strconv.ParseInt(v.(string), 10, 64) + if err != nil { + return 0, err + } + return i, nil + } + return 0, fmt.Errorf("unsupported type: %v", v) +} + +func asBool(bs []byte) (bool, error) { + if len(bs) == 0 { + return false, nil + } + if bs[0] == 0x00 { + return false, nil + } else if bs[0] == 0x01 { + return true, nil + } + return strconv.ParseBool(string(bs)) +} diff --git a/vendor/github.com/go-xorm/xorm/dialect_mssql.go b/vendor/github.com/go-xorm/xorm/dialect_mssql.go index 70fcaf6ea08..6d2291dc1da 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_mssql.go +++ b/vendor/github.com/go-xorm/xorm/dialect_mssql.go @@ -215,10 +215,10 @@ func (db *mssql) SqlType(c *core.Column) string { var res string switch t := c.SQLType.Name; t { case core.Bool: - res = core.TinyInt - if c.Default == "true" { + res = core.Bit + if strings.EqualFold(c.Default, "true") { c.Default = "1" - } else if c.Default == "false" { + } else { c.Default = "0" } case core.Serial: @@ -250,6 +250,9 @@ func (db *mssql) SqlType(c *core.Column) string { case core.Uuid: res = core.Varchar c.Length = 40 + case core.TinyInt: + res = core.TinyInt + c.Length = 0 default: res = t } @@ -335,9 +338,15 @@ func (db *mssql) TableCheckSql(tableName string) (string, []interface{}) { func (db *mssql) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { args := []interface{}{} s := `select a.name as name, b.name as ctype,a.max_length,a.precision,a.scale,a.is_nullable as nullable, - replace(replace(isnull(c.text,''),'(',''),')','') as vdefault - from sys.columns a left join sys.types b on a.user_type_id=b.user_type_id - left join sys.syscomments c on a.default_object_id=c.id + replace(replace(isnull(c.text,''),'(',''),')','') as vdefault, + ISNULL(i.is_primary_key, 0) + from sys.columns a + left join sys.types b on a.user_type_id=b.user_type_id + left join sys.syscomments c on a.default_object_id=c.id + LEFT OUTER JOIN + sys.index_columns ic ON ic.object_id = a.object_id AND ic.column_id = a.column_id + LEFT OUTER JOIN + sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id where a.object_id=object_id('` + tableName + `')` db.LogSQL(s, args) @@ -352,8 +361,8 @@ func (db *mssql) GetColumns(tableName string) ([]string, map[string]*core.Column for rows.Next() { var name, ctype, vdefault string var maxLen, precision, scale int - var nullable bool - err = rows.Scan(&name, &ctype, &maxLen, &precision, &scale, &nullable, &vdefault) + var nullable, isPK bool + err = rows.Scan(&name, &ctype, &maxLen, &precision, &scale, &nullable, &vdefault, &isPK) if err != nil { return nil, nil, err } @@ -363,6 +372,7 @@ func (db *mssql) GetColumns(tableName string) ([]string, map[string]*core.Column col.Name = strings.Trim(name, "` ") col.Nullable = nullable col.Default = vdefault + col.IsPrimaryKey = isPK ct := strings.ToUpper(ctype) if ct == "DECIMAL" { col.Length = precision @@ -468,9 +478,10 @@ WHERE IXS.TYPE_DESC='NONCLUSTERED' and OBJECT_NAME(IXS.OBJECT_ID) =? } colName = strings.Trim(colName, "` ") - + var isRegular bool if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { indexName = indexName[5+len(tableName):] + isRegular = true } var index *core.Index @@ -479,6 +490,7 @@ WHERE IXS.TYPE_DESC='NONCLUSTERED' and OBJECT_NAME(IXS.OBJECT_ID) =? index = new(core.Index) index.Type = indexType index.Name = indexName + index.IsRegular = isRegular indexes[indexName] = index } index.AddColumn(colName) @@ -534,7 +546,6 @@ type odbcDriver struct { func (p *odbcDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { kv := strings.Split(dataSourceName, ";") var dbName string - for _, c := range kv { vv := strings.Split(strings.TrimSpace(c), "=") if len(vv) == 2 { diff --git a/vendor/github.com/go-xorm/xorm/dialect_mysql.go b/vendor/github.com/go-xorm/xorm/dialect_mysql.go index 55cfdd7640b..99100b23251 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_mysql.go +++ b/vendor/github.com/go-xorm/xorm/dialect_mysql.go @@ -299,7 +299,7 @@ func (db *mysql) TableCheckSql(tableName string) (string, []interface{}) { func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { args := []interface{}{db.DbName, tableName} s := "SELECT `COLUMN_NAME`, `IS_NULLABLE`, `COLUMN_DEFAULT`, `COLUMN_TYPE`," + - " `COLUMN_KEY`, `EXTRA` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?" + " `COLUMN_KEY`, `EXTRA`,`COLUMN_COMMENT` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?" db.LogSQL(s, args) rows, err := db.DB().Query(s, args...) @@ -314,13 +314,14 @@ func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column col := new(core.Column) col.Indexes = make(map[string]int) - var columnName, isNullable, colType, colKey, extra string + var columnName, isNullable, colType, colKey, extra, comment string var colDefault *string - err = rows.Scan(&columnName, &isNullable, &colDefault, &colType, &colKey, &extra) + err = rows.Scan(&columnName, &isNullable, &colDefault, &colType, &colKey, &extra, &comment) if err != nil { return nil, nil, err } col.Name = strings.Trim(columnName, "` ") + col.Comment = comment if "YES" == isNullable { col.Nullable = true } @@ -407,7 +408,7 @@ func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column func (db *mysql) GetTables() ([]*core.Table, error) { args := []interface{}{db.DbName} - s := "SELECT `TABLE_NAME`, `ENGINE`, `TABLE_ROWS`, `AUTO_INCREMENT` from " + + s := "SELECT `TABLE_NAME`, `ENGINE`, `TABLE_ROWS`, `AUTO_INCREMENT`, `TABLE_COMMENT` from " + "`INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? AND (`ENGINE`='MyISAM' OR `ENGINE` = 'InnoDB' OR `ENGINE` = 'TokuDB')" db.LogSQL(s, args) @@ -420,14 +421,15 @@ func (db *mysql) GetTables() ([]*core.Table, error) { tables := make([]*core.Table, 0) for rows.Next() { table := core.NewEmptyTable() - var name, engine, tableRows string + var name, engine, tableRows, comment string var autoIncr *string - err = rows.Scan(&name, &engine, &tableRows, &autoIncr) + err = rows.Scan(&name, &engine, &tableRows, &autoIncr, &comment) if err != nil { return nil, err } table.Name = name + table.Comment = comment table.StoreEngine = engine tables = append(tables, table) } diff --git a/vendor/github.com/go-xorm/xorm/dialect_oracle.go b/vendor/github.com/go-xorm/xorm/dialect_oracle.go index 8c43aa4cecb..ac0081b38f7 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_oracle.go +++ b/vendor/github.com/go-xorm/xorm/dialect_oracle.go @@ -824,6 +824,12 @@ func (db *oracle) GetIndexes(tableName string) (map[string]*core.Index, error) { indexName = strings.Trim(indexName, `" `) + var isRegular bool + if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { + indexName = indexName[5+len(tableName):] + isRegular = true + } + if uniqueness == "UNIQUE" { indexType = core.UniqueType } else { @@ -836,6 +842,7 @@ func (db *oracle) GetIndexes(tableName string) (map[string]*core.Index, error) { index = new(core.Index) index.Type = indexType index.Name = indexName + index.IsRegular = isRegular indexes[indexName] = index } index.AddColumn(colName) diff --git a/vendor/github.com/go-xorm/xorm/dialect_postgres.go b/vendor/github.com/go-xorm/xorm/dialect_postgres.go index 05fc1235ef4..83e9a1015c4 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_postgres.go +++ b/vendor/github.com/go-xorm/xorm/dialect_postgres.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/url" - "sort" "strconv" "strings" @@ -781,6 +780,9 @@ func (db *postgres) SqlType(c *core.Column) string { case core.TinyInt: res = core.SmallInt return res + case core.Bit: + res = core.Boolean + return res case core.MediumInt, core.Int, core.Integer: if c.IsAutoIncrement { return core.Serial @@ -1078,9 +1080,10 @@ func (db *postgres) GetIndexes(tableName string) (map[string]*core.Index, error) } cs := strings.Split(indexdef, "(") colNames = strings.Split(cs[1][0:len(cs[1])-1], ",") - + var isRegular bool if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { newIdxName := indexName[5+len(tableName):] + isRegular = true if newIdxName != "" { indexName = newIdxName } @@ -1090,6 +1093,7 @@ func (db *postgres) GetIndexes(tableName string) (map[string]*core.Index, error) for _, colName := range colNames { index.Cols = append(index.Cols, strings.Trim(colName, `" `)) } + index.IsRegular = isRegular indexes[index.Name] = index } return indexes, nil @@ -1112,10 +1116,6 @@ func (vs values) Get(k string) (v string) { return vs[k] } -func errorf(s string, args ...interface{}) { - panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) -} - func parseURL(connstr string) (string, error) { u, err := url.Parse(connstr) if err != nil { @@ -1126,46 +1126,18 @@ func parseURL(connstr string) (string, error) { return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) } - var kvs []string escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) - accrue := func(k, v string) { - if v != "" { - kvs = append(kvs, k+"="+escaper.Replace(v)) - } - } - - if u.User != nil { - v := u.User.Username() - accrue("user", v) - - v, _ = u.User.Password() - accrue("password", v) - } - - i := strings.Index(u.Host, ":") - if i < 0 { - accrue("host", u.Host) - } else { - accrue("host", u.Host[:i]) - accrue("port", u.Host[i+1:]) - } if u.Path != "" { - accrue("dbname", u.Path[1:]) + return escaper.Replace(u.Path[1:]), nil } - q := u.Query() - for k := range q { - accrue(k, q.Get(k)) - } - - sort.Strings(kvs) // Makes testing easier (not a performance concern) - return strings.Join(kvs, " "), nil + return "", nil } -func parseOpts(name string, o values) { +func parseOpts(name string, o values) error { if len(name) == 0 { - return + return fmt.Errorf("invalid options: %s", name) } name = strings.TrimSpace(name) @@ -1174,31 +1146,36 @@ func parseOpts(name string, o values) { for _, p := range ps { kv := strings.Split(p, "=") if len(kv) < 2 { - errorf("invalid option: %q", p) + return fmt.Errorf("invalid option: %q", p) } o.Set(kv[0], kv[1]) } + + return nil } func (p *pqDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { db := &core.Uri{DbType: core.POSTGRES} - o := make(values) var err error + if strings.HasPrefix(dataSourceName, "postgresql://") || strings.HasPrefix(dataSourceName, "postgres://") { - dataSourceName, err = parseURL(dataSourceName) + db.DbName, err = parseURL(dataSourceName) + if err != nil { + return nil, err + } + } else { + o := make(values) + err = parseOpts(dataSourceName, o) if err != nil { return nil, err } - } - parseOpts(dataSourceName, o) - db.DbName = o.Get("dbname") + db.DbName = o.Get("dbname") + } + if db.DbName == "" { return nil, errors.New("dbname is empty") } - /*db.Schema = o.Get("schema") - if len(db.Schema) == 0 { - db.Schema = "public" - }*/ + return db, nil } diff --git a/vendor/github.com/go-xorm/xorm/dialect_sqlite3.go b/vendor/github.com/go-xorm/xorm/dialect_sqlite3.go index c190c4d900f..a55b1615e71 100644 --- a/vendor/github.com/go-xorm/xorm/dialect_sqlite3.go +++ b/vendor/github.com/go-xorm/xorm/dialect_sqlite3.go @@ -14,10 +14,6 @@ import ( "github.com/go-xorm/core" ) -// func init() { -// RegisterDialect("sqlite3", &sqlite3{}) -// } - var ( sqlite3ReservedWords = map[string]bool{ "ABORT": true, @@ -310,11 +306,25 @@ func (db *sqlite3) GetColumns(tableName string) ([]string, map[string]*core.Colu for _, colStr := range colCreates { reg = regexp.MustCompile(`,\s`) colStr = reg.ReplaceAllString(colStr, ",") + if strings.HasPrefix(strings.TrimSpace(colStr), "PRIMARY KEY") { + parts := strings.Split(strings.TrimSpace(colStr), "(") + if len(parts) == 2 { + pkCols := strings.Split(strings.TrimRight(strings.TrimSpace(parts[1]), ")"), ",") + for _, pk := range pkCols { + if col, ok := cols[strings.Trim(strings.TrimSpace(pk), "`")]; ok { + col.IsPrimaryKey = true + } + } + } + continue + } + fields := strings.Fields(strings.TrimSpace(colStr)) col := new(core.Column) col.Indexes = make(map[string]int) col.Nullable = true col.DefaultIsEmpty = true + for idx, field := range fields { if idx == 0 { col.Name = strings.Trim(strings.Trim(field, "`[] "), `"`) @@ -405,8 +415,10 @@ func (db *sqlite3) GetIndexes(tableName string) (map[string]*core.Index, error) } indexName := strings.Trim(sql[nNStart+6:nNEnd], "` []") + var isRegular bool if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { index.Name = indexName[5+len(tableName):] + isRegular = true } else { index.Name = indexName } @@ -425,6 +437,7 @@ func (db *sqlite3) GetIndexes(tableName string) (map[string]*core.Index, error) for _, col := range colIndexes { index.Cols = append(index.Cols, strings.Trim(col, "` []")) } + index.IsRegular = isRegular indexes[index.Name] = index } diff --git a/vendor/github.com/go-xorm/xorm/doc.go b/vendor/github.com/go-xorm/xorm/doc.go index 5b36fcd80ba..a687e694768 100644 --- a/vendor/github.com/go-xorm/xorm/doc.go +++ b/vendor/github.com/go-xorm/xorm/doc.go @@ -8,7 +8,7 @@ Package xorm is a simple and powerful ORM for Go. Installation -Make sure you have installed Go 1.1+ and then: +Make sure you have installed Go 1.6+ and then: go get github.com/go-xorm/xorm @@ -51,11 +51,15 @@ There are 8 major ORM methods and many helpful methods to use to operate databas // INSERT INTO struct1 () values () // INSERT INTO struct2 () values (),(),() -2. Query one record from database +2. Query one record or one variable from database has, err := engine.Get(&user) // SELECT * FROM user LIMIT 1 + var id int64 + has, err := engine.Table("user").Where("name = ?", name).Get(&id) + // SELECT id FROM user WHERE name = ? LIMIT 1 + 3. Query multiple records from database var sliceOfStructs []Struct @@ -86,7 +90,7 @@ another is Rows 5. Update one or more records - affected, err := engine.Id(...).Update(&user) + affected, err := engine.ID(...).Update(&user) // UPDATE user SET ... 6. Delete one or more records, Delete MUST has condition @@ -99,6 +103,9 @@ another is Rows counts, err := engine.Count(&user) // SELECT count(*) AS total FROM user + counts, err := engine.SQL("select count(*) FROM user").Count() + // select count(*) FROM user + 8. Sum records sumFloat64, err := engine.Sum(&user, "id") diff --git a/vendor/github.com/go-xorm/xorm/engine.go b/vendor/github.com/go-xorm/xorm/engine.go index 134e6b147d9..444611afb16 100644 --- a/vendor/github.com/go-xorm/xorm/engine.go +++ b/vendor/github.com/go-xorm/xorm/engine.go @@ -19,6 +19,7 @@ import ( "sync" "time" + "github.com/go-xorm/builder" "github.com/go-xorm/core" ) @@ -40,12 +41,29 @@ type Engine struct { showExecTime bool logger core.ILogger - TZLocation *time.Location + TZLocation *time.Location // The timezone of the application DatabaseTZ *time.Location // The timezone of the database disableGlobalCache bool tagHandlers map[string]tagHandler + + engineGroup *EngineGroup +} + +// BufferSize sets buffer size for iterate +func (engine *Engine) BufferSize(size int) *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.BufferSize(size) +} + +// CondDeleted returns the conditions whether a record is soft deleted. +func (engine *Engine) CondDeleted(colName string) builder.Cond { + if engine.dialect.DBType() == core.MSSQL { + return builder.IsNull{colName} + } + return builder.IsNull{colName}.Or(builder.Eq{colName: zeroTime1}) } // ShowSQL show SQL statement or not on logger if log level is great than INFO @@ -78,6 +96,11 @@ func (engine *Engine) SetLogger(logger core.ILogger) { engine.dialect.SetLogger(logger) } +// SetLogLevel sets the logger level +func (engine *Engine) SetLogLevel(level core.LogLevel) { + engine.logger.SetLevel(level) +} + // SetDisableGlobalCache disable global cache or not func (engine *Engine) SetDisableGlobalCache(disable bool) { if engine.disableGlobalCache != disable { @@ -143,7 +166,6 @@ func (engine *Engine) Quote(value string) string { // QuoteTo quotes string and writes into the buffer func (engine *Engine) QuoteTo(buf *bytes.Buffer, value string) { - if buf == nil { return } @@ -169,7 +191,7 @@ func (engine *Engine) quote(sql string) string { return engine.dialect.QuoteStr() + sql + engine.dialect.QuoteStr() } -// SqlType will be depracated, please use SQLType instead +// SqlType will be deprecated, please use SQLType instead // // Deprecated: use SQLType instead func (engine *Engine) SqlType(c *core.Column) string { @@ -201,26 +223,36 @@ func (engine *Engine) SetDefaultCacher(cacher core.Cacher) { engine.Cacher = cacher } +// GetDefaultCacher returns the default cacher +func (engine *Engine) GetDefaultCacher() core.Cacher { + return engine.Cacher +} + // NoCache If you has set default cacher, and you want temporilly stop use cache, // you can use NoCache() func (engine *Engine) NoCache() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.NoCache() } // NoCascade If you do not want to auto cascade load object func (engine *Engine) NoCascade() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.NoCascade() } // MapCacher Set a table use a special cacher -func (engine *Engine) MapCacher(bean interface{}, cacher core.Cacher) { +func (engine *Engine) MapCacher(bean interface{}, cacher core.Cacher) error { v := rValue(bean) - tb := engine.autoMapType(v) + tb, err := engine.autoMapType(v) + if err != nil { + return err + } + tb.Cacher = cacher + return nil } // NewDB provides an interface to operate database directly @@ -240,7 +272,7 @@ func (engine *Engine) Dialect() core.Dialect { // NewSession New a session func (engine *Engine) NewSession() *Session { - session := &Session{Engine: engine} + session := &Session{engine: engine} session.Init() return session } @@ -254,7 +286,6 @@ func (engine *Engine) Close() error { func (engine *Engine) Ping() error { session := engine.NewSession() defer session.Close() - engine.logger.Infof("PING DATABASE %v", engine.DriverName()) return session.Ping() } @@ -262,43 +293,13 @@ func (engine *Engine) Ping() error { func (engine *Engine) logSQL(sqlStr string, sqlArgs ...interface{}) { if engine.showSQL && !engine.showExecTime { if len(sqlArgs) > 0 { - engine.logger.Infof("[SQL] %v %v", sqlStr, sqlArgs) + engine.logger.Infof("[SQL] %v %#v", sqlStr, sqlArgs) } else { engine.logger.Infof("[SQL] %v", sqlStr) } } } -func (engine *Engine) logSQLQueryTime(sqlStr string, args []interface{}, executionBlock func() (*core.Stmt, *core.Rows, error)) (*core.Stmt, *core.Rows, error) { - if engine.showSQL && engine.showExecTime { - b4ExecTime := time.Now() - stmt, res, err := executionBlock() - execDuration := time.Since(b4ExecTime) - if len(args) > 0 { - engine.logger.Infof("[SQL] %s %v - took: %v", sqlStr, args, execDuration) - } else { - engine.logger.Infof("[SQL] %s - took: %v", sqlStr, execDuration) - } - return stmt, res, err - } - return executionBlock() -} - -func (engine *Engine) logSQLExecutionTime(sqlStr string, args []interface{}, executionBlock func() (sql.Result, error)) (sql.Result, error) { - if engine.showSQL && engine.showExecTime { - b4ExecTime := time.Now() - res, err := executionBlock() - execDuration := time.Since(b4ExecTime) - if len(args) > 0 { - engine.logger.Infof("[sql] %s [args] %v - took: %v", sqlStr, args, execDuration) - } else { - engine.logger.Infof("[sql] %s - took: %v", sqlStr, execDuration) - } - return res, err - } - return executionBlock() -} - // Sql provides raw sql input parameter. When you have a complex SQL statement // and cannot use Where, Id, In and etc. Methods to describe, you can use SQL. // @@ -315,7 +316,7 @@ func (engine *Engine) Sql(querystring string, args ...interface{}) *Session { // This code will execute "select * from user" and set the records to users func (engine *Engine) SQL(query interface{}, args ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.SQL(query, args...) } @@ -324,14 +325,14 @@ func (engine *Engine) SQL(query interface{}, args ...interface{}) *Session { // invoked. Call NoAutoTime if you dont' want to fill automatically. func (engine *Engine) NoAutoTime() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.NoAutoTime() } // NoAutoCondition disable auto generate Where condition from bean or not func (engine *Engine) NoAutoCondition(no ...bool) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.NoAutoCondition(no...) } @@ -565,56 +566,56 @@ func (engine *Engine) tbName(v reflect.Value) string { // Cascade use cascade or not func (engine *Engine) Cascade(trueOrFalse ...bool) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Cascade(trueOrFalse...) } // Where method provide a condition query func (engine *Engine) Where(query interface{}, args ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Where(query, args...) } -// Id will be depracated, please use ID instead +// Id will be deprecated, please use ID instead func (engine *Engine) Id(id interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Id(id) } // ID method provoide a condition as (id) = ? func (engine *Engine) ID(id interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.ID(id) } // Before apply before Processor, affected bean is passed to closure arg func (engine *Engine) Before(closures func(interface{})) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Before(closures) } // After apply after insert Processor, affected bean is passed to closure arg func (engine *Engine) After(closures func(interface{})) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.After(closures) } // Charset set charset when create table, only support mysql now func (engine *Engine) Charset(charset string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Charset(charset) } // StoreEngine set store engine when create table, only support mysql now func (engine *Engine) StoreEngine(storeEngine string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.StoreEngine(storeEngine) } @@ -623,35 +624,35 @@ func (engine *Engine) StoreEngine(storeEngine string) *Session { // but distinct will not provide id func (engine *Engine) Distinct(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Distinct(columns...) } // Select customerize your select columns or contents func (engine *Engine) Select(str string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Select(str) } // Cols only use the parameters as select or update columns func (engine *Engine) Cols(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Cols(columns...) } // AllCols indicates that all columns should be use func (engine *Engine) AllCols() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.AllCols() } // MustCols specify some columns must use even if they are empty func (engine *Engine) MustCols(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.MustCols(columns...) } @@ -662,77 +663,84 @@ func (engine *Engine) MustCols(columns ...string) *Session { // it will use parameters's columns func (engine *Engine) UseBool(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.UseBool(columns...) } // Omit only not use the parameters as select or update columns func (engine *Engine) Omit(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Omit(columns...) } // Nullable set null when column is zero-value and nullable for update func (engine *Engine) Nullable(columns ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Nullable(columns...) } // In will generate "column IN (?, ?)" func (engine *Engine) In(column string, args ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.In(column, args...) } +// NotIn will generate "column NOT IN (?, ?)" +func (engine *Engine) NotIn(column string, args ...interface{}) *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.NotIn(column, args...) +} + // Incr provides a update string like "column = column + ?" func (engine *Engine) Incr(column string, arg ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Incr(column, arg...) } // Decr provides a update string like "column = column - ?" func (engine *Engine) Decr(column string, arg ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Decr(column, arg...) } // SetExpr provides a update string like "column = {expression}" func (engine *Engine) SetExpr(column string, expression string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.SetExpr(column, expression) } // Table temporarily change the Get, Find, Update's table func (engine *Engine) Table(tableNameOrBean interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Table(tableNameOrBean) } // Alias set the table alias func (engine *Engine) Alias(alias string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Alias(alias) } // Limit will generate "LIMIT start, limit" func (engine *Engine) Limit(limit int, start ...int) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Limit(limit, start...) } // Desc will generate "ORDER BY column1 DESC, column2 DESC" func (engine *Engine) Desc(colNames ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Desc(colNames...) } @@ -744,39 +752,53 @@ func (engine *Engine) Desc(colNames ...string) *Session { // func (engine *Engine) Asc(colNames ...string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Asc(colNames...) } // OrderBy will generate "ORDER BY order" func (engine *Engine) OrderBy(order string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.OrderBy(order) } +// Prepare enables prepare statement +func (engine *Engine) Prepare() *Session { + session := engine.NewSession() + session.isAutoClose = true + return session.Prepare() +} + // Join the join_operator should be one of INNER, LEFT OUTER, CROSS etc - this will be prepended to JOIN func (engine *Engine) Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Join(joinOperator, tablename, condition, args...) } // GroupBy generate group by statement func (engine *Engine) GroupBy(keys string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.GroupBy(keys) } // Having generate having statement func (engine *Engine) Having(conditions string) *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Having(conditions) } -func (engine *Engine) autoMapType(v reflect.Value) *core.Table { +// UnMapType removes the datbase mapper of a type +func (engine *Engine) UnMapType(t reflect.Type) { + engine.mutex.Lock() + defer engine.mutex.Unlock() + delete(engine.Tables, t) +} + +func (engine *Engine) autoMapType(v reflect.Value) (*core.Table, error) { t := v.Type() engine.mutex.Lock() defer engine.mutex.Unlock() @@ -785,24 +807,23 @@ func (engine *Engine) autoMapType(v reflect.Value) *core.Table { var err error table, err = engine.mapType(v) if err != nil { - engine.logger.Error(err) - } else { - engine.Tables[t] = table - if engine.Cacher != nil { - if v.CanAddr() { - engine.GobRegister(v.Addr().Interface()) - } else { - engine.GobRegister(v.Interface()) - } + return nil, err + } + + engine.Tables[t] = table + if engine.Cacher != nil { + if v.CanAddr() { + engine.GobRegister(v.Addr().Interface()) + } else { + engine.GobRegister(v.Interface()) } } } - return table + return table, nil } // GobRegister register one struct to gob for cache use func (engine *Engine) GobRegister(v interface{}) *Engine { - //fmt.Printf("Type: %[1]T => Data: %[1]#v\n", v) gob.Register(v) return engine } @@ -813,10 +834,19 @@ type Table struct { Name string } +// IsValid if table is valid +func (t *Table) IsValid() bool { + return t.Table != nil && len(t.Name) > 0 +} + // TableInfo get table info according to bean's content func (engine *Engine) TableInfo(bean interface{}) *Table { v := rValue(bean) - return &Table{engine.autoMapType(v), engine.tbName(v)} + tb, err := engine.autoMapType(v) + if err != nil { + engine.logger.Error(err) + } + return &Table{tb, engine.tbName(v)} } func addIndex(indexName string, table *core.Table, col *core.Column, indexType int) { @@ -911,6 +941,7 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { k := strings.ToUpper(key) ctx.tagName = k + ctx.params = []string{} pStart := strings.Index(k, "(") if pStart == 0 { @@ -918,18 +949,18 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { } if pStart > -1 { if !strings.HasSuffix(k, ")") { - return nil, errors.New("cannot match ) charactor") + return nil, fmt.Errorf("field %s tag %s cannot match ) charactor", col.FieldName, key) } ctx.tagName = k[:pStart] - ctx.params = strings.Split(k[pStart+1:len(k)-1], ",") + ctx.params = strings.Split(key[pStart+1:len(k)-1], ",") } if j > 0 { ctx.preTag = strings.ToUpper(tags[j-1]) } if j < len(tags)-1 { - ctx.nextTag = strings.ToUpper(tags[j+1]) + ctx.nextTag = tags[j+1] } else { ctx.nextTag = "" } @@ -993,6 +1024,10 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { col = core.NewColumn(engine.ColumnMapper.Obj2Table(t.Field(i).Name), t.Field(i).Name, sqlType, sqlType.DefaultLength, sqlType.DefaultLength2, true) + + if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) { + idFieldColName = col.Name + } } if col.IsAutoIncrement { col.Nullable = false @@ -1000,9 +1035,6 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { table.AddColumn(col) - if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) { - idFieldColName = col.Name - } } // end for if idFieldColName != "" && len(table.PrimaryKeys) == 0 { @@ -1066,21 +1098,54 @@ func (engine *Engine) IdOfV(rv reflect.Value) core.PK { // IDOfV get id from one value of struct func (engine *Engine) IDOfV(rv reflect.Value) core.PK { + pk, err := engine.idOfV(rv) + if err != nil { + engine.logger.Error(err) + return nil + } + return pk +} + +func (engine *Engine) idOfV(rv reflect.Value) (core.PK, error) { v := reflect.Indirect(rv) - table := engine.autoMapType(v) + table, err := engine.autoMapType(v) + if err != nil { + return nil, err + } + pk := make([]interface{}, len(table.PrimaryKeys)) for i, col := range table.PKColumns() { + var err error pkField := v.FieldByName(col.FieldName) switch pkField.Kind() { case reflect.String: - pk[i] = pkField.String() + pk[i], err = engine.idTypeAssertion(col, pkField.String()) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - pk[i] = pkField.Int() + pk[i], err = engine.idTypeAssertion(col, strconv.FormatInt(pkField.Int(), 10)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - pk[i] = pkField.Uint() + // id of uint will be converted to int64 + pk[i], err = engine.idTypeAssertion(col, strconv.FormatUint(pkField.Uint(), 10)) + } + + if err != nil { + return nil, err } } - return core.PK(pk) + return core.PK(pk), nil +} + +func (engine *Engine) idTypeAssertion(col *core.Column, sid string) (interface{}, error) { + if col.SQLType.IsNumeric() { + n, err := strconv.ParseInt(sid, 10, 64) + if err != nil { + return nil, err + } + return n, nil + } else if col.SQLType.IsText() { + return sid, nil + } else { + return nil, errors.New("not supported") + } } // CreateIndexes create indexes @@ -1101,13 +1166,6 @@ func (engine *Engine) getCacher2(table *core.Table) core.Cacher { return table.Cacher } -func (engine *Engine) getCacher(v reflect.Value) core.Cacher { - if table := engine.autoMapType(v); table != nil { - return table.Cacher - } - return engine.Cacher -} - // ClearCacheBean if enabled cache, clear the cache bean func (engine *Engine) ClearCacheBean(bean interface{}, id string) error { v := rValue(bean) @@ -1116,7 +1174,10 @@ func (engine *Engine) ClearCacheBean(bean interface{}, id string) error { return errors.New("error params") } tableName := engine.tbName(v) - table := engine.autoMapType(v) + table, err := engine.autoMapType(v) + if err != nil { + return err + } cacher := table.Cacher if cacher == nil { cacher = engine.Cacher @@ -1137,7 +1198,11 @@ func (engine *Engine) ClearCache(beans ...interface{}) error { return errors.New("error params") } tableName := engine.tbName(v) - table := engine.autoMapType(v) + table, err := engine.autoMapType(v) + if err != nil { + return err + } + cacher := table.Cacher if cacher == nil { cacher = engine.Cacher @@ -1154,19 +1219,23 @@ func (engine *Engine) ClearCache(beans ...interface{}) error { // table, column, index, unique. but will not delete or change anything. // If you change some field, you should change the database manually. func (engine *Engine) Sync(beans ...interface{}) error { + session := engine.NewSession() + defer session.Close() + for _, bean := range beans { v := rValue(bean) tableName := engine.tbName(v) - table := engine.autoMapType(v) + table, err := engine.autoMapType(v) + if err != nil { + return err + } - s := engine.NewSession() - defer s.Close() - isExist, err := s.Table(bean).isTableExist(tableName) + isExist, err := session.Table(bean).isTableExist(tableName) if err != nil { return err } if !isExist { - err = engine.CreateTables(bean) + err = session.createTable(bean) if err != nil { return err } @@ -1177,11 +1246,11 @@ func (engine *Engine) Sync(beans ...interface{}) error { }*/ var isEmpty bool if isEmpty { - err = engine.DropTables(bean) + err = session.dropTable(bean) if err != nil { return err } - err = engine.CreateTables(bean) + err = session.createTable(bean) if err != nil { return err } @@ -1192,9 +1261,9 @@ func (engine *Engine) Sync(beans ...interface{}) error { return err } if !isExist { - session := engine.NewSession() - session.Statement.setRefValue(v) - defer session.Close() + if err := session.statement.setRefValue(v); err != nil { + return err + } err = session.addColumn(col.Name) if err != nil { return err @@ -1203,19 +1272,19 @@ func (engine *Engine) Sync(beans ...interface{}) error { } for name, index := range table.Indexes { - session := engine.NewSession() - session.Statement.setRefValue(v) - defer session.Close() + if err := session.statement.setRefValue(v); err != nil { + return err + } if index.Type == core.UniqueType { - //isExist, err := session.isIndexExist(table.Name, name, true) isExist, err := session.isIndexExist2(tableName, index.Cols, true) if err != nil { return err } if !isExist { - session := engine.NewSession() - session.Statement.setRefValue(v) - defer session.Close() + if err := session.statement.setRefValue(v); err != nil { + return err + } + err = session.addUnique(tableName, name) if err != nil { return err @@ -1227,9 +1296,10 @@ func (engine *Engine) Sync(beans ...interface{}) error { return err } if !isExist { - session := engine.NewSession() - session.Statement.setRefValue(v) - defer session.Close() + if err := session.statement.setRefValue(v); err != nil { + return err + } + err = session.addIndex(tableName, name) if err != nil { return err @@ -1251,35 +1321,6 @@ func (engine *Engine) Sync2(beans ...interface{}) error { return s.Sync2(beans...) } -func (engine *Engine) unMap(beans ...interface{}) (e error) { - engine.mutex.Lock() - defer engine.mutex.Unlock() - for _, bean := range beans { - t := rType(bean) - if _, ok := engine.Tables[t]; ok { - delete(engine.Tables, t) - } - } - return -} - -// Drop all mapped table -func (engine *Engine) dropAll() error { - session := engine.NewSession() - defer session.Close() - - err := session.Begin() - if err != nil { - return err - } - err = session.dropAll() - if err != nil { - session.Rollback() - return err - } - return session.Commit() -} - // CreateTables create tabls according bean func (engine *Engine) CreateTables(beans ...interface{}) error { session := engine.NewSession() @@ -1291,7 +1332,7 @@ func (engine *Engine) CreateTables(beans ...interface{}) error { } for _, bean := range beans { - err = session.CreateTable(bean) + err = session.createTable(bean) if err != nil { session.Rollback() return err @@ -1311,7 +1352,7 @@ func (engine *Engine) DropTables(beans ...interface{}) error { } for _, bean := range beans { - err = session.DropTable(bean) + err = session.dropTable(bean) if err != nil { session.Rollback() return err @@ -1320,10 +1361,11 @@ func (engine *Engine) DropTables(beans ...interface{}) error { return session.Commit() } -func (engine *Engine) createAll() error { +// DropIndexes drop indexes of a table +func (engine *Engine) DropIndexes(bean interface{}) error { session := engine.NewSession() defer session.Close() - return session.createAll() + return session.DropIndexes(bean) } // Exec raw sql @@ -1334,10 +1376,24 @@ func (engine *Engine) Exec(sql string, args ...interface{}) (sql.Result, error) } // Query a raw sql and return records as []map[string][]byte -func (engine *Engine) Query(sql string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { +func (engine *Engine) Query(sqlorArgs ...interface{}) (resultsSlice []map[string][]byte, err error) { session := engine.NewSession() defer session.Close() - return session.Query(sql, paramStr...) + return session.Query(sqlorArgs...) +} + +// QueryString runs a raw sql and return records as []map[string]string +func (engine *Engine) QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) { + session := engine.NewSession() + defer session.Close() + return session.QueryString(sqlorArgs...) +} + +// QueryInterface runs a raw sql and return records as []map[string]interface{} +func (engine *Engine) QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) { + session := engine.NewSession() + defer session.Close() + return session.QueryInterface(sqlorArgs...) } // Insert one or more records @@ -1381,6 +1437,13 @@ func (engine *Engine) Get(bean interface{}) (bool, error) { return session.Get(bean) } +// Exist returns true if the record exist otherwise return false +func (engine *Engine) Exist(bean ...interface{}) (bool, error) { + session := engine.NewSession() + defer session.Close() + return session.Exist(bean...) +} + // Find retrieve records from table, condiBeans's non-empty fields // are conditions. beans could be []Struct, []*Struct, map[int64]Struct // map[int64]*Struct @@ -1406,10 +1469,10 @@ func (engine *Engine) Rows(bean interface{}) (*Rows, error) { } // Count counts the records. bean's non-empty fields are conditions. -func (engine *Engine) Count(bean interface{}) (int64, error) { +func (engine *Engine) Count(bean ...interface{}) (int64, error) { session := engine.NewSession() defer session.Close() - return session.Count(bean) + return session.Count(bean...) } // Sum sum the records by some column. bean's non-empty fields are conditions. @@ -1419,6 +1482,13 @@ func (engine *Engine) Sum(bean interface{}, colName string) (float64, error) { return session.Sum(bean, colName) } +// SumInt sum the records by some column. bean's non-empty fields are conditions. +func (engine *Engine) SumInt(bean interface{}, colName string) (int64, error) { + session := engine.NewSession() + defer session.Close() + return session.SumInt(bean, colName) +} + // Sums sum the records by some columns. bean's non-empty fields are conditions. func (engine *Engine) Sums(bean interface{}, colNames ...string) ([]float64, error) { session := engine.NewSession() @@ -1474,7 +1544,6 @@ func (engine *Engine) Import(r io.Reader) ([]sql.Result, error) { results = append(results, result) if err != nil { return nil, err - //lastError = err } } } @@ -1482,49 +1551,32 @@ func (engine *Engine) Import(r io.Reader) ([]sql.Result, error) { return results, lastError } -// TZTime change one time to xorm time location -func (engine *Engine) TZTime(t time.Time) time.Time { - if !t.IsZero() { // if time is not initialized it's not suitable for Time.In() - return t.In(engine.TZLocation) +// nowTime return current time +func (engine *Engine) nowTime(col *core.Column) (interface{}, time.Time) { + t := time.Now() + var tz = engine.DatabaseTZ + if !col.DisableTimeZone && col.TimeZone != nil { + tz = col.TimeZone } - return t -} - -// NowTime return current time -func (engine *Engine) NowTime(sqlTypeName string) interface{} { - t := time.Now() - return engine.FormatTime(sqlTypeName, t) -} - -// NowTime2 return current time -func (engine *Engine) NowTime2(sqlTypeName string) (interface{}, time.Time) { - t := time.Now() - return engine.FormatTime(sqlTypeName, t), t -} - -// FormatTime format time -func (engine *Engine) FormatTime(sqlTypeName string, t time.Time) (v interface{}) { - return engine.formatTime(engine.TZLocation, sqlTypeName, t) + return engine.formatTime(col.SQLType.Name, t.In(tz)), t.In(engine.TZLocation) } func (engine *Engine) formatColTime(col *core.Column, t time.Time) (v interface{}) { - if col.DisableTimeZone { - return engine.formatTime(nil, col.SQLType.Name, t) - } else if col.TimeZone != nil { - return engine.formatTime(col.TimeZone, col.SQLType.Name, t) + if t.IsZero() { + if col.Nullable { + return nil + } + return "" } - return engine.formatTime(engine.TZLocation, col.SQLType.Name, t) + + if col.TimeZone != nil { + return engine.formatTime(col.SQLType.Name, t.In(col.TimeZone)) + } + return engine.formatTime(col.SQLType.Name, t.In(engine.DatabaseTZ)) } -func (engine *Engine) formatTime(tz *time.Location, sqlTypeName string, t time.Time) (v interface{}) { - if engine.dialect.DBType() == core.ORACLE { - return t - } - if tz != nil { - t = t.In(tz) - } else { - t = engine.TZTime(t) - } +// formatTime format time as column type +func (engine *Engine) formatTime(sqlTypeName string, t time.Time) (v interface{}) { switch sqlTypeName { case core.Time: s := t.Format("2006-01-02 15:04:05") //time.RFC3339 @@ -1532,18 +1584,10 @@ func (engine *Engine) formatTime(tz *time.Location, sqlTypeName string, t time.T case core.Date: v = t.Format("2006-01-02") case core.DateTime, core.TimeStamp: - if engine.dialect.DBType() == "ql" { - v = t - } else if engine.dialect.DBType() == "sqlite3" { - v = t.UTC().Format("2006-01-02 15:04:05") - } else { - v = t.Format("2006-01-02 15:04:05") - } + v = t.Format("2006-01-02 15:04:05") case core.TimeStampz: if engine.dialect.DBType() == core.MSSQL { v = t.Format("2006-01-02T15:04:05.9999999Z07:00") - } else if engine.DriverName() == "mssql" { - v = t } else { v = t.Format(time.RFC3339Nano) } @@ -1555,9 +1599,39 @@ func (engine *Engine) formatTime(tz *time.Location, sqlTypeName string, t time.T return } +// GetColumnMapper returns the column name mapper +func (engine *Engine) GetColumnMapper() core.IMapper { + return engine.ColumnMapper +} + +// GetTableMapper returns the table name mapper +func (engine *Engine) GetTableMapper() core.IMapper { + return engine.TableMapper +} + +// GetTZLocation returns time zone of the application +func (engine *Engine) GetTZLocation() *time.Location { + return engine.TZLocation +} + +// SetTZLocation sets time zone of the application +func (engine *Engine) SetTZLocation(tz *time.Location) { + engine.TZLocation = tz +} + +// GetTZDatabase returns time zone of the database +func (engine *Engine) GetTZDatabase() *time.Location { + return engine.DatabaseTZ +} + +// SetTZDatabase sets time zone of the database +func (engine *Engine) SetTZDatabase(tz *time.Location) { + engine.DatabaseTZ = tz +} + // Unscoped always disable struct tag "deleted" func (engine *Engine) Unscoped() *Session { session := engine.NewSession() - session.IsAutoClose = true + session.isAutoClose = true return session.Unscoped() } diff --git a/vendor/github.com/go-xorm/xorm/engine_cond.go b/vendor/github.com/go-xorm/xorm/engine_cond.go new file mode 100644 index 00000000000..6c8e3879cee --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_cond.go @@ -0,0 +1,230 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/go-xorm/builder" + "github.com/go-xorm/core" +) + +func (engine *Engine) buildConds(table *core.Table, bean interface{}, + includeVersion bool, includeUpdated bool, includeNil bool, + includeAutoIncr bool, allUseBool bool, useAllCols bool, unscoped bool, + mustColumnMap map[string]bool, tableName, aliasName string, addedTableName bool) (builder.Cond, error) { + var conds []builder.Cond + for _, col := range table.Columns() { + if !includeVersion && col.IsVersion { + continue + } + if !includeUpdated && col.IsUpdated { + continue + } + if !includeAutoIncr && col.IsAutoIncrement { + continue + } + + if engine.dialect.DBType() == core.MSSQL && (col.SQLType.Name == core.Text || col.SQLType.IsBlob() || col.SQLType.Name == core.TimeStampz) { + continue + } + if col.SQLType.IsJson() { + continue + } + + var colName string + if addedTableName { + var nm = tableName + if len(aliasName) > 0 { + nm = aliasName + } + colName = engine.Quote(nm) + "." + engine.Quote(col.Name) + } else { + colName = engine.Quote(col.Name) + } + + fieldValuePtr, err := col.ValueOf(bean) + if err != nil { + engine.logger.Error(err) + continue + } + + if col.IsDeleted && !unscoped { // tag "deleted" is enabled + conds = append(conds, engine.CondDeleted(colName)) + } + + fieldValue := *fieldValuePtr + if fieldValue.Interface() == nil { + continue + } + + fieldType := reflect.TypeOf(fieldValue.Interface()) + requiredField := useAllCols + + if b, ok := getFlagForColumn(mustColumnMap, col); ok { + if b { + requiredField = true + } else { + continue + } + } + + if fieldType.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + if includeNil { + conds = append(conds, builder.Eq{colName: nil}) + } + continue + } else if !fieldValue.IsValid() { + continue + } else { + // dereference ptr type to instance type + fieldValue = fieldValue.Elem() + fieldType = reflect.TypeOf(fieldValue.Interface()) + requiredField = true + } + } + + var val interface{} + switch fieldType.Kind() { + case reflect.Bool: + if allUseBool || requiredField { + val = fieldValue.Interface() + } else { + // if a bool in a struct, it will not be as a condition because it default is false, + // please use Where() instead + continue + } + case reflect.String: + if !requiredField && fieldValue.String() == "" { + continue + } + // for MyString, should convert to string or panic + if fieldType.String() != reflect.String.String() { + val = fieldValue.String() + } else { + val = fieldValue.Interface() + } + case reflect.Int8, reflect.Int16, reflect.Int, reflect.Int32, reflect.Int64: + if !requiredField && fieldValue.Int() == 0 { + continue + } + val = fieldValue.Interface() + case reflect.Float32, reflect.Float64: + if !requiredField && fieldValue.Float() == 0.0 { + continue + } + val = fieldValue.Interface() + case reflect.Uint8, reflect.Uint16, reflect.Uint, reflect.Uint32, reflect.Uint64: + if !requiredField && fieldValue.Uint() == 0 { + continue + } + t := int64(fieldValue.Uint()) + val = reflect.ValueOf(&t).Interface() + case reflect.Struct: + if fieldType.ConvertibleTo(core.TimeType) { + t := fieldValue.Convert(core.TimeType).Interface().(time.Time) + if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { + continue + } + val = engine.formatColTime(col, t) + } else if _, ok := reflect.New(fieldType).Interface().(core.Conversion); ok { + continue + } else if valNul, ok := fieldValue.Interface().(driver.Valuer); ok { + val, _ = valNul.Value() + if val == nil { + continue + } + } else { + if col.SQLType.IsJson() { + if col.SQLType.IsText() { + bytes, err := json.Marshal(fieldValue.Interface()) + if err != nil { + engine.logger.Error(err) + continue + } + val = string(bytes) + } else if col.SQLType.IsBlob() { + var bytes []byte + var err error + bytes, err = json.Marshal(fieldValue.Interface()) + if err != nil { + engine.logger.Error(err) + continue + } + val = bytes + } + } else { + engine.autoMapType(fieldValue) + if table, ok := engine.Tables[fieldValue.Type()]; ok { + if len(table.PrimaryKeys) == 1 { + pkField := reflect.Indirect(fieldValue).FieldByName(table.PKColumns()[0].FieldName) + // fix non-int pk issues + //if pkField.Int() != 0 { + if pkField.IsValid() && !isZero(pkField.Interface()) { + val = pkField.Interface() + } else { + continue + } + } else { + //TODO: how to handler? + return nil, fmt.Errorf("not supported %v as %v", fieldValue.Interface(), table.PrimaryKeys) + } + } else { + val = fieldValue.Interface() + } + } + } + case reflect.Array: + continue + case reflect.Slice, reflect.Map: + if fieldValue == reflect.Zero(fieldType) { + continue + } + if fieldValue.IsNil() || !fieldValue.IsValid() || fieldValue.Len() == 0 { + continue + } + + if col.SQLType.IsText() { + bytes, err := json.Marshal(fieldValue.Interface()) + if err != nil { + engine.logger.Error(err) + continue + } + val = string(bytes) + } else if col.SQLType.IsBlob() { + var bytes []byte + var err error + if (fieldType.Kind() == reflect.Array || fieldType.Kind() == reflect.Slice) && + fieldType.Elem().Kind() == reflect.Uint8 { + if fieldValue.Len() > 0 { + val = fieldValue.Bytes() + } else { + continue + } + } else { + bytes, err = json.Marshal(fieldValue.Interface()) + if err != nil { + engine.logger.Error(err) + continue + } + val = bytes + } + } else { + continue + } + default: + val = fieldValue.Interface() + } + + conds = append(conds, builder.Eq{colName: val}) + } + + return builder.And(conds...), nil +} diff --git a/vendor/github.com/go-xorm/xorm/engine_group.go b/vendor/github.com/go-xorm/xorm/engine_group.go new file mode 100644 index 00000000000..1de425f372c --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_group.go @@ -0,0 +1,194 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "github.com/go-xorm/core" +) + +// EngineGroup defines an engine group +type EngineGroup struct { + *Engine + slaves []*Engine + policy GroupPolicy +} + +// NewEngineGroup creates a new engine group +func NewEngineGroup(args1 interface{}, args2 interface{}, policies ...GroupPolicy) (*EngineGroup, error) { + var eg EngineGroup + if len(policies) > 0 { + eg.policy = policies[0] + } else { + eg.policy = RoundRobinPolicy() + } + + driverName, ok1 := args1.(string) + conns, ok2 := args2.([]string) + if ok1 && ok2 { + engines := make([]*Engine, len(conns)) + for i, conn := range conns { + engine, err := NewEngine(driverName, conn) + if err != nil { + return nil, err + } + engine.engineGroup = &eg + engines[i] = engine + } + + eg.Engine = engines[0] + eg.slaves = engines[1:] + return &eg, nil + } + + master, ok3 := args1.(*Engine) + slaves, ok4 := args2.([]*Engine) + if ok3 && ok4 { + master.engineGroup = &eg + for i := 0; i < len(slaves); i++ { + slaves[i].engineGroup = &eg + } + eg.Engine = master + eg.slaves = slaves + return &eg, nil + } + return nil, ErrParamsType +} + +// Close the engine +func (eg *EngineGroup) Close() error { + err := eg.Engine.Close() + if err != nil { + return err + } + + for i := 0; i < len(eg.slaves); i++ { + err := eg.slaves[i].Close() + if err != nil { + return err + } + } + return nil +} + +// Master returns the master engine +func (eg *EngineGroup) Master() *Engine { + return eg.Engine +} + +// Ping tests if database is alive +func (eg *EngineGroup) Ping() error { + if err := eg.Engine.Ping(); err != nil { + return err + } + + for _, slave := range eg.slaves { + if err := slave.Ping(); err != nil { + return err + } + } + return nil +} + +// SetColumnMapper set the column name mapping rule +func (eg *EngineGroup) SetColumnMapper(mapper core.IMapper) { + eg.Engine.ColumnMapper = mapper + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ColumnMapper = mapper + } +} + +// SetDefaultCacher set the default cacher +func (eg *EngineGroup) SetDefaultCacher(cacher core.Cacher) { + eg.Engine.SetDefaultCacher(cacher) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetDefaultCacher(cacher) + } +} + +// SetLogger set the new logger +func (eg *EngineGroup) SetLogger(logger core.ILogger) { + eg.Engine.SetLogger(logger) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetLogger(logger) + } +} + +// SetLogLevel sets the logger level +func (eg *EngineGroup) SetLogLevel(level core.LogLevel) { + eg.Engine.SetLogLevel(level) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetLogLevel(level) + } +} + +// SetMapper set the name mapping rules +func (eg *EngineGroup) SetMapper(mapper core.IMapper) { + eg.Engine.SetMapper(mapper) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetMapper(mapper) + } +} + +// SetMaxIdleConns set the max idle connections on pool, default is 2 +func (eg *EngineGroup) SetMaxIdleConns(conns int) { + eg.Engine.db.SetMaxIdleConns(conns) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].db.SetMaxIdleConns(conns) + } +} + +// SetMaxOpenConns is only available for go 1.2+ +func (eg *EngineGroup) SetMaxOpenConns(conns int) { + eg.Engine.db.SetMaxOpenConns(conns) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].db.SetMaxOpenConns(conns) + } +} + +// SetPolicy set the group policy +func (eg *EngineGroup) SetPolicy(policy GroupPolicy) *EngineGroup { + eg.policy = policy + return eg +} + +// SetTableMapper set the table name mapping rule +func (eg *EngineGroup) SetTableMapper(mapper core.IMapper) { + eg.Engine.TableMapper = mapper + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].TableMapper = mapper + } +} + +// ShowExecTime show SQL statement and execute time or not on logger if log level is great than INFO +func (eg *EngineGroup) ShowExecTime(show ...bool) { + eg.Engine.ShowExecTime(show...) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ShowExecTime(show...) + } +} + +// ShowSQL show SQL statement or not on logger if log level is great than INFO +func (eg *EngineGroup) ShowSQL(show ...bool) { + eg.Engine.ShowSQL(show...) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].ShowSQL(show...) + } +} + +// Slave returns one of the physical databases which is a slave according the policy +func (eg *EngineGroup) Slave() *Engine { + switch len(eg.slaves) { + case 0: + return eg.Engine + case 1: + return eg.slaves[0] + } + return eg.policy.Slave(eg) +} + +// Slaves returns all the slaves +func (eg *EngineGroup) Slaves() []*Engine { + return eg.slaves +} diff --git a/vendor/github.com/go-xorm/xorm/engine_group_policy.go b/vendor/github.com/go-xorm/xorm/engine_group_policy.go new file mode 100644 index 00000000000..5b56e8995fd --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_group_policy.go @@ -0,0 +1,116 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "math/rand" + "sync" + "time" +) + +// GroupPolicy is be used by chosing the current slave from slaves +type GroupPolicy interface { + Slave(*EngineGroup) *Engine +} + +// GroupPolicyHandler should be used when a function is a GroupPolicy +type GroupPolicyHandler func(*EngineGroup) *Engine + +// Slave implements the chosen of slaves +func (h GroupPolicyHandler) Slave(eg *EngineGroup) *Engine { + return h(eg) +} + +// RandomPolicy implmentes randomly chose the slave of slaves +func RandomPolicy() GroupPolicyHandler { + var r = rand.New(rand.NewSource(time.Now().UnixNano())) + return func(g *EngineGroup) *Engine { + return g.Slaves()[r.Intn(len(g.Slaves()))] + } +} + +// WeightRandomPolicy implmentes randomly chose the slave of slaves +func WeightRandomPolicy(weights []int) GroupPolicyHandler { + var rands = make([]int, 0, len(weights)) + for i := 0; i < len(weights); i++ { + for n := 0; n < weights[i]; n++ { + rands = append(rands, i) + } + } + var r = rand.New(rand.NewSource(time.Now().UnixNano())) + + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + idx := rands[r.Intn(len(rands))] + if idx >= len(slaves) { + idx = len(slaves) - 1 + } + return slaves[idx] + } +} + +func RoundRobinPolicy() GroupPolicyHandler { + var pos = -1 + var lock sync.Mutex + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + + lock.Lock() + defer lock.Unlock() + pos++ + if pos >= len(slaves) { + pos = 0 + } + + return slaves[pos] + } +} + +func WeightRoundRobinPolicy(weights []int) GroupPolicyHandler { + var rands = make([]int, 0, len(weights)) + for i := 0; i < len(weights); i++ { + for n := 0; n < weights[i]; n++ { + rands = append(rands, i) + } + } + var pos = -1 + var lock sync.Mutex + + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + lock.Lock() + defer lock.Unlock() + pos++ + if pos >= len(rands) { + pos = 0 + } + + idx := rands[pos] + if idx >= len(slaves) { + idx = len(slaves) - 1 + } + return slaves[idx] + } +} + +// LeastConnPolicy implements GroupPolicy, every time will get the least connections slave +func LeastConnPolicy() GroupPolicyHandler { + return func(g *EngineGroup) *Engine { + var slaves = g.Slaves() + connections := 0 + idx := 0 + for i := 0; i < len(slaves); i++ { + openConnections := slaves[i].DB().Stats().OpenConnections + if i == 0 { + connections = openConnections + idx = i + } else if openConnections <= connections { + connections = openConnections + idx = i + } + } + return slaves[idx] + } +} diff --git a/vendor/github.com/go-xorm/xorm/engine_maxlife.go b/vendor/github.com/go-xorm/xorm/engine_maxlife.go new file mode 100644 index 00000000000..22666c5f44c --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/engine_maxlife.go @@ -0,0 +1,22 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.6 + +package xorm + +import "time" + +// SetConnMaxLifetime sets the maximum amount of time a connection may be reused. +func (engine *Engine) SetConnMaxLifetime(d time.Duration) { + engine.db.SetConnMaxLifetime(d) +} + +// SetConnMaxLifetime sets the maximum amount of time a connection may be reused. +func (eg *EngineGroup) SetConnMaxLifetime(d time.Duration) { + eg.Engine.SetConnMaxLifetime(d) + for i := 0; i < len(eg.slaves); i++ { + eg.slaves[i].SetConnMaxLifetime(d) + } +} diff --git a/vendor/github.com/go-xorm/xorm/error.go b/vendor/github.com/go-xorm/xorm/error.go index 2a334f47c23..cfeefc31e8e 100644 --- a/vendor/github.com/go-xorm/xorm/error.go +++ b/vendor/github.com/go-xorm/xorm/error.go @@ -23,4 +23,6 @@ var ( ErrNeedDeletedCond = errors.New("Delete need at least one condition") // ErrNotImplemented not implemented ErrNotImplemented = errors.New("Not implemented") + // ErrConditionType condition type unsupported + ErrConditionType = errors.New("Unsupported conditon type") ) diff --git a/vendor/github.com/go-xorm/xorm/helpers.go b/vendor/github.com/go-xorm/xorm/helpers.go index 398ec679fe0..f39ed472560 100644 --- a/vendor/github.com/go-xorm/xorm/helpers.go +++ b/vendor/github.com/go-xorm/xorm/helpers.go @@ -196,25 +196,43 @@ func isArrayValueZero(v reflect.Value) bool { func int64ToIntValue(id int64, tp reflect.Type) reflect.Value { var v interface{} - switch tp.Kind() { - case reflect.Int16: - v = int16(id) - case reflect.Int32: - v = int32(id) - case reflect.Int: - v = int(id) - case reflect.Int64: - v = id - case reflect.Uint16: - v = uint16(id) - case reflect.Uint32: - v = uint32(id) - case reflect.Uint64: - v = uint64(id) - case reflect.Uint: - v = uint(id) + kind := tp.Kind() + + if kind == reflect.Ptr { + kind = tp.Elem().Kind() } - return reflect.ValueOf(v).Convert(tp) + + switch kind { + case reflect.Int16: + temp := int16(id) + v = &temp + case reflect.Int32: + temp := int32(id) + v = &temp + case reflect.Int: + temp := int(id) + v = &temp + case reflect.Int64: + temp := id + v = &temp + case reflect.Uint16: + temp := uint16(id) + v = &temp + case reflect.Uint32: + temp := uint32(id) + v = &temp + case reflect.Uint64: + temp := uint64(id) + v = &temp + case reflect.Uint: + temp := uint(id) + v = &temp + } + + if tp.Kind() == reflect.Ptr { + return reflect.ValueOf(v).Convert(tp) + } + return reflect.ValueOf(v).Elem().Convert(tp) } func int64ToInt(id int64, tp reflect.Type) interface{} { @@ -302,180 +320,6 @@ func sliceEq(left, right []string) bool { return true } -func reflect2value(rawValue *reflect.Value) (str string, err error) { - aa := reflect.TypeOf((*rawValue).Interface()) - vv := reflect.ValueOf((*rawValue).Interface()) - switch aa.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - str = strconv.FormatInt(vv.Int(), 10) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - str = strconv.FormatUint(vv.Uint(), 10) - case reflect.Float32, reflect.Float64: - str = strconv.FormatFloat(vv.Float(), 'f', -1, 64) - case reflect.String: - str = vv.String() - case reflect.Array, reflect.Slice: - switch aa.Elem().Kind() { - case reflect.Uint8: - data := rawValue.Interface().([]byte) - str = string(data) - default: - err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) - } - // time type - case reflect.Struct: - if aa.ConvertibleTo(core.TimeType) { - str = vv.Convert(core.TimeType).Interface().(time.Time).Format(time.RFC3339Nano) - } else { - err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) - } - case reflect.Bool: - str = strconv.FormatBool(vv.Bool()) - case reflect.Complex128, reflect.Complex64: - str = fmt.Sprintf("%v", vv.Complex()) - /* TODO: unsupported types below - case reflect.Map: - case reflect.Ptr: - case reflect.Uintptr: - case reflect.UnsafePointer: - case reflect.Chan, reflect.Func, reflect.Interface: - */ - default: - err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) - } - return -} - -func value2Bytes(rawValue *reflect.Value) (data []byte, err error) { - var str string - str, err = reflect2value(rawValue) - if err != nil { - return - } - data = []byte(str) - return -} - -func value2String(rawValue *reflect.Value) (data string, err error) { - data, err = reflect2value(rawValue) - if err != nil { - return - } - return -} - -func rows2Strings(rows *core.Rows) (resultsSlice []map[string]string, err error) { - fields, err := rows.Columns() - if err != nil { - return nil, err - } - for rows.Next() { - result, err := row2mapStr(rows, fields) - if err != nil { - return nil, err - } - resultsSlice = append(resultsSlice, result) - } - - return resultsSlice, nil -} - -func rows2maps(rows *core.Rows) (resultsSlice []map[string][]byte, err error) { - fields, err := rows.Columns() - if err != nil { - return nil, err - } - for rows.Next() { - result, err := row2map(rows, fields) - if err != nil { - return nil, err - } - resultsSlice = append(resultsSlice, result) - } - - return resultsSlice, nil -} - -func row2map(rows *core.Rows, fields []string) (resultsMap map[string][]byte, err error) { - result := make(map[string][]byte) - scanResultContainers := make([]interface{}, len(fields)) - for i := 0; i < len(fields); i++ { - var scanResultContainer interface{} - scanResultContainers[i] = &scanResultContainer - } - if err := rows.Scan(scanResultContainers...); err != nil { - return nil, err - } - - for ii, key := range fields { - rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) - //if row is null then ignore - if rawValue.Interface() == nil { - //fmt.Println("ignore ...", key, rawValue) - continue - } - - if data, err := value2Bytes(&rawValue); err == nil { - result[key] = data - } else { - return nil, err // !nashtsai! REVIEW, should return err or just error log? - } - } - return result, nil -} - -func row2mapStr(rows *core.Rows, fields []string) (resultsMap map[string]string, err error) { - result := make(map[string]string) - scanResultContainers := make([]interface{}, len(fields)) - for i := 0; i < len(fields); i++ { - var scanResultContainer interface{} - scanResultContainers[i] = &scanResultContainer - } - if err := rows.Scan(scanResultContainers...); err != nil { - return nil, err - } - - for ii, key := range fields { - rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) - //if row is null then ignore - if rawValue.Interface() == nil { - //fmt.Println("ignore ...", key, rawValue) - continue - } - - if data, err := value2String(&rawValue); err == nil { - result[key] = data - } else { - return nil, err // !nashtsai! REVIEW, should return err or just error log? - } - } - return result, nil -} - -func txQuery2(tx *core.Tx, sqlStr string, params ...interface{}) (resultsSlice []map[string]string, err error) { - rows, err := tx.Query(sqlStr, params...) - if err != nil { - return nil, err - } - defer rows.Close() - - return rows2Strings(rows) -} - -func query2(db *core.DB, sqlStr string, params ...interface{}) (resultsSlice []map[string]string, err error) { - s, err := db.Prepare(sqlStr) - if err != nil { - return nil, err - } - defer s.Close() - rows, err := s.Query(params...) - if err != nil { - return nil, err - } - defer rows.Close() - return rows2Strings(rows) -} - func setColumnInt(bean interface{}, col *core.Column, t int64) { v, err := col.ValueOf(bean) if err != nil { @@ -514,7 +358,7 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, for _, col := range table.Columns() { if useCol && !col.IsVersion && !col.IsCreated && !col.IsUpdated { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); !ok { + if _, ok := getFlagForColumn(session.statement.columnMap, col); !ok { continue } } @@ -542,6 +386,10 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, if len(fieldValue.String()) == 0 { continue } + case reflect.Ptr: + if fieldValue.Pointer() == 0 { + continue + } } } @@ -549,28 +397,32 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, continue } - if session.Statement.ColumnStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); !ok { + if session.statement.ColumnStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); !ok { + continue + } else if _, ok := session.statement.incrColumns[col.Name]; ok { + continue + } else if _, ok := session.statement.decrColumns[col.Name]; ok { continue } } - if session.Statement.OmitStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); ok { + if session.statement.OmitStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); ok { continue } } // !evalphobia! set fieldValue as nil when column is nullable and zero-value - if _, ok := getFlagForColumn(session.Statement.nullableMap, col); ok { + if _, ok := getFlagForColumn(session.statement.nullableMap, col); ok { if col.Nullable && isZero(fieldValue.Interface()) { var nilValue *int fieldValue = reflect.ValueOf(nilValue) } } - if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime /*&& isZero(fieldValue.Interface())*/ { + if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime /*&& isZero(fieldValue.Interface())*/ { // if time is non-empty, then set to auto time - val, t := session.Engine.NowTime2(col.SQLType.Name) + val, t := session.engine.nowTime(col) args = append(args, val) var colName = col.Name @@ -578,7 +430,7 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, col := table.GetColumn(colName) setColumnTime(bean, col, t) }) - } else if col.IsVersion && session.Statement.checkVersion { + } else if col.IsVersion && session.statement.checkVersion { args = append(args, 1) } else { arg, err := session.value2Interface(col, fieldValue) @@ -589,7 +441,7 @@ func genCols(table *core.Table, session *Session, bean interface{}, useCol bool, } if includeQuote { - colNames = append(colNames, session.Engine.Quote(col.Name)+" = ?") + colNames = append(colNames, session.engine.Quote(col.Name)+" = ?") } else { colNames = append(colNames, col.Name) } @@ -602,7 +454,6 @@ func indexName(tableName, idxName string) string { } func getFlagForColumn(m map[string]bool, col *core.Column) (val bool, has bool) { - if len(m) == 0 { return false, false } diff --git a/vendor/github.com/go-xorm/xorm/helpler_time.go b/vendor/github.com/go-xorm/xorm/helpler_time.go new file mode 100644 index 00000000000..f4013e27e1a --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/helpler_time.go @@ -0,0 +1,21 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import "time" + +const ( + zeroTime0 = "0000-00-00 00:00:00" + zeroTime1 = "0001-01-01 00:00:00" +) + +func formatTime(t time.Time) string { + return t.Format("2006-01-02 15:04:05") +} + +func isTimeZero(t time.Time) bool { + return t.IsZero() || formatTime(t) == zeroTime0 || + formatTime(t) == zeroTime1 +} diff --git a/vendor/github.com/go-xorm/xorm/interface.go b/vendor/github.com/go-xorm/xorm/interface.go new file mode 100644 index 00000000000..9a3b6da0b2b --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/interface.go @@ -0,0 +1,103 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "database/sql" + "reflect" + "time" + + "github.com/go-xorm/core" +) + +// Interface defines the interface which Engine, EngineGroup and Session will implementate. +type Interface interface { + AllCols() *Session + Alias(alias string) *Session + Asc(colNames ...string) *Session + BufferSize(size int) *Session + Cols(columns ...string) *Session + Count(...interface{}) (int64, error) + CreateIndexes(bean interface{}) error + CreateUniques(bean interface{}) error + Decr(column string, arg ...interface{}) *Session + Desc(...string) *Session + Delete(interface{}) (int64, error) + Distinct(columns ...string) *Session + DropIndexes(bean interface{}) error + Exec(string, ...interface{}) (sql.Result, error) + Exist(bean ...interface{}) (bool, error) + Find(interface{}, ...interface{}) error + Get(interface{}) (bool, error) + GroupBy(keys string) *Session + ID(interface{}) *Session + In(string, ...interface{}) *Session + Incr(column string, arg ...interface{}) *Session + Insert(...interface{}) (int64, error) + InsertOne(interface{}) (int64, error) + IsTableEmpty(bean interface{}) (bool, error) + IsTableExist(beanOrTableName interface{}) (bool, error) + Iterate(interface{}, IterFunc) error + Limit(int, ...int) *Session + NoAutoCondition(...bool) *Session + NotIn(string, ...interface{}) *Session + Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session + Omit(columns ...string) *Session + OrderBy(order string) *Session + Ping() error + Query(sqlOrAgrs ...interface{}) (resultsSlice []map[string][]byte, err error) + QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) + QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) + Rows(bean interface{}) (*Rows, error) + SetExpr(string, string) *Session + SQL(interface{}, ...interface{}) *Session + Sum(bean interface{}, colName string) (float64, error) + SumInt(bean interface{}, colName string) (int64, error) + Sums(bean interface{}, colNames ...string) ([]float64, error) + SumsInt(bean interface{}, colNames ...string) ([]int64, error) + Table(tableNameOrBean interface{}) *Session + Unscoped() *Session + Update(bean interface{}, condiBeans ...interface{}) (int64, error) + UseBool(...string) *Session + Where(interface{}, ...interface{}) *Session +} + +// EngineInterface defines the interface which Engine, EngineGroup will implementate. +type EngineInterface interface { + Interface + + Before(func(interface{})) *Session + Charset(charset string) *Session + CreateTables(...interface{}) error + DBMetas() ([]*core.Table, error) + Dialect() core.Dialect + DropTables(...interface{}) error + DumpAllToFile(fp string, tp ...core.DbType) error + GetColumnMapper() core.IMapper + GetDefaultCacher() core.Cacher + GetTableMapper() core.IMapper + GetTZDatabase() *time.Location + GetTZLocation() *time.Location + NewSession() *Session + NoAutoTime() *Session + Quote(string) string + SetDefaultCacher(core.Cacher) + SetLogLevel(core.LogLevel) + SetMapper(core.IMapper) + SetTZDatabase(tz *time.Location) + SetTZLocation(tz *time.Location) + ShowSQL(show ...bool) + Sync(...interface{}) error + Sync2(...interface{}) error + StoreEngine(storeEngine string) *Session + TableInfo(bean interface{}) *Table + UnMapType(reflect.Type) +} + +var ( + _ Interface = &Session{} + _ EngineInterface = &Engine{} + _ EngineInterface = &EngineGroup{} +) diff --git a/vendor/github.com/go-xorm/xorm/processors.go b/vendor/github.com/go-xorm/xorm/processors.go index 77dd30e5785..dcd9c6ac0b4 100644 --- a/vendor/github.com/go-xorm/xorm/processors.go +++ b/vendor/github.com/go-xorm/xorm/processors.go @@ -29,13 +29,6 @@ type AfterSetProcessor interface { AfterSet(string, Cell) } -// !nashtsai! TODO enable BeforeValidateProcessor when xorm start to support validations -//// Executed before an object is validated -//type BeforeValidateProcessor interface { -// BeforeValidate() -//} -// -- - // AfterInsertProcessor executed after an object is persisted to the database type AfterInsertProcessor interface { AfterInsert() @@ -50,3 +43,36 @@ type AfterUpdateProcessor interface { type AfterDeleteProcessor interface { AfterDelete() } + +// AfterLoadProcessor executed after an ojbect has been loaded from database +type AfterLoadProcessor interface { + AfterLoad() +} + +// AfterLoadSessionProcessor executed after an ojbect has been loaded from database with session parameter +type AfterLoadSessionProcessor interface { + AfterLoad(*Session) +} + +type executedProcessorFunc func(*Session, interface{}) error + +type executedProcessor struct { + fun executedProcessorFunc + session *Session + bean interface{} +} + +func (executor *executedProcessor) execute() error { + return executor.fun(executor.session, executor.bean) +} + +func (session *Session) executeProcessors() error { + processors := session.afterProcessors + session.afterProcessors = make([]executedProcessor, 0) + for _, processor := range processors { + if err := processor.execute(); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/go-xorm/xorm/rows.go b/vendor/github.com/go-xorm/xorm/rows.go index a91d08b7791..31e29ae26f6 100644 --- a/vendor/github.com/go-xorm/xorm/rows.go +++ b/vendor/github.com/go-xorm/xorm/rows.go @@ -17,7 +17,6 @@ type Rows struct { NoTypeCheck bool session *Session - stmt *core.Stmt rows *core.Rows fields []string beanType reflect.Type @@ -29,50 +28,33 @@ func newRows(session *Session, bean interface{}) (*Rows, error) { rows.session = session rows.beanType = reflect.Indirect(reflect.ValueOf(bean)).Type() - defer rows.session.resetStatement() - var sqlStr string var args []interface{} + var err error - rows.session.Statement.setRefValue(rValue(bean)) - if len(session.Statement.TableName()) <= 0 { + if err = rows.session.statement.setRefValue(rValue(bean)); err != nil { + return nil, err + } + + if len(session.statement.TableName()) <= 0 { return nil, ErrTableNotFound } - if rows.session.Statement.RawSQL == "" { - sqlStr, args = rows.session.Statement.genGetSQL(bean) - } else { - sqlStr = rows.session.Statement.RawSQL - args = rows.session.Statement.RawParams - } - - for _, filter := range rows.session.Engine.dialect.Filters() { - sqlStr = filter.Do(sqlStr, session.Engine.dialect, rows.session.Statement.RefTable) - } - - rows.session.saveLastSQL(sqlStr, args...) - var err error - if rows.session.prepareStmt { - rows.stmt, err = rows.session.DB().Prepare(sqlStr) + if rows.session.statement.RawSQL == "" { + sqlStr, args, err = rows.session.statement.genGetSQL(bean) if err != nil { - rows.lastError = err - rows.Close() - return nil, err - } - - rows.rows, err = rows.stmt.Query(args...) - if err != nil { - rows.lastError = err - rows.Close() return nil, err } } else { - rows.rows, err = rows.session.DB().Query(sqlStr, args...) - if err != nil { - rows.lastError = err - rows.Close() - return nil, err - } + sqlStr = rows.session.statement.RawSQL + args = rows.session.statement.RawParams + } + + rows.rows, err = rows.session.queryRows(sqlStr, args...) + if err != nil { + rows.lastError = err + rows.Close() + return nil, err } rows.fields, err = rows.rows.Columns() @@ -113,15 +95,26 @@ func (rows *Rows) Scan(bean interface{}) error { } dataStruct := rValue(bean) - rows.session.Statement.setRefValue(dataStruct) - _, err := rows.session.row2Bean(rows.rows, rows.fields, len(rows.fields), bean, &dataStruct, rows.session.Statement.RefTable) + if err := rows.session.statement.setRefValue(dataStruct); err != nil { + return err + } - return err + scanResults, err := rows.session.row2Slice(rows.rows, rows.fields, bean) + if err != nil { + return err + } + + _, err = rows.session.slice2Bean(scanResults, rows.fields, bean, &dataStruct, rows.session.statement.RefTable) + if err != nil { + return err + } + + return rows.session.executeProcessors() } // Close session if session.IsAutoClose is true, and claimed any opened resources func (rows *Rows) Close() error { - if rows.session.IsAutoClose { + if rows.session.isAutoClose { defer rows.session.Close() } @@ -129,17 +122,10 @@ func (rows *Rows) Close() error { if rows.rows != nil { rows.lastError = rows.rows.Close() if rows.lastError != nil { - defer rows.stmt.Close() return rows.lastError } } - if rows.stmt != nil { - rows.lastError = rows.stmt.Close() - } } else { - if rows.stmt != nil { - defer rows.stmt.Close() - } if rows.rows != nil { defer rows.rows.Close() } diff --git a/vendor/github.com/go-xorm/xorm/session.go b/vendor/github.com/go-xorm/xorm/session.go index 0e4cfcf1574..5c6cb5f9def 100644 --- a/vendor/github.com/go-xorm/xorm/session.go +++ b/vendor/github.com/go-xorm/xorm/session.go @@ -11,7 +11,6 @@ import ( "fmt" "hash/crc32" "reflect" - "strconv" "strings" "time" @@ -22,17 +21,16 @@ import ( // kind of database operations. type Session struct { db *core.DB - Engine *Engine - Tx *core.Tx - Statement Statement - IsAutoCommit bool - IsCommitedOrRollbacked bool - TransType string - IsAutoClose bool + engine *Engine + tx *core.Tx + statement Statement + isAutoCommit bool + isCommitedOrRollbacked bool + isAutoClose bool // Automatically reset the statement after operations that execute a SQL // query such as Count(), Find(), Get(), ... - AutoResetStatement bool + autoResetStatement bool // !nashtsai! storing these beans due to yet committed tx afterInsertBeans map[interface{}]*[]func(interface{}) @@ -43,14 +41,17 @@ type Session struct { beforeClosures []func(interface{}) afterClosures []func(interface{}) + afterProcessors []executedProcessor + prepareStmt bool stmtCache map[uint32]*core.Stmt //key: hash.Hash32 of (queryStr, len(queryStr)) - cascadeDeep int // !evalphobia! stored the last executed query on this session //beforeSQLExec func(string, ...interface{}) lastSQL string lastSQLArgs []interface{} + + err error } // Clone copy all the session's content and return a new session @@ -61,12 +62,12 @@ func (session *Session) Clone() *Session { // Init reset the session as the init status. func (session *Session) Init() { - session.Statement.Init() - session.Statement.Engine = session.Engine - session.IsAutoCommit = true - session.IsCommitedOrRollbacked = false - session.IsAutoClose = false - session.AutoResetStatement = true + session.statement.Init() + session.statement.Engine = session.engine + session.isAutoCommit = true + session.isCommitedOrRollbacked = false + session.isAutoClose = false + session.autoResetStatement = true session.prepareStmt = false // !nashtsai! is lazy init better? @@ -75,6 +76,9 @@ func (session *Session) Init() { session.afterDeleteBeans = make(map[interface{}]*[]func(interface{}), 0) session.beforeClosures = make([]func(interface{}), 0) session.afterClosures = make([]func(interface{}), 0) + session.stmtCache = make(map[uint32]*core.Stmt) + + session.afterProcessors = make([]executedProcessor, 0) session.lastSQL = "" session.lastSQLArgs = []interface{}{} @@ -89,19 +93,23 @@ func (session *Session) Close() { if session.db != nil { // When Close be called, if session is a transaction and do not call // Commit or Rollback, then call Rollback. - if session.Tx != nil && !session.IsCommitedOrRollbacked { + if session.tx != nil && !session.isCommitedOrRollbacked { session.Rollback() } - session.Tx = nil + session.tx = nil session.stmtCache = nil - session.Init() session.db = nil } } +// IsClosed returns if session is closed +func (session *Session) IsClosed() bool { + return session.db == nil +} + func (session *Session) resetStatement() { - if session.AutoResetStatement { - session.Statement.Init() + if session.autoResetStatement { + session.statement.Init() } } @@ -129,75 +137,75 @@ func (session *Session) After(closures func(interface{})) *Session { // Table can input a string or pointer to struct for special a table to operate. func (session *Session) Table(tableNameOrBean interface{}) *Session { - session.Statement.Table(tableNameOrBean) + session.statement.Table(tableNameOrBean) return session } // Alias set the table alias func (session *Session) Alias(alias string) *Session { - session.Statement.Alias(alias) + session.statement.Alias(alias) return session } // NoCascade indicate that no cascade load child object func (session *Session) NoCascade() *Session { - session.Statement.UseCascade = false + session.statement.UseCascade = false return session } // ForUpdate Set Read/Write locking for UPDATE func (session *Session) ForUpdate() *Session { - session.Statement.IsForUpdate = true + session.statement.IsForUpdate = true return session } // NoAutoCondition disable generate SQL condition from beans func (session *Session) NoAutoCondition(no ...bool) *Session { - session.Statement.NoAutoCondition(no...) + session.statement.NoAutoCondition(no...) return session } // Limit provide limit and offset query condition func (session *Session) Limit(limit int, start ...int) *Session { - session.Statement.Limit(limit, start...) + session.statement.Limit(limit, start...) return session } // OrderBy provide order by query condition, the input parameter is the content // after order by on a sql statement. func (session *Session) OrderBy(order string) *Session { - session.Statement.OrderBy(order) + session.statement.OrderBy(order) return session } // Desc provide desc order by query condition, the input parameters are columns. func (session *Session) Desc(colNames ...string) *Session { - session.Statement.Desc(colNames...) + session.statement.Desc(colNames...) return session } // Asc provide asc order by query condition, the input parameters are columns. func (session *Session) Asc(colNames ...string) *Session { - session.Statement.Asc(colNames...) + session.statement.Asc(colNames...) return session } // StoreEngine is only avialble mysql dialect currently func (session *Session) StoreEngine(storeEngine string) *Session { - session.Statement.StoreEngine = storeEngine + session.statement.StoreEngine = storeEngine return session } // Charset is only avialble mysql dialect currently func (session *Session) Charset(charset string) *Session { - session.Statement.Charset = charset + session.statement.Charset = charset return session } // Cascade indicates if loading sub Struct func (session *Session) Cascade(trueOrFalse ...bool) *Session { if len(trueOrFalse) >= 1 { - session.Statement.UseCascade = trueOrFalse[0] + session.statement.UseCascade = trueOrFalse[0] } return session } @@ -205,32 +213,32 @@ func (session *Session) Cascade(trueOrFalse ...bool) *Session { // NoCache ask this session do not retrieve data from cache system and // get data from database directly. func (session *Session) NoCache() *Session { - session.Statement.UseCache = false + session.statement.UseCache = false return session } // Join join_operator should be one of INNER, LEFT OUTER, CROSS etc - this will be prepended to JOIN func (session *Session) Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session { - session.Statement.Join(joinOperator, tablename, condition, args...) + session.statement.Join(joinOperator, tablename, condition, args...) return session } // GroupBy Generate Group By statement func (session *Session) GroupBy(keys string) *Session { - session.Statement.GroupBy(keys) + session.statement.GroupBy(keys) return session } // Having Generate Having statement func (session *Session) Having(conditions string) *Session { - session.Statement.Having(conditions) + session.statement.Having(conditions) return session } // DB db return the wrapper of sql.DB func (session *Session) DB() *core.DB { if session.db == nil { - session.db = session.Engine.db + session.db = session.engine.db session.stmtCache = make(map[uint32]*core.Stmt, 0) } return session.db @@ -243,25 +251,25 @@ func cleanupProcessorsClosures(slices *[]func(interface{})) { } func (session *Session) canCache() bool { - if session.Statement.RefTable == nil || - session.Statement.JoinStr != "" || - session.Statement.RawSQL != "" || - !session.Statement.UseCache || - session.Statement.IsForUpdate || - session.Tx != nil || - len(session.Statement.selectStr) > 0 { + if session.statement.RefTable == nil || + session.statement.JoinStr != "" || + session.statement.RawSQL != "" || + !session.statement.UseCache || + session.statement.IsForUpdate || + session.tx != nil || + len(session.statement.selectStr) > 0 { return false } return true } -func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { +func (session *Session) doPrepare(db *core.DB, sqlStr string) (stmt *core.Stmt, err error) { crc := crc32.ChecksumIEEE([]byte(sqlStr)) // TODO try hash(sqlStr+len(sqlStr)) var has bool stmt, has = session.stmtCache[crc] if !has { - stmt, err = session.DB().Prepare(sqlStr) + stmt, err = db.Prepare(sqlStr) if err != nil { return nil, err } @@ -273,18 +281,18 @@ func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { func (session *Session) getField(dataStruct *reflect.Value, key string, table *core.Table, idx int) *reflect.Value { var col *core.Column if col = table.GetColumnIdx(key, idx); col == nil { - //session.Engine.logger.Warnf("table %v has no column %v. %v", table.Name, key, table.ColumnsSeq()) + //session.engine.logger.Warnf("table %v has no column %v. %v", table.Name, key, table.ColumnsSeq()) return nil } fieldValue, err := col.ValueOfV(dataStruct) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return nil } if !fieldValue.IsValid() || !fieldValue.CanSet() { - session.Engine.logger.Warnf("table %v's column %v is not valid or cannot set", table.Name, key) + session.engine.logger.Warnf("table %v's column %v is not valid or cannot set", table.Name, key) return nil } return fieldValue @@ -293,28 +301,40 @@ func (session *Session) getField(dataStruct *reflect.Value, key string, table *c // Cell cell is a result of one column field type Cell *interface{} -func (session *Session) rows2Beans(rows *core.Rows, fields []string, fieldsCount int, +func (session *Session) rows2Beans(rows *core.Rows, fields []string, table *core.Table, newElemFunc func([]string) reflect.Value, sliceValueSetFunc func(*reflect.Value, core.PK) error) error { for rows.Next() { var newValue = newElemFunc(fields) bean := newValue.Interface() - dataStruct := rValue(bean) - pk, err := session.row2Bean(rows, fields, fieldsCount, bean, &dataStruct, table) - if err != nil { - return err - } + dataStruct := newValue.Elem() - err = sliceValueSetFunc(&newValue, pk) + // handle beforeClosures + scanResults, err := session.row2Slice(rows, fields, bean) if err != nil { return err } + pk, err := session.slice2Bean(scanResults, fields, bean, &dataStruct, table) + if err != nil { + return err + } + session.afterProcessors = append(session.afterProcessors, executedProcessor{ + fun: func(*Session, interface{}) error { + return sliceValueSetFunc(&newValue, pk) + }, + session: session, + bean: bean, + }) } return nil } -func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount int, bean interface{}, dataStruct *reflect.Value, table *core.Table) (core.PK, error) { - scanResults := make([]interface{}, fieldsCount) +func (session *Session) row2Slice(rows *core.Rows, fields []string, bean interface{}) ([]interface{}, error) { + for _, closure := range session.beforeClosures { + closure(bean) + } + + scanResults := make([]interface{}, len(fields)) for i := 0; i < len(fields); i++ { var cell interface{} scanResults[i] = &cell @@ -328,7 +348,10 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i b.BeforeSet(key, Cell(scanResults[ii].(*interface{}))) } } + return scanResults, nil +} +func (session *Session) slice2Bean(scanResults []interface{}, fields []string, bean interface{}, dataStruct *reflect.Value, table *core.Table) (core.PK, error) { defer func() { if b, hasAfterSet := bean.(AfterSetProcessor); hasAfterSet { for ii, key := range fields { @@ -337,6 +360,40 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i } }() + // handle afterClosures + for _, closure := range session.afterClosures { + session.afterProcessors = append(session.afterProcessors, executedProcessor{ + fun: func(sess *Session, bean interface{}) error { + closure(bean) + return nil + }, + session: session, + bean: bean, + }) + } + + if a, has := bean.(AfterLoadProcessor); has { + session.afterProcessors = append(session.afterProcessors, executedProcessor{ + fun: func(sess *Session, bean interface{}) error { + a.AfterLoad() + return nil + }, + session: session, + bean: bean, + }) + } + + if a, has := bean.(AfterLoadSessionProcessor); has { + session.afterProcessors = append(session.afterProcessors, executedProcessor{ + fun: func(sess *Session, bean interface{}) error { + a.AfterLoad(sess) + return nil + }, + session: session, + bean: bean, + }) + } + var tempMap = make(map[string]int) var pk core.PK for ii, key := range fields { @@ -361,9 +418,11 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if fieldValue.CanAddr() { if structConvert, ok := fieldValue.Addr().Interface().(core.Conversion); ok { if data, err := value2Bytes(&rawValue); err == nil { - structConvert.FromDB(data) + if err := structConvert.FromDB(data); err != nil { + return nil, err + } } else { - session.Engine.logger.Error(err) + return nil, err } continue } @@ -376,7 +435,7 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i } fieldValue.Interface().(core.Conversion).FromDB(data) } else { - session.Engine.logger.Error(err) + return nil, err } continue } @@ -403,17 +462,19 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i hasAssigned = true if len(bs) > 0 { + if fieldType.Kind() == reflect.String { + fieldValue.SetString(string(bs)) + continue + } if fieldValue.CanAddr() { err := json.Unmarshal(bs, fieldValue.Addr().Interface()) if err != nil { - session.Engine.logger.Error(key, err) return nil, err } } else { x := reflect.New(fieldType) err := json.Unmarshal(bs, x.Interface()) if err != nil { - session.Engine.logger.Error(key, err) return nil, err } fieldValue.Set(x.Elem()) @@ -438,14 +499,12 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if fieldValue.CanAddr() { err := json.Unmarshal(bs, fieldValue.Addr().Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } } else { x := reflect.New(fieldType) err := json.Unmarshal(bs, x.Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } fieldValue.Set(x.Elem()) @@ -462,14 +521,19 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i x := reflect.New(fieldType) err := json.Unmarshal(vv.Bytes(), x.Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } fieldValue.Set(x.Elem()) } else { - for i := 0; i < fieldValue.Len(); i++ { - if i < vv.Len() { - fieldValue.Index(i).Set(vv.Index(i)) + if fieldValue.Len() > 0 { + for i := 0; i < fieldValue.Len(); i++ { + if i < vv.Len() { + fieldValue.Index(i).Set(vv.Index(i)) + } + } + } else { + for i := 0; i < vv.Len(); i++ { + fieldValue.Set(reflect.Append(*fieldValue, vv.Index(i))) } } } @@ -509,57 +573,38 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i } case reflect.Struct: if fieldType.ConvertibleTo(core.TimeType) { + dbTZ := session.engine.DatabaseTZ + if col.TimeZone != nil { + dbTZ = col.TimeZone + } + if rawValueType == core.TimeType { hasAssigned = true t := vv.Convert(core.TimeType).Interface().(time.Time) z, _ := t.Zone() - dbTZ := session.Engine.DatabaseTZ - if dbTZ == nil { - if session.Engine.dialect.DBType() == core.SQLITE { - dbTZ = time.UTC - } else { - dbTZ = time.Local - } - } - // set new location if database don't save timezone or give an incorrect timezone if len(z) == 0 || t.Year() == 0 || t.Location().String() != dbTZ.String() { // !nashtsai! HACK tmp work around for lib/pq doesn't properly time with location - session.Engine.logger.Debugf("empty zone key[%v] : %v | zone: %v | location: %+v\n", key, t, z, *t.Location()) + session.engine.logger.Debugf("empty zone key[%v] : %v | zone: %v | location: %+v\n", key, t, z, *t.Location()) t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), dbTZ) } - // !nashtsai! convert to engine location - if col.TimeZone == nil { - t = t.In(session.Engine.TZLocation) - } else { - t = t.In(col.TimeZone) - } + t = t.In(session.engine.TZLocation) fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) - - // t = fieldValue.Interface().(time.Time) - // z, _ = t.Zone() - // session.Engine.LogDebug("fieldValue key[%v]: %v | zone: %v | location: %+v\n", key, t, z, *t.Location()) } else if rawValueType == core.IntType || rawValueType == core.Int64Type || rawValueType == core.Int32Type { hasAssigned = true - var tz *time.Location - if col.TimeZone == nil { - tz = session.Engine.TZLocation - } else { - tz = col.TimeZone - } - t := time.Unix(vv.Int(), 0).In(tz) - //vv = reflect.ValueOf(t) + + t := time.Unix(vv.Int(), 0).In(session.engine.TZLocation) fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) } else { if d, ok := vv.Interface().([]uint8); ok { hasAssigned = true t, err := session.byte2Time(col, d) if err != nil { - session.Engine.logger.Error("byte2Time error:", err.Error()) + session.engine.logger.Error("byte2Time error:", err.Error()) hasAssigned = false } else { fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) @@ -568,20 +613,20 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i hasAssigned = true t, err := session.str2Time(col, d) if err != nil { - session.Engine.logger.Error("byte2Time error:", err.Error()) + session.engine.logger.Error("byte2Time error:", err.Error()) hasAssigned = false } else { fieldValue.Set(reflect.ValueOf(t).Convert(fieldType)) } } else { - panic(fmt.Sprintf("rawValueType is %v, value is %v", rawValueType, vv.Interface())) + return nil, fmt.Errorf("rawValueType is %v, value is %v", rawValueType, vv.Interface()) } } } else if nulVal, ok := fieldValue.Addr().Interface().(sql.Scanner); ok { // !! 增加支持sql.Scanner接口的结构,如sql.NullString hasAssigned = true if err := nulVal.Scan(vv.Interface()); err != nil { - session.Engine.logger.Error("sql.Sanner error:", err.Error()) + session.engine.logger.Error("sql.Sanner error:", err.Error()) hasAssigned = false } } else if col.SQLType.IsJson() { @@ -591,7 +636,6 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if len([]byte(vv.String())) > 0 { err := json.Unmarshal([]byte(vv.String()), x.Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } fieldValue.Set(x.Elem()) @@ -602,76 +646,45 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if len(vv.Bytes()) > 0 { err := json.Unmarshal(vv.Bytes(), x.Interface()) if err != nil { - session.Engine.logger.Error(err) return nil, err } fieldValue.Set(x.Elem()) } } - } else if session.Statement.UseCascade { - table := session.Engine.autoMapType(*fieldValue) - if table != nil { - hasAssigned = true - if len(table.PrimaryKeys) != 1 { - panic("unsupported non or composited primary key cascade") - } - var pk = make(core.PK, len(table.PrimaryKeys)) + } else if session.statement.UseCascade { + table, err := session.engine.autoMapType(*fieldValue) + if err != nil { + return nil, err + } - switch rawValueType.Kind() { - case reflect.Int64: - pk[0] = vv.Int() - case reflect.Int: - pk[0] = int(vv.Int()) - case reflect.Int32: - pk[0] = int32(vv.Int()) - case reflect.Int16: - pk[0] = int16(vv.Int()) - case reflect.Int8: - pk[0] = int8(vv.Int()) - case reflect.Uint64: - pk[0] = vv.Uint() - case reflect.Uint: - pk[0] = uint(vv.Uint()) - case reflect.Uint32: - pk[0] = uint32(vv.Uint()) - case reflect.Uint16: - pk[0] = uint16(vv.Uint()) - case reflect.Uint8: - pk[0] = uint8(vv.Uint()) - case reflect.String: - pk[0] = vv.String() - case reflect.Slice: - pk[0], _ = strconv.ParseInt(string(rawValue.Interface().([]byte)), 10, 64) - default: - panic(fmt.Sprintf("unsupported primary key type: %v, %v", rawValueType, fieldValue)) - } + hasAssigned = true + if len(table.PrimaryKeys) != 1 { + return nil, errors.New("unsupported non or composited primary key cascade") + } + var pk = make(core.PK, len(table.PrimaryKeys)) + pk[0], err = asKind(vv, rawValueType) + if err != nil { + return nil, err + } - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - structInter := reflect.New(fieldValue.Type()) - newsession := session.Engine.NewSession() - defer newsession.Close() - has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) - if err != nil { - return nil, err - } - if has { - //v := structInter.Elem().Interface() - //fieldValue.Set(reflect.ValueOf(v)) - fieldValue.Set(structInter.Elem()) - } else { - return nil, errors.New("cascade obj is not exist") - } + if !isPKZero(pk) { + // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch + // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne + // property to be fetched lazily + structInter := reflect.New(fieldValue.Type()) + has, err := session.ID(pk).NoCascade().get(structInter.Interface()) + if err != nil { + return nil, err + } + if has { + fieldValue.Set(structInter.Elem()) + } else { + return nil, errors.New("cascade obj is not exist") } - } else { - session.Engine.logger.Error("unsupported struct type in Scan: ", fieldValue.Type().String()) } } case reflect.Ptr: // !nashtsai! TODO merge duplicated codes above - //typeStr := fieldType.String() switch fieldType { // following types case matching ptr's native type, therefore assign ptr directly case core.PtrStringType: @@ -769,10 +782,9 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if len([]byte(vv.String())) > 0 { err := json.Unmarshal([]byte(vv.String()), &x) if err != nil { - session.Engine.logger.Error(err) - } else { - fieldValue.Set(reflect.ValueOf(&x)) + return nil, err } + fieldValue.Set(reflect.ValueOf(&x)) } hasAssigned = true case core.Complex128Type: @@ -780,24 +792,23 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i if len([]byte(vv.String())) > 0 { err := json.Unmarshal([]byte(vv.String()), &x) if err != nil { - session.Engine.logger.Error(err) - } else { - fieldValue.Set(reflect.ValueOf(&x)) + return nil, err } + fieldValue.Set(reflect.ValueOf(&x)) } hasAssigned = true } // switch fieldType - // default: - // session.Engine.LogError("unsupported type in Scan: ", reflect.TypeOf(v).String()) } // switch fieldType.Kind() // !nashtsai! for value can't be assigned directly fallback to convert to []byte then back to value if !hasAssigned { data, err := value2Bytes(&rawValue) - if err == nil { - session.bytes2Value(col, fieldValue, data) - } else { - session.Engine.logger.Error(err.Error()) + if err != nil { + return nil, err + } + + if err = session.bytes2Value(col, fieldValue, data); err != nil { + return nil, err } } } @@ -805,19 +816,11 @@ func (session *Session) row2Bean(rows *core.Rows, fields []string, fieldsCount i return pk, nil } -func (session *Session) queryPreprocess(sqlStr *string, paramStr ...interface{}) { - for _, filter := range session.Engine.dialect.Filters() { - *sqlStr = filter.Do(*sqlStr, session.Engine.dialect, session.Statement.RefTable) - } - - session.saveLastSQL(*sqlStr, paramStr...) -} - // saveLastSQL stores executed query information func (session *Session) saveLastSQL(sql string, args ...interface{}) { session.lastSQL = sql session.lastSQLArgs = args - session.Engine.logSQL(sql, args...) + session.engine.logSQL(sql, args...) } // LastSQL returns last query information @@ -827,8 +830,8 @@ func (session *Session) LastSQL() (string, []interface{}) { // tbName get some table's table name func (session *Session) tbNameNoSchema(table *core.Table) string { - if len(session.Statement.AltTableName) > 0 { - return session.Statement.AltTableName + if len(session.statement.AltTableName) > 0 { + return session.statement.AltTableName } return table.Name @@ -836,6 +839,6 @@ func (session *Session) tbNameNoSchema(table *core.Table) string { // Unscoped always disable struct tag "deleted" func (session *Session) Unscoped() *Session { - session.Statement.Unscoped() + session.statement.Unscoped() return session } diff --git a/vendor/github.com/go-xorm/xorm/session_cols.go b/vendor/github.com/go-xorm/xorm/session_cols.go index 91185defc8c..9972cb0ae4b 100644 --- a/vendor/github.com/go-xorm/xorm/session_cols.go +++ b/vendor/github.com/go-xorm/xorm/session_cols.go @@ -6,43 +6,43 @@ package xorm // Incr provides a query string like "count = count + 1" func (session *Session) Incr(column string, arg ...interface{}) *Session { - session.Statement.Incr(column, arg...) + session.statement.Incr(column, arg...) return session } // Decr provides a query string like "count = count - 1" func (session *Session) Decr(column string, arg ...interface{}) *Session { - session.Statement.Decr(column, arg...) + session.statement.Decr(column, arg...) return session } // SetExpr provides a query string like "column = {expression}" func (session *Session) SetExpr(column string, expression string) *Session { - session.Statement.SetExpr(column, expression) + session.statement.SetExpr(column, expression) return session } // Select provides some columns to special func (session *Session) Select(str string) *Session { - session.Statement.Select(str) + session.statement.Select(str) return session } // Cols provides some columns to special func (session *Session) Cols(columns ...string) *Session { - session.Statement.Cols(columns...) + session.statement.Cols(columns...) return session } // AllCols ask all columns func (session *Session) AllCols() *Session { - session.Statement.AllCols() + session.statement.AllCols() return session } // MustCols specify some columns must use even if they are empty func (session *Session) MustCols(columns ...string) *Session { - session.Statement.MustCols(columns...) + session.statement.MustCols(columns...) return session } @@ -52,7 +52,7 @@ func (session *Session) MustCols(columns ...string) *Session { // If no parameters, it will use all the bool field of struct, or // it will use parameters's columns func (session *Session) UseBool(columns ...string) *Session { - session.Statement.UseBool(columns...) + session.statement.UseBool(columns...) return session } @@ -60,25 +60,25 @@ func (session *Session) UseBool(columns ...string) *Session { // distinct will not be cached because cache system need id, // but distinct will not provide id func (session *Session) Distinct(columns ...string) *Session { - session.Statement.Distinct(columns...) + session.statement.Distinct(columns...) return session } // Omit Only not use the parameters as select or update columns func (session *Session) Omit(columns ...string) *Session { - session.Statement.Omit(columns...) + session.statement.Omit(columns...) return session } // Nullable Set null when column is zero-value and nullable for update func (session *Session) Nullable(columns ...string) *Session { - session.Statement.Nullable(columns...) + session.statement.Nullable(columns...) return session } // NoAutoTime means do not automatically give created field and updated field // the current time on the current session temporarily func (session *Session) NoAutoTime() *Session { - session.Statement.UseAutoTime = false + session.statement.UseAutoTime = false return session } diff --git a/vendor/github.com/go-xorm/xorm/session_cond.go b/vendor/github.com/go-xorm/xorm/session_cond.go index 948a90bc1fc..e1d528f2dbd 100644 --- a/vendor/github.com/go-xorm/xorm/session_cond.go +++ b/vendor/github.com/go-xorm/xorm/session_cond.go @@ -17,25 +17,25 @@ func (session *Session) Sql(query string, args ...interface{}) *Session { // SQL provides raw sql input parameter. When you have a complex SQL statement // and cannot use Where, Id, In and etc. Methods to describe, you can use SQL. func (session *Session) SQL(query interface{}, args ...interface{}) *Session { - session.Statement.SQL(query, args...) + session.statement.SQL(query, args...) return session } // Where provides custom query condition. func (session *Session) Where(query interface{}, args ...interface{}) *Session { - session.Statement.Where(query, args...) + session.statement.Where(query, args...) return session } // And provides custom query condition. func (session *Session) And(query interface{}, args ...interface{}) *Session { - session.Statement.And(query, args...) + session.statement.And(query, args...) return session } // Or provides custom query condition. func (session *Session) Or(query interface{}, args ...interface{}) *Session { - session.Statement.Or(query, args...) + session.statement.Or(query, args...) return session } @@ -48,23 +48,23 @@ func (session *Session) Id(id interface{}) *Session { // ID provides converting id as a query condition func (session *Session) ID(id interface{}) *Session { - session.Statement.ID(id) + session.statement.ID(id) return session } // In provides a query string like "id in (1, 2, 3)" func (session *Session) In(column string, args ...interface{}) *Session { - session.Statement.In(column, args...) + session.statement.In(column, args...) return session } // NotIn provides a query string like "id in (1, 2, 3)" func (session *Session) NotIn(column string, args ...interface{}) *Session { - session.Statement.NotIn(column, args...) + session.statement.NotIn(column, args...) return session } -// Conds returns session query conditions +// Conds returns session query conditions except auto bean conditions func (session *Session) Conds() builder.Cond { - return session.Statement.cond + return session.statement.cond } diff --git a/vendor/github.com/go-xorm/xorm/session_convert.go b/vendor/github.com/go-xorm/xorm/session_convert.go index 36ab465f5ee..1f9d8aa1bd0 100644 --- a/vendor/github.com/go-xorm/xorm/session_convert.go +++ b/vendor/github.com/go-xorm/xorm/session_convert.go @@ -23,41 +23,38 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti var x time.Time var err error - if sdata == "0000-00-00 00:00:00" || - sdata == "0001-01-01 00:00:00" { + var parseLoc = session.engine.DatabaseTZ + if col.TimeZone != nil { + parseLoc = col.TimeZone + } + + if sdata == zeroTime0 || sdata == zeroTime1 { } else if !strings.ContainsAny(sdata, "- :") { // !nashtsai! has only found that mymysql driver is using this for time type column // time stamp sd, err := strconv.ParseInt(sdata, 10, 64) if err == nil { x = time.Unix(sd, 0) - // !nashtsai! HACK mymysql driver is causing Local location being change to CHAT and cause wrong time conversion - if col.TimeZone == nil { - x = x.In(session.Engine.TZLocation) - } else { - x = x.In(col.TimeZone) - } - session.Engine.logger.Debugf("time(0) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(0) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else { - session.Engine.logger.Debugf("time(0) err key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + //session.engine.logger.Debugf("time(0) err key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } } else if len(sdata) > 19 && strings.Contains(sdata, "-") { - x, err = time.ParseInLocation(time.RFC3339Nano, sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(1) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation(time.RFC3339Nano, sdata, parseLoc) + session.engine.logger.Debugf("time(1) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) if err != nil { - x, err = time.ParseInLocation("2006-01-02 15:04:05.999999999", sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(2) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02 15:04:05.999999999", sdata, parseLoc) + //session.engine.logger.Debugf("time(2) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } if err != nil { - x, err = time.ParseInLocation("2006-01-02 15:04:05.9999999 Z07:00", sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(3) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02 15:04:05.9999999 Z07:00", sdata, parseLoc) + //session.engine.logger.Debugf("time(3) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } - } else if len(sdata) == 19 && strings.Contains(sdata, "-") { - x, err = time.ParseInLocation("2006-01-02 15:04:05", sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(4) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02 15:04:05", sdata, parseLoc) + //session.engine.logger.Debugf("time(4) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else if len(sdata) == 10 && sdata[4] == '-' && sdata[7] == '-' { - x, err = time.ParseInLocation("2006-01-02", sdata, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(5) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02", sdata, parseLoc) + //session.engine.logger.Debugf("time(5) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else if col.SQLType.Name == core.Time { if strings.Contains(sdata, " ") { ssd := strings.Split(sdata, " ") @@ -65,13 +62,13 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti } sdata = strings.TrimSpace(sdata) - if session.Engine.dialect.DBType() == core.MYSQL && len(sdata) > 8 { + if session.engine.dialect.DBType() == core.MYSQL && len(sdata) > 8 { sdata = sdata[len(sdata)-8:] } st := fmt.Sprintf("2006-01-02 %v", sdata) - x, err = time.ParseInLocation("2006-01-02 15:04:05", st, session.Engine.TZLocation) - session.Engine.logger.Debugf("time(6) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) + x, err = time.ParseInLocation("2006-01-02 15:04:05", st, parseLoc) + //session.engine.logger.Debugf("time(6) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) } else { outErr = fmt.Errorf("unsupported time format %v", sdata) return @@ -80,7 +77,7 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti outErr = fmt.Errorf("unsupported time format %v: %v", sdata, err) return } - outTime = x + outTime = x.In(session.engine.TZLocation) return } @@ -108,7 +105,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, x.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(x.Elem()) @@ -122,7 +119,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, x.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(x.Elem()) @@ -135,7 +132,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, x.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(x.Elem()) @@ -147,8 +144,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, case reflect.String: fieldValue.SetString(string(data)) case reflect.Bool: - d := string(data) - v, err := strconv.ParseBool(d) + v, err := asBool(data) if err != nil { return fmt.Errorf("arg %v as bool: %s", key, err.Error()) } @@ -159,7 +155,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - session.Engine.dialect.DBType() == core.MYSQL { // !nashtsai! TODO dialect needs to provide conversion interface API + session.engine.dialect.DBType() == core.MYSQL { // !nashtsai! TODO dialect needs to provide conversion interface API if len(data) == 1 { x = int64(data[0]) } else { @@ -207,41 +203,39 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, } v = x fieldValue.Set(reflect.ValueOf(v).Convert(fieldType)) - } else if session.Statement.UseCascade { - table := session.Engine.autoMapType(*fieldValue) - if table != nil { - // TODO: current only support 1 primary key - if len(table.PrimaryKeys) > 1 { - panic("unsupported composited primary key cascade") - } - var pk = make(core.PK, len(table.PrimaryKeys)) - rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) - var err error - pk[0], err = str2PK(string(data), rawValueType) + } else if session.statement.UseCascade { + table, err := session.engine.autoMapType(*fieldValue) + if err != nil { + return err + } + + // TODO: current only support 1 primary key + if len(table.PrimaryKeys) > 1 { + return errors.New("unsupported composited primary key cascade") + } + + var pk = make(core.PK, len(table.PrimaryKeys)) + rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) + pk[0], err = str2PK(string(data), rawValueType) + if err != nil { + return err + } + + if !isPKZero(pk) { + // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch + // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne + // property to be fetched lazily + structInter := reflect.New(fieldValue.Type()) + has, err := session.ID(pk).NoCascade().get(structInter.Interface()) if err != nil { return err } - - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - structInter := reflect.New(fieldValue.Type()) - newsession := session.Engine.NewSession() - defer newsession.Close() - has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) - if err != nil { - return err - } - if has { - v = structInter.Elem().Interface() - fieldValue.Set(reflect.ValueOf(v)) - } else { - return errors.New("cascade obj is not exist") - } + if has { + v = structInter.Elem().Interface() + fieldValue.Set(reflect.ValueOf(v)) + } else { + return errors.New("cascade obj is not exist") } - } else { - return fmt.Errorf("unsupported struct type in Scan: %s", fieldValue.Type().String()) } } } @@ -267,7 +261,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, &x) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(reflect.ValueOf(&x).Convert(fieldType)) @@ -278,7 +272,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, if len(data) > 0 { err := json.Unmarshal(data, &x) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return err } fieldValue.Set(reflect.ValueOf(&x).Convert(fieldType)) @@ -350,7 +344,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - strings.Contains(session.Engine.DriverName(), "mysql") { + strings.Contains(session.engine.DriverName(), "mysql") { if len(data) == 1 { x = int64(data[0]) } else { @@ -375,7 +369,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - strings.Contains(session.Engine.DriverName(), "mysql") { + strings.Contains(session.engine.DriverName(), "mysql") { if len(data) == 1 { x = int(data[0]) } else { @@ -403,7 +397,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - session.Engine.dialect.DBType() == core.MYSQL { + session.engine.dialect.DBType() == core.MYSQL { if len(data) == 1 { x = int32(data[0]) } else { @@ -431,7 +425,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - strings.Contains(session.Engine.DriverName(), "mysql") { + strings.Contains(session.engine.DriverName(), "mysql") { if len(data) == 1 { x = int8(data[0]) } else { @@ -459,7 +453,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, var err error // for mysql, when use bit, it returned \x01 if col.SQLType.Name == core.Bit && - strings.Contains(session.Engine.DriverName(), "mysql") { + strings.Contains(session.engine.DriverName(), "mysql") { if len(data) == 1 { x = int16(data[0]) } else { @@ -491,37 +485,37 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, v = x fieldValue.Set(reflect.ValueOf(&x)) default: - if session.Statement.UseCascade { + if session.statement.UseCascade { structInter := reflect.New(fieldType.Elem()) - table := session.Engine.autoMapType(structInter.Elem()) - if table != nil { - if len(table.PrimaryKeys) > 1 { - panic("unsupported composited primary key cascade") - } - var pk = make(core.PK, len(table.PrimaryKeys)) - var err error - rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) - pk[0], err = str2PK(string(data), rawValueType) + table, err := session.engine.autoMapType(structInter.Elem()) + if err != nil { + return err + } + + if len(table.PrimaryKeys) > 1 { + return errors.New("unsupported composited primary key cascade") + } + + var pk = make(core.PK, len(table.PrimaryKeys)) + rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) + pk[0], err = str2PK(string(data), rawValueType) + if err != nil { + return err + } + + if !isPKZero(pk) { + // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch + // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne + // property to be fetched lazily + has, err := session.ID(pk).NoCascade().get(structInter.Interface()) if err != nil { return err } - - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - newsession := session.Engine.NewSession() - defer newsession.Close() - has, err := newsession.Id(pk).NoCascade().Get(structInter.Interface()) - if err != nil { - return err - } - if has { - v = structInter.Interface() - fieldValue.Set(reflect.ValueOf(v)) - } else { - return errors.New("cascade obj is not exist") - } + if has { + v = structInter.Interface() + fieldValue.Set(reflect.ValueOf(v)) + } else { + return errors.New("cascade obj is not exist") } } } else { @@ -570,7 +564,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val if fieldValue.IsNil() { return nil, nil } else if !fieldValue.IsValid() { - session.Engine.logger.Warn("the field[", col.FieldName, "] is invalid") + session.engine.logger.Warn("the field[", col.FieldName, "] is invalid") return nil, nil } else { // !nashtsai! deference pointer type to instance type @@ -588,12 +582,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val case reflect.Struct: if fieldType.ConvertibleTo(core.TimeType) { t := fieldValue.Convert(core.TimeType).Interface().(time.Time) - if session.Engine.dialect.DBType() == core.MSSQL { - if t.IsZero() { - return nil, nil - } - } - tf := session.Engine.FormatTime(col.SQLType.Name, t) + tf := session.engine.formatColTime(col, t) return tf, nil } @@ -603,7 +592,10 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val return v.Value() } - fieldTable := session.Engine.autoMapType(fieldValue) + fieldTable, err := session.engine.autoMapType(fieldValue) + if err != nil { + return nil, err + } if len(fieldTable.PrimaryKeys) == 1 { pkField := reflect.Indirect(fieldValue).FieldByName(fieldTable.PKColumns()[0].FieldName) return pkField.Interface(), nil @@ -614,14 +606,14 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val if col.SQLType.IsText() { bytes, err := json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } return string(bytes), nil } else if col.SQLType.IsBlob() { bytes, err := json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } return bytes, nil @@ -630,7 +622,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val case reflect.Complex64, reflect.Complex128: bytes, err := json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } return string(bytes), nil @@ -642,7 +634,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val if col.SQLType.IsText() { bytes, err := json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } return string(bytes), nil @@ -655,7 +647,7 @@ func (session *Session) value2Interface(col *core.Column, fieldValue reflect.Val } else { bytes, err = json.Marshal(fieldValue.Interface()) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) return 0, err } } diff --git a/vendor/github.com/go-xorm/xorm/session_delete.go b/vendor/github.com/go-xorm/xorm/session_delete.go index 1c458fe1ea4..688b122ca6d 100644 --- a/vendor/github.com/go-xorm/xorm/session_delete.go +++ b/vendor/github.com/go-xorm/xorm/session_delete.go @@ -12,26 +12,26 @@ import ( "github.com/go-xorm/core" ) -func (session *Session) cacheDelete(sqlStr string, args ...interface{}) error { - if session.Statement.RefTable == nil || - session.Tx != nil { +func (session *Session) cacheDelete(table *core.Table, tableName, sqlStr string, args ...interface{}) error { + if table == nil || + session.tx != nil { return ErrCacheFailed } - for _, filter := range session.Engine.dialect.Filters() { - sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) + for _, filter := range session.engine.dialect.Filters() { + sqlStr = filter.Do(sqlStr, session.engine.dialect, table) } - newsql := session.Statement.convertIDSQL(sqlStr) + newsql := session.statement.convertIDSQL(sqlStr) if newsql == "" { return ErrCacheFailed } - cacher := session.Engine.getCacher2(session.Statement.RefTable) - tableName := session.Statement.TableName() + cacher := session.engine.getCacher2(table) + pkColumns := table.PKColumns() ids, err := core.GetCacheSql(cacher, tableName, newsql, args) if err != nil { - resultsSlice, err := session.query(newsql, args...) + resultsSlice, err := session.queryBytes(newsql, args...) if err != nil { return err } @@ -40,7 +40,7 @@ func (session *Session) cacheDelete(sqlStr string, args ...interface{}) error { for _, data := range resultsSlice { var id int64 var pk core.PK = make([]interface{}, 0) - for _, col := range session.Statement.RefTable.PKColumns() { + for _, col := range pkColumns { if v, ok := data[col.Name]; !ok { return errors.New("no id") } else if col.SQLType.IsText() { @@ -58,33 +58,30 @@ func (session *Session) cacheDelete(sqlStr string, args ...interface{}) error { ids = append(ids, pk) } } - } /*else { - session.Engine.LogDebug("delete cache sql %v", newsql) - cacher.DelIds(tableName, genSqlKey(newsql, args)) - }*/ + } for _, id := range ids { - session.Engine.logger.Debug("[cacheDelete] delete cache obj", tableName, id) + session.engine.logger.Debug("[cacheDelete] delete cache obj:", tableName, id) sid, err := id.ToString() if err != nil { return err } cacher.DelBean(tableName, sid) } - session.Engine.logger.Debug("[cacheDelete] clear cache sql", tableName) + session.engine.logger.Debug("[cacheDelete] clear cache table:", tableName) cacher.ClearIds(tableName) return nil } // Delete records, bean's non-empty fields are conditions func (session *Session) Delete(bean interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - session.Statement.setRefValue(rValue(bean)) - var table = session.Statement.RefTable + if err := session.statement.setRefValue(rValue(bean)); err != nil { + return 0, err + } // handle before delete processors for _, closure := range session.beforeClosures { @@ -96,13 +93,17 @@ func (session *Session) Delete(bean interface{}) (int64, error) { processor.BeforeDelete() } - // -- - condSQL, condArgs, _ := session.Statement.genConds(bean) - if len(condSQL) == 0 && session.Statement.LimitN == 0 { + condSQL, condArgs, err := session.statement.genConds(bean) + if err != nil { + return 0, err + } + if len(condSQL) == 0 && session.statement.LimitN == 0 { return 0, ErrNeedDeletedCond } - var tableName = session.Engine.Quote(session.Statement.TableName()) + var tableNameNoQuote = session.statement.TableName() + var tableName = session.engine.Quote(tableNameNoQuote) + var table = session.statement.RefTable var deleteSQL string if len(condSQL) > 0 { deleteSQL = fmt.Sprintf("DELETE FROM %v WHERE %v", tableName, condSQL) @@ -111,15 +112,15 @@ func (session *Session) Delete(bean interface{}) (int64, error) { } var orderSQL string - if len(session.Statement.OrderStr) > 0 { - orderSQL += fmt.Sprintf(" ORDER BY %s", session.Statement.OrderStr) + if len(session.statement.OrderStr) > 0 { + orderSQL += fmt.Sprintf(" ORDER BY %s", session.statement.OrderStr) } - if session.Statement.LimitN > 0 { - orderSQL += fmt.Sprintf(" LIMIT %d", session.Statement.LimitN) + if session.statement.LimitN > 0 { + orderSQL += fmt.Sprintf(" LIMIT %d", session.statement.LimitN) } if len(orderSQL) > 0 { - switch session.Engine.dialect.DBType() { + switch session.engine.dialect.DBType() { case core.POSTGRES: inSQL := fmt.Sprintf("ctid IN (SELECT ctid FROM %s%s)", tableName, orderSQL) if len(condSQL) > 0 { @@ -144,7 +145,7 @@ func (session *Session) Delete(bean interface{}) (int64, error) { var realSQL string argsForCache := make([]interface{}, 0, len(condArgs)*2) - if session.Statement.unscoped || table.DeletedColumn() == nil { // tag "deleted" is disabled + if session.statement.unscoped || table.DeletedColumn() == nil { // tag "deleted" is disabled realSQL = deleteSQL copy(argsForCache, condArgs) argsForCache = append(condArgs, argsForCache...) @@ -155,12 +156,12 @@ func (session *Session) Delete(bean interface{}) (int64, error) { deletedColumn := table.DeletedColumn() realSQL = fmt.Sprintf("UPDATE %v SET %v = ? WHERE %v", - session.Engine.Quote(session.Statement.TableName()), - session.Engine.Quote(deletedColumn.Name), + session.engine.Quote(session.statement.TableName()), + session.engine.Quote(deletedColumn.Name), condSQL) if len(orderSQL) > 0 { - switch session.Engine.dialect.DBType() { + switch session.engine.dialect.DBType() { case core.POSTGRES: inSQL := fmt.Sprintf("ctid IN (SELECT ctid FROM %s%s)", tableName, orderSQL) if len(condSQL) > 0 { @@ -183,12 +184,12 @@ func (session *Session) Delete(bean interface{}) (int64, error) { } } - // !oinume! Insert NowTime to the head of session.Statement.Params + // !oinume! Insert nowTime to the head of session.statement.Params condArgs = append(condArgs, "") paramsLen := len(condArgs) copy(condArgs[1:paramsLen], condArgs[0:paramsLen-1]) - val, t := session.Engine.NowTime2(deletedColumn.SQLType.Name) + val, t := session.engine.nowTime(deletedColumn) condArgs[0] = val var colName = deletedColumn.Name @@ -198,17 +199,18 @@ func (session *Session) Delete(bean interface{}) (int64, error) { }) } - if cacher := session.Engine.getCacher2(session.Statement.RefTable); cacher != nil && session.Statement.UseCache { - session.cacheDelete(deleteSQL, argsForCache...) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheDelete(table, tableNameNoQuote, deleteSQL, argsForCache...) } + session.statement.RefTable = table res, err := session.exec(realSQL, condArgs...) if err != nil { return 0, err } // handle after delete processors - if session.IsAutoCommit { + if session.isAutoCommit { for _, closure := range session.afterClosures { closure(bean) } diff --git a/vendor/github.com/go-xorm/xorm/session_exist.go b/vendor/github.com/go-xorm/xorm/session_exist.go new file mode 100644 index 00000000000..049c1ddff14 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/session_exist.go @@ -0,0 +1,77 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "errors" + "fmt" + "reflect" + + "github.com/go-xorm/builder" +) + +// Exist returns true if the record exist otherwise return false +func (session *Session) Exist(bean ...interface{}) (bool, error) { + if session.isAutoClose { + defer session.Close() + } + + var sqlStr string + var args []interface{} + var err error + + if session.statement.RawSQL == "" { + if len(bean) == 0 { + tableName := session.statement.TableName() + if len(tableName) <= 0 { + return false, ErrTableNotFound + } + + if session.statement.cond.IsValid() { + condSQL, condArgs, err := builder.ToSQL(session.statement.cond) + if err != nil { + return false, err + } + + sqlStr = fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT 1", tableName, condSQL) + args = condArgs + } else { + sqlStr = fmt.Sprintf("SELECT * FROM %s LIMIT 1", tableName) + args = []interface{}{} + } + } else { + beanValue := reflect.ValueOf(bean[0]) + if beanValue.Kind() != reflect.Ptr { + return false, errors.New("needs a pointer") + } + + if beanValue.Elem().Kind() == reflect.Struct { + if err := session.statement.setRefValue(beanValue.Elem()); err != nil { + return false, err + } + } + + if len(session.statement.TableName()) <= 0 { + return false, ErrTableNotFound + } + session.statement.Limit(1) + sqlStr, args, err = session.statement.genGetSQL(bean[0]) + if err != nil { + return false, err + } + } + } else { + sqlStr = session.statement.RawSQL + args = session.statement.RawParams + } + + rows, err := session.queryRows(sqlStr, args...) + if err != nil { + return false, err + } + defer rows.Close() + + return rows.Next(), nil +} diff --git a/vendor/github.com/go-xorm/xorm/session_find.go b/vendor/github.com/go-xorm/xorm/session_find.go index 748e319f793..f95dcfef2cb 100644 --- a/vendor/github.com/go-xorm/xorm/session_find.go +++ b/vendor/github.com/go-xorm/xorm/session_find.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "reflect" - "strconv" "strings" "github.com/go-xorm/builder" @@ -24,11 +23,13 @@ const ( // are conditions. beans could be []Struct, []*Struct, map[int64]Struct // map[int64]*Struct func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) error { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } + return session.find(rowsSlicePtr, condiBean...) +} +func (session *Session) find(rowsSlicePtr interface{}, condiBean ...interface{}) error { sliceValue := reflect.Indirect(reflect.ValueOf(rowsSlicePtr)) if sliceValue.Kind() != reflect.Slice && sliceValue.Kind() != reflect.Map { return errors.New("needs a pointer to a slice or a map") @@ -37,77 +38,79 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) sliceElementType := sliceValue.Type().Elem() var tp = tpStruct - if session.Statement.RefTable == nil { + if session.statement.RefTable == nil { if sliceElementType.Kind() == reflect.Ptr { if sliceElementType.Elem().Kind() == reflect.Struct { pv := reflect.New(sliceElementType.Elem()) - session.Statement.setRefValue(pv.Elem()) + if err := session.statement.setRefValue(pv.Elem()); err != nil { + return err + } } else { tp = tpNonStruct } } else if sliceElementType.Kind() == reflect.Struct { pv := reflect.New(sliceElementType) - session.Statement.setRefValue(pv.Elem()) + if err := session.statement.setRefValue(pv.Elem()); err != nil { + return err + } } else { tp = tpNonStruct } } - var table = session.Statement.RefTable + var table = session.statement.RefTable - var addedTableName = (len(session.Statement.JoinStr) > 0) + var addedTableName = (len(session.statement.JoinStr) > 0) var autoCond builder.Cond if tp == tpStruct { - if !session.Statement.noAutoCondition && len(condiBean) > 0 { + if !session.statement.noAutoCondition && len(condiBean) > 0 { var err error - autoCond, err = session.Statement.buildConds(table, condiBean[0], true, true, false, true, addedTableName) + autoCond, err = session.statement.buildConds(table, condiBean[0], true, true, false, true, addedTableName) if err != nil { - panic(err) + return err } } else { // !oinume! Add " IS NULL" to WHERE whatever condiBean is given. // See https://github.com/go-xorm/xorm/issues/179 - if col := table.DeletedColumn(); col != nil && !session.Statement.unscoped { // tag "deleted" is enabled - var colName = session.Engine.Quote(col.Name) + if col := table.DeletedColumn(); col != nil && !session.statement.unscoped { // tag "deleted" is enabled + var colName = session.engine.Quote(col.Name) if addedTableName { - var nm = session.Statement.TableName() - if len(session.Statement.TableAlias) > 0 { - nm = session.Statement.TableAlias + var nm = session.statement.TableName() + if len(session.statement.TableAlias) > 0 { + nm = session.statement.TableAlias } - colName = session.Engine.Quote(nm) + "." + colName - } - if session.Engine.dialect.DBType() == core.MSSQL { - autoCond = builder.IsNull{colName} - } else { - autoCond = builder.IsNull{colName}.Or(builder.Eq{colName: "0001-01-01 00:00:00"}) + colName = session.engine.Quote(nm) + "." + colName } + + autoCond = session.engine.CondDeleted(colName) } } } var sqlStr string var args []interface{} - if session.Statement.RawSQL == "" { - if len(session.Statement.TableName()) <= 0 { + var err error + if session.statement.RawSQL == "" { + if len(session.statement.TableName()) <= 0 { return ErrTableNotFound } - var columnStr = session.Statement.ColumnStr - if len(session.Statement.selectStr) > 0 { - columnStr = session.Statement.selectStr + var columnStr = session.statement.ColumnStr + if len(session.statement.selectStr) > 0 { + columnStr = session.statement.selectStr } else { - if session.Statement.JoinStr == "" { + if session.statement.JoinStr == "" { if columnStr == "" { - if session.Statement.GroupByStr != "" { - columnStr = session.Statement.Engine.Quote(strings.Replace(session.Statement.GroupByStr, ",", session.Engine.Quote(","), -1)) + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) } else { - columnStr = session.Statement.genColumnStr() + columnStr = session.statement.genColumnStr() } } } else { if columnStr == "" { - if session.Statement.GroupByStr != "" { - columnStr = session.Statement.Engine.Quote(strings.Replace(session.Statement.GroupByStr, ",", session.Engine.Quote(","), -1)) + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) } else { columnStr = "*" } @@ -118,31 +121,37 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) } } - condSQL, condArgs, _ := builder.ToSQL(session.Statement.cond.And(autoCond)) + session.statement.cond = session.statement.cond.And(autoCond) + condSQL, condArgs, err := builder.ToSQL(session.statement.cond) + if err != nil { + return err + } - args = append(session.Statement.joinArgs, condArgs...) - sqlStr = session.Statement.genSelectSQL(columnStr, condSQL) + args = append(session.statement.joinArgs, condArgs...) + sqlStr, err = session.statement.genSelectSQL(columnStr, condSQL) + if err != nil { + return err + } // for mssql and use limit qs := strings.Count(sqlStr, "?") if len(args)*2 == qs { args = append(args, args...) } } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams + sqlStr = session.statement.RawSQL + args = session.statement.RawParams } - var err error if session.canCache() { - if cacher := session.Engine.getCacher2(table); cacher != nil && - !session.Statement.IsDistinct && - !session.Statement.unscoped { + if cacher := session.engine.getCacher2(table); cacher != nil && + !session.statement.IsDistinct && + !session.statement.unscoped { err = session.cacheFind(sliceElementType, sqlStr, rowsSlicePtr, args...) if err != ErrCacheFailed { return err } err = nil // !nashtsai! reset err to nil for ErrCacheFailed - session.Engine.logger.Warn("Cache Find Failed") + session.engine.logger.Warn("Cache Find Failed") } } @@ -150,21 +159,13 @@ func (session *Session) Find(rowsSlicePtr interface{}, condiBean ...interface{}) } func (session *Session) noCacheFind(table *core.Table, containerValue reflect.Value, sqlStr string, args ...interface{}) error { - var rawRows *core.Rows - var err error - - session.queryPreprocess(&sqlStr, args...) - if session.IsAutoCommit { - _, rawRows, err = session.innerQuery(sqlStr, args...) - } else { - rawRows, err = session.Tx.Query(sqlStr, args...) - } + rows, err := session.queryRows(sqlStr, args...) if err != nil { return err } - defer rawRows.Close() + defer rows.Close() - fields, err := rawRows.Columns() + fields, err := rows.Columns() if err != nil { return err } @@ -234,20 +235,29 @@ func (session *Session) noCacheFind(table *core.Table, containerValue reflect.Va if elemType.Kind() == reflect.Struct { var newValue = newElemFunc(fields) dataStruct := rValue(newValue.Interface()) - return session.rows2Beans(rawRows, fields, len(fields), session.Engine.autoMapType(dataStruct), newElemFunc, containerValueSetFunc) + tb, err := session.engine.autoMapType(dataStruct) + if err != nil { + return err + } + err = session.rows2Beans(rows, fields, tb, newElemFunc, containerValueSetFunc) + rows.Close() + if err != nil { + return err + } + return session.executeProcessors() } - for rawRows.Next() { + for rows.Next() { var newValue = newElemFunc(fields) bean := newValue.Interface() switch elemType.Kind() { case reflect.Slice: - err = rawRows.ScanSlice(bean) + err = rows.ScanSlice(bean) case reflect.Map: - err = rawRows.ScanMap(bean) + err = rows.ScanMap(bean) default: - err = rawRows.Scan(bean) + err = rows.Scan(bean) } if err != nil { @@ -278,22 +288,21 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in return ErrCacheFailed } - for _, filter := range session.Engine.dialect.Filters() { - sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) + for _, filter := range session.engine.dialect.Filters() { + sqlStr = filter.Do(sqlStr, session.engine.dialect, session.statement.RefTable) } - newsql := session.Statement.convertIDSQL(sqlStr) + newsql := session.statement.convertIDSQL(sqlStr) if newsql == "" { return ErrCacheFailed } - tableName := session.Statement.TableName() - - table := session.Statement.RefTable - cacher := session.Engine.getCacher2(table) + tableName := session.statement.TableName() + table := session.statement.RefTable + cacher := session.engine.getCacher2(table) ids, err := core.GetCacheSql(cacher, tableName, newsql, args) if err != nil { - rows, err := session.DB().Query(newsql, args...) + rows, err := session.queryRows(newsql, args...) if err != nil { return err } @@ -304,7 +313,7 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in for rows.Next() { i++ if i > 500 { - session.Engine.logger.Debug("[cacheFind] ids length > 500, no cache") + session.engine.logger.Debug("[cacheFind] ids length > 500, no cache") return ErrCacheFailed } var res = make([]string, len(table.PrimaryKeys)) @@ -312,32 +321,24 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in if err != nil { return err } - var pk core.PK = make([]interface{}, len(table.PrimaryKeys)) for i, col := range table.PKColumns() { - if col.SQLType.IsNumeric() { - n, err := strconv.ParseInt(res[i], 10, 64) - if err != nil { - return err - } - pk[i] = n - } else if col.SQLType.IsText() { - pk[i] = res[i] - } else { - return errors.New("not supported") + pk[i], err = session.engine.idTypeAssertion(col, res[i]) + if err != nil { + return err } } ids = append(ids, pk) } - session.Engine.logger.Debug("[cacheFind] cache sql:", ids, tableName, newsql, args) + session.engine.logger.Debug("[cacheFind] cache sql:", ids, tableName, sqlStr, newsql, args) err = core.PutCacheSql(cacher, ids, tableName, newsql, args) if err != nil { return err } } else { - session.Engine.logger.Debug("[cacheFind] cache hit sql:", newsql, args) + session.engine.logger.Debug("[cacheFind] cache hit sql:", tableName, sqlStr, newsql, args) } sliceValue := reflect.Indirect(reflect.ValueOf(rowsSlicePtr)) @@ -352,20 +353,20 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in return err } bean := cacher.GetBean(tableName, sid) - if bean == nil { + if bean == nil || reflect.ValueOf(bean).Elem().Type() != t { ides = append(ides, id) ididxes[sid] = idx } else { - session.Engine.logger.Debug("[cacheFind] cache hit bean:", tableName, id, bean) + session.engine.logger.Debug("[cacheFind] cache hit bean:", tableName, id, bean) - pk := session.Engine.IdOf(bean) + pk := session.engine.IdOf(bean) xid, err := pk.ToString() if err != nil { return err } if sid != xid { - session.Engine.logger.Error("[cacheFind] error cache", xid, sid, bean) + session.engine.logger.Error("[cacheFind] error cache", xid, sid, bean) return ErrCacheFailed } temps[idx] = bean @@ -373,9 +374,6 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in } if len(ides) > 0 { - newSession := session.Engine.NewSession() - defer newSession.Close() - slices := reflect.New(reflect.SliceOf(t)) beans := slices.Interface() @@ -385,18 +383,18 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in ff = append(ff, ie[0]) } - newSession.In("`"+table.PrimaryKeys[0]+"`", ff...) + session.In("`"+table.PrimaryKeys[0]+"`", ff...) } else { for _, ie := range ides { cond := builder.NewCond() for i, name := range table.PrimaryKeys { cond = cond.And(builder.Eq{"`" + name + "`": ie[i]}) } - newSession.Or(cond) + session.Or(cond) } } - err = newSession.NoCache().Find(beans) + err = session.NoCache().Table(tableName).find(beans) if err != nil { return err } @@ -407,7 +405,10 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in if rv.Kind() != reflect.Ptr { rv = rv.Addr() } - id := session.Engine.IdOfV(rv) + id, err := session.engine.idOfV(rv) + if err != nil { + return err + } sid, err := id.ToString() if err != nil { return err @@ -415,7 +416,7 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in bean := rv.Interface() temps[ididxes[sid]] = bean - session.Engine.logger.Debug("[cacheFind] cache bean:", tableName, id, bean, temps) + session.engine.logger.Debug("[cacheFind] cache bean:", tableName, id, bean, temps) cacher.PutBean(tableName, sid, bean) } } @@ -423,7 +424,7 @@ func (session *Session) cacheFind(t reflect.Type, sqlStr string, rowsSlicePtr in for j := 0; j < len(temps); j++ { bean := temps[j] if bean == nil { - session.Engine.logger.Warn("[cacheFind] cache no hit:", tableName, ids[j], temps) + session.engine.logger.Warn("[cacheFind] cache no hit:", tableName, ids[j], temps) // return errors.New("cache error") // !nashtsai! no need to return error, but continue instead continue } diff --git a/vendor/github.com/go-xorm/xorm/session_get.go b/vendor/github.com/go-xorm/xorm/session_get.go index 0c78ed94835..8faf53c02c7 100644 --- a/vendor/github.com/go-xorm/xorm/session_get.go +++ b/vendor/github.com/go-xorm/xorm/session_get.go @@ -15,42 +15,49 @@ import ( // Get retrieve one record from database, bean's non-empty fields // will be as conditions func (session *Session) Get(bean interface{}) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } + return session.get(bean) +} +func (session *Session) get(bean interface{}) (bool, error) { beanValue := reflect.ValueOf(bean) if beanValue.Kind() != reflect.Ptr { - return false, errors.New("needs a pointer to a struct") - } - - // FIXME: remove this after support non-struct Get - if beanValue.Elem().Kind() != reflect.Struct { - return false, errors.New("needs a pointer to a struct") + return false, errors.New("needs a pointer to a value") + } else if beanValue.Elem().Kind() == reflect.Ptr { + return false, errors.New("a pointer to a pointer is not allowed") } if beanValue.Elem().Kind() == reflect.Struct { - session.Statement.setRefValue(beanValue.Elem()) + if err := session.statement.setRefValue(beanValue.Elem()); err != nil { + return false, err + } } var sqlStr string var args []interface{} + var err error - if session.Statement.RawSQL == "" { - if len(session.Statement.TableName()) <= 0 { + if session.statement.RawSQL == "" { + if len(session.statement.TableName()) <= 0 { return false, ErrTableNotFound } - session.Statement.Limit(1) - sqlStr, args = session.Statement.genGetSQL(bean) + session.statement.Limit(1) + sqlStr, args, err = session.statement.genGetSQL(bean) + if err != nil { + return false, err + } } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams + sqlStr = session.statement.RawSQL + args = session.statement.RawParams } - if session.canCache() { - if cacher := session.Engine.getCacher2(session.Statement.RefTable); cacher != nil && - !session.Statement.unscoped { + table := session.statement.RefTable + + if session.canCache() && beanValue.Elem().Kind() == reflect.Struct { + if cacher := session.engine.getCacher2(table); cacher != nil && + !session.statement.unscoped { has, err := session.cacheGet(bean, sqlStr, args...) if err != ErrCacheFailed { return has, err @@ -58,47 +65,51 @@ func (session *Session) Get(bean interface{}) (bool, error) { } } - return session.nocacheGet(beanValue.Elem().Kind(), bean, sqlStr, args...) + return session.nocacheGet(beanValue.Elem().Kind(), table, bean, sqlStr, args...) } -func (session *Session) nocacheGet(beanKind reflect.Kind, bean interface{}, sqlStr string, args ...interface{}) (bool, error) { - var rawRows *core.Rows - var err error - session.queryPreprocess(&sqlStr, args...) - if session.IsAutoCommit { - _, rawRows, err = session.innerQuery(sqlStr, args...) - } else { - rawRows, err = session.Tx.Query(sqlStr, args...) - } +func (session *Session) nocacheGet(beanKind reflect.Kind, table *core.Table, bean interface{}, sqlStr string, args ...interface{}) (bool, error) { + rows, err := session.queryRows(sqlStr, args...) if err != nil { return false, err } + defer rows.Close() - defer rawRows.Close() + if !rows.Next() { + return false, nil + } - if rawRows.Next() { - fields, err := rawRows.Columns() + switch beanKind { + case reflect.Struct: + fields, err := rows.Columns() if err != nil { - // WARN: Alougth rawRows return true, but get fields failed + // WARN: Alougth rows return true, but get fields failed return true, err } - switch beanKind { - case reflect.Struct: - dataStruct := rValue(bean) - session.Statement.setRefValue(dataStruct) - _, err = session.row2Bean(rawRows, fields, len(fields), bean, &dataStruct, session.Statement.RefTable) - case reflect.Slice: - err = rawRows.ScanSlice(bean) - case reflect.Map: - err = rawRows.ScanMap(bean) - default: - err = rawRows.Scan(bean) + scanResults, err := session.row2Slice(rows, fields, bean) + if err != nil { + return false, err + } + // close it before covert data + rows.Close() + + dataStruct := rValue(bean) + _, err = session.slice2Bean(scanResults, fields, bean, &dataStruct, table) + if err != nil { + return true, err } - return true, err + return true, session.executeProcessors() + case reflect.Slice: + err = rows.ScanSlice(bean) + case reflect.Map: + err = rows.ScanMap(bean) + default: + err = rows.Scan(bean) } - return false, nil + + return true, err } func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interface{}) (has bool, err error) { @@ -107,22 +118,22 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf return false, ErrCacheFailed } - for _, filter := range session.Engine.dialect.Filters() { - sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) + for _, filter := range session.engine.dialect.Filters() { + sqlStr = filter.Do(sqlStr, session.engine.dialect, session.statement.RefTable) } - newsql := session.Statement.convertIDSQL(sqlStr) + newsql := session.statement.convertIDSQL(sqlStr) if newsql == "" { return false, ErrCacheFailed } - cacher := session.Engine.getCacher2(session.Statement.RefTable) - tableName := session.Statement.TableName() - session.Engine.logger.Debug("[cacheGet] find sql:", newsql, args) + cacher := session.engine.getCacher2(session.statement.RefTable) + tableName := session.statement.TableName() + session.engine.logger.Debug("[cacheGet] find sql:", newsql, args) + table := session.statement.RefTable ids, err := core.GetCacheSql(cacher, tableName, newsql, args) - table := session.Statement.RefTable if err != nil { var res = make([]string, len(table.PrimaryKeys)) - rows, err := session.DB().Query(newsql, args...) + rows, err := session.NoCache().queryRows(newsql, args...) if err != nil { return false, err } @@ -153,19 +164,19 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf } ids = []core.PK{pk} - session.Engine.logger.Debug("[cacheGet] cache ids:", newsql, ids) + session.engine.logger.Debug("[cacheGet] cache ids:", newsql, ids) err = core.PutCacheSql(cacher, ids, tableName, newsql, args) if err != nil { return false, err } } else { - session.Engine.logger.Debug("[cacheGet] cache hit sql:", newsql) + session.engine.logger.Debug("[cacheGet] cache hit sql:", newsql, ids) } if len(ids) > 0 { structValue := reflect.Indirect(reflect.ValueOf(bean)) id := ids[0] - session.Engine.logger.Debug("[cacheGet] get bean:", tableName, id) + session.engine.logger.Debug("[cacheGet] get bean:", tableName, id) sid, err := id.ToString() if err != nil { return false, err @@ -173,15 +184,15 @@ func (session *Session) cacheGet(bean interface{}, sqlStr string, args ...interf cacheBean := cacher.GetBean(tableName, sid) if cacheBean == nil { cacheBean = bean - has, err = session.nocacheGet(reflect.Struct, cacheBean, sqlStr, args...) + has, err = session.nocacheGet(reflect.Struct, table, cacheBean, sqlStr, args...) if err != nil || !has { return has, err } - session.Engine.logger.Debug("[cacheGet] cache bean:", tableName, id, cacheBean) + session.engine.logger.Debug("[cacheGet] cache bean:", tableName, id, cacheBean) cacher.PutBean(tableName, sid, cacheBean) } else { - session.Engine.logger.Debug("[cacheGet] cache hit bean:", tableName, id, cacheBean) + session.engine.logger.Debug("[cacheGet] cache hit bean:", tableName, id, cacheBean) has = true } structValue.Set(reflect.Indirect(reflect.ValueOf(cacheBean))) diff --git a/vendor/github.com/go-xorm/xorm/session_insert.go b/vendor/github.com/go-xorm/xorm/session_insert.go index 5b607b1fecb..129ee23098a 100644 --- a/vendor/github.com/go-xorm/xorm/session_insert.go +++ b/vendor/github.com/go-xorm/xorm/session_insert.go @@ -19,17 +19,16 @@ func (session *Session) Insert(beans ...interface{}) (int64, error) { var affected int64 var err error - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - defer session.resetStatement() for _, bean := range beans { sliceValue := reflect.Indirect(reflect.ValueOf(bean)) if sliceValue.Kind() == reflect.Slice { size := sliceValue.Len() if size > 0 { - if session.Engine.SupportInsertMany() { + if session.engine.SupportInsertMany() { cnt, err := session.innerInsertMulti(bean) if err != nil { return affected, err @@ -67,13 +66,15 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error return 0, errors.New("could not insert a empty slice") } - session.Statement.setRefValue(sliceValue.Index(0)) + if err := session.statement.setRefValue(reflect.ValueOf(sliceValue.Index(0).Interface())); err != nil { + return 0, err + } - if len(session.Statement.TableName()) <= 0 { + if len(session.statement.TableName()) <= 0 { return 0, ErrTableNotFound } - table := session.Statement.RefTable + table := session.statement.RefTable size := sliceValue.Len() var colNames []string @@ -114,18 +115,18 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error if col.IsDeleted { continue } - if session.Statement.ColumnStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); !ok { + if session.statement.ColumnStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); !ok { continue } } - if session.Statement.OmitStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); ok { + if session.statement.OmitStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); ok { continue } } - if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime { - val, t := session.Engine.NowTime2(col.SQLType.Name) + if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime { + val, t := session.engine.nowTime(col) args = append(args, val) var colName = col.Name @@ -133,7 +134,7 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error col := table.GetColumn(colName) setColumnTime(bean, col, t) }) - } else if col.IsVersion && session.Statement.checkVersion { + } else if col.IsVersion && session.statement.checkVersion { args = append(args, 1) var colName = col.Name session.afterClosures = append(session.afterClosures, func(bean interface{}) { @@ -169,18 +170,18 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error if col.IsDeleted { continue } - if session.Statement.ColumnStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); !ok { + if session.statement.ColumnStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); !ok { continue } } - if session.Statement.OmitStr != "" { - if _, ok := getFlagForColumn(session.Statement.columnMap, col); ok { + if session.statement.OmitStr != "" { + if _, ok := getFlagForColumn(session.statement.columnMap, col); ok { continue } } - if (col.IsCreated || col.IsUpdated) && session.Statement.UseAutoTime { - val, t := session.Engine.NowTime2(col.SQLType.Name) + if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime { + val, t := session.engine.nowTime(col) args = append(args, val) var colName = col.Name @@ -188,7 +189,7 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error col := table.GetColumn(colName) setColumnTime(bean, col, t) }) - } else if col.IsVersion && session.Statement.checkVersion { + } else if col.IsVersion && session.statement.checkVersion { args = append(args, 1) var colName = col.Name session.afterClosures = append(session.afterClosures, func(bean interface{}) { @@ -212,25 +213,26 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error var sql = "INSERT INTO %s (%v%v%v) VALUES (%v)" var statement string - if session.Engine.dialect.DBType() == core.ORACLE { + var tableName = session.statement.TableName() + if session.engine.dialect.DBType() == core.ORACLE { sql = "INSERT ALL INTO %s (%v%v%v) VALUES (%v) SELECT 1 FROM DUAL" temp := fmt.Sprintf(") INTO %s (%v%v%v) VALUES (", - session.Engine.Quote(session.Statement.TableName()), - session.Engine.QuoteStr(), - strings.Join(colNames, session.Engine.QuoteStr() + ", " + session.Engine.QuoteStr()), - session.Engine.QuoteStr()) + session.engine.Quote(tableName), + session.engine.QuoteStr(), + strings.Join(colNames, session.engine.QuoteStr()+", "+session.engine.QuoteStr()), + session.engine.QuoteStr()) statement = fmt.Sprintf(sql, - session.Engine.Quote(session.Statement.TableName()), - session.Engine.QuoteStr(), - strings.Join(colNames, session.Engine.QuoteStr() + ", " + session.Engine.QuoteStr()), - session.Engine.QuoteStr(), + session.engine.Quote(tableName), + session.engine.QuoteStr(), + strings.Join(colNames, session.engine.QuoteStr()+", "+session.engine.QuoteStr()), + session.engine.QuoteStr(), strings.Join(colMultiPlaces, temp)) } else { statement = fmt.Sprintf(sql, - session.Engine.Quote(session.Statement.TableName()), - session.Engine.QuoteStr(), - strings.Join(colNames, session.Engine.QuoteStr() + ", " + session.Engine.QuoteStr()), - session.Engine.QuoteStr(), + session.engine.Quote(tableName), + session.engine.QuoteStr(), + strings.Join(colNames, session.engine.QuoteStr()+", "+session.engine.QuoteStr()), + session.engine.QuoteStr(), strings.Join(colMultiPlaces, "),(")) } res, err := session.exec(statement, args...) @@ -238,8 +240,8 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error return 0, err } - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - session.cacheInsert(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheInsert(table, tableName) } lenAfterClosures := len(session.afterClosures) @@ -247,7 +249,7 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error elemValue := reflect.Indirect(sliceValue.Index(i)).Addr().Interface() // handle AfterInsertProcessor - if session.IsAutoCommit { + if session.isAutoCommit { // !nashtsai! does user expect it's same slice to passed closure when using Before()/After() when insert multi?? for _, closure := range session.afterClosures { closure(elemValue) @@ -278,8 +280,7 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error // InsertMulti insert multiple records func (session *Session) InsertMulti(rowsSlicePtr interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } @@ -297,12 +298,14 @@ func (session *Session) InsertMulti(rowsSlicePtr interface{}) (int64, error) { } func (session *Session) innerInsert(bean interface{}) (int64, error) { - session.Statement.setRefValue(rValue(bean)) - if len(session.Statement.TableName()) <= 0 { + if err := session.statement.setRefValue(rValue(bean)); err != nil { + return 0, err + } + if len(session.statement.TableName()) <= 0 { return 0, ErrTableNotFound } - table := session.Statement.RefTable + table := session.statement.RefTable // handle BeforeInsertProcessor for _, closure := range session.beforeClosures { @@ -314,19 +317,19 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { processor.BeforeInsert() } // -- - colNames, args, err := genCols(session.Statement.RefTable, session, bean, false, false) + colNames, args, err := genCols(session.statement.RefTable, session, bean, false, false) if err != nil { return 0, err } // insert expr columns, override if exists - exprColumns := session.Statement.getExpr() + exprColumns := session.statement.getExpr() exprColVals := make([]string, 0, len(exprColumns)) for _, v := range exprColumns { // remove the expr columns for i, colName := range colNames { if colName == v.colName { - colNames = append(colNames[:i], colNames[i + 1:]...) - args = append(args[:i], args[i + 1:]...) + colNames = append(colNames[:i], colNames[i+1:]...) + args = append(args[:i], args[i+1:]...) } } @@ -335,22 +338,34 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { exprColVals = append(exprColVals, v.expr) } - colPlaces := strings.Repeat("?, ", len(colNames) - len(exprColumns)) + colPlaces := strings.Repeat("?, ", len(colNames)-len(exprColumns)) if len(exprColVals) > 0 { colPlaces = colPlaces + strings.Join(exprColVals, ", ") } else { - colPlaces = colPlaces[0 : len(colPlaces) - 2] + if len(colPlaces) > 0 { + colPlaces = colPlaces[0 : len(colPlaces)-2] + } } - sqlStr := fmt.Sprintf("INSERT INTO %s (%v%v%v) VALUES (%v)", - session.Engine.Quote(session.Statement.TableName()), - session.Engine.QuoteStr(), - strings.Join(colNames, session.Engine.Quote(", ")), - session.Engine.QuoteStr(), - colPlaces) + var sqlStr string + var tableName = session.statement.TableName() + if len(colPlaces) > 0 { + sqlStr = fmt.Sprintf("INSERT INTO %s (%v%v%v) VALUES (%v)", + session.engine.Quote(tableName), + session.engine.QuoteStr(), + strings.Join(colNames, session.engine.Quote(", ")), + session.engine.QuoteStr(), + colPlaces) + } else { + if session.engine.dialect.DBType() == core.MYSQL { + sqlStr = fmt.Sprintf("INSERT INTO %s VALUES ()", session.engine.Quote(tableName)) + } else { + sqlStr = fmt.Sprintf("INSERT INTO %s DEFAULT VALUES", session.engine.Quote(tableName)) + } + } handleAfterInsertProcessorFunc := func(bean interface{}) { - if session.IsAutoCommit { + if session.isAutoCommit { for _, closure := range session.afterClosures { closure(bean) } @@ -379,23 +394,22 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { // for postgres, many of them didn't implement lastInsertId, so we should // implemented it ourself. - if session.Engine.dialect.DBType() == core.ORACLE && len(table.AutoIncrement) > 0 { - //assert table.AutoIncrement != "" - res, err := session.query("select seq_atable.currval from dual", args...) + if session.engine.dialect.DBType() == core.ORACLE && len(table.AutoIncrement) > 0 { + res, err := session.queryBytes("select seq_atable.currval from dual", args...) if err != nil { return 0, err } - handleAfterInsertProcessorFunc(bean) + defer handleAfterInsertProcessorFunc(bean) - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - session.cacheInsert(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheInsert(table, tableName) } - if table.Version != "" && session.Statement.checkVersion { + if table.Version != "" && session.statement.checkVersion { verValue, err := table.VersionColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } else if verValue.IsValid() && verValue.CanSet() { verValue.SetInt(1) } @@ -413,7 +427,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { aiValue, err := table.AutoIncrColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() { @@ -423,24 +437,24 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { aiValue.Set(int64ToIntValue(id, aiValue.Type())) return 1, nil - } else if session.Engine.dialect.DBType() == core.POSTGRES && len(table.AutoIncrement) > 0 { + } else if session.engine.dialect.DBType() == core.POSTGRES && len(table.AutoIncrement) > 0 { //assert table.AutoIncrement != "" - sqlStr = sqlStr + " RETURNING " + session.Engine.Quote(table.AutoIncrement) - res, err := session.query(sqlStr, args...) + sqlStr = sqlStr + " RETURNING " + session.engine.Quote(table.AutoIncrement) + res, err := session.queryBytes(sqlStr, args...) if err != nil { return 0, err } - handleAfterInsertProcessorFunc(bean) + defer handleAfterInsertProcessorFunc(bean) - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - session.cacheInsert(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheInsert(table, tableName) } - if table.Version != "" && session.Statement.checkVersion { + if table.Version != "" && session.statement.checkVersion { verValue, err := table.VersionColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } else if verValue.IsValid() && verValue.CanSet() { verValue.SetInt(1) } @@ -458,7 +472,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { aiValue, err := table.AutoIncrColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() { @@ -476,14 +490,14 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { defer handleAfterInsertProcessorFunc(bean) - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - session.cacheInsert(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + session.cacheInsert(table, tableName) } - if table.Version != "" && session.Statement.checkVersion { + if table.Version != "" && session.statement.checkVersion { verValue, err := table.VersionColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } else if verValue.IsValid() && verValue.CanSet() { verValue.SetInt(1) } @@ -501,7 +515,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { aiValue, err := table.AutoIncrColumn().ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() { @@ -518,24 +532,21 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { // The in parameter bean must a struct or a point to struct. The return // parameter is inserted and error func (session *Session) InsertOne(bean interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } return session.innerInsert(bean) } -func (session *Session) cacheInsert(tables ...string) error { - if session.Statement.RefTable == nil { +func (session *Session) cacheInsert(table *core.Table, tables ...string) error { + if table == nil { return ErrCacheFailed } - table := session.Statement.RefTable - cacher := session.Engine.getCacher2(table) - + cacher := session.engine.getCacher2(table) for _, t := range tables { - session.Engine.logger.Debug("[cache] clear sql:", t) + session.engine.logger.Debug("[cache] clear sql:", t) cacher.ClearIds(t) } diff --git a/vendor/github.com/go-xorm/xorm/session_iterate.go b/vendor/github.com/go-xorm/xorm/session_iterate.go index 7c148095902..071fce49921 100644 --- a/vendor/github.com/go-xorm/xorm/session_iterate.go +++ b/vendor/github.com/go-xorm/xorm/session_iterate.go @@ -19,6 +19,14 @@ func (session *Session) Rows(bean interface{}) (*Rows, error) { // are conditions. beans could be []Struct, []*Struct, map[int64]Struct // map[int64]*Struct func (session *Session) Iterate(bean interface{}, fun IterFunc) error { + if session.isAutoClose { + defer session.Close() + } + + if session.statement.bufferSize > 0 { + return session.bufferIterate(bean, fun) + } + rows, err := session.Rows(bean) if err != nil { return err @@ -40,3 +48,49 @@ func (session *Session) Iterate(bean interface{}, fun IterFunc) error { } return err } + +// BufferSize sets the buffersize for iterate +func (session *Session) BufferSize(size int) *Session { + session.statement.bufferSize = size + return session +} + +func (session *Session) bufferIterate(bean interface{}, fun IterFunc) error { + if session.isAutoClose { + defer session.Close() + } + + var bufferSize = session.statement.bufferSize + var limit = session.statement.LimitN + if limit > 0 && bufferSize > limit { + bufferSize = limit + } + var start = session.statement.Start + v := rValue(bean) + sliceType := reflect.SliceOf(v.Type()) + var idx = 0 + for { + slice := reflect.New(sliceType) + if err := session.Limit(bufferSize, start).find(slice.Interface(), bean); err != nil { + return err + } + + for i := 0; i < slice.Elem().Len(); i++ { + if err := fun(idx, slice.Elem().Index(i).Addr().Interface()); err != nil { + return err + } + idx++ + } + + start = start + slice.Elem().Len() + if limit > 0 && idx+bufferSize > limit { + bufferSize = limit - idx + } + + if bufferSize <= 0 || slice.Elem().Len() < bufferSize || idx == limit { + break + } + } + + return nil +} diff --git a/vendor/github.com/go-xorm/xorm/session_query.go b/vendor/github.com/go-xorm/xorm/session_query.go new file mode 100644 index 00000000000..5b4e0dc45d0 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/session_query.go @@ -0,0 +1,252 @@ +// Copyright 2017 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "github.com/go-xorm/builder" + "github.com/go-xorm/core" +) + +func (session *Session) genQuerySQL(sqlorArgs ...interface{}) (string, []interface{}, error) { + if len(sqlorArgs) > 0 { + return sqlorArgs[0].(string), sqlorArgs[1:], nil + } + + if session.statement.RawSQL != "" { + return session.statement.RawSQL, session.statement.RawParams, nil + } + + if len(session.statement.TableName()) <= 0 { + return "", nil, ErrTableNotFound + } + + var columnStr = session.statement.ColumnStr + if len(session.statement.selectStr) > 0 { + columnStr = session.statement.selectStr + } else { + if session.statement.JoinStr == "" { + if columnStr == "" { + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) + } else { + columnStr = session.statement.genColumnStr() + } + } + } else { + if columnStr == "" { + if session.statement.GroupByStr != "" { + columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) + } else { + columnStr = "*" + } + } + } + if columnStr == "" { + columnStr = "*" + } + } + + condSQL, condArgs, err := builder.ToSQL(session.statement.cond) + if err != nil { + return "", nil, err + } + + args := append(session.statement.joinArgs, condArgs...) + sqlStr, err := session.statement.genSelectSQL(columnStr, condSQL) + if err != nil { + return "", nil, err + } + // for mssql and use limit + qs := strings.Count(sqlStr, "?") + if len(args)*2 == qs { + args = append(args, args...) + } + + return sqlStr, args, nil +} + +// Query runs a raw sql and return records as []map[string][]byte +func (session *Session) Query(sqlorArgs ...interface{}) ([]map[string][]byte, error) { + if session.isAutoClose { + defer session.Close() + } + + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + + return session.queryBytes(sqlStr, args...) +} + +func value2String(rawValue *reflect.Value) (str string, err error) { + aa := reflect.TypeOf((*rawValue).Interface()) + vv := reflect.ValueOf((*rawValue).Interface()) + switch aa.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + str = strconv.FormatInt(vv.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + str = strconv.FormatUint(vv.Uint(), 10) + case reflect.Float32, reflect.Float64: + str = strconv.FormatFloat(vv.Float(), 'f', -1, 64) + case reflect.String: + str = vv.String() + case reflect.Array, reflect.Slice: + switch aa.Elem().Kind() { + case reflect.Uint8: + data := rawValue.Interface().([]byte) + str = string(data) + if str == "\x00" { + str = "0" + } + default: + err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) + } + // time type + case reflect.Struct: + if aa.ConvertibleTo(core.TimeType) { + str = vv.Convert(core.TimeType).Interface().(time.Time).Format(time.RFC3339Nano) + } else { + err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) + } + case reflect.Bool: + str = strconv.FormatBool(vv.Bool()) + case reflect.Complex128, reflect.Complex64: + str = fmt.Sprintf("%v", vv.Complex()) + /* TODO: unsupported types below + case reflect.Map: + case reflect.Ptr: + case reflect.Uintptr: + case reflect.UnsafePointer: + case reflect.Chan, reflect.Func, reflect.Interface: + */ + default: + err = fmt.Errorf("Unsupported struct type %v", vv.Type().Name()) + } + return +} + +func row2mapStr(rows *core.Rows, fields []string) (resultsMap map[string]string, err error) { + result := make(map[string]string) + scanResultContainers := make([]interface{}, len(fields)) + for i := 0; i < len(fields); i++ { + var scanResultContainer interface{} + scanResultContainers[i] = &scanResultContainer + } + if err := rows.Scan(scanResultContainers...); err != nil { + return nil, err + } + + for ii, key := range fields { + rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) + // if row is null then as empty string + if rawValue.Interface() == nil { + result[key] = "" + continue + } + + if data, err := value2String(&rawValue); err == nil { + result[key] = data + } else { + return nil, err + } + } + return result, nil +} + +func rows2Strings(rows *core.Rows) (resultsSlice []map[string]string, err error) { + fields, err := rows.Columns() + if err != nil { + return nil, err + } + for rows.Next() { + result, err := row2mapStr(rows, fields) + if err != nil { + return nil, err + } + resultsSlice = append(resultsSlice, result) + } + + return resultsSlice, nil +} + +// QueryString runs a raw sql and return records as []map[string]string +func (session *Session) QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) { + if session.isAutoClose { + defer session.Close() + } + + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + + rows, err := session.queryRows(sqlStr, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + return rows2Strings(rows) +} + +func row2mapInterface(rows *core.Rows, fields []string) (resultsMap map[string]interface{}, err error) { + resultsMap = make(map[string]interface{}, len(fields)) + scanResultContainers := make([]interface{}, len(fields)) + for i := 0; i < len(fields); i++ { + var scanResultContainer interface{} + scanResultContainers[i] = &scanResultContainer + } + if err := rows.Scan(scanResultContainers...); err != nil { + return nil, err + } + + for ii, key := range fields { + resultsMap[key] = reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])).Interface() + } + return +} + +func rows2Interfaces(rows *core.Rows) (resultsSlice []map[string]interface{}, err error) { + fields, err := rows.Columns() + if err != nil { + return nil, err + } + for rows.Next() { + result, err := row2mapInterface(rows, fields) + if err != nil { + return nil, err + } + resultsSlice = append(resultsSlice, result) + } + + return resultsSlice, nil +} + +// QueryInterface runs a raw sql and return records as []map[string]interface{} +func (session *Session) QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) { + if session.isAutoClose { + defer session.Close() + } + + sqlStr, args, err := session.genQuerySQL(sqlorArgs...) + if err != nil { + return nil, err + } + + rows, err := session.queryRows(sqlStr, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + return rows2Interfaces(rows) +} diff --git a/vendor/github.com/go-xorm/xorm/session_raw.go b/vendor/github.com/go-xorm/xorm/session_raw.go index 9351d5cf94b..69bf9b3c6bf 100644 --- a/vendor/github.com/go-xorm/xorm/session_raw.go +++ b/vendor/github.com/go-xorm/xorm/session_raw.go @@ -6,21 +6,140 @@ package xorm import ( "database/sql" + "reflect" + "time" "github.com/go-xorm/core" ) -func (session *Session) query(sqlStr string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { - session.queryPreprocess(&sqlStr, paramStr...) - - if session.IsAutoCommit { - return session.innerQuery2(sqlStr, paramStr...) +func (session *Session) queryPreprocess(sqlStr *string, paramStr ...interface{}) { + for _, filter := range session.engine.dialect.Filters() { + *sqlStr = filter.Do(*sqlStr, session.engine.dialect, session.statement.RefTable) } - return session.txQuery(session.Tx, sqlStr, paramStr...) + + session.lastSQL = *sqlStr + session.lastSQLArgs = paramStr } -func (session *Session) txQuery(tx *core.Tx, sqlStr string, params ...interface{}) (resultsSlice []map[string][]byte, err error) { - rows, err := tx.Query(sqlStr, params...) +func (session *Session) queryRows(sqlStr string, args ...interface{}) (*core.Rows, error) { + defer session.resetStatement() + + session.queryPreprocess(&sqlStr, args...) + + if session.engine.showSQL { + if session.engine.showExecTime { + b4ExecTime := time.Now() + defer func() { + execDuration := time.Since(b4ExecTime) + if len(args) > 0 { + session.engine.logger.Infof("[SQL] %s %#v - took: %v", sqlStr, args, execDuration) + } else { + session.engine.logger.Infof("[SQL] %s - took: %v", sqlStr, execDuration) + } + }() + } else { + if len(args) > 0 { + session.engine.logger.Infof("[SQL] %v %#v", sqlStr, args) + } else { + session.engine.logger.Infof("[SQL] %v", sqlStr) + } + } + } + + if session.isAutoCommit { + var db *core.DB + if session.engine.engineGroup != nil { + db = session.engine.engineGroup.Slave().DB() + } else { + db = session.DB() + } + + if session.prepareStmt { + // don't clear stmt since session will cache them + stmt, err := session.doPrepare(db, sqlStr) + if err != nil { + return nil, err + } + + rows, err := stmt.Query(args...) + if err != nil { + return nil, err + } + return rows, nil + } + + rows, err := db.Query(sqlStr, args...) + if err != nil { + return nil, err + } + return rows, nil + } + + rows, err := session.tx.Query(sqlStr, args...) + if err != nil { + return nil, err + } + return rows, nil +} + +func (session *Session) queryRow(sqlStr string, args ...interface{}) *core.Row { + return core.NewRow(session.queryRows(sqlStr, args...)) +} + +func value2Bytes(rawValue *reflect.Value) ([]byte, error) { + str, err := value2String(rawValue) + if err != nil { + return nil, err + } + return []byte(str), nil +} + +func row2map(rows *core.Rows, fields []string) (resultsMap map[string][]byte, err error) { + result := make(map[string][]byte) + scanResultContainers := make([]interface{}, len(fields)) + for i := 0; i < len(fields); i++ { + var scanResultContainer interface{} + scanResultContainers[i] = &scanResultContainer + } + if err := rows.Scan(scanResultContainers...); err != nil { + return nil, err + } + + for ii, key := range fields { + rawValue := reflect.Indirect(reflect.ValueOf(scanResultContainers[ii])) + //if row is null then ignore + if rawValue.Interface() == nil { + result[key] = []byte{} + continue + } + + if data, err := value2Bytes(&rawValue); err == nil { + result[key] = data + } else { + return nil, err // !nashtsai! REVIEW, should return err or just error log? + } + } + return result, nil +} + +func rows2maps(rows *core.Rows) (resultsSlice []map[string][]byte, err error) { + fields, err := rows.Columns() + if err != nil { + return nil, err + } + for rows.Next() { + result, err := row2map(rows, fields) + if err != nil { + return nil, err + } + resultsSlice = append(resultsSlice, result) + } + + return resultsSlice, nil +} + +func (session *Session) queryBytes(sqlStr string, args ...interface{}) ([]map[string][]byte, error) { + rows, err := session.queryRows(sqlStr, args...) if err != nil { return nil, err } @@ -29,73 +148,37 @@ func (session *Session) txQuery(tx *core.Tx, sqlStr string, params ...interface{ return rows2maps(rows) } -func (session *Session) innerQuery(sqlStr string, params ...interface{}) (*core.Stmt, *core.Rows, error) { - var callback func() (*core.Stmt, *core.Rows, error) - if session.prepareStmt { - callback = func() (*core.Stmt, *core.Rows, error) { - stmt, err := session.doPrepare(sqlStr) - if err != nil { - return nil, nil, err - } - rows, err := stmt.Query(params...) - if err != nil { - return nil, nil, err - } - return stmt, rows, nil - } - } else { - callback = func() (*core.Stmt, *core.Rows, error) { - rows, err := session.DB().Query(sqlStr, params...) - if err != nil { - return nil, nil, err - } - return nil, rows, err - } - } - stmt, rows, err := session.Engine.logSQLQueryTime(sqlStr, params, callback) - if err != nil { - return nil, nil, err - } - return stmt, rows, nil -} - -func (session *Session) innerQuery2(sqlStr string, params ...interface{}) ([]map[string][]byte, error) { - _, rows, err := session.innerQuery(sqlStr, params...) - if rows != nil { - defer rows.Close() - } - if err != nil { - return nil, err - } - return rows2maps(rows) -} - -// Query a raw sql and return records as []map[string][]byte -func (session *Session) Query(sqlStr string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { +func (session *Session) exec(sqlStr string, args ...interface{}) (sql.Result, error) { defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() + + session.queryPreprocess(&sqlStr, args...) + + if session.engine.showSQL { + if session.engine.showExecTime { + b4ExecTime := time.Now() + defer func() { + execDuration := time.Since(b4ExecTime) + if len(args) > 0 { + session.engine.logger.Infof("[SQL] %s %#v - took: %v", sqlStr, args, execDuration) + } else { + session.engine.logger.Infof("[SQL] %s - took: %v", sqlStr, execDuration) + } + }() + } else { + if len(args) > 0 { + session.engine.logger.Infof("[SQL] %v %#v", sqlStr, args) + } else { + session.engine.logger.Infof("[SQL] %v", sqlStr) + } + } } - return session.query(sqlStr, paramStr...) -} - -// ============================= -// for string -// ============================= -func (session *Session) query2(sqlStr string, paramStr ...interface{}) (resultsSlice []map[string]string, err error) { - session.queryPreprocess(&sqlStr, paramStr...) - - if session.IsAutoCommit { - return query2(session.DB(), sqlStr, paramStr...) + if !session.isAutoCommit { + return session.tx.Exec(sqlStr, args...) } - return txQuery2(session.Tx, sqlStr, paramStr...) -} -// Execute sql -func (session *Session) innerExec(sqlStr string, args ...interface{}) (sql.Result, error) { if session.prepareStmt { - stmt, err := session.doPrepare(sqlStr) + stmt, err := session.doPrepare(session.DB(), sqlStr) if err != nil { return nil, err } @@ -110,33 +193,9 @@ func (session *Session) innerExec(sqlStr string, args ...interface{}) (sql.Resul return session.DB().Exec(sqlStr, args...) } -func (session *Session) exec(sqlStr string, args ...interface{}) (sql.Result, error) { - for _, filter := range session.Engine.dialect.Filters() { - // TODO: for table name, it's no need to RefTable - sqlStr = filter.Do(sqlStr, session.Engine.dialect, session.Statement.RefTable) - } - - session.saveLastSQL(sqlStr, args...) - - return session.Engine.logSQLExecutionTime(sqlStr, args, func() (sql.Result, error) { - if session.IsAutoCommit { - // FIXME: oci8 can not auto commit (github.com/mattn/go-oci8) - if session.Engine.dialect.DBType() == core.ORACLE { - session.Begin() - r, err := session.Tx.Exec(sqlStr, args...) - session.Commit() - return r, err - } - return session.innerExec(sqlStr, args...) - } - return session.Tx.Exec(sqlStr, args...) - }) -} - // Exec raw sql func (session *Session) Exec(sqlStr string, args ...interface{}) (sql.Result, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } diff --git a/vendor/github.com/go-xorm/xorm/session_schema.go b/vendor/github.com/go-xorm/xorm/session_schema.go index 21fa2996149..a2708b736c0 100644 --- a/vendor/github.com/go-xorm/xorm/session_schema.go +++ b/vendor/github.com/go-xorm/xorm/session_schema.go @@ -16,38 +16,50 @@ import ( // Ping test if database is ok func (session *Session) Ping() error { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } + session.engine.logger.Infof("PING DATABASE %v", session.engine.DriverName()) return session.DB().Ping() } // CreateTable create a table according a bean func (session *Session) CreateTable(bean interface{}) error { - v := rValue(bean) - session.Statement.setRefValue(v) - - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - return session.createOneTable() + return session.createTable(bean) +} + +func (session *Session) createTable(bean interface{}) error { + v := rValue(bean) + if err := session.statement.setRefValue(v); err != nil { + return err + } + + sqlStr := session.statement.genCreateTableSQL() + _, err := session.exec(sqlStr) + return err } // CreateIndexes create indexes func (session *Session) CreateIndexes(bean interface{}) error { - v := rValue(bean) - session.Statement.setRefValue(v) - - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - sqls := session.Statement.genIndexSQL() + return session.createIndexes(bean) +} + +func (session *Session) createIndexes(bean interface{}) error { + v := rValue(bean) + if err := session.statement.setRefValue(v); err != nil { + return err + } + + sqls := session.statement.genIndexSQL() for _, sqlStr := range sqls { _, err := session.exec(sqlStr) if err != nil { @@ -59,15 +71,19 @@ func (session *Session) CreateIndexes(bean interface{}) error { // CreateUniques create uniques func (session *Session) CreateUniques(bean interface{}) error { - v := rValue(bean) - session.Statement.setRefValue(v) - - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } + return session.createUniques(bean) +} - sqls := session.Statement.genUniqueSQL() +func (session *Session) createUniques(bean interface{}) error { + v := rValue(bean) + if err := session.statement.setRefValue(v); err != nil { + return err + } + + sqls := session.statement.genUniqueSQL() for _, sqlStr := range sqls { _, err := session.exec(sqlStr) if err != nil { @@ -77,41 +93,22 @@ func (session *Session) CreateUniques(bean interface{}) error { return nil } -func (session *Session) createOneTable() error { - sqlStr := session.Statement.genCreateTableSQL() - _, err := session.exec(sqlStr) - return err -} - -// to be deleted -func (session *Session) createAll() error { - if session.IsAutoClose { - defer session.Close() - } - - for _, table := range session.Engine.Tables { - session.Statement.RefTable = table - session.Statement.tableName = table.Name - err := session.createOneTable() - session.resetStatement() - if err != nil { - return err - } - } - return nil -} - // DropIndexes drop indexes func (session *Session) DropIndexes(bean interface{}) error { - v := rValue(bean) - session.Statement.setRefValue(v) - - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } - sqls := session.Statement.genDelIndexSQL() + return session.dropIndexes(bean) +} + +func (session *Session) dropIndexes(bean interface{}) error { + v := rValue(bean) + if err := session.statement.setRefValue(v); err != nil { + return err + } + + sqls := session.statement.genDelIndexSQL() for _, sqlStr := range sqls { _, err := session.exec(sqlStr) if err != nil { @@ -123,15 +120,23 @@ func (session *Session) DropIndexes(bean interface{}) error { // DropTable drop table will drop table if exist, if drop failed, it will return error func (session *Session) DropTable(beanOrTableName interface{}) error { - tableName, err := session.Engine.tableName(beanOrTableName) + if session.isAutoClose { + defer session.Close() + } + + return session.dropTable(beanOrTableName) +} + +func (session *Session) dropTable(beanOrTableName interface{}) error { + tableName, err := session.engine.tableName(beanOrTableName) if err != nil { return err } var needDrop = true - if !session.Engine.dialect.SupportDropIfExists() { - sqlStr, args := session.Engine.dialect.TableCheckSql(tableName) - results, err := session.query(sqlStr, args...) + if !session.engine.dialect.SupportDropIfExists() { + sqlStr, args := session.engine.dialect.TableCheckSql(tableName) + results, err := session.queryBytes(sqlStr, args...) if err != nil { return err } @@ -139,7 +144,7 @@ func (session *Session) DropTable(beanOrTableName interface{}) error { } if needDrop { - sqlStr := session.Engine.Dialect().DropTableSql(tableName) + sqlStr := session.engine.Dialect().DropTableSql(tableName) _, err = session.exec(sqlStr) return err } @@ -148,7 +153,11 @@ func (session *Session) DropTable(beanOrTableName interface{}) error { // IsTableExist if a table is exist func (session *Session) IsTableExist(beanOrTableName interface{}) (bool, error) { - tableName, err := session.Engine.tableName(beanOrTableName) + if session.isAutoClose { + defer session.Close() + } + + tableName, err := session.engine.tableName(beanOrTableName) if err != nil { return false, err } @@ -157,12 +166,8 @@ func (session *Session) IsTableExist(beanOrTableName interface{}) (bool, error) } func (session *Session) isTableExist(tableName string) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - sqlStr, args := session.Engine.dialect.TableCheckSql(tableName) - results, err := session.query(sqlStr, args...) + sqlStr, args := session.engine.dialect.TableCheckSql(tableName) + results, err := session.queryBytes(sqlStr, args...) return len(results) > 0, err } @@ -172,6 +177,9 @@ func (session *Session) IsTableEmpty(bean interface{}) (bool, error) { t := v.Type() if t.Kind() == reflect.String { + if session.isAutoClose { + defer session.Close() + } return session.isTableEmpty(bean.(string)) } else if t.Kind() == reflect.Struct { rows, err := session.Count(bean) @@ -181,15 +189,9 @@ func (session *Session) IsTableEmpty(bean interface{}) (bool, error) { } func (session *Session) isTableEmpty(tableName string) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - var total int64 - sqlStr := fmt.Sprintf("select count(*) from %s", session.Engine.Quote(tableName)) - err := session.DB().QueryRow(sqlStr).Scan(&total) - session.saveLastSQL(sqlStr) + sqlStr := fmt.Sprintf("select count(*) from %s", session.engine.Quote(tableName)) + err := session.queryRow(sqlStr).Scan(&total) if err != nil { if err == sql.ErrNoRows { err = nil @@ -200,30 +202,9 @@ func (session *Session) isTableEmpty(tableName string) (bool, error) { return total == 0, nil } -func (session *Session) isIndexExist(tableName, idxName string, unique bool) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - var idx string - if unique { - idx = uniqueName(tableName, idxName) - } else { - idx = indexName(tableName, idxName) - } - sqlStr, args := session.Engine.dialect.IndexCheckSql(tableName, idx) - results, err := session.query(sqlStr, args...) - return len(results) > 0, err -} - // find if index is exist according cols func (session *Session) isIndexExist2(tableName string, cols []string, unique bool) (bool, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - indexes, err := session.Engine.dialect.GetIndexes(tableName) + indexes, err := session.engine.dialect.GetIndexes(tableName) if err != nil { return false, err } @@ -240,62 +221,34 @@ func (session *Session) isIndexExist2(tableName string, cols []string, unique bo } func (session *Session) addColumn(colName string) error { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - col := session.Statement.RefTable.GetColumn(colName) - sql, args := session.Statement.genAddColumnStr(col) + col := session.statement.RefTable.GetColumn(colName) + sql, args := session.statement.genAddColumnStr(col) _, err := session.exec(sql, args...) return err } func (session *Session) addIndex(tableName, idxName string) error { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - index := session.Statement.RefTable.Indexes[idxName] - sqlStr := session.Engine.dialect.CreateIndexSql(tableName, index) - + index := session.statement.RefTable.Indexes[idxName] + sqlStr := session.engine.dialect.CreateIndexSql(tableName, index) _, err := session.exec(sqlStr) return err } func (session *Session) addUnique(tableName, uqeName string) error { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - index := session.Statement.RefTable.Indexes[uqeName] - sqlStr := session.Engine.dialect.CreateIndexSql(tableName, index) + index := session.statement.RefTable.Indexes[uqeName] + sqlStr := session.engine.dialect.CreateIndexSql(tableName, index) _, err := session.exec(sqlStr) return err } -// To be deleted -func (session *Session) dropAll() error { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - for _, table := range session.Engine.Tables { - session.Statement.Init() - session.Statement.RefTable = table - sqlStr := session.Engine.Dialect().DropTableSql(session.Statement.TableName()) - _, err := session.exec(sqlStr) - if err != nil { - return err - } - } - return nil -} - // Sync2 synchronize structs to database tables func (session *Session) Sync2(beans ...interface{}) error { - engine := session.Engine + engine := session.engine + + if session.isAutoClose { + session.isAutoClose = false + defer session.Close() + } tables, err := engine.DBMetas() if err != nil { @@ -322,17 +275,17 @@ func (session *Session) Sync2(beans ...interface{}) error { } if oriTable == nil { - err = session.StoreEngine(session.Statement.StoreEngine).CreateTable(bean) + err = session.StoreEngine(session.statement.StoreEngine).createTable(bean) if err != nil { return err } - err = session.CreateUniques(bean) + err = session.createUniques(bean) if err != nil { return err } - err = session.CreateIndexes(bean) + err = session.createIndexes(bean) if err != nil { return err } @@ -357,7 +310,7 @@ func (session *Session) Sync2(beans ...interface{}) error { engine.dialect.DBType() == core.POSTGRES { engine.logger.Infof("Table %s column %s change type from %s to %s\n", tbName, col.Name, curType, expectedType) - _, err = engine.Exec(engine.dialect.ModifyColumnSql(table.Name, col)) + _, err = session.exec(engine.dialect.ModifyColumnSql(table.Name, col)) } else { engine.logger.Warnf("Table %s column %s db type is %s, struct type is %s\n", tbName, col.Name, curType, expectedType) @@ -367,7 +320,7 @@ func (session *Session) Sync2(beans ...interface{}) error { if oriCol.Length < col.Length { engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n", tbName, col.Name, oriCol.Length, col.Length) - _, err = engine.Exec(engine.dialect.ModifyColumnSql(table.Name, col)) + _, err = session.exec(engine.dialect.ModifyColumnSql(table.Name, col)) } } } else { @@ -381,7 +334,7 @@ func (session *Session) Sync2(beans ...interface{}) error { if oriCol.Length < col.Length { engine.logger.Infof("Table %s column %s change type from varchar(%d) to varchar(%d)\n", tbName, col.Name, oriCol.Length, col.Length) - _, err = engine.Exec(engine.dialect.ModifyColumnSql(table.Name, col)) + _, err = session.exec(engine.dialect.ModifyColumnSql(table.Name, col)) } } } @@ -394,10 +347,8 @@ func (session *Session) Sync2(beans ...interface{}) error { tbName, col.Name, oriCol.Nullable, col.Nullable) } } else { - session := engine.NewSession() - session.Statement.RefTable = table - session.Statement.tableName = tbName - defer session.Close() + session.statement.RefTable = table + session.statement.tableName = tbName err = session.addColumn(col.Name) } if err != nil { @@ -421,7 +372,7 @@ func (session *Session) Sync2(beans ...interface{}) error { if oriIndex != nil { if oriIndex.Type != index.Type { sql := engine.dialect.DropIndexSql(tbName, oriIndex) - _, err = engine.Exec(sql) + _, err = session.exec(sql) if err != nil { return err } @@ -437,7 +388,7 @@ func (session *Session) Sync2(beans ...interface{}) error { for name2, index2 := range oriTable.Indexes { if _, ok := foundIndexNames[name2]; !ok { sql := engine.dialect.DropIndexSql(tbName, index2) - _, err = engine.Exec(sql) + _, err = session.exec(sql) if err != nil { return err } @@ -446,16 +397,12 @@ func (session *Session) Sync2(beans ...interface{}) error { for name, index := range addedNames { if index.Type == core.UniqueType { - session := engine.NewSession() - session.Statement.RefTable = table - session.Statement.tableName = tbName - defer session.Close() + session.statement.RefTable = table + session.statement.tableName = tbName err = session.addUnique(tbName, name) } else if index.Type == core.IndexType { - session := engine.NewSession() - session.Statement.RefTable = table - session.Statement.tableName = tbName - defer session.Close() + session.statement.RefTable = table + session.statement.tableName = tbName err = session.addIndex(tbName, name) } if err != nil { diff --git a/vendor/github.com/go-xorm/xorm/session_stats.go b/vendor/github.com/go-xorm/xorm/session_stats.go new file mode 100644 index 00000000000..c2cac830697 --- /dev/null +++ b/vendor/github.com/go-xorm/xorm/session_stats.go @@ -0,0 +1,98 @@ +// Copyright 2016 The Xorm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xorm + +import ( + "database/sql" + "errors" + "reflect" +) + +// Count counts the records. bean's non-empty fields +// are conditions. +func (session *Session) Count(bean ...interface{}) (int64, error) { + if session.isAutoClose { + defer session.Close() + } + + var sqlStr string + var args []interface{} + var err error + if session.statement.RawSQL == "" { + sqlStr, args, err = session.statement.genCountSQL(bean...) + if err != nil { + return 0, err + } + } else { + sqlStr = session.statement.RawSQL + args = session.statement.RawParams + } + + var total int64 + err = session.queryRow(sqlStr, args...).Scan(&total) + if err == sql.ErrNoRows || err == nil { + return total, nil + } + + return 0, err +} + +// sum call sum some column. bean's non-empty fields are conditions. +func (session *Session) sum(res interface{}, bean interface{}, columnNames ...string) error { + if session.isAutoClose { + defer session.Close() + } + + v := reflect.ValueOf(res) + if v.Kind() != reflect.Ptr { + return errors.New("need a pointer to a variable") + } + + var isSlice = v.Elem().Kind() == reflect.Slice + var sqlStr string + var args []interface{} + var err error + if len(session.statement.RawSQL) == 0 { + sqlStr, args, err = session.statement.genSumSQL(bean, columnNames...) + if err != nil { + return err + } + } else { + sqlStr = session.statement.RawSQL + args = session.statement.RawParams + } + + if isSlice { + err = session.queryRow(sqlStr, args...).ScanSlice(res) + } else { + err = session.queryRow(sqlStr, args...).Scan(res) + } + if err == sql.ErrNoRows || err == nil { + return nil + } + return err +} + +// Sum call sum some column. bean's non-empty fields are conditions. +func (session *Session) Sum(bean interface{}, columnName string) (res float64, err error) { + return res, session.sum(&res, bean, columnName) +} + +// SumInt call sum some column. bean's non-empty fields are conditions. +func (session *Session) SumInt(bean interface{}, columnName string) (res int64, err error) { + return res, session.sum(&res, bean, columnName) +} + +// Sums call sum some columns. bean's non-empty fields are conditions. +func (session *Session) Sums(bean interface{}, columnNames ...string) ([]float64, error) { + var res = make([]float64, len(columnNames), len(columnNames)) + return res, session.sum(&res, bean, columnNames...) +} + +// SumsInt sum specify columns and return as []int64 instead of []float64 +func (session *Session) SumsInt(bean interface{}, columnNames ...string) ([]int64, error) { + var res = make([]int64, len(columnNames), len(columnNames)) + return res, session.sum(&res, bean, columnNames...) +} diff --git a/vendor/github.com/go-xorm/xorm/session_sum.go b/vendor/github.com/go-xorm/xorm/session_sum.go deleted file mode 100644 index e1409c7ff43..00000000000 --- a/vendor/github.com/go-xorm/xorm/session_sum.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2016 The Xorm Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xorm - -import "database/sql" - -// Count counts the records. bean's non-empty fields -// are conditions. -func (session *Session) Count(bean interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - var sqlStr string - var args []interface{} - if session.Statement.RawSQL == "" { - sqlStr, args = session.Statement.genCountSQL(bean) - } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams - } - - session.queryPreprocess(&sqlStr, args...) - - var err error - var total int64 - if session.IsAutoCommit { - err = session.DB().QueryRow(sqlStr, args...).Scan(&total) - } else { - err = session.Tx.QueryRow(sqlStr, args...).Scan(&total) - } - - if err == sql.ErrNoRows || err == nil { - return total, nil - } - - return 0, err -} - -// Sum call sum some column. bean's non-empty fields are conditions. -func (session *Session) Sum(bean interface{}, columnName string) (float64, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - var sqlStr string - var args []interface{} - if len(session.Statement.RawSQL) == 0 { - sqlStr, args = session.Statement.genSumSQL(bean, columnName) - } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams - } - - session.queryPreprocess(&sqlStr, args...) - - var err error - var res float64 - if session.IsAutoCommit { - err = session.DB().QueryRow(sqlStr, args...).Scan(&res) - } else { - err = session.Tx.QueryRow(sqlStr, args...).Scan(&res) - } - - if err == sql.ErrNoRows || err == nil { - return res, nil - } - return 0, err -} - -// Sums call sum some columns. bean's non-empty fields are conditions. -func (session *Session) Sums(bean interface{}, columnNames ...string) ([]float64, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - var sqlStr string - var args []interface{} - if len(session.Statement.RawSQL) == 0 { - sqlStr, args = session.Statement.genSumSQL(bean, columnNames...) - } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams - } - - session.queryPreprocess(&sqlStr, args...) - - var err error - var res = make([]float64, len(columnNames), len(columnNames)) - if session.IsAutoCommit { - err = session.DB().QueryRow(sqlStr, args...).ScanSlice(&res) - } else { - err = session.Tx.QueryRow(sqlStr, args...).ScanSlice(&res) - } - - if err == sql.ErrNoRows || err == nil { - return res, nil - } - return nil, err -} - -// SumsInt sum specify columns and return as []int64 instead of []float64 -func (session *Session) SumsInt(bean interface{}, columnNames ...string) ([]int64, error) { - defer session.resetStatement() - if session.IsAutoClose { - defer session.Close() - } - - var sqlStr string - var args []interface{} - if len(session.Statement.RawSQL) == 0 { - sqlStr, args = session.Statement.genSumSQL(bean, columnNames...) - } else { - sqlStr = session.Statement.RawSQL - args = session.Statement.RawParams - } - - session.queryPreprocess(&sqlStr, args...) - - var err error - var res = make([]int64, len(columnNames), len(columnNames)) - if session.IsAutoCommit { - err = session.DB().QueryRow(sqlStr, args...).ScanSlice(&res) - } else { - err = session.Tx.QueryRow(sqlStr, args...).ScanSlice(&res) - } - - if err == sql.ErrNoRows || err == nil { - return res, nil - } - return nil, err -} diff --git a/vendor/github.com/go-xorm/xorm/session_tx.go b/vendor/github.com/go-xorm/xorm/session_tx.go index 302bc104d54..84d2f7f9dcf 100644 --- a/vendor/github.com/go-xorm/xorm/session_tx.go +++ b/vendor/github.com/go-xorm/xorm/session_tx.go @@ -6,14 +6,14 @@ package xorm // Begin a transaction func (session *Session) Begin() error { - if session.IsAutoCommit { + if session.isAutoCommit { tx, err := session.DB().Begin() if err != nil { return err } - session.IsAutoCommit = false - session.IsCommitedOrRollbacked = false - session.Tx = tx + session.isAutoCommit = false + session.isCommitedOrRollbacked = false + session.tx = tx session.saveLastSQL("BEGIN TRANSACTION") } return nil @@ -21,25 +21,23 @@ func (session *Session) Begin() error { // Rollback When using transaction, you can rollback if any error func (session *Session) Rollback() error { - if !session.IsAutoCommit && !session.IsCommitedOrRollbacked { - session.saveLastSQL(session.Engine.dialect.RollBackStr()) - session.IsCommitedOrRollbacked = true - return session.Tx.Rollback() + if !session.isAutoCommit && !session.isCommitedOrRollbacked { + session.saveLastSQL(session.engine.dialect.RollBackStr()) + session.isCommitedOrRollbacked = true + return session.tx.Rollback() } return nil } // Commit When using transaction, Commit will commit all operations. func (session *Session) Commit() error { - if !session.IsAutoCommit && !session.IsCommitedOrRollbacked { + if !session.isAutoCommit && !session.isCommitedOrRollbacked { session.saveLastSQL("COMMIT") - session.IsCommitedOrRollbacked = true + session.isCommitedOrRollbacked = true var err error - if err = session.Tx.Commit(); err == nil { + if err = session.tx.Commit(); err == nil { // handle processors after tx committed - closureCallFunc := func(closuresPtr *[]func(interface{}), bean interface{}) { - if closuresPtr != nil { for _, closure := range *closuresPtr { closure(bean) diff --git a/vendor/github.com/go-xorm/xorm/session_update.go b/vendor/github.com/go-xorm/xorm/session_update.go index 0f2d1b5cefb..f558745667f 100644 --- a/vendor/github.com/go-xorm/xorm/session_update.go +++ b/vendor/github.com/go-xorm/xorm/session_update.go @@ -15,20 +15,20 @@ import ( "github.com/go-xorm/core" ) -func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { - if session.Statement.RefTable == nil || - session.Tx != nil { +func (session *Session) cacheUpdate(table *core.Table, tableName, sqlStr string, args ...interface{}) error { + if table == nil || + session.tx != nil { return ErrCacheFailed } - oldhead, newsql := session.Statement.convertUpdateSQL(sqlStr) + oldhead, newsql := session.statement.convertUpdateSQL(sqlStr) if newsql == "" { return ErrCacheFailed } - for _, filter := range session.Engine.dialect.Filters() { - newsql = filter.Do(newsql, session.Engine.dialect, session.Statement.RefTable) + for _, filter := range session.engine.dialect.Filters() { + newsql = filter.Do(newsql, session.engine.dialect, table) } - session.Engine.logger.Debug("[cacheUpdate] new sql", oldhead, newsql) + session.engine.logger.Debug("[cacheUpdate] new sql", oldhead, newsql) var nStart int if len(args) > 0 { @@ -39,13 +39,12 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { nStart = strings.Count(oldhead, "$") } } - table := session.Statement.RefTable - cacher := session.Engine.getCacher2(table) - tableName := session.Statement.TableName() - session.Engine.logger.Debug("[cacheUpdate] get cache sql", newsql, args[nStart:]) + + cacher := session.engine.getCacher2(table) + session.engine.logger.Debug("[cacheUpdate] get cache sql", newsql, args[nStart:]) ids, err := core.GetCacheSql(cacher, tableName, newsql, args[nStart:]) if err != nil { - rows, err := session.DB().Query(newsql, args[nStart:]...) + rows, err := session.NoCache().queryRows(newsql, args[nStart:]...) if err != nil { return err } @@ -75,9 +74,9 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { ids = append(ids, pk) } - session.Engine.logger.Debug("[cacheUpdate] find updated id", ids) + session.engine.logger.Debug("[cacheUpdate] find updated id", ids) } /*else { - session.Engine.LogDebug("[xorm:cacheUpdate] del cached sql:", tableName, newsql, args) + session.engine.LogDebug("[xorm:cacheUpdate] del cached sql:", tableName, newsql, args) cacher.DelIds(tableName, genSqlKey(newsql, args)) }*/ @@ -103,36 +102,36 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { colName := sps2[len(sps2)-1] if strings.Contains(colName, "`") { colName = strings.TrimSpace(strings.Replace(colName, "`", "", -1)) - } else if strings.Contains(colName, session.Engine.QuoteStr()) { - colName = strings.TrimSpace(strings.Replace(colName, session.Engine.QuoteStr(), "", -1)) + } else if strings.Contains(colName, session.engine.QuoteStr()) { + colName = strings.TrimSpace(strings.Replace(colName, session.engine.QuoteStr(), "", -1)) } else { - session.Engine.logger.Debug("[cacheUpdate] cannot find column", tableName, colName) + session.engine.logger.Debug("[cacheUpdate] cannot find column", tableName, colName) return ErrCacheFailed } if col := table.GetColumn(colName); col != nil { fieldValue, err := col.ValueOf(bean) if err != nil { - session.Engine.logger.Error(err) + session.engine.logger.Error(err) } else { - session.Engine.logger.Debug("[cacheUpdate] set bean field", bean, colName, fieldValue.Interface()) - if col.IsVersion && session.Statement.checkVersion { + session.engine.logger.Debug("[cacheUpdate] set bean field", bean, colName, fieldValue.Interface()) + if col.IsVersion && session.statement.checkVersion { fieldValue.SetInt(fieldValue.Int() + 1) } else { fieldValue.Set(reflect.ValueOf(args[idx])) } } } else { - session.Engine.logger.Errorf("[cacheUpdate] ERROR: column %v is not table %v's", + session.engine.logger.Errorf("[cacheUpdate] ERROR: column %v is not table %v's", colName, table.Name) } } - session.Engine.logger.Debug("[cacheUpdate] update cache", tableName, id, bean) + session.engine.logger.Debug("[cacheUpdate] update cache", tableName, id, bean) cacher.PutBean(tableName, sid, bean) } } - session.Engine.logger.Debug("[cacheUpdate] clear cached table sql:", tableName) + session.engine.logger.Debug("[cacheUpdate] clear cached table sql:", tableName) cacher.ClearIds(tableName) return nil } @@ -144,8 +143,7 @@ func (session *Session) cacheUpdate(sqlStr string, args ...interface{}) error { // You should call UseBool if you have bool to use. // 2.float32 & float64 may be not inexact as conditions func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int64, error) { - defer session.resetStatement() - if session.IsAutoClose { + if session.isAutoClose { defer session.Close() } @@ -169,19 +167,21 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 var isMap = t.Kind() == reflect.Map var isStruct = t.Kind() == reflect.Struct if isStruct { - session.Statement.setRefValue(v) + if err := session.statement.setRefValue(v); err != nil { + return 0, err + } - if len(session.Statement.TableName()) <= 0 { + if len(session.statement.TableName()) <= 0 { return 0, ErrTableNotFound } - if session.Statement.ColumnStr == "" { - colNames, args = buildUpdates(session.Engine, session.Statement.RefTable, bean, false, false, - false, false, session.Statement.allUseBool, session.Statement.useAllCols, - session.Statement.mustColumnMap, session.Statement.nullableMap, - session.Statement.columnMap, true, session.Statement.unscoped) + if session.statement.ColumnStr == "" { + colNames, args = buildUpdates(session.engine, session.statement.RefTable, bean, false, false, + false, false, session.statement.allUseBool, session.statement.useAllCols, + session.statement.mustColumnMap, session.statement.nullableMap, + session.statement.columnMap, true, session.statement.unscoped) } else { - colNames, args, err = genCols(session.Statement.RefTable, session, bean, true, true) + colNames, args, err = genCols(session.statement.RefTable, session, bean, true, true) if err != nil { return 0, err } @@ -192,68 +192,84 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 bValue := reflect.Indirect(reflect.ValueOf(bean)) for _, v := range bValue.MapKeys() { - colNames = append(colNames, session.Engine.Quote(v.String())+" = ?") + colNames = append(colNames, session.engine.Quote(v.String())+" = ?") args = append(args, bValue.MapIndex(v).Interface()) } } else { return 0, ErrParamsType } - table := session.Statement.RefTable + table := session.statement.RefTable - if session.Statement.UseAutoTime && table != nil && table.Updated != "" { - colNames = append(colNames, session.Engine.Quote(table.Updated)+" = ?") - col := table.UpdatedColumn() - val, t := session.Engine.NowTime2(col.SQLType.Name) - args = append(args, val) + if session.statement.UseAutoTime && table != nil && table.Updated != "" { + if _, ok := session.statement.columnMap[strings.ToLower(table.Updated)]; !ok { + colNames = append(colNames, session.engine.Quote(table.Updated)+" = ?") + col := table.UpdatedColumn() + val, t := session.engine.nowTime(col) + args = append(args, val) - var colName = col.Name - if isStruct { - session.afterClosures = append(session.afterClosures, func(bean interface{}) { - col := table.GetColumn(colName) - setColumnTime(bean, col, t) - }) + var colName = col.Name + if isStruct { + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + setColumnTime(bean, col, t) + }) + } } } //for update action to like "column = column + ?" - incColumns := session.Statement.getInc() + incColumns := session.statement.getInc() for _, v := range incColumns { - colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+session.Engine.Quote(v.colName)+" + ?") + colNames = append(colNames, session.engine.Quote(v.colName)+" = "+session.engine.Quote(v.colName)+" + ?") args = append(args, v.arg) } //for update action to like "column = column - ?" - decColumns := session.Statement.getDec() + decColumns := session.statement.getDec() for _, v := range decColumns { - colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+session.Engine.Quote(v.colName)+" - ?") + colNames = append(colNames, session.engine.Quote(v.colName)+" = "+session.engine.Quote(v.colName)+" - ?") args = append(args, v.arg) } //for update action to like "column = expression" - exprColumns := session.Statement.getExpr() + exprColumns := session.statement.getExpr() for _, v := range exprColumns { - colNames = append(colNames, session.Engine.Quote(v.colName)+" = "+v.expr) + colNames = append(colNames, session.engine.Quote(v.colName)+" = "+v.expr) } - session.Statement.processIDParam() + if err = session.statement.processIDParam(); err != nil { + return 0, err + } var autoCond builder.Cond - if !session.Statement.noAutoCondition && len(condiBean) > 0 { - var err error - autoCond, err = session.Statement.buildConds(session.Statement.RefTable, condiBean[0], true, true, false, true, false) - if err != nil { - return 0, err + if !session.statement.noAutoCondition && len(condiBean) > 0 { + if c, ok := condiBean[0].(map[string]interface{}); ok { + autoCond = builder.Eq(c) + } else { + ct := reflect.TypeOf(condiBean[0]) + k := ct.Kind() + if k == reflect.Ptr { + k = ct.Elem().Kind() + } + if k == reflect.Struct { + var err error + autoCond, err = session.statement.buildConds(session.statement.RefTable, condiBean[0], true, true, false, true, false) + if err != nil { + return 0, err + } + } else { + return 0, ErrConditionType + } } } - st := session.Statement - defer session.resetStatement() + st := &session.statement var sqlStr string var condArgs []interface{} var condSQL string - cond := session.Statement.cond.And(autoCond) + cond := session.statement.cond.And(autoCond) - var doIncVer = (table != nil && table.Version != "" && session.Statement.checkVersion) + var doIncVer = (table != nil && table.Version != "" && session.statement.checkVersion) var verValue *reflect.Value if doIncVer { verValue, err = table.VersionColumn().ValueOf(bean) @@ -261,11 +277,15 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 return 0, err } - cond = cond.And(builder.Eq{session.Engine.Quote(table.Version): verValue.Interface()}) - colNames = append(colNames, session.Engine.Quote(table.Version)+" = "+session.Engine.Quote(table.Version)+" + 1") + cond = cond.And(builder.Eq{session.engine.Quote(table.Version): verValue.Interface()}) + colNames = append(colNames, session.engine.Quote(table.Version)+" = "+session.engine.Quote(table.Version)+" + 1") + } + + condSQL, condArgs, err = builder.ToSQL(cond) + if err != nil { + return 0, err } - condSQL, condArgs, _ = builder.ToSQL(cond) if len(condSQL) > 0 { condSQL = "WHERE " + condSQL } @@ -274,6 +294,7 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 condSQL = condSQL + fmt.Sprintf(" ORDER BY %v", st.OrderStr) } + var tableName = session.statement.TableName() // TODO: Oracle support needed var top string if st.LimitN > 0 { @@ -282,27 +303,53 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 } else if st.Engine.dialect.DBType() == core.SQLITE { tempCondSQL := condSQL + fmt.Sprintf(" LIMIT %d", st.LimitN) cond = cond.And(builder.Expr(fmt.Sprintf("rowid IN (SELECT rowid FROM %v %v)", - session.Engine.Quote(session.Statement.TableName()), tempCondSQL), condArgs...)) - condSQL, condArgs, _ = builder.ToSQL(cond) + session.engine.Quote(tableName), tempCondSQL), condArgs...)) + condSQL, condArgs, err = builder.ToSQL(cond) + if err != nil { + return 0, err + } if len(condSQL) > 0 { condSQL = "WHERE " + condSQL } } else if st.Engine.dialect.DBType() == core.POSTGRES { tempCondSQL := condSQL + fmt.Sprintf(" LIMIT %d", st.LimitN) cond = cond.And(builder.Expr(fmt.Sprintf("CTID IN (SELECT CTID FROM %v %v)", - session.Engine.Quote(session.Statement.TableName()), tempCondSQL), condArgs...)) - condSQL, condArgs, _ = builder.ToSQL(cond) + session.engine.Quote(tableName), tempCondSQL), condArgs...)) + condSQL, condArgs, err = builder.ToSQL(cond) + if err != nil { + return 0, err + } + if len(condSQL) > 0 { condSQL = "WHERE " + condSQL } } else if st.Engine.dialect.DBType() == core.MSSQL { - top = fmt.Sprintf("top (%d) ", st.LimitN) + if st.OrderStr != "" && st.Engine.dialect.DBType() == core.MSSQL && + table != nil && len(table.PrimaryKeys) == 1 { + cond = builder.Expr(fmt.Sprintf("%s IN (SELECT TOP (%d) %s FROM %v%v)", + table.PrimaryKeys[0], st.LimitN, table.PrimaryKeys[0], + session.engine.Quote(tableName), condSQL), condArgs...) + + condSQL, condArgs, err = builder.ToSQL(cond) + if err != nil { + return 0, err + } + if len(condSQL) > 0 { + condSQL = "WHERE " + condSQL + } + } else { + top = fmt.Sprintf("TOP (%d) ", st.LimitN) + } } } + if len(colNames) <= 0 { + return 0, errors.New("No content found to be updated") + } + sqlStr = fmt.Sprintf("UPDATE %v%v SET %v %v", top, - session.Engine.Quote(session.Statement.TableName()), + session.engine.Quote(tableName), strings.Join(colNames, ", "), condSQL) @@ -316,19 +363,20 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 } if table != nil { - if cacher := session.Engine.getCacher2(table); cacher != nil && session.Statement.UseCache { - cacher.ClearIds(session.Statement.TableName()) - cacher.ClearBeans(session.Statement.TableName()) + if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { + //session.cacheUpdate(table, tableName, sqlStr, args...) + cacher.ClearIds(tableName) + cacher.ClearBeans(tableName) } } // handle after update processors - if session.IsAutoCommit { + if session.isAutoCommit { for _, closure := range session.afterClosures { closure(bean) } if processor, ok := interface{}(bean).(AfterUpdateProcessor); ok { - session.Engine.logger.Debug("[event]", session.Statement.TableName(), " has after update processor") + session.engine.logger.Debug("[event]", tableName, " has after update processor") processor.AfterUpdate() } } else { diff --git a/vendor/github.com/go-xorm/xorm/statement.go b/vendor/github.com/go-xorm/xorm/statement.go index 82101ff20e9..6400425b20e 100644 --- a/vendor/github.com/go-xorm/xorm/statement.go +++ b/vendor/github.com/go-xorm/xorm/statement.go @@ -73,6 +73,7 @@ type Statement struct { decrColumns map[string]decrParam exprColumns map[string]exprParam cond builder.Cond + bufferSize int } // Init reset all the statement's fields @@ -111,6 +112,7 @@ func (statement *Statement) Init() { statement.decrColumns = make(map[string]decrParam) statement.exprColumns = make(map[string]exprParam) statement.cond = builder.NewCond() + statement.bufferSize = 0 } // NoAutoCondition if you do not want convert bean's field as query condition, then use this function @@ -158,6 +160,9 @@ func (statement *Statement) And(query interface{}, args ...interface{}) *Stateme case string: cond := builder.Expr(query.(string), args...) statement.cond = statement.cond.And(cond) + case map[string]interface{}: + cond := builder.Eq(query.(map[string]interface{})) + statement.cond = statement.cond.And(cond) case builder.Cond: cond := query.(builder.Cond) statement.cond = statement.cond.And(cond) @@ -179,6 +184,9 @@ func (statement *Statement) Or(query interface{}, args ...interface{}) *Statemen case string: cond := builder.Expr(query.(string), args...) statement.cond = statement.cond.Or(cond) + case map[string]interface{}: + cond := builder.Eq(query.(map[string]interface{})) + statement.cond = statement.cond.Or(cond) case builder.Cond: cond := query.(builder.Cond) statement.cond = statement.cond.Or(cond) @@ -207,9 +215,14 @@ func (statement *Statement) NotIn(column string, args ...interface{}) *Statement return statement } -func (statement *Statement) setRefValue(v reflect.Value) { - statement.RefTable = statement.Engine.autoMapType(reflect.Indirect(v)) +func (statement *Statement) setRefValue(v reflect.Value) error { + var err error + statement.RefTable, err = statement.Engine.autoMapType(reflect.Indirect(v)) + if err != nil { + return err + } statement.tableName = statement.Engine.tbName(v) + return nil } // Table tempororily set table name, the parameter could be a string or a pointer of struct @@ -219,7 +232,12 @@ func (statement *Statement) Table(tableNameOrBean interface{}) *Statement { if t.Kind() == reflect.String { statement.AltTableName = tableNameOrBean.(string) } else if t.Kind() == reflect.Struct { - statement.RefTable = statement.Engine.autoMapType(v) + var err error + statement.RefTable, err = statement.Engine.autoMapType(v) + if err != nil { + statement.Engine.logger.Error(err) + return statement + } statement.AltTableName = statement.Engine.tbName(v) } return statement @@ -262,6 +280,9 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{}, fieldValue := *fieldValuePtr fieldType := reflect.TypeOf(fieldValue.Interface()) + if fieldType == nil { + continue + } requiredField := useAllCols includeNil := useAllCols @@ -366,7 +387,7 @@ func buildUpdates(engine *Engine, table *core.Table, bean interface{}, if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { continue } - val = engine.FormatTime(col.SQLType.Name, t) + val = engine.formatColTime(col, t) } else if nulType, ok := fieldValue.Interface().(driver.Valuer); ok { val, _ = nulType.Value() } else { @@ -480,224 +501,6 @@ func (statement *Statement) colName(col *core.Column, tableName string) string { return statement.Engine.Quote(col.Name) } -func buildConds(engine *Engine, table *core.Table, bean interface{}, - includeVersion bool, includeUpdated bool, includeNil bool, - includeAutoIncr bool, allUseBool bool, useAllCols bool, unscoped bool, - mustColumnMap map[string]bool, tableName, aliasName string, addedTableName bool) (builder.Cond, error) { - var conds []builder.Cond - for _, col := range table.Columns() { - if !includeVersion && col.IsVersion { - continue - } - if !includeUpdated && col.IsUpdated { - continue - } - if !includeAutoIncr && col.IsAutoIncrement { - continue - } - - if engine.dialect.DBType() == core.MSSQL && (col.SQLType.Name == core.Text || col.SQLType.IsBlob() || col.SQLType.Name == core.TimeStampz) { - continue - } - if col.SQLType.IsJson() { - continue - } - - var colName string - if addedTableName { - var nm = tableName - if len(aliasName) > 0 { - nm = aliasName - } - colName = engine.Quote(nm) + "." + engine.Quote(col.Name) - } else { - colName = engine.Quote(col.Name) - } - - fieldValuePtr, err := col.ValueOf(bean) - if err != nil { - engine.logger.Error(err) - continue - } - - if col.IsDeleted && !unscoped { // tag "deleted" is enabled - if engine.dialect.DBType() == core.MSSQL { - conds = append(conds, builder.IsNull{colName}) - } else { - conds = append(conds, builder.IsNull{colName}.Or(builder.Eq{colName: "0001-01-01 00:00:00"})) - } - } - - fieldValue := *fieldValuePtr - if fieldValue.Interface() == nil { - continue - } - - fieldType := reflect.TypeOf(fieldValue.Interface()) - requiredField := useAllCols - - if b, ok := getFlagForColumn(mustColumnMap, col); ok { - if b { - requiredField = true - } else { - continue - } - } - - if fieldType.Kind() == reflect.Ptr { - if fieldValue.IsNil() { - if includeNil { - conds = append(conds, builder.Eq{colName: nil}) - } - continue - } else if !fieldValue.IsValid() { - continue - } else { - // dereference ptr type to instance type - fieldValue = fieldValue.Elem() - fieldType = reflect.TypeOf(fieldValue.Interface()) - requiredField = true - } - } - - var val interface{} - switch fieldType.Kind() { - case reflect.Bool: - if allUseBool || requiredField { - val = fieldValue.Interface() - } else { - // if a bool in a struct, it will not be as a condition because it default is false, - // please use Where() instead - continue - } - case reflect.String: - if !requiredField && fieldValue.String() == "" { - continue - } - // for MyString, should convert to string or panic - if fieldType.String() != reflect.String.String() { - val = fieldValue.String() - } else { - val = fieldValue.Interface() - } - case reflect.Int8, reflect.Int16, reflect.Int, reflect.Int32, reflect.Int64: - if !requiredField && fieldValue.Int() == 0 { - continue - } - val = fieldValue.Interface() - case reflect.Float32, reflect.Float64: - if !requiredField && fieldValue.Float() == 0.0 { - continue - } - val = fieldValue.Interface() - case reflect.Uint8, reflect.Uint16, reflect.Uint, reflect.Uint32, reflect.Uint64: - if !requiredField && fieldValue.Uint() == 0 { - continue - } - t := int64(fieldValue.Uint()) - val = reflect.ValueOf(&t).Interface() - case reflect.Struct: - if fieldType.ConvertibleTo(core.TimeType) { - t := fieldValue.Convert(core.TimeType).Interface().(time.Time) - if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { - continue - } - val = engine.FormatTime(col.SQLType.Name, t) - } else if _, ok := reflect.New(fieldType).Interface().(core.Conversion); ok { - continue - } else if valNul, ok := fieldValue.Interface().(driver.Valuer); ok { - val, _ = valNul.Value() - if val == nil { - continue - } - } else { - if col.SQLType.IsJson() { - if col.SQLType.IsText() { - bytes, err := json.Marshal(fieldValue.Interface()) - if err != nil { - engine.logger.Error(err) - continue - } - val = string(bytes) - } else if col.SQLType.IsBlob() { - var bytes []byte - var err error - bytes, err = json.Marshal(fieldValue.Interface()) - if err != nil { - engine.logger.Error(err) - continue - } - val = bytes - } - } else { - engine.autoMapType(fieldValue) - if table, ok := engine.Tables[fieldValue.Type()]; ok { - if len(table.PrimaryKeys) == 1 { - pkField := reflect.Indirect(fieldValue).FieldByName(table.PKColumns()[0].FieldName) - // fix non-int pk issues - //if pkField.Int() != 0 { - if pkField.IsValid() && !isZero(pkField.Interface()) { - val = pkField.Interface() - } else { - continue - } - } else { - //TODO: how to handler? - panic(fmt.Sprintln("not supported", fieldValue.Interface(), "as", table.PrimaryKeys)) - } - } else { - val = fieldValue.Interface() - } - } - } - case reflect.Array: - continue - case reflect.Slice, reflect.Map: - if fieldValue == reflect.Zero(fieldType) { - continue - } - if fieldValue.IsNil() || !fieldValue.IsValid() || fieldValue.Len() == 0 { - continue - } - - if col.SQLType.IsText() { - bytes, err := json.Marshal(fieldValue.Interface()) - if err != nil { - engine.logger.Error(err) - continue - } - val = string(bytes) - } else if col.SQLType.IsBlob() { - var bytes []byte - var err error - if (fieldType.Kind() == reflect.Array || fieldType.Kind() == reflect.Slice) && - fieldType.Elem().Kind() == reflect.Uint8 { - if fieldValue.Len() > 0 { - val = fieldValue.Bytes() - } else { - continue - } - } else { - bytes, err = json.Marshal(fieldValue.Interface()) - if err != nil { - engine.logger.Error(err) - continue - } - val = bytes - } - } else { - continue - } - default: - val = fieldValue.Interface() - } - - conds = append(conds, builder.Eq{colName: val}) - } - - return builder.And(conds...), nil -} - // TableName return current tableName func (statement *Statement) TableName() string { if statement.AltTableName != "" { @@ -800,6 +603,22 @@ func (statement *Statement) col2NewColsWithQuote(columns ...string) []string { return newColumns } +func (statement *Statement) colmap2NewColsWithQuote() []string { + newColumns := make([]string, 0, len(statement.columnMap)) + for col := range statement.columnMap { + fields := strings.Split(strings.TrimSpace(col), ".") + if len(fields) == 1 { + newColumns = append(newColumns, statement.Engine.quote(fields[0])) + } else if len(fields) == 2 { + newColumns = append(newColumns, statement.Engine.quote(fields[0])+"."+ + statement.Engine.quote(fields[1])) + } else { + panic(errors.New("unwanted colnames")) + } + } + return newColumns +} + // Distinct generates "DISTINCT col1, col2 " statement func (statement *Statement) Distinct(columns ...string) *Statement { statement.IsDistinct = true @@ -826,7 +645,7 @@ func (statement *Statement) Cols(columns ...string) *Statement { statement.columnMap[strings.ToLower(nc)] = true } - newColumns := statement.col2NewColsWithQuote(columns...) + newColumns := statement.colmap2NewColsWithQuote() statement.ColumnStr = strings.Join(newColumns, ", ") statement.ColumnStr = strings.Replace(statement.ColumnStr, statement.Engine.quote("*"), "*", -1) return statement @@ -1088,33 +907,50 @@ func (statement *Statement) genDelIndexSQL() []string { func (statement *Statement) genAddColumnStr(col *core.Column) (string, []interface{}) { quote := statement.Engine.Quote - sql := fmt.Sprintf("ALTER TABLE %v ADD %v;", quote(statement.TableName()), + sql := fmt.Sprintf("ALTER TABLE %v ADD %v", quote(statement.TableName()), col.String(statement.Engine.dialect)) + if statement.Engine.dialect.DBType() == core.MYSQL && len(col.Comment) > 0 { + sql += " COMMENT '" + col.Comment + "'" + } + sql += ";" return sql, []interface{}{} } func (statement *Statement) buildConds(table *core.Table, bean interface{}, includeVersion bool, includeUpdated bool, includeNil bool, includeAutoIncr bool, addedTableName bool) (builder.Cond, error) { - return buildConds(statement.Engine, table, bean, includeVersion, includeUpdated, includeNil, includeAutoIncr, statement.allUseBool, statement.useAllCols, + return statement.Engine.buildConds(table, bean, includeVersion, includeUpdated, includeNil, includeAutoIncr, statement.allUseBool, statement.useAllCols, statement.unscoped, statement.mustColumnMap, statement.TableName(), statement.TableAlias, addedTableName) } -func (statement *Statement) genConds(bean interface{}) (string, []interface{}, error) { +func (statement *Statement) mergeConds(bean interface{}) error { if !statement.noAutoCondition { var addedTableName = (len(statement.JoinStr) > 0) autoCond, err := statement.buildConds(statement.RefTable, bean, true, true, false, true, addedTableName) if err != nil { - return "", nil, err + return err } statement.cond = statement.cond.And(autoCond) } - statement.processIDParam() + if err := statement.processIDParam(); err != nil { + return err + } + return nil +} + +func (statement *Statement) genConds(bean interface{}) (string, []interface{}, error) { + if err := statement.mergeConds(bean); err != nil { + return "", nil, err + } return builder.ToSQL(statement.cond) } -func (statement *Statement) genGetSQL(bean interface{}) (string, []interface{}) { - statement.setRefValue(rValue(bean)) +func (statement *Statement) genGetSQL(bean interface{}) (string, []interface{}, error) { + v := rValue(bean) + isStruct := v.Kind() == reflect.Struct + if isStruct { + statement.setRefValue(v) + } var columnStr = statement.ColumnStr if len(statement.selectStr) > 0 { @@ -1133,22 +969,46 @@ func (statement *Statement) genGetSQL(bean interface{}) (string, []interface{}) if len(columnStr) == 0 { if len(statement.GroupByStr) > 0 { columnStr = statement.Engine.Quote(strings.Replace(statement.GroupByStr, ",", statement.Engine.Quote(","), -1)) - } else { - columnStr = "*" } } } } - condSQL, condArgs, _ := statement.genConds(bean) + if len(columnStr) == 0 { + columnStr = "*" + } - return statement.genSelectSQL(columnStr, condSQL), append(statement.joinArgs, condArgs...) + if isStruct { + if err := statement.mergeConds(bean); err != nil { + return "", nil, err + } + } + condSQL, condArgs, err := builder.ToSQL(statement.cond) + if err != nil { + return "", nil, err + } + + sqlStr, err := statement.genSelectSQL(columnStr, condSQL) + if err != nil { + return "", nil, err + } + + return sqlStr, append(statement.joinArgs, condArgs...), nil } -func (statement *Statement) genCountSQL(bean interface{}) (string, []interface{}) { - statement.setRefValue(rValue(bean)) - - condSQL, condArgs, _ := statement.genConds(bean) +func (statement *Statement) genCountSQL(beans ...interface{}) (string, []interface{}, error) { + var condSQL string + var condArgs []interface{} + var err error + if len(beans) > 0 { + statement.setRefValue(rValue(beans[0])) + condSQL, condArgs, err = statement.genConds(beans[0]) + } else { + condSQL, condArgs, err = builder.ToSQL(statement.cond) + } + if err != nil { + return "", nil, err + } var selectSQL = statement.selectStr if len(selectSQL) <= 0 { @@ -1158,23 +1018,40 @@ func (statement *Statement) genCountSQL(bean interface{}) (string, []interface{} selectSQL = "count(*)" } } - return statement.genSelectSQL(selectSQL, condSQL), append(statement.joinArgs, condArgs...) + sqlStr, err := statement.genSelectSQL(selectSQL, condSQL) + if err != nil { + return "", nil, err + } + + return sqlStr, append(statement.joinArgs, condArgs...), nil } -func (statement *Statement) genSumSQL(bean interface{}, columns ...string) (string, []interface{}) { +func (statement *Statement) genSumSQL(bean interface{}, columns ...string) (string, []interface{}, error) { statement.setRefValue(rValue(bean)) var sumStrs = make([]string, 0, len(columns)) for _, colName := range columns { - sumStrs = append(sumStrs, fmt.Sprintf("COALESCE(sum(%s),0)", statement.Engine.Quote(colName))) + if !strings.Contains(colName, " ") && !strings.Contains(colName, "(") { + colName = statement.Engine.Quote(colName) + } + sumStrs = append(sumStrs, fmt.Sprintf("COALESCE(sum(%s),0)", colName)) + } + sumSelect := strings.Join(sumStrs, ", ") + + condSQL, condArgs, err := statement.genConds(bean) + if err != nil { + return "", nil, err } - condSQL, condArgs, _ := statement.genConds(bean) + sqlStr, err := statement.genSelectSQL(sumSelect, condSQL) + if err != nil { + return "", nil, err + } - return statement.genSelectSQL(strings.Join(sumStrs, ", "), condSQL), append(statement.joinArgs, condArgs...) + return sqlStr, append(statement.joinArgs, condArgs...), nil } -func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string) { +func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string, err error) { var distinct string if statement.IsDistinct && !strings.HasPrefix(columnStr, "count") { distinct = "DISTINCT " @@ -1185,15 +1062,23 @@ func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string) { var top string var mssqlCondi string - statement.processIDParam() + if err := statement.processIDParam(); err != nil { + return "", err + } var buf bytes.Buffer if len(condSQL) > 0 { fmt.Fprintf(&buf, " WHERE %v", condSQL) } var whereStr = buf.String() + var fromStr = " FROM " + + if dialect.DBType() == core.MSSQL && strings.Contains(statement.TableName(), "..") { + fromStr += statement.TableName() + } else { + fromStr += quote(statement.TableName()) + } - var fromStr = " FROM " + quote(statement.TableName()) if statement.TableAlias != "" { if dialect.DBType() == core.ORACLE { fromStr += " " + quote(statement.TableAlias) @@ -1246,7 +1131,7 @@ func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string) { } // !nashtsai! REVIEW Sprintf is considered slowest mean of string concatnation, better to work with builder pattern - a = fmt.Sprintf("SELECT %v%v%v%v%v", top, distinct, columnStr, fromStr, whereStr) + a = fmt.Sprintf("SELECT %v%v%v%v%v", distinct, top, columnStr, fromStr, whereStr) if len(mssqlCondi) > 0 { if len(whereStr) > 0 { a += " AND " + mssqlCondi @@ -1282,19 +1167,23 @@ func (statement *Statement) genSelectSQL(columnStr, condSQL string) (a string) { return } -func (statement *Statement) processIDParam() { +func (statement *Statement) processIDParam() error { if statement.idParam == nil { - return + return nil + } + + if len(statement.RefTable.PrimaryKeys) != len(*statement.idParam) { + return fmt.Errorf("ID condition is error, expect %d primarykeys, there are %d", + len(statement.RefTable.PrimaryKeys), + len(*statement.idParam), + ) } for i, col := range statement.RefTable.PKColumns() { var colName = statement.colName(col, statement.TableName()) - if i < len(*(statement.idParam)) { - statement.cond = statement.cond.And(builder.Eq{colName: (*(statement.idParam))[i]}) - } else { - statement.cond = statement.cond.And(builder.Eq{colName: ""}) - } + statement.cond = statement.cond.And(builder.Eq{colName: (*(statement.idParam))[i]}) } + return nil } func (statement *Statement) joinColumns(cols []*core.Column, includeTableName bool) string { @@ -1328,7 +1217,8 @@ func (statement *Statement) convertIDSQL(sqlStr string) string { top = fmt.Sprintf("TOP %d ", statement.LimitN) } - return fmt.Sprintf("SELECT %s%s FROM %v", top, colstrs, sqls[1]) + newsql := fmt.Sprintf("SELECT %s%s FROM %v", top, colstrs, sqls[1]) + return newsql } return "" } diff --git a/vendor/github.com/go-xorm/xorm/tag.go b/vendor/github.com/go-xorm/xorm/tag.go index 4b0e3f54a57..e1c821fb540 100644 --- a/vendor/github.com/go-xorm/xorm/tag.go +++ b/vendor/github.com/go-xorm/xorm/tag.go @@ -54,6 +54,7 @@ var ( "UNIQUE": UniqueTagHandler, "CACHE": CacheTagHandler, "NOCACHE": NoCacheTagHandler, + "COMMENT": CommentTagHandler, } ) @@ -192,6 +193,14 @@ func UniqueTagHandler(ctx *tagContext) error { return nil } +// CommentTagHandler add comment to column +func CommentTagHandler(ctx *tagContext) error { + if len(ctx.params) > 0 { + ctx.col.Comment = strings.Trim(ctx.params[0], "' ") + } + return nil +} + // SQLTypeTagHandler describes SQL Type tag handler func SQLTypeTagHandler(ctx *tagContext) error { ctx.col.SQLType = core.SQLType{Name: ctx.tagName} diff --git a/vendor/github.com/go-xorm/xorm/xorm.go b/vendor/github.com/go-xorm/xorm/xorm.go index 2cfbe9ecd31..4fdadf2fade 100644 --- a/vendor/github.com/go-xorm/xorm/xorm.go +++ b/vendor/github.com/go-xorm/xorm/xorm.go @@ -17,7 +17,7 @@ import ( const ( // Version show the xorm's version - Version string = "0.6.2.0326" + Version string = "0.6.4.0910" ) func regDrvsNDialects() bool { @@ -50,10 +50,13 @@ func close(engine *Engine) { engine.Close() } +func init() { + regDrvsNDialects() +} + // NewEngine new a db manager according to the parameter. Currently support four // drivers func NewEngine(driverName string, dataSourceName string) (*Engine, error) { - regDrvsNDialects() driver := core.QueryDriver(driverName) if driver == nil { return nil, fmt.Errorf("Unsupported driver name: %v", driverName) @@ -89,6 +92,12 @@ func NewEngine(driverName string, dataSourceName string) (*Engine, error) { tagHandlers: defaultTagHandlers, } + if uri.DbType == core.SQLITE { + engine.DatabaseTZ = time.UTC + } else { + engine.DatabaseTZ = time.Local + } + logger := NewSimpleLogger(os.Stdout) logger.SetLevel(core.LOG_INFO) engine.SetLogger(logger) From 5a3ba68a9cf0cbb5f9587287028fddbd83c7a9c6 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 16 Mar 2018 00:08:25 +0100 Subject: [PATCH 0090/3000] database: fixes after xorm update --- pkg/services/sqlstore/alert.go | 2 +- pkg/services/sqlstore/dashboard_folder_test.go | 1 + pkg/services/sqlstore/org_test.go | 3 +++ pkg/services/sqlstore/quota.go | 13 +++++++++---- pkg/services/sqlstore/quota_test.go | 4 ++-- pkg/services/sqlstore/sqlstore.go | 8 ++++++-- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index f449bec5849..e99367e4d6f 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -255,7 +255,7 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error { } alert.State = cmd.State - alert.StateChanges += 1 + alert.StateChanges++ alert.NewStateDate = timeNow() alert.EvalData = cmd.EvalData diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index ea8f1216706..4c92c097931 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -46,6 +46,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id}, } err := SearchDashboards(query) + So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) So(query.Result[0].Id, ShouldEqual, dashInRoot.Id) diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index c57d15a48d5..63b20aa6e86 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -2,6 +2,7 @@ package sqlstore import ( "testing" + "time" . "github.com/smartystreets/goconvey/convey" @@ -241,6 +242,8 @@ func TestAccountDataAccess(t *testing.T) { func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error { cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId} for _, item := range items { + item.Created = time.Now() + item.Updated = time.Now() cmd.Items = append(cmd.Items, &item) } return UpdateDashboardAcl(&cmd) diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 0a857efce40..3db3fc2657e 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -2,6 +2,7 @@ package sqlstore import ( "fmt" + "time" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" @@ -98,8 +99,9 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - OrgId: cmd.OrgId, + Target: cmd.Target, + OrgId: cmd.OrgId, + Updated: time.Now(), } has, err := sess.Get("a) if err != nil { @@ -107,6 +109,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { } quota.Limit = cmd.Limit if has == false { + quota.Created = time.Now() //No quota in the DB for this target, so create a new one. if _, err := sess.Insert("a); err != nil { return err @@ -198,8 +201,9 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - UserId: cmd.UserId, + Target: cmd.Target, + UserId: cmd.UserId, + Updated: time.Now(), } has, err := sess.Get("a) if err != nil { @@ -207,6 +211,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { } quota.Limit = cmd.Limit if has == false { + quota.Created = time.Now() //No quota in the DB for this target, so create a new one. if _, err := sess.Insert("a); err != nil { return err diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go index 5ef618e166d..ed6565b1c3f 100644 --- a/pkg/services/sqlstore/quota_test.go +++ b/pkg/services/sqlstore/quota_test.go @@ -104,12 +104,12 @@ func TestQuotaCommandsAndQueries(t *testing.T) { }) }) Convey("Given saved user quota for org", func() { - userQoutaCmd := m.UpdateUserQuotaCmd{ + userQuotaCmd := m.UpdateUserQuotaCmd{ UserId: userId, Target: "org_user", Limit: 10, } - err := UpdateUserQuota(&userQoutaCmd) + err := UpdateUserQuota(&userQuotaCmd) So(err, ShouldBeNil) Convey("Should be able to get saved quota by user id and target", func() { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 5843c5c300b..493243f5185 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" @@ -225,8 +226,8 @@ var ( func InitTestDB(t *testing.T) *xorm.Engine { selectedDb := dbSqlite - //selectedDb := dbMySql - //selectedDb := dbPostgres + // selectedDb := dbMySql + // selectedDb := dbPostgres var x *xorm.Engine var err error @@ -245,6 +246,9 @@ func InitTestDB(t *testing.T) *xorm.Engine { x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr) } + x.DatabaseTZ = time.UTC + x.TZLocation = time.UTC + // x.ShowSQL() if err != nil { From 9cdd7cb04c0114398f0d6f080c332ca968e44356 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 16 Mar 2018 00:25:15 +0100 Subject: [PATCH 0091/3000] database: expose SetConnMaxLifetime as config setting For MySQL, setting this to be shorter than the wait_timeout MySQL setting solves the issue with connection errors after the session has timed out for the connection to the database via xorm. --- conf/defaults.ini | 3 +++ conf/sample.ini | 3 +++ docs/sources/installation/configuration.md | 5 +++++ pkg/services/sqlstore/sqlstore.go | 26 +++++++++++++--------- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 4a2240f1924..557c5e49ee1 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -82,6 +82,9 @@ max_idle_conn = 2 # Max conn setting default is 0 (mean not set) max_open_conn = +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +conn_max_lifetime = 14400 + # Set to true to log the sql calls and execution times. log_queries = diff --git a/conf/sample.ini b/conf/sample.ini index 3e45ac44d61..fa30c301014 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -90,6 +90,9 @@ # Max conn setting default is 0 (mean not set) ;max_open_conn = +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +;conn_max_lifetime = 14400 + # Set to true to log the sql calls and execution times. log_queries = diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 66072a98f84..6169280b798 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -234,7 +234,12 @@ The maximum number of connections in the idle connection pool. ### max_open_conn The maximum number of open connections to the database. +### conn_max_lifetime + +Sets the maximum amount of time a connection may be reused. The default is 14400 (which means 14400 seconds or 4 hours). For MySQL, this setting should be shorter than the [`wait_timeout`](https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_wait_timeout) variable. + ### log_queries + Set to `true` to log the sql calls and execution times.
    diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 493243f5185..ae1e3fc482a 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -34,6 +34,7 @@ type DatabaseConfig struct { ServerCertName string MaxOpenConn int MaxIdleConn int + ConnMaxLifetime int } var ( @@ -158,18 +159,20 @@ func getEngine() (*xorm.Engine, error) { engine, err := xorm.NewEngine(DbCfg.Type, cnnstr) if err != nil { return nil, err - } else { - engine.SetMaxOpenConns(DbCfg.MaxOpenConn) - engine.SetMaxIdleConns(DbCfg.MaxIdleConn) - debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false) - if !debugSql { - engine.SetLogger(&xorm.DiscardLogger{}) - } else { - engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) - engine.ShowSQL(true) - engine.ShowExecTime(true) - } } + + engine.SetMaxOpenConns(DbCfg.MaxOpenConn) + engine.SetMaxIdleConns(DbCfg.MaxIdleConn) + engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime)) + debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false) + if !debugSql { + engine.SetLogger(&xorm.DiscardLogger{}) + } else { + engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) + engine.ShowSQL(true) + engine.ShowExecTime(true) + } + return engine, nil } @@ -203,6 +206,7 @@ func LoadConfig() { } DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0) DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0) + DbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400) if DbCfg.Type == "sqlite3" { UseSQLite3 = true From 3ca1e06509eec9ea3fd3781d8ffb7264926dce4f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 15 Mar 2018 21:23:33 +0100 Subject: [PATCH 0092/3000] session: fork Macaron mysql session middleware This changes forks the mysql part of the Macaron session middleware. In the forked mysql file: - takes in a config setting for SetConnMaxLifetime (this solves wait_timeout problem if it is set to a shorter interval than wait_timeout) - removes the panic when an error is returned in the Exist function. - retries the exist query once - retries the GC query once --- pkg/api/common_test.go | 2 +- pkg/api/http_server.go | 2 +- pkg/middleware/middleware_test.go | 2 +- pkg/middleware/recovery_test.go | 2 +- pkg/middleware/session.go | 4 +- pkg/services/session/mysql.go | 218 ++++++++++++++++++++++++++++++ pkg/services/session/session.go | 5 +- pkg/setting/setting.go | 5 +- 8 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 pkg/services/session/mysql.go diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index e1cbd20edb3..a4a547d8bbf 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -99,7 +99,7 @@ func setupScenarioContext(url string) *scenarioContext { })) sc.m.Use(middleware.GetContextHandler()) - sc.m.Use(middleware.Sessioner(&session.Options{})) + sc.m.Use(middleware.Sessioner(&session.Options{}, 0)) return sc } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index b911780913d..c6a9286a5d8 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -175,7 +175,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron { m.Use(hs.healthHandler) m.Use(hs.metricsEndpoint) m.Use(middleware.GetContextHandler()) - m.Use(middleware.Sessioner(&setting.SessionOptions)) + m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime)) m.Use(middleware.OrgRedirect()) // needs to be after context handler diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 83efc65d4d4..c8e9e535cfa 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -338,7 +338,7 @@ func middlewareScenario(desc string, fn scenarioFunc) { sc.m.Use(GetContextHandler()) // mock out gc goroutine session.StartSessionGC = func() {} - sc.m.Use(Sessioner(&ms.Options{})) + sc.m.Use(Sessioner(&ms.Options{}, 0)) sc.m.Use(OrgRedirect()) sc.m.Use(AddDefaultResponseHeaders()) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 32545b7caca..c63a0e81e57 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -63,7 +63,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) { sc.m.Use(GetContextHandler()) // mock out gc goroutine session.StartSessionGC = func() {} - sc.m.Use(Sessioner(&ms.Options{})) + sc.m.Use(Sessioner(&ms.Options{}, 0)) sc.m.Use(OrgRedirect()) sc.m.Use(AddDefaultResponseHeaders()) diff --git a/pkg/middleware/session.go b/pkg/middleware/session.go index 5654a42cb7d..19cfa368b49 100644 --- a/pkg/middleware/session.go +++ b/pkg/middleware/session.go @@ -8,8 +8,8 @@ import ( "github.com/grafana/grafana/pkg/services/session" ) -func Sessioner(options *ms.Options) macaron.Handler { - session.Init(options) +func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler { + session.Init(options, sessionConnMaxLifetime) return func(ctx *m.ReqContext) { ctx.Next() diff --git a/pkg/services/session/mysql.go b/pkg/services/session/mysql.go new file mode 100644 index 00000000000..f8c5d828cfa --- /dev/null +++ b/pkg/services/session/mysql.go @@ -0,0 +1,218 @@ +// Copyright 2013 Beego Authors +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "database/sql" + "fmt" + "log" + "sync" + "time" + + _ "github.com/go-sql-driver/mysql" + + "github.com/go-macaron/session" +) + +// MysqlStore represents a mysql session store implementation. +type MysqlStore struct { + c *sql.DB + sid string + lock sync.RWMutex + data map[interface{}]interface{} +} + +// NewMysqlStore creates and returns a mysql session store. +func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *MysqlStore { + return &MysqlStore{ + c: c, + sid: sid, + data: kv, + } +} + +// Set sets value to given key in session. +func (s *MysqlStore) Set(key, val interface{}) error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data[key] = val + return nil +} + +// Get gets value by given key in session. +func (s *MysqlStore) Get(key interface{}) interface{} { + s.lock.RLock() + defer s.lock.RUnlock() + + return s.data[key] +} + +// Delete delete a key from session. +func (s *MysqlStore) Delete(key interface{}) error { + s.lock.Lock() + defer s.lock.Unlock() + + delete(s.data, key) + return nil +} + +// ID returns current session ID. +func (s *MysqlStore) ID() string { + return s.sid +} + +// Release releases resource and save data to provider. +func (s *MysqlStore) Release() error { + data, err := session.EncodeGob(s.data) + if err != nil { + return err + } + + _, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?", + data, time.Now().Unix(), s.sid) + return err +} + +// Flush deletes all session data. +func (s *MysqlStore) Flush() error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data = make(map[interface{}]interface{}) + return nil +} + +// MysqlProvider represents a mysql session provider implementation. +type MysqlProvider struct { + c *sql.DB + expire int64 +} + +// Init initializes mysql session provider. +// connStr: username:password@protocol(address)/dbname?param=value +func (p *MysqlProvider) Init(expire int64, connStr string) (err error) { + p.expire = expire + + p.c, err = sql.Open("mysql", connStr) + p.c.SetConnMaxLifetime(time.Second * time.Duration(sessionConnMaxLifetime)) + if err != nil { + return err + } + return p.c.Ping() +} + +// Read returns raw session store by session ID. +func (p *MysqlProvider) Read(sid string) (session.RawStore, error) { + var data []byte + err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) + if err == sql.ErrNoRows { + _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", + sid, "", time.Now().Unix()) + } + if err != nil { + return nil, err + } + + var kv map[interface{}]interface{} + if len(data) == 0 { + kv = make(map[interface{}]interface{}) + } else { + kv, err = session.DecodeGob(data) + if err != nil { + return nil, err + } + } + + return NewMysqlStore(p.c, sid, kv), nil +} + +// Exist returns true if session with given ID exists. +func (p *MysqlProvider) Exist(sid string) bool { + exists, err := p.queryExists(sid) + + if err != nil { + exists, err = p.queryExists(sid) + } + + if err != nil { + log.Printf("session/mysql: error checking if session exists: %v", err) + return false + } + + return exists +} + +func (p *MysqlProvider) queryExists(sid string) (bool, error) { + var data []byte + err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) + + if err != nil && err != sql.ErrNoRows { + return false, err + } + + return err != sql.ErrNoRows, nil +} + +// Destory deletes a session by session ID. +func (p *MysqlProvider) Destory(sid string) error { + _, err := p.c.Exec("DELETE FROM session WHERE `key`=?", sid) + return err +} + +// Regenerate regenerates a session store from old session ID to new one. +func (p *MysqlProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { + if p.Exist(sid) { + return nil, fmt.Errorf("new sid '%s' already exists", sid) + } + + if !p.Exist(oldsid) { + if _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", + oldsid, "", time.Now().Unix()); err != nil { + return nil, err + } + } + + if _, err = p.c.Exec("UPDATE session SET `key`=? WHERE `key`=?", sid, oldsid); err != nil { + return nil, err + } + + return p.Read(sid) +} + +// Count counts and returns number of sessions. +func (p *MysqlProvider) Count() (total int) { + if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil { + panic("session/mysql: error counting records: " + err.Error()) + } + return total +} + +// GC calls GC to clean expired sessions. +func (p *MysqlProvider) GC() { + var err error + if _, err = p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil { + _, err = p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire) + } + + if err != nil { + log.Printf("session/mysql: error garbage collecting: %v", err) + } +} + +func init() { + session.Register("mysql", &MysqlProvider{}) +} diff --git a/pkg/services/session/session.go b/pkg/services/session/session.go index 2ca9296b97f..0161b2113c8 100644 --- a/pkg/services/session/session.go +++ b/pkg/services/session/session.go @@ -6,7 +6,6 @@ import ( ms "github.com/go-macaron/session" _ "github.com/go-macaron/session/memcache" - _ "github.com/go-macaron/session/mysql" _ "github.com/go-macaron/session/postgres" _ "github.com/go-macaron/session/redis" "github.com/grafana/grafana/pkg/log" @@ -25,6 +24,7 @@ var sessionOptions *ms.Options var StartSessionGC func() var GetSessionCount func() int var sessionLogger = log.New("session") +var sessionConnMaxLifetime int64 func init() { StartSessionGC = func() { @@ -37,9 +37,10 @@ func init() { } } -func Init(options *ms.Options) { +func Init(options *ms.Options, connMaxLifetime int64) { var err error sessionOptions = prepareOptions(options) + sessionConnMaxLifetime = connMaxLifetime sessionManager, err = ms.NewManager(options.Provider, *options) if err != nil { panic(err) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6099388f668..c19043c69d0 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -131,7 +131,8 @@ var ( PluginAppsSkipVerifyTLS bool // Session settings. - SessionOptions session.Options + SessionOptions session.Options + SessionConnMaxLifetime int64 // Global setting objects. Cfg *ini.File @@ -634,6 +635,8 @@ func readSessionConfig() { if SessionOptions.CookiePath == "" { SessionOptions.CookiePath = "/" } + + SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(0) } func initLogging() { From fc9014f9204df635577c4bc0cb57a0bfdb21e25b Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 16 Mar 2018 12:36:36 +0100 Subject: [PATCH 0093/3000] added indent to dashboards inside folder in search dropdown, and added indent to dashboard icon in search item --- public/app/core/components/search/search_results.html | 2 +- public/sass/components/_search.scss | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 4e5bc88e0a9..7435f8d0b7e 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -20,7 +20,7 @@
    - +
    Date: Mon, 19 Mar 2018 11:06:10 +0100 Subject: [PATCH 0094/3000] docs: Using Microsoft SQL Server in Grafana --- docs/sources/features/datasources/index.md | 1 + docs/sources/features/datasources/mssql.md | 229 +++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 docs/sources/features/datasources/mssql.md diff --git a/docs/sources/features/datasources/index.md b/docs/sources/features/datasources/index.md index 54606d20988..a892f38a448 100644 --- a/docs/sources/features/datasources/index.md +++ b/docs/sources/features/datasources/index.md @@ -30,6 +30,7 @@ The following datasources are officially supported: * [Prometheus]({{< relref "prometheus.md" >}}) * [MySQL]({{< relref "mysql.md" >}}) * [Postgres]({{< relref "postgres.md" >}}) +* [Microsoft SQL Server (MSSQL)]({{< relref "mssql.md" >}}) ## Data source plugins diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md new file mode 100644 index 00000000000..2bcfd9dc59c --- /dev/null +++ b/docs/sources/features/datasources/mssql.md @@ -0,0 +1,229 @@ ++++ +title = "Using Microsoft SQL Server in Grafana" +description = "Guide for using Microsoft SQL Server in Grafana" +keywords = ["grafana", "MSSQL", "Microsoft", "SQL", "guide"] +type = "docs" +[menu.docs] +name = "Microsoft SQL Server" +parent = "datasources" +weight = 7 ++++ + +# Using Microsoft SQL Server in Grafana + +Grafana ships with a built-in Microsoft SQL Server (MSSQL) data source plugin that allows you to query and visualize data from any Microsoft SQL Server 2005 or newer. + +## Adding the data source + +1. Open the side menu by clicking the Grafana icon in the top header. +2. In the side menu under the `Configuration` link you should find a link named `Data Sources`. +3. Click the `+ Add data source` button in the top header. +4. Select *Microsoft SQL Server* from the *Type* dropdown. + +### Database User Permissions (Important!) + +The database user you specify when you add the data source should only be granted SELECT permissions on +the specified database & tables you want to query. Grafana does not validate that the query is safe. The query +could include any SQL statement. For example, statements like `DELETE FROM user;` and `DROP TABLE user;` would be +executed. To protect against this we **Highly** recommmend you create a specific MSSQL user with restricted permissions. + +Example: + +```sql + CREATE USER grafanareader WITH PASSWORD 'password' + GRANT SELECT ON dbo.YourTable3 TO grafanareader +``` + +Make sure the user does not get any unwanted privileges from the public role. + +## Macros + +To simplify syntax and to allow for dynamic parts, like date range filters, the query can contain macros. + +Macro example | Description +------------ | ------------- +*$__time(dateColumn)* | Will be replaced by an expression to rename the column to `time`. For example, *`dateColumn as time`* +*$__utcTime(dateColumn)* | Will be replaced by an expression to convert a DATETIME column type to UTC depending on the server's local timeoffset and rename it to `time`. For example, *`DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), dateColumn) ) AS time`* +*$__timeEpoch(dateColumn)* | Will be replaced by an expression to convert a DATETIME column type to unix timestamp and rename it to `time`. For example, *`DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), dateColumn) ) AS time`* +*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *`dateColumn >= DATEADD(s, 1494410783+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND dateColumn <= DATEADD(s, 1494497183+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')`* +*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *`DATEADD(second, 1494410783+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')`* +*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *`DATEADD(second, 1494497183+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')`* +*$__timeGroup(dateColumn,'5m', NULL)* | Will be replaced by an expression usable in GROUP BY clause. For example, *`cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), dateColumns))/300 as int)*300 as int)`* +*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *`dateColumn > 1494410783 AND dateColumn < 1494497183`* +*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *`1494410783`* +*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *`1494497183`* + +We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. + +The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed. + +## Table queries + +If the `Format as` query option is set to `Table` then you can basically do any type of SQL query. The table panel will automatically show the results of whatever columns & rows your query returns. + +Query editor with example query: + +![](/img/docs/v47/mssql_table_query.png) + + +The query: + +```sql +SELECT COLUMN_NAME AS [Name], + DATA_TYPE AS [Type], + CHARACTER_OCTET_LENGTH AS [Length], + NUMERIC_PRECISION as [Precisopn], + NUMERIC_PRECISION_RADIX AS [Radix], + NUMERIC_SCALE AS [Scale] +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_NAME = 'mssql_types'; +``` + +You can control the name of the Table panel columns by using regular `AS ` SQL column selection syntax. + +The resulting table panel: + +![](/img/docs/v47/mssql_table.png) + +### Time series queries + +If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you ommit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. If you select multiple value columns along with a `metric` column, the names ("MetircName - ColumnName") will be combined to make the metric name. + +Example with `metric` column + +```sql +SELECT + [time_date_time] as [time], + [value_double] as [value], + [metric1] as [metric] +FROM [test_data] +WHERE $__timeFilter([time_date_time]) +ORDER BY [time_date_time] +``` + +Example with multiple `value` culumns + +```sql +SELECT + [time_date_time] as [time], + [value_double1] as [metric_name1], + [value_int2] as [metric_name2] +FROM [test_data] +WHERE $__timeFilter([time_date_time]) +ORDER BY [time_date_time] +``` + +Example with multiple `value` culumns combined with a `metric` column + +```sql +SELECT + [time_date_time] as [time], + [value_double1] as [value1], + [value_int2] as [value2], + [metric_col] as [metric] +FROM [test_data] +WHERE $__timeFilter([time_date_time]) +ORDER BY [time_date_time] +``` +The result of the above query would look something like the below + +![](/img/docs/v47/mssql_metric_value.png) + +Currently, there is no support for a dynamic group by time based on time range & panel width. +This is something we plan to add. + +## Templating + +Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns makes it easy to change the data being displayed in your dashboard. + +Checkout the [Templating]({{< relref "reference/templating.md" >}}) documentation for an introduction to the templating feature and the different types of template variables. + +### Query Variable + +If you add a template variable of the type `Query`, you can write a MSSQL query that can +return things like measurement names, key names or key values that are shown as a dropdown select box. + +For example, you can have a variable that contains all values for the `hostname` column in a table if you specify a query like this in the templating variable *Query* setting. + +```sql +SELECT hostname FROM host +``` + +A query can return multiple columns and Grafana will automatically create a list from them. For example, the query below will return a list with values from `hostname` and `hostname2`. + +```sql +SELECT [host].[hostname], [other_host].[hostname2] FROM host JOIN other_host ON [host].[city] = [other_host].[city] +``` + +Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value: + +```sql +SELECT hostname __text, id __value FROM host +``` + +You can also create nested variables. For example if you had another variable named `region`. Then you could have +the hosts variable only show hosts from the current selected region with a query like this (if `region` is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values): + +```sql +SELECT hostname FROM host WHERE region IN ($region) +``` + +### Using Variables in Queries + +From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses. + +From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`. + +If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. + +There are two syntaxes: + +`$` Example with a template variable named `hostname`: + +```sql +SELECT + atimestamp time, + aint value +FROM table +WHERE $__timeFilter(atimestamp) and hostname in($hostname) +ORDER BY atimestamp +``` + +`[[varname]]` Example with a template variable named `hostname`: + +```sql +SELECT + atimestamp as time, + aint as value +FROM table +WHERE $__timeFilter(atimestamp) and hostname in([[hostname]]) +ORDER BY atimestamp +``` + +## Annotations + +[Annotations]({{< relref "reference/annotations.md" >}}) allows you to overlay rich event information on top of graphs. You add annotation queries via the Dashboard menu / Annotations view. + +An example query: + +```sql +SELECT + DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) as [time], + metric1 as [text], + convert(varvhar, metric1) + ',' + convert(varchar, metric2) as [tags] +FROM + test_data +WHERE + $__timeFilter(time_column) +``` + +Name | Description +------------ | ------------- +time | The name of the date/time field. could be in a native sql time datatype +text | Event description field. +tags | Optional field name to use for event tags as a comma separated string. + +## Alerting + +Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule +conditions. From 74c3f732c15df7e59a587f12f98bdce2af3bb505 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Mar 2018 22:08:10 +0100 Subject: [PATCH 0095/3000] docs: update using mssql in grafana --- docs/sources/features/datasources/mssql.md | 430 +++++++++++++++++---- 1 file changed, 363 insertions(+), 67 deletions(-) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index 2bcfd9dc59c..325e1fe3596 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -11,6 +11,8 @@ weight = 7 # Using Microsoft SQL Server in Grafana +> Only available in Grafana v5.1+. + Grafana ships with a built-in Microsoft SQL Server (MSSQL) data source plugin that allows you to query and visualize data from any Microsoft SQL Server 2005 or newer. ## Adding the data source @@ -20,6 +22,17 @@ Grafana ships with a built-in Microsoft SQL Server (MSSQL) data source plugin th 3. Click the `+ Add data source` button in the top header. 4. Select *Microsoft SQL Server* from the *Type* dropdown. +### Data source options + +Name | Description +------------ | ------------- +*Name* | The data source name. This is how you refer to the data source in panels & queries. +*Default* | Default data source means that it will be pre-selected for new panels. +*Host* | The IP address/hostname and optional port of your MSSQL instance. If port is omitted, default 1443 will be used. +*Database* | Name of your MSSQL database. +*User* | Database user's login/username +*Password* | Database user's password + ### Database User Permissions (Important!) The database user you specify when you add the data source should only be granted SELECT permissions on @@ -36,101 +49,213 @@ Example: Make sure the user does not get any unwanted privileges from the public role. +## Query Editor +{{< docs-imagebox img="/img/docs/v51/mssql_query_editor.png" class="docs-image--no-shadow" >}} + +You find the MSSQL query editor in the metrics tab in Graph, Singlestat or Table panel's edit mode. You enter edit mode by clicking the +panel title, then edit. The editor allows you to define a SQL query to select data to be visualized. + +1. Select *Format as* `Time series` (for use in Graph or Singlestat panel's among others) or `Table` (for use in Table panel among others). +2. This is the actual editor where you write your SQL queries. +3. Show help section for MSSQL below the query editor. +4. Show actual executed SQL query. Will be available first after a successful query has been executed. +5. Add an additional query where an additional query editor will be displayed. + +
    + ## Macros To simplify syntax and to allow for dynamic parts, like date range filters, the query can contain macros. Macro example | Description ------------ | ------------- -*$__time(dateColumn)* | Will be replaced by an expression to rename the column to `time`. For example, *`dateColumn as time`* -*$__utcTime(dateColumn)* | Will be replaced by an expression to convert a DATETIME column type to UTC depending on the server's local timeoffset and rename it to `time`. For example, *`DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), dateColumn) ) AS time`* -*$__timeEpoch(dateColumn)* | Will be replaced by an expression to convert a DATETIME column type to unix timestamp and rename it to `time`. For example, *`DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), dateColumn) ) AS time`* -*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *`dateColumn >= DATEADD(s, 1494410783+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND dateColumn <= DATEADD(s, 1494497183+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')`* -*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *`DATEADD(second, 1494410783+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')`* -*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *`DATEADD(second, 1494497183+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')`* -*$__timeGroup(dateColumn,'5m', NULL)* | Will be replaced by an expression usable in GROUP BY clause. For example, *`cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), dateColumns))/300 as int)*300 as int)`* -*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *`dateColumn > 1494410783 AND dateColumn < 1494497183`* -*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *`1494410783`* -*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *`1494497183`* +*$__time(dateColumn)* | Will be replaced by an expression to rename the column to *time*. For example, *dateColumn as time* +*$__utcTime(dateColumn)* | Will be replaced by an expression to convert a DATETIME column type to UTC depending on the server's local timeoffset and rename it to *time*.
    For example, *DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), dateColumn) ) AS time* +*$__timeEpoch(dateColumn)* | Will be replaced by an expression to convert a DATETIME column type to unix timestamp and rename it to *time*.
    For example, *DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), dateColumn) ) AS time* +*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name.
    For example, *dateColumn >= DATEADD(s, 1494410783+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND dateColumn <= DATEADD(s, 1494497183+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')* +*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *DATEADD(second, 1494410783+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')* +*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *DATEADD(second, 1494497183+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')* +*$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value.
    For example, *cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column))/300 as int)*300 as int)*. +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* +*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* +*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed. ## Table queries - If the `Format as` query option is set to `Table` then you can basically do any type of SQL query. The table panel will automatically show the results of whatever columns & rows your query returns. +**Example database table:** + +```sql +CREATE TABLE [event] ( + time_sec bigint, + description nvarchar(100), + tags nvarchar(100), +) +``` + +```sql +CREATE TABLE [mssql_types] ( + c_bit bit, c_tinyint tinyint, c_smallint smallint, c_int int, c_bigint bigint, c_money money, c_smallmoney smallmoney, c_numeric numeric(10,5), + c_real real, c_decimal decimal(10,2), c_float float, + c_char char(10), c_varchar varchar(10), c_text text, + c_nchar nchar(12), c_nvarchar nvarchar(12), c_ntext ntext, + c_datetime datetime, c_datetime2 datetime2, c_smalldatetime smalldatetime, c_date date, c_time time, c_datetimeoffset datetimeoffset +) + +INSERT INTO [mssql_types] +SELECT + 1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12, + 1.11, 2.22, 3.33, + 'char10', 'varchar10', 'text', + N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺', + GETDATE(), CAST(GETDATE() AS DATETIME2), CAST(GETDATE() AS SMALLDATETIME), CAST(GETDATE() AS DATE), CAST(GETDATE() AS TIME), SWITCHOFFSET(CAST(GETDATE() AS DATETIMEOFFSET), '-07:00')) +``` + Query editor with example query: -![](/img/docs/v47/mssql_table_query.png) +{{< docs-imagebox img="/img/docs/v51/mssql_table_query.png" max-width="500px" class="docs-image--no-shadow" >}} The query: ```sql -SELECT COLUMN_NAME AS [Name], - DATA_TYPE AS [Type], - CHARACTER_OCTET_LENGTH AS [Length], - NUMERIC_PRECISION as [Precisopn], - NUMERIC_PRECISION_RADIX AS [Radix], - NUMERIC_SCALE AS [Scale] -FROM INFORMATION_SCHEMA.COLUMNS -WHERE TABLE_NAME = 'mssql_types'; +SELECT * FROM [mssql_types] ``` -You can control the name of the Table panel columns by using regular `AS ` SQL column selection syntax. +You can control the name of the Table panel columns by using regular `AS ` SQL column selection syntax. Example: + +```sql +SELECT + c_bit as [column1], c_tinyint as [column2] +FROM + [mssql_types] +``` The resulting table panel: -![](/img/docs/v47/mssql_table.png) +{{< docs-imagebox img="/img/docs/v51/mssql_table_result.png" max-width="1489px" class="docs-image--no-shadow" >}} -### Time series queries +## Time series queries -If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you ommit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. If you select multiple value columns along with a `metric` column, the names ("MetircName - ColumnName") will be combined to make the metric name. +If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you ommit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. -Example with `metric` column +**Example database table:** + +```sql +CREATE TABLE [event] ( + time_sec bigint, + description nvarchar(100), + tags nvarchar(100), +) +``` + + +```sql +CREATE TABLE metric_values ( + time datetime, + measurement nvarchar(100), + valueOne int, + valueTwo int, +) + +INSERT metric_values (time, measurement, valueOne, valueTwo) VALUES('2018-03-15 12:30:00', 'Metric A', 62, 6) +INSERT metric_values (time, measurement, valueOne, valueTwo) VALUES('2018-03-15 12:30:00', 'Metric B', 49, 11) +... +INSERT metric_values (time, measurement, valueOne, valueTwo) VALUES('2018-03-15 13:55:00', 'Metric A', 14, 25) +INSERT metric_values (time, measurement, valueOne, valueTwo) VALUES('2018-03-15 13:55:00', 'Metric B', 48, 10) + +``` + +{{< docs-imagebox img="/img/docs/v51/mssql_time_series_one.png" class="docs-image--no-shadow docs-image--right" >}} + +**Example with one `value` and one `metric` column.** ```sql SELECT - [time_date_time] as [time], - [value_double] as [value], - [metric1] as [metric] -FROM [test_data] -WHERE $__timeFilter([time_date_time]) -ORDER BY [time_date_time] + time, + valueOne, + measurement as metric +FROM + metric_values +WHERE + $__timeFilter(time) +ORDER BY 1 ``` -Example with multiple `value` culumns +When above query are used in a graph panel the result will be two series named `Metric A` and `Metric B` with value of `valueOne` and `valueTwo` plotted over `time`. + +
    + +{{< docs-imagebox img="/img/docs/v51/mssql_time_series_two.png" class="docs-image--no-shadow docs-image--right" >}} + +**Example with multiple `value` culumns:** ```sql SELECT - [time_date_time] as [time], - [value_double1] as [metric_name1], - [value_int2] as [metric_name2] -FROM [test_data] -WHERE $__timeFilter([time_date_time]) -ORDER BY [time_date_time] + time, + valueOne, + valueTwo +FROM + metric_values +WHERE + $__timeFilter(time) +ORDER BY 1 ``` -Example with multiple `value` culumns combined with a `metric` column +When above query are used in a graph panel the result will be two series named `valueOne` and `valueTwo` with value of `valueOne` and `valueTwo` plotted over `time`. + +
    + +{{< docs-imagebox img="/img/docs/v51/mssql_time_series_three.png" class="docs-image--no-shadow docs-image--right" >}} + +**Example using the $__timeGroup macro:** ```sql SELECT - [time_date_time] as [time], - [value_double1] as [value1], - [value_int2] as [value2], - [metric_col] as [metric] -FROM [test_data] -WHERE $__timeFilter([time_date_time]) -ORDER BY [time_date_time] + $__timeGroup(time, '3m') as time, + measurement as metric, + avg(valueOne) +FROM + metric_values +WHERE + $__timeFilter(time) +GROUP BY + $__timeGroup(time, '3m'), + measurement +ORDER BY 1 ``` -The result of the above query would look something like the below -![](/img/docs/v47/mssql_metric_value.png) +When above query are used in a graph panel the result will be two series named `Metric A` and `Metric B` with an average of `valueOne` plotted over `time`. +Any two series lacking a value in a 3 minute window will render a line between those two lines. You'll notice that the graph to the right never goes down to zero. -Currently, there is no support for a dynamic group by time based on time range & panel width. -This is something we plan to add. +
    + +{{< docs-imagebox img="/img/docs/v51/mssql_time_series_four.png" class="docs-image--no-shadow docs-image--right" >}} + +**Example using the $__timeGroup macro with fill parameter set to zero:** + +```sql +SELECT + $__timeGroup(time, '3m', 0) as time, + measurement as metric, + sum(valueTwo) +FROM + metric_values +WHERE + $__timeFilter(time) +GROUP BY + $__timeGroup(time, '3m'), + measurement +ORDER BY 1 +``` + +When above query are used in a graph panel the result will be two series named `Metric A` and `Metric B` with a sum of `valueTwo` plotted over `time`. +Any series lacking a value in a 3 minute window will have a value of zero which you'll see rendered in the graph to the right. ## Templating @@ -169,10 +294,9 @@ SELECT hostname FROM host WHERE region IN ($region) ``` ### Using Variables in Queries - -From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses. - -From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`. +> From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses. +> +> From Grafana 5.0.0, template variable values are only quoted when the template variable is a `multi-value`. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. @@ -204,25 +328,197 @@ ORDER BY atimestamp [Annotations]({{< relref "reference/annotations.md" >}}) allows you to overlay rich event information on top of graphs. You add annotation queries via the Dashboard menu / Annotations view. -An example query: - -```sql -SELECT - DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) as [time], - metric1 as [text], - convert(varvhar, metric1) + ',' + convert(varchar, metric2) as [tags] -FROM - test_data -WHERE - $__timeFilter(time_column) -``` +**Columns:** Name | Description ------------ | ------------- -time | The name of the date/time field. could be in a native sql time datatype +time | The name of the date/time field. Could be in a native sql time datatype or epoch seconds. text | Event description field. tags | Optional field name to use for event tags as a comma separated string. +**Example database tables:** + +```sql +CREATE TABLE [events] ( + time_sec bigint, + description nvarchar(100), + tags nvarchar(100), +) +``` + +We also use the database table defined in [Time series queries](#time-series-queries). + +**Example query using time column of type epoch seconds:** + +```sql +SELECT + time_sec as time, + description as [text], + tags +FROM + [events] +WHERE + $__unixEpochFilter(time_sec) +ORDER BY 1 +``` + +**Example query using time column of type datetime:** + +```sql +SELECT + time, + measurement as text, + convert(varchar, valueOne) + ',' + convert(varchar, valueTwo) as tags +FROM + metric_values +WHERE + $__timeFilter(time_column) +ORDER BY 1 +``` + +## Stored procedure support +Stored procedures have been verified to work. However, please note that we haven't done anything special to support this why there may exist edge cases where it won't work as you would expect. +Stored procedures should be supported in table, time series and annotation queries as long as you use the same naming of columns and return data in the same format as describe above under respective section. + +Please note that any macro function will not work inside a stored procedure. + +### Examples +{{< docs-imagebox img="/img/docs/v51/mssql_metrics_graph.png" class="docs-image--no-shadow docs-image--right" >}} +For the following examples the database table defined in [Time series queries](#time-series-queries). Let's say that we want to visualize 4 series in a graph panel, i.e. all combinations of columns `valueOne`, `valueTwo` and `measurement`. Graph panel to the right visualizes what we want to achieve. To solve this we actually need to use two queries: + +**First query:** +```sql +SELECT + $__timeGroup(time, '5m') as time, + measurement + ' - value one' as metric, + avg(valueOne) as valueOne +FROM + metric_values +WHERE + $__timeFilter(time) +GROUP BY + $__timeGroup(time, '5m'), + measurement +ORDER BY 1 +``` + +**Second query:** +```sql +SELECT + $__timeGroup(time, '5m') as time, + measurement + ' - value two' as metric, + avg(valueTwo) as valueTwo +FROM + metric_values +GROUP BY + $__timeGroup(time, '5m'), + measurement +ORDER BY 1 +``` + +#### Stored procedure using time in epoch format +We can define a stored procedure that will return all data we need to render 4 series in a graph panel like above. +In this case the stored procedure accepts two parameters `@from` and `@to` of `int` data types which should be a timerange (from-to) in epoch format +which will be used to filter the data to return from the stored procedure. + +We're mimicking the `$__timeGroup(time, '5m')` in the select and group by expressions and that's why there's a lot of lengthy expressions needed - +these could be extracted to MSSQL functions, if wanted. + +```sql +CREATE PROCEDURE sp_test_epoch( + @from int, + @to int +) AS +BEGIN + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + measurement + ' - value one' as metric, + avg(valueOne) as value + FROM + metric_values + WHERE + time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + measurement + UNION ALL + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + measurement + ' - value two' as metric, + avg(valueTwo) as value + FROM + metric_values + WHERE + time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + measurement + ORDER BY 1 +END +``` + +Then we can use the following query for our graph panel. + +```sql +DECLARE + @from int = $__unixEpochFrom(), + @to int = $__unixEpochTo() + +EXEC dbo.sp_test_epoch @from, @to +``` + +#### Stored procedure using time in datetime format +We can define a stored procedure that will return all data we need to render 4 series in a graph panel like above. +In this case the stored procedure accepts two parameters `@from` and `@to` of `datetime` data types which should be a timerange (from-to) +which will be used to filter the data to return from the stored procedure. + +We're mimicking the `$__timeGroup(time, '5m')` in the select and group by expressions and that's why there's a lot of lengthy expressions needed - +these could be extracted to MSSQL functions, if wanted. + +```sql +CREATE PROCEDURE sp_test_datetime( + @from datetime, + @to datetime +) AS +BEGIN + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + measurement + ' - value one' as metric, + avg(valueOne) as value + FROM + metric_values + WHERE + time >= @from AND time <= @to + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + measurement + UNION ALL + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + measurement + ' - value two' as metric, + avg(valueTwo) as value + FROM + metric_values + WHERE + time >= @from AND time <= @to + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + measurement + ORDER BY 1 +END + +``` + +Then we can use the following query for our graph panel. + +```sql +DECLARE + @from datetime = $__timeFrom(), + @to datetime = $__timeTo() + +EXEC dbo.sp_test_datetime @from, @to +``` + ## Alerting Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule From 2a50bc35a3916dd3778c221ed88563d287672ad9 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 20 Mar 2018 11:07:31 +0100 Subject: [PATCH 0096/3000] converted file to ts --- .../datasource/graphite/add_graphite_func.js | 155 ----------------- .../datasource/graphite/add_graphite_func.ts | 159 ++++++++++++++++++ 2 files changed, 159 insertions(+), 155 deletions(-) delete mode 100644 public/app/plugins/datasource/graphite/add_graphite_func.js create mode 100644 public/app/plugins/datasource/graphite/add_graphite_func.ts diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.js b/public/app/plugins/datasource/graphite/add_graphite_func.js deleted file mode 100644 index 1d72c2c11eb..00000000000 --- a/public/app/plugins/datasource/graphite/add_graphite_func.js +++ /dev/null @@ -1,155 +0,0 @@ -define(['angular', 'lodash', 'jquery', 'rst2html', 'tether-drop'], function(angular, _, $, rst2html, Drop) { - 'use strict'; - - angular.module('grafana.directives').directive('graphiteAddFunc', function($compile) { - var inputTemplate = - ''; - - var buttonTemplate = - '
    ' + - ''; - - return { - link: function($scope, elem) { - var ctrl = $scope.ctrl; - - var $input = $(inputTemplate); - var $button = $(buttonTemplate); - - $input.appendTo(elem); - $button.appendTo(elem); - - ctrl.datasource.getFuncDefs().then(function(funcDefs) { - var allFunctions = _.map(funcDefs, 'name').sort(); - - $scope.functionMenu = createFunctionDropDownMenu(funcDefs); - - $input.attr('data-provide', 'typeahead'); - $input.typeahead({ - source: allFunctions, - minLength: 1, - items: 10, - updater: function(value) { - var funcDef = ctrl.datasource.getFuncDef(value); - if (!funcDef) { - // try find close match - value = value.toLowerCase(); - funcDef = _.find(allFunctions, function(funcName) { - return funcName.toLowerCase().indexOf(value) === 0; - }); - - if (!funcDef) { - return; - } - } - - $scope.$apply(function() { - ctrl.addFunction(funcDef); - }); - - $input.trigger('blur'); - return ''; - }, - }); - - $button.click(function() { - $button.hide(); - $input.show(); - $input.focus(); - }); - - $input.keyup(function() { - elem.toggleClass('open', $input.val() === ''); - }); - - $input.blur(function() { - // clicking the function dropdown menu wont - // work if you remove class at once - setTimeout(function() { - $input.val(''); - $input.hide(); - $button.show(); - elem.removeClass('open'); - }, 200); - }); - - $compile(elem.contents())($scope); - }); - - var drop; - var cleanUpDrop = function() { - if (drop) { - drop.destroy(); - drop = null; - } - }; - - $(elem) - .on('mouseenter', 'ul.dropdown-menu li', function() { - cleanUpDrop(); - - var funcDef; - try { - funcDef = ctrl.datasource.getFuncDef($('a', this).text()); - } catch (e) { - // ignore - } - - if (funcDef && funcDef.description) { - var shortDesc = funcDef.description; - if (shortDesc.length > 500) { - shortDesc = shortDesc.substring(0, 497) + '...'; - } - - var contentElement = document.createElement('div'); - contentElement.innerHTML = '

    ' + funcDef.name + '

    ' + rst2html(shortDesc); - - drop = new Drop({ - target: this, - content: contentElement, - classes: 'drop-popover', - openOn: 'always', - tetherOptions: { - attachment: 'bottom left', - targetAttachment: 'bottom right', - }, - }); - } - }) - .on('mouseout', 'ul.dropdown-menu li', function() { - cleanUpDrop(); - }); - - $scope.$on('$destroy', cleanUpDrop); - }, - }; - }); - - function createFunctionDropDownMenu(funcDefs) { - var categories = {}; - - _.forEach(funcDefs, function(funcDef) { - if (!funcDef.category) { - return; - } - if (!categories[funcDef.category]) { - categories[funcDef.category] = []; - } - categories[funcDef.category].push({ - text: funcDef.name, - click: "ctrl.addFunction('" + funcDef.name + "')", - }); - }); - - return _.sortBy( - _.map(categories, function(submenu, category) { - return { - text: category, - submenu: _.sortBy(submenu, 'text'), - }; - }), - 'text' - ); - } -}); diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.ts b/public/app/plugins/datasource/graphite/add_graphite_func.ts new file mode 100644 index 00000000000..360d606c924 --- /dev/null +++ b/public/app/plugins/datasource/graphite/add_graphite_func.ts @@ -0,0 +1,159 @@ +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import rst2html from 'rst2html'; +import Drop from 'tether-drop'; + +export function graphiteAddFunc($compile) { + var inputTemplate = + ''; + + var buttonTemplate = + '' + + ''; + + return { + link: function($scope, elem) { + var ctrl = $scope.ctrl; + + var $input = $(inputTemplate); + var $button = $(buttonTemplate); + + $input.appendTo(elem); + $button.appendTo(elem); + + ctrl.datasource.getFuncDefs().then(function(funcDefs) { + var allFunctions = _.map(funcDefs, 'name').sort(); + + $scope.functionMenu = createFunctionDropDownMenu(funcDefs); + + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ + source: allFunctions, + minLength: 1, + items: 10, + updater: function(value) { + var funcDef = ctrl.datasource.getFuncDef(value); + if (!funcDef) { + // try find close match + value = value.toLowerCase(); + funcDef = _.find(allFunctions, function(funcName) { + return funcName.toLowerCase().indexOf(value) === 0; + }); + + if (!funcDef) { + return ''; + } + } + + $scope.$apply(function() { + ctrl.addFunction(funcDef); + }); + + $input.trigger('blur'); + return ''; + }, + }); + + $button.click(function() { + $button.hide(); + $input.show(); + $input.focus(); + }); + + $input.keyup(function() { + elem.toggleClass('open', $input.val() === ''); + }); + + $input.blur(function() { + // clicking the function dropdown menu wont + // work if you remove class at once + setTimeout(function() { + $input.val(''); + $input.hide(); + $button.show(); + elem.removeClass('open'); + }, 200); + }); + + $compile(elem.contents())($scope); + }); + + var drop; + var cleanUpDrop = function() { + if (drop) { + drop.destroy(); + drop = null; + } + }; + + $(elem) + .on('mouseenter', 'ul.dropdown-menu li', function() { + cleanUpDrop(); + + var funcDef; + try { + funcDef = ctrl.datasource.getFuncDef($('a', this).text()); + } catch (e) { + // ignore + } + + if (funcDef && funcDef.description) { + var shortDesc = funcDef.description; + if (shortDesc.length > 500) { + shortDesc = shortDesc.substring(0, 497) + '...'; + } + + var contentElement = document.createElement('div'); + contentElement.innerHTML = '

    ' + funcDef.name + '

    ' + rst2html(shortDesc); + + drop = new Drop({ + target: this, + content: contentElement, + classes: 'drop-popover', + openOn: 'always', + tetherOptions: { + attachment: 'bottom left', + targetAttachment: 'bottom right', + }, + }); + } + }) + .on('mouseout', 'ul.dropdown-menu li', function() { + cleanUpDrop(); + }); + + $scope.$on('$destroy', cleanUpDrop); + }, + }; +} + +angular.module('grafana.directives').directive('graphiteAddFunc', graphiteAddFunc); + +function createFunctionDropDownMenu(funcDefs) { + var categories = {}; + + _.forEach(funcDefs, function(funcDef) { + if (!funcDef.category) { + return; + } + if (!categories[funcDef.category]) { + categories[funcDef.category] = []; + } + categories[funcDef.category].push({ + text: funcDef.name, + click: "ctrl.addFunction('" + funcDef.name + "')", + }); + }); + + return _.sortBy( + _.map(categories, function(submenu, category) { + return { + text: category, + submenu: _.sortBy(submenu, 'text'), + }; + }), + 'text' + ); +} From 230f018c7820542162e7ae4f9e52bf56fcd7011a Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Mar 2018 15:37:18 +0500 Subject: [PATCH 0097/3000] Added validation of input parameters. --- public/app/plugins/panel/graph/align_yaxes.ts | 12 ++++++++++++ .../plugins/panel/graph/specs/align_yaxes.jest.ts | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/public/app/plugins/panel/graph/align_yaxes.ts b/public/app/plugins/panel/graph/align_yaxes.ts index b60d75e7b66..71bfcd8423d 100644 --- a/public/app/plugins/panel/graph/align_yaxes.ts +++ b/public/app/plugins/panel/graph/align_yaxes.ts @@ -6,6 +6,10 @@ import _ from 'lodash'; * @param align Y level */ export function alignYLevel(yaxis, alignLevel) { + if (isNaN(alignLevel) || !checkCorrectAxis(yaxis)) { + return; + } + var [yLeft, yRight] = yaxis; moveLevelToZero(yLeft, yRight, alignLevel); @@ -92,6 +96,14 @@ function restoreLevelFromZero(yLeft, yRight, alignLevel) { } } +function checkCorrectAxis(axis) { + return axis.length === 2 && checkCorrectAxes(axis[0]) && checkCorrectAxes(axis[1]); +} + +function checkCorrectAxes(axes) { + return 'min' in axes && 'max' in axes; +} + function checkOneSide(yLeft, yRight) { // on the one hand with respect to zero return (yLeft.min >= 0 && yRight.min >= 0) || (yLeft.max <= 0 && yRight.max <= 0); diff --git a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts index ff540fd223f..963ecfbfa1f 100644 --- a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts +++ b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts @@ -197,4 +197,15 @@ describe('Graph Y axes aligner', function() { expect(yaxes).toMatchObject(expected); }); }); + + describe('on level not number value', () => { + it('Should ignore without errors', () => { + alignY = 'q'; + yaxes = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; + expected = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; + + alignYLevel(yaxes, alignY); + expect(yaxes).toMatchObject(expected); + }); + }); }); From 1588295375574bfda62d34271707535dfed6c16d Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Mar 2018 15:38:48 +0500 Subject: [PATCH 0098/3000] Changed the way this feature was activated. And changed tolltip. --- public/app/plugins/panel/graph/axes_editor.html | 11 ++++++++--- public/app/plugins/panel/graph/graph.ts | 5 +++-- public/app/plugins/panel/graph/module.ts | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index 2c08755c17a..f17c9ce105f 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -31,9 +31,14 @@
    -
    - - +
    +
    + +
    +
    + + +
    diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 73b261fce3a..713d7079152 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -158,8 +158,9 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && 'align' in panel.yaxes[1] && panel.yaxes[1].align !== null) { - alignYLevel(yaxis, parseFloat(panel.yaxes[1].align)); + if (yaxis.length > 1 && panel.yaxes[1].alignment) { + var align = panel.yaxes[1].align || 0; + alignYLevel(yaxis, parseFloat(align)); } } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index c198d118115..7e7b270bd61 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -46,7 +46,8 @@ class GraphCtrl extends MetricsPanelCtrl { min: null, max: null, format: 'short', - align: null, + alignment: false, + align: 0, }, ], xaxis: { From e015047ed1c234eed8d14c91b3b6eb5f7f5b7612 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Mar 2018 16:09:52 +0500 Subject: [PATCH 0099/3000] Fixed unit test. --- public/app/plugins/panel/graph/specs/align_yaxes.jest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts index 963ecfbfa1f..da3aff91275 100644 --- a/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts +++ b/public/app/plugins/panel/graph/specs/align_yaxes.jest.ts @@ -200,11 +200,10 @@ describe('Graph Y axes aligner', function() { describe('on level not number value', () => { it('Should ignore without errors', () => { - alignY = 'q'; yaxes = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; expected = [{ min: 5, max: 10 }, { min: 2, max: 4 }]; - alignYLevel(yaxes, alignY); + alignYLevel(yaxes, 'q'); expect(yaxes).toMatchObject(expected); }); }); From 2a90370230e8515711dad7de448c2c034d571c4b Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 20 Mar 2018 12:36:02 +0100 Subject: [PATCH 0100/3000] converted file to ts --- .../datasource/graphite/func_editor.js | 309 ----------------- .../datasource/graphite/func_editor.ts | 318 ++++++++++++++++++ 2 files changed, 318 insertions(+), 309 deletions(-) delete mode 100644 public/app/plugins/datasource/graphite/func_editor.js create mode 100644 public/app/plugins/datasource/graphite/func_editor.ts diff --git a/public/app/plugins/datasource/graphite/func_editor.js b/public/app/plugins/datasource/graphite/func_editor.js deleted file mode 100644 index 5648aa4935b..00000000000 --- a/public/app/plugins/datasource/graphite/func_editor.js +++ /dev/null @@ -1,309 +0,0 @@ -define([ - 'angular', - 'lodash', - 'jquery', - 'rst2html', -], -function (angular, _, $, rst2html) { - 'use strict'; - - angular - .module('grafana.directives') - .directive('graphiteFuncEditor', function($compile, templateSrv, popoverSrv) { - - var funcSpanTemplate = '{{func.def.name}}('; - var paramTemplate = ''; - - var funcControlsTemplate = - '
    ' + - '' + - '' + - '' + - '' + - '
    '; - - return { - restrict: 'A', - link: function postLink($scope, elem) { - var $funcLink = $(funcSpanTemplate); - var $funcControls = $(funcControlsTemplate); - var ctrl = $scope.ctrl; - var func = $scope.func; - var scheduledRelink = false; - var paramCountAtLink = 0; - var cancelBlur = null; - - function clickFuncParam(paramIndex) { - /*jshint validthis:true */ - - var $link = $(this); - var $comma = $link.prev('.comma'); - var $input = $link.next(); - - $input.val(func.params[paramIndex]); - - $comma.removeClass('query-part__last'); - $link.hide(); - $input.show(); - $input.focus(); - $input.select(); - - var typeahead = $input.data('typeahead'); - if (typeahead) { - $input.val(''); - typeahead.lookup(); - } - } - - function scheduledRelinkIfNeeded() { - if (paramCountAtLink === func.params.length) { - return; - } - - if (!scheduledRelink) { - scheduledRelink = true; - setTimeout(function() { - relink(); - scheduledRelink = false; - }, 200); - } - } - - function paramDef(index) { - if (index < func.def.params.length) { - return func.def.params[index]; - } - if (_.last(func.def.params).multiple) { - return _.assign({}, _.last(func.def.params), {optional: true}); - } - return {}; - } - - function switchToLink(inputElem, paramIndex) { - /*jshint validthis:true */ - var $input = $(inputElem); - - clearTimeout(cancelBlur); - cancelBlur = null; - - var $link = $input.prev(); - var $comma = $link.prev('.comma'); - var newValue = $input.val(); - - // remove optional empty params - if (newValue !== '' || paramDef(paramIndex).optional) { - func.updateParam(newValue, paramIndex); - $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : ' '); - } - - scheduledRelinkIfNeeded(); - - $scope.$apply(function() { - ctrl.targetChanged(); - }); - - if ($link.hasClass('query-part__last') && newValue === '') { - $comma.addClass('query-part__last'); - } else { - $link.removeClass('query-part__last'); - } - - $input.hide(); - $link.show(); - } - - // this = input element - function inputBlur(paramIndex) { - /*jshint validthis:true */ - var inputElem = this; - // happens long before the click event on the typeahead options - // need to have long delay because the blur - cancelBlur = setTimeout(function() { - switchToLink(inputElem, paramIndex); - }, 200); - } - - function inputKeyPress(paramIndex, e) { - /*jshint validthis:true */ - if(e.which === 13) { - $(this).blur(); - } - } - - function inputKeyDown() { - /*jshint validthis:true */ - this.style.width = (3 + this.value.length) * 8 + 'px'; - } - - function addTypeahead($input, paramIndex) { - $input.attr('data-provide', 'typeahead'); - - var options = paramDef(paramIndex).options; - if (paramDef(paramIndex).type === 'int') { - options = _.map(options, function(val) { return val.toString(); }); - } - - $input.typeahead({ - source: options, - minLength: 0, - items: 20, - updater: function (value) { - $input.val(value); - switchToLink($input[0], paramIndex); - return value; - } - }); - - var typeahead = $input.data('typeahead'); - typeahead.lookup = function () { - this.query = this.$element.val() || ''; - return this.process(this.source); - }; - } - - function toggleFuncControls() { - var targetDiv = elem.closest('.tight-form'); - - if (elem.hasClass('show-function-controls')) { - elem.removeClass('show-function-controls'); - targetDiv.removeClass('has-open-function'); - $funcControls.hide(); - return; - } - - elem.addClass('show-function-controls'); - targetDiv.addClass('has-open-function'); - - $funcControls.show(); - } - - function addElementsAndCompile() { - $funcControls.appendTo(elem); - $funcLink.appendTo(elem); - - var defParams = _.clone(func.def.params); - var lastParam = _.last(func.def.params); - - while (func.params.length >= defParams.length && lastParam && lastParam.multiple) { - defParams.push(_.assign({}, lastParam, {optional: true})); - } - - _.each(defParams, function(param, index) { - if (param.optional && func.params.length < index) { - return false; - } - - var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); - - var last = (index >= func.params.length - 1) && param.optional && !paramValue; - if (last && param.multiple) { - paramValue = '+'; - } - - if (index > 0) { - $(', ').appendTo(elem); - } - - var $paramLink = $( - '' - + (paramValue || ' ') + ''); - var $input = $(paramTemplate); - $input.attr('placeholder', param.name); - - paramCountAtLink++; - - $paramLink.appendTo(elem); - $input.appendTo(elem); - - $input.blur(_.partial(inputBlur, index)); - $input.keyup(inputKeyDown); - $input.keypress(_.partial(inputKeyPress, index)); - $paramLink.click(_.partial(clickFuncParam, index)); - - if (param.options) { - addTypeahead($input, index); - } - }); - - $(')').appendTo(elem); - - $compile(elem.contents())($scope); - } - - function ifJustAddedFocusFirstParam() { - if ($scope.func.added) { - $scope.func.added = false; - setTimeout(function() { - elem.find('.graphite-func-param-link').first().click(); - }, 10); - } - } - - function registerFuncControlsToggle() { - $funcLink.click(toggleFuncControls); - } - - function registerFuncControlsActions() { - $funcControls.click(function(e) { - var $target = $(e.target); - if ($target.hasClass('fa-remove')) { - toggleFuncControls(); - $scope.$apply(function() { - ctrl.removeFunction($scope.func); - }); - return; - } - - if ($target.hasClass('fa-arrow-left')) { - $scope.$apply(function() { - _.move(ctrl.queryModel.functions, $scope.$index, $scope.$index - 1); - ctrl.targetChanged(); - }); - return; - } - - if ($target.hasClass('fa-arrow-right')) { - $scope.$apply(function() { - _.move(ctrl.queryModel.functions, $scope.$index, $scope.$index + 1); - ctrl.targetChanged(); - }); - return; - } - - if ($target.hasClass('fa-question-circle')) { - var funcDef = ctrl.datasource.getFuncDef(func.def.name); - if (funcDef && funcDef.description) { - popoverSrv.show({ - element: e.target, - position: 'bottom left', - classNames: 'drop-popover drop-function-def', - template: '
    ' - + '

    ' + funcDef.name + '

    ' + rst2html(funcDef.description) + '
    ', - openOn: 'click', - }); - } else { - window.open( - "http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + func.def.name,'_blank'); - } - return; - } - }); - } - - function relink() { - elem.children().remove(); - - addElementsAndCompile(); - ifJustAddedFocusFirstParam(); - registerFuncControlsToggle(); - registerFuncControlsActions(); - } - - relink(); - } - }; - - }); - -}); diff --git a/public/app/plugins/datasource/graphite/func_editor.ts b/public/app/plugins/datasource/graphite/func_editor.ts new file mode 100644 index 00000000000..1a4c6d4313a --- /dev/null +++ b/public/app/plugins/datasource/graphite/func_editor.ts @@ -0,0 +1,318 @@ +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import rst2html from 'rst2html'; + +export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { + var funcSpanTemplate = '{{func.def.name}}('; + var paramTemplate = ''; + + var funcControlsTemplate = + '
    ' + + '' + + '' + + '' + + '' + + '
    '; + + return { + restrict: 'A', + link: function postLink($scope, elem) { + var $funcLink = $(funcSpanTemplate); + var $funcControls = $(funcControlsTemplate); + var ctrl = $scope.ctrl; + var func = $scope.func; + var scheduledRelink = false; + var paramCountAtLink = 0; + var cancelBlur = null; + + function clickFuncParam(paramIndex) { + /*jshint validthis:true */ + + var $link = $(this); + var $comma = $link.prev('.comma'); + var $input = $link.next(); + + $input.val(func.params[paramIndex]); + + $comma.removeClass('query-part__last'); + $link.hide(); + $input.show(); + $input.focus(); + $input.select(); + + var typeahead = $input.data('typeahead'); + if (typeahead) { + $input.val(''); + typeahead.lookup(); + } + } + + function scheduledRelinkIfNeeded() { + if (paramCountAtLink === func.params.length) { + return; + } + + if (!scheduledRelink) { + scheduledRelink = true; + setTimeout(function() { + relink(); + scheduledRelink = false; + }, 200); + } + } + + function paramDef(index) { + if (index < func.def.params.length) { + return func.def.params[index]; + } + if (_.last(func.def.params).multiple) { + return _.assign({}, _.last(func.def.params), { optional: true }); + } + return {}; + } + + function switchToLink(inputElem, paramIndex) { + /*jshint validthis:true */ + var $input = $(inputElem); + + clearTimeout(cancelBlur); + cancelBlur = null; + + var $link = $input.prev(); + var $comma = $link.prev('.comma'); + var newValue = $input.val(); + + // remove optional empty params + if (newValue !== '' || paramDef(paramIndex).optional) { + func.updateParam(newValue, paramIndex); + $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : ' '); + } + + scheduledRelinkIfNeeded(); + + $scope.$apply(function() { + ctrl.targetChanged(); + }); + + if ($link.hasClass('query-part__last') && newValue === '') { + $comma.addClass('query-part__last'); + } else { + $link.removeClass('query-part__last'); + } + + $input.hide(); + $link.show(); + } + + // this = input element + function inputBlur(paramIndex) { + /*jshint validthis:true */ + var inputElem = this; + // happens long before the click event on the typeahead options + // need to have long delay because the blur + cancelBlur = setTimeout(function() { + switchToLink(inputElem, paramIndex); + }, 200); + } + + function inputKeyPress(paramIndex, e) { + /*jshint validthis:true */ + if (e.which === 13) { + $(this).blur(); + } + } + + function inputKeyDown() { + /*jshint validthis:true */ + this.style.width = (3 + this.value.length) * 8 + 'px'; + } + + function addTypeahead($input, paramIndex) { + $input.attr('data-provide', 'typeahead'); + + var options = paramDef(paramIndex).options; + if (paramDef(paramIndex).type === 'int') { + options = _.map(options, function(val) { + return val.toString(); + }); + } + + $input.typeahead({ + source: options, + minLength: 0, + items: 20, + updater: function(value) { + $input.val(value); + switchToLink($input[0], paramIndex); + return value; + }, + }); + + var typeahead = $input.data('typeahead'); + typeahead.lookup = function() { + this.query = this.$element.val() || ''; + return this.process(this.source); + }; + } + + function toggleFuncControls() { + var targetDiv = elem.closest('.tight-form'); + + if (elem.hasClass('show-function-controls')) { + elem.removeClass('show-function-controls'); + targetDiv.removeClass('has-open-function'); + $funcControls.hide(); + return; + } + + elem.addClass('show-function-controls'); + targetDiv.addClass('has-open-function'); + + $funcControls.show(); + } + + function addElementsAndCompile() { + $funcControls.appendTo(elem); + $funcLink.appendTo(elem); + + var defParams = _.clone(func.def.params); + var lastParam = _.last(func.def.params); + + while (func.params.length >= defParams.length && lastParam && lastParam.multiple) { + defParams.push(_.assign({}, lastParam, { optional: true })); + } + + _.each(defParams, function(param, index) { + if (param.optional && func.params.length < index) { + return false; + } + + var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); + + var last = index >= func.params.length - 1 && param.optional && !paramValue; + if (last && param.multiple) { + paramValue = '+'; + } + + if (index > 0) { + $(', ').appendTo(elem); + } + + var $paramLink = $( + '' + + (paramValue || ' ') + + '' + ); + var $input = $(paramTemplate); + $input.attr('placeholder', param.name); + + paramCountAtLink++; + + $paramLink.appendTo(elem); + $input.appendTo(elem); + + $input.blur(_.partial(inputBlur, index)); + $input.keyup(inputKeyDown); + $input.keypress(_.partial(inputKeyPress, index)); + $paramLink.click(_.partial(clickFuncParam, index)); + + if (param.options) { + addTypeahead($input, index); + } + + return true; + }); + + $(')').appendTo(elem); + + $compile(elem.contents())($scope); + } + + function ifJustAddedFocusFirstParam() { + if ($scope.func.added) { + $scope.func.added = false; + setTimeout(function() { + elem + .find('.graphite-func-param-link') + .first() + .click(); + }, 10); + } + } + + function registerFuncControlsToggle() { + $funcLink.click(toggleFuncControls); + } + + function registerFuncControlsActions() { + $funcControls.click(function(e) { + var $target = $(e.target); + if ($target.hasClass('fa-remove')) { + toggleFuncControls(); + $scope.$apply(function() { + ctrl.removeFunction($scope.func); + }); + return; + } + + if ($target.hasClass('fa-arrow-left')) { + $scope.$apply(function() { + _.move(ctrl.queryModel.functions, $scope.$index, $scope.$index - 1); + ctrl.targetChanged(); + }); + return; + } + + if ($target.hasClass('fa-arrow-right')) { + $scope.$apply(function() { + _.move(ctrl.queryModel.functions, $scope.$index, $scope.$index + 1); + ctrl.targetChanged(); + }); + return; + } + + if ($target.hasClass('fa-question-circle')) { + var funcDef = ctrl.datasource.getFuncDef(func.def.name); + if (funcDef && funcDef.description) { + popoverSrv.show({ + element: e.target, + position: 'bottom left', + classNames: 'drop-popover drop-function-def', + template: + '
    ' + + '

    ' + + funcDef.name + + '

    ' + + rst2html(funcDef.description) + + '
    ', + openOn: 'click', + }); + } else { + window.open( + 'http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions.' + func.def.name, + '_blank' + ); + } + return; + } + }); + } + + function relink() { + elem.children().remove(); + + addElementsAndCompile(); + ifJustAddedFocusFirstParam(); + registerFuncControlsToggle(); + registerFuncControlsActions(); + } + + relink(); + }, + }; +} + +angular.module('grafana.directives').directive('graphiteFuncEditor', graphiteFuncEditor); From ae4c6e4648ffb1382d737de6ffac2e4f4b06a611 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 12:57:26 +0100 Subject: [PATCH 0101/3000] mssql: fix precision for time column in table mode ref #11306 --- pkg/tsdb/mssql/mssql.go | 2 +- .../datasource/mssql/response_parser.ts | 2 +- .../mssql/specs/datasource_specs.ts | 135 +++++++++--------- 3 files changed, 68 insertions(+), 71 deletions(-) diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index af68ca0424e..6da61d63e42 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -124,7 +124,7 @@ func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, if timeIndex != -1 { switch value := values[timeIndex].(type) { case time.Time: - values[timeIndex] = float64(value.Unix()) + values[timeIndex] = (float64(value.Unix()) * 1000) + float64(value.Nanosecond()/1e6) // in case someone is trying to map times beyond 2262 :D } } diff --git a/public/app/plugins/datasource/mssql/response_parser.ts b/public/app/plugins/datasource/mssql/response_parser.ts index b7d96d820cb..c98a9652b0e 100644 --- a/public/app/plugins/datasource/mssql/response_parser.ts +++ b/public/app/plugins/datasource/mssql/response_parser.ts @@ -128,7 +128,7 @@ export default class ResponseParser { const row = table.rows[i]; list.push({ annotation: options.annotation, - time: Math.floor(row[timeColumnIndex]) * 1000, + time: row[timeColumnIndex], text: row[textColumnIndex], tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [], }); diff --git a/public/app/plugins/datasource/mssql/specs/datasource_specs.ts b/public/app/plugins/datasource/mssql/specs/datasource_specs.ts index a144f21a258..18fcb6331be 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource_specs.ts @@ -1,24 +1,26 @@ -import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common'; +import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import moment from 'moment'; import helpers from 'test/specs/helpers'; -import {MssqlDatasource} from '../datasource'; -import {CustomVariable} from 'app/features/templating/custom_variable'; +import { MssqlDatasource } from '../datasource'; +import { CustomVariable } from 'app/features/templating/custom_variable'; describe('MSSQLDatasource', function() { var ctx = new helpers.ServiceTestContext(); - var instanceSettings = {name: 'mssql'}; + var instanceSettings = { name: 'mssql' }; beforeEach(angularMocks.module('grafana.core')); beforeEach(angularMocks.module('grafana.services')); beforeEach(ctx.providePhase(['backendSrv'])); - beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(MssqlDatasource, {instanceSettings: instanceSettings}); - $httpBackend.when('GET', /\.html$/).respond(''); - })); + beforeEach( + angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { + ctx.$q = $q; + ctx.$httpBackend = $httpBackend; + ctx.$rootScope = $rootScope; + ctx.ds = $injector.instantiate(MssqlDatasource, { instanceSettings: instanceSettings }); + $httpBackend.when('GET', /\.html$/).respond(''); + }) + ); describe('When performing annotationQuery', function() { let results; @@ -28,12 +30,12 @@ describe('MSSQLDatasource', function() { const options = { annotation: { name: annotationName, - rawQuery: 'select time, text, tags from table;' + rawQuery: 'select time, text, tags from table;', }, range: { from: moment(1432288354), - to: moment(1432288401) - } + to: moment(1432288401), + }, }; const response = { @@ -42,23 +44,25 @@ describe('MSSQLDatasource', function() { refId: annotationName, tables: [ { - columns: [{text: 'time'}, {text: 'text'}, {text: 'tags'}], + columns: [{ text: 'time' }, { text: 'text' }, { text: 'tags' }], rows: [ - [1432288355, 'some text', 'TagA,TagB'], - [1432288390, 'some text2', ' TagB , TagC'], - [1432288400, 'some text3'] - ] - } - ] - } - } + [1521546171129, 'some text', 'TagA,TagB'], + [1521546531404, 'some text2', ' TagB , TagC'], + [1521546901702, 'some text3'], + ], + }, + ], + }, + }, }; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({data: response, status: 200}); + return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.annotationQuery(options).then(function(data) { results = data; }); + ctx.ds.annotationQuery(options).then(function(data) { + results = data; + }); ctx.$rootScope.$apply(); }); @@ -83,28 +87,26 @@ describe('MSSQLDatasource', function() { results: { tempvar: { meta: { - rowCount: 3 + rowCount: 3, }, refId: 'tempvar', tables: [ { - columns: [{text: 'title'}, {text: 'text'}], - rows: [ - ['aTitle', 'some text'], - ['aTitle2', 'some text2'], - ['aTitle3', 'some text3'] - ] - } - ] - } - } + columns: [{ text: 'title' }, { text: 'text' }], + rows: [['aTitle', 'some text'], ['aTitle2', 'some text2'], ['aTitle3', 'some text3']], + }, + ], + }, + }, }; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({data: response, status: 200}); + return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); + ctx.ds.metricFindQuery(query).then(function(data) { + results = data; + }); ctx.$rootScope.$apply(); }); @@ -122,28 +124,26 @@ describe('MSSQLDatasource', function() { results: { tempvar: { meta: { - rowCount: 3 + rowCount: 3, }, refId: 'tempvar', tables: [ { - columns: [{text: '__value'}, {text: '__text'}], - rows: [ - ['value1', 'aTitle'], - ['value2', 'aTitle2'], - ['value3', 'aTitle3'] - ] - } - ] - } - } + columns: [{ text: '__value' }, { text: '__text' }], + rows: [['value1', 'aTitle'], ['value2', 'aTitle2'], ['value3', 'aTitle3']], + }, + ], + }, + }, }; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({data: response, status: 200}); + return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); + ctx.ds.metricFindQuery(query).then(function(data) { + results = data; + }); ctx.$rootScope.$apply(); }); @@ -163,28 +163,26 @@ describe('MSSQLDatasource', function() { results: { tempvar: { meta: { - rowCount: 3 + rowCount: 3, }, refId: 'tempvar', tables: [ { - columns: [{text: '__text'}, {text: '__value'}], - rows: [ - ['aTitle', 'same'], - ['aTitle', 'same'], - ['aTitle', 'diff'] - ] - } - ] - } - } + columns: [{ text: '__text' }, { text: '__value' }], + rows: [['aTitle', 'same'], ['aTitle', 'same'], ['aTitle', 'diff']], + }, + ], + }, + }, }; beforeEach(function() { ctx.backendSrv.datasourceRequest = function(options) { - return ctx.$q.when({data: response, status: 200}); + return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); + ctx.ds.metricFindQuery(query).then(function(data) { + results = data; + }); ctx.$rootScope.$apply(); }); @@ -197,7 +195,7 @@ describe('MSSQLDatasource', function() { describe('When interpolating variables', () => { beforeEach(function() { - ctx.variable = new CustomVariable({},{}); + ctx.variable = new CustomVariable({}, {}); }); describe('and value is a string', () => { @@ -214,23 +212,22 @@ describe('MSSQLDatasource', function() { describe('and value is an array of strings', () => { it('should return comma separated quoted values', () => { - expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql('\'a\',\'b\',\'c\''); + expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'"); }); }); describe('and variable allows multi-value and value is a string', () => { it('should return a quoted value', () => { ctx.variable.multi = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('\'abc\''); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); }); }); describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('\'abc\''); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); }); }); - }); }); From d34cd8730eef69ea64926f7acf62e54f338d02e8 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 13:01:32 +0100 Subject: [PATCH 0102/3000] mssql: convert tests to jest --- ...datasource_specs.ts => datasource.jest.ts} | 99 +++++++++---------- 1 file changed, 47 insertions(+), 52 deletions(-) rename public/app/plugins/datasource/mssql/specs/{datasource_specs.ts => datasource.jest.ts} (67%) diff --git a/public/app/plugins/datasource/mssql/specs/datasource_specs.ts b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts similarity index 67% rename from public/app/plugins/datasource/mssql/specs/datasource_specs.ts rename to public/app/plugins/datasource/mssql/specs/datasource.jest.ts index 18fcb6331be..dd2d4a60cec 100644 --- a/public/app/plugins/datasource/mssql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mssql/specs/datasource.jest.ts @@ -1,26 +1,21 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import moment from 'moment'; -import helpers from 'test/specs/helpers'; import { MssqlDatasource } from '../datasource'; +import { TemplateSrvStub } from 'test/specs/helpers'; import { CustomVariable } from 'app/features/templating/custom_variable'; +import q from 'q'; describe('MSSQLDatasource', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { name: 'mssql' }; + const ctx: any = { + backendSrv: {}, + templateSrv: new TemplateSrvStub(), + }; - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach(ctx.providePhase(['backendSrv'])); + beforeEach(function() { + ctx.$q = q; + ctx.instanceSettings = { name: 'mssql' }; - beforeEach( - angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(MssqlDatasource, { instanceSettings: instanceSettings }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); + ctx.ds = new MssqlDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.$q, ctx.templateSrv); + }); describe('When performing annotationQuery', function() { let results; @@ -46,9 +41,9 @@ describe('MSSQLDatasource', function() { { columns: [{ text: 'time' }, { text: 'text' }, { text: 'tags' }], rows: [ - [1521546171129, 'some text', 'TagA,TagB'], - [1521546531404, 'some text2', ' TagB , TagC'], - [1521546901702, 'some text3'], + [1521545610656, 'some text', 'TagA,TagB'], + [1521546251185, 'some text2', ' TagB , TagC'], + [1521546501378, 'some text3'], ], }, ], @@ -56,27 +51,27 @@ describe('MSSQLDatasource', function() { }, }; - beforeEach(function() { - ctx.backendSrv.datasourceRequest = function(options) { + beforeEach(() => { + ctx.backendSrv.datasourceRequest = options => { return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.annotationQuery(options).then(function(data) { + + return ctx.ds.annotationQuery(options).then(data => { results = data; }); - ctx.$rootScope.$apply(); }); it('should return annotation list', function() { - expect(results.length).to.be(3); + expect(results.length).toBe(3); - expect(results[0].text).to.be('some text'); - expect(results[0].tags[0]).to.be('TagA'); - expect(results[0].tags[1]).to.be('TagB'); + expect(results[0].text).toBe('some text'); + expect(results[0].tags[0]).toBe('TagA'); + expect(results[0].tags[1]).toBe('TagB'); - expect(results[1].tags[0]).to.be('TagB'); - expect(results[1].tags[1]).to.be('TagC'); + expect(results[1].tags[0]).toBe('TagB'); + expect(results[1].tags[1]).toBe('TagC'); - expect(results[2].tags.length).to.be(0); + expect(results[2].tags.length).toBe(0); }); }); @@ -104,16 +99,16 @@ describe('MSSQLDatasource', function() { ctx.backendSrv.datasourceRequest = function(options) { return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { + + return ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of all column values', function() { - expect(results.length).to.be(6); - expect(results[0].text).to.be('aTitle'); - expect(results[5].text).to.be('some text3'); + expect(results.length).toBe(6); + expect(results[0].text).toBe('aTitle'); + expect(results[5].text).toBe('some text3'); }); }); @@ -141,18 +136,18 @@ describe('MSSQLDatasource', function() { ctx.backendSrv.datasourceRequest = function(options) { return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { + + return ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of as text, value', function() { - expect(results.length).to.be(3); - expect(results[0].text).to.be('aTitle'); - expect(results[0].value).to.be('value1'); - expect(results[2].text).to.be('aTitle3'); - expect(results[2].value).to.be('value3'); + expect(results.length).toBe(3); + expect(results[0].text).toBe('aTitle'); + expect(results[0].value).toBe('value1'); + expect(results[2].text).toBe('aTitle3'); + expect(results[2].value).toBe('value3'); }); }); @@ -180,16 +175,16 @@ describe('MSSQLDatasource', function() { ctx.backendSrv.datasourceRequest = function(options) { return ctx.$q.when({ data: response, status: 200 }); }; - ctx.ds.metricFindQuery(query).then(function(data) { + + return ctx.ds.metricFindQuery(query).then(function(data) { results = data; }); - ctx.$rootScope.$apply(); }); it('should return list of unique keys', function() { - expect(results.length).to.be(1); - expect(results[0].text).to.be('aTitle'); - expect(results[0].value).to.be('same'); + expect(results.length).toBe(1); + expect(results[0].text).toBe('aTitle'); + expect(results[0].value).toBe('same'); }); }); @@ -200,33 +195,33 @@ describe('MSSQLDatasource', function() { describe('and value is a string', () => { it('should return an unquoted value', () => { - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc'); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc'); }); }); describe('and value is a number', () => { it('should return an unquoted value', () => { - expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000); + expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000); }); }); describe('and value is an array of strings', () => { it('should return comma separated quoted values', () => { - expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'"); + expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'"); }); }); describe('and variable allows multi-value and value is a string', () => { it('should return a quoted value', () => { ctx.variable.multi = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); }); }); describe('and variable allows all and value is a string', () => { it('should return a quoted value', () => { ctx.variable.includeAll = true; - expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'"); + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); }); }); }); From 3bdd0062912abe2e9afa1540d26e61a0a171ce1e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 20 Mar 2018 14:26:03 +0100 Subject: [PATCH 0103/3000] changed var to const --- public/app/plugins/datasource/graphite/add_graphite_func.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.ts b/public/app/plugins/datasource/graphite/add_graphite_func.ts index 360d606c924..f2a596c7071 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.ts +++ b/public/app/plugins/datasource/graphite/add_graphite_func.ts @@ -5,10 +5,10 @@ import rst2html from 'rst2html'; import Drop from 'tether-drop'; export function graphiteAddFunc($compile) { - var inputTemplate = + const inputTemplate = ''; - var buttonTemplate = + const buttonTemplate = '' + ''; From 54c4b6a11ae43c08792eed529c8f6d27326d7bcf Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 20 Mar 2018 14:39:19 +0100 Subject: [PATCH 0104/3000] Remove unused kibana images --- public/img/kibana.png | Bin 5290 -> 0 bytes public/img/small.png | Bin 335 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/img/kibana.png delete mode 100644 public/img/small.png diff --git a/public/img/kibana.png b/public/img/kibana.png deleted file mode 100644 index 85220e3322682aa4154ee6f893c4aac6d65ef8a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5290 zcmX|F3p7+~*q#}CX12?)31KjMLLqX$wl9rKBF$lzS$NDtYDH zHmSvPLB!H6(!Y~_S#Ikc+U1*X>CwBm(3M??b=es9n}`VXn6&pDP)j`JeDw8y3% zlR9xtka3t1r2;;^+f|9wgQ@AP<(wi!pMd35_^m+o!vu_ zb=G6a<4R%u?GYR}HHVoR4LP~fAd%Fz-79ZZ0OvxTykIgv9$-k^n1qIft!Ru7f^hS4VaFsO}VQ8+b-#VKEclSVg2&}0`XH@<(dbe70HuQ z3m&PNY8fFSyZxYqwA$3}e}c&A&E=Fmkh1#Ms-5-&2hSv*d+v0rdAKy|P>E?5`)Hvs z{K~a{y(QJ5!QaiU{t)!3HXgFsk^c$vwdQin-ltE?!=|?mNnuPJ7#~?n&n7U!IfVB~ zGcB0x2Pq$Y9(670;qz7P^DyI-i|#snhcZ$m-{hC)rnR#xU)}T%yMn|jrgh?@){ybe z$}1-l9wa(dVN^TK-fsvuGb#>^-jRQ}yT49zuj%G{^Zk2TpT|-XosymBF%BLdQiR3V z(nJdqFwxV!7Y)JJhzbxZi?ei#M=<*!fiWn+hiI?oo(+T&jKJuV5OWY_$3jbFgtzu{ zHy#N$lo((G5vBtX%ZEP?axgOp<~z(j1<4tK*#QvK2gw^7;3;NLx$A&MM65GI`6vs- zJ+XEr#ztc(J?V2O^u7U~rYMi|nlTGEomZ=pK^Od>m8aHhN-seOizdJZR+ds`{WecL zl4=MJ_YzE>S+ggqv93LYIhe*Sr{LQZfiem7V|P(_ZD7P0tkepG!cw#M>1NuH&E zW6_wjYQzZq_Fpnz?!rr8Z7%c#hX-J7K%@ykw(j8T#oIFIiXY_KOMniNX{Vz_)_N82 z;3*4e*jF1Z`eV^}yCK-=gYc4~F{#mc`d|%5__H#>9%WPX&+GDG;Pij*q14iBJ1l+$Z+5><@}mdVsg`BxLP)SY0|3a4-G$^aPiDYSW*! zW?8Q)%ffrCBYUdfe0W_|7L~Yz2|2*k0eHm= zWk81@4*Y;L-loWh8CV63w8eLfN-P!XKf1^4;s(Gk!S(UEBUBjI^5 zJ0{|tO6P^YRckuJi$OFbKEN!u-I%@hh1B!-;a8e%AjC@BPZl%}z^cHja>{-v-4ALs z01sT&22B4~;QtLw0J{LwY66ta8$fik%nah+)q0BwG#Ja2!E8la$5tiBeR6b9cECByfmmBFlF?KPB#bEF`W*I-ls@gSmSk!_d+XC0v4qFFoyLuHfLV!lT*122VpMn(^Z3zsyC-0uX2*4qKPCjBq9Y0ah0Vo zHw2QzTS)O!Do}!?j{BnHPI@p%_iapqH5=fU)z+Crs`#Q=RyoNK6jA`mJ~|B2w6>(~ z9pG(64TPIhxBEw@Ztn|s0vn^6Z2ZY?if1qWS}RDJgT-Hd&ZPu8+O4cnw-|yU>VSza zdbcItdCP;HG5Qa7o+#mO(R)&Ay?A6>;uqQNlfK9Amu`2s;qw~^y(!T;7F3d0W(<9#H~r^J8~1Cs){Y)*xki+Wq%c>TL~&RUw$i*Mc#N3aUyP9k_Bm5GZb%&t-f*y1h@H ztD^~sl_K4zbSQ~E-u9Ht{W@>koSTjYh}RvNLL&j8MMt$T=>2mWDIp()E36k@B+oo) zPL><=)OQ3jr9rpZOIsa(#a9m_Q86FdGe2)dW$0>zhJSNo;d;YWPbXYzZ|)f>+}sgY zVN|&ckukmL_MnMcL1h{<4kWfV?W$iDDbmoxiB(B@B^+l~o`fPI15mO4aqnQ9HLs|0=v3|P z=-uY1s4&9-u>?;mpZqv%2TAMZSw2_~HU_K=6rs=z!hG~PFXzG9KL_!;emzgdj9YR? z#_n=G3Bsn1n#@_8w2GJ9dL7W@GlH=|#GjhV6)3X005sKm@#Pd*-vz1@_M}S?E`aVA zu2h9D3p;4^=$$)+C-RgA`f~*+vtORuLTuQ5=54Bb(AXXt{le-K9p8R@H{PHs z!BT{XH&DQpJ1qJoWu4oW@`V=m`v?03Sya@}0CCqb3)!P=QyZC;rQRP9_x;E*%5`iD z;0UgzL}u)zoH2xgE9;lYRGILGp;+VVnC5)}Aj+W<) zyCJYR4WT1D`bd|H5M@nCxo-9%Zjh;;r9z}1B8OUHca~yl8ZdLSp0{ZI?;_aP%liIN zqUn8*$a$g52^b0YKqLF4g0YJuS}eSEqj@mh{a(^%|K*2QZ$z&nG~9Exz_==}il4Y_ ze8iS2T`wPzp1@am85WZ0;fF90@KQ3HsBC(Q%O&w zq@)^wA2JKc6|;rs+KJunlm-)F5`63W<4^OZdMpzCSWBG)ye{O!SKkmaV_gkZHHWz2 z&>Mlk3~cmhNKyd|)7&zbd?9URupz%f(Fs54?gDK#|MlRw2_LunGq$mdV|sux7Yq+q zRF~kKfJyhiO{FqHP18EdgOI7#%I3L6VOLr<@l5u*2HdRFqRyr9>i^F+AqOtzrGLpY5q_G0E&0q{i&c{mQ=Cuc9!yd-w z@uAF3)6b6Tw8K9%ejDM%aoT-EIlLi}#pfxnjFSK?ID7BO zk_pflI=XU`P3R~&*JI}eI{$73W~Is4ZVp5R_t-A%%+AO{zQp@R*>@>Y+L^}PG}g5P zMo)0UFNomXa6WCYa$faURyvIe?Miyr=KQ+9Mr;SyZn9enly25ay(wEni~t{ywjVf)NVb(6kQjdnEjS7nE zN8LQ=^9Mc%0nv#av&s+ExVY7`J)(nMM650OV}!RHC;Vj!iSs2-T(^Z3o(3 zr$WI1p#1Hgs?kWVbUU?+Td9P*h}K+#?67rb zK=XZ;>SToeNt)bJhMfjirvrbeojk!jhW>^8dw46DMZW0tM~7ZKQt{rh!FAwoo`$L* ztVO3?SV~jMvuKc=JnBJ8(YiV4JL+Pav$w@YyJy|WQ2wawf>8ojx1F4fXq`TPNR`8A zj$&}uQ}gA}pd$`MtNVwf!2{oPyCdMeUSHy#$wVH`Ypv~=8|oV1*(El1<(LfPjk8AG zZjqiaiJmo8i8KZ0 ztwnQusZMmiX$O^%<>K?mru*~M1ANGtmN(>1WdyUG)h{fyw9%DM-g>>oM0Rc@sk8&f zvmKPO%FDj&=%{UikV=Xabmb@Lq>>PwOyUvbLeDEYbe^v~$$VcTgKJkg(;cV&kkhpF zdmXgPP>hR=HPbE8`3E7%b6A}(LTcvKJiM@H=%h9-Yr&y=B+ZQ&vcDFeC^EeB1;tFu zSc$cisxCC;9#H()kLNO&}6B)mrk-~10wluW(bH`{;_-odiz@Zj^Fp*eyMsBjkJ^o#MUqV zxReA99NJ=(aiPLK=Y<{%Y$}%iiaq)f=1&W4;ibf2joE#0+O)Klt?g2xvwC@sGK}>P z?wixP|5oGTOD!t)fvi>OspWY5)QDy+$CIHD!?rvHwZFGbOw!Wz5x6ebWx^-b%9R79 z&gEVRjNSa*G3XO`MPq%M30NU^%)2=TJw4{@1-V#BUyX@ zM)P3Qbv*7SgY=O+g$CX^l>M`k$JR!BM9-rjJK=BH9D+#Skf*K>KA}g6<(2kpL{@0% z7A(dIH!M0P1tLd(L6Q4fw zqpe4x#41Ytm`GEyZV}mQw0>r`uO|&iC_Nn1q`{pn!v0Fjr)qO5Hgq$!J!tM4HoYEU zJ4+iL@Yp%=wF`We?xG-q)GtR}bCYhSo(HXDN6l`?{2t}XoEmqV#dksfroq+i6+x#B zJZLR;gpjCGgzP~(LY~;5Yi`lajQ60~hdF_S&J5X8Q4{9ASOHdkZ{>qcgkIG zUqV-I2B|OCr1n(K!r$Q25#j>MHAiq%WV_V-@N*WfCdBO6+4rW(1%i0}JDS`h7wiY@ zTyTBP!tRp8l(jMD8Fy{&R1sm`Ius%ZO*7toP)ce%&TwD;Q{g@BrF7P1 zVzGUR+U(g1h6NC!`FKa%=nJs449;TVKElu4{zhXx$j|8m%ui{xYI*Lsw=b9wlWyEn;6wV!Ic~*EOEZSHQKsZ9SwMdpZ)q^th-@GA-K#(-7zI z91Xo`p*BP%{2TgPWF|i!K9;dX(zrJ8FhAAY&r>|DK!JKYx32Mp zE$}`hWtyuK=eT+=bRa$@lao>`y|a0WPrmvw?0T+X{se?M7TD4~qg8{kvuStTi}QKJ;_RscDo8|d2V^5nGp@pz zuOiseGB5+Di^pF-XV_iu01}9YwoCHi&0}AYNHfm?3_k*#HrpvU2q`>%;UB?~WVojp zia7ZVHX3sT1qEVYvSZ30`q5QyJ{T3nE@uHc7KbME&eStN|{P}Xjw*cE)6~b$*DF!75{w+Q46ck(=FO& z_LAx3F?OP000mO1^@s60003NNklo?`jrj{lDapKK63x=d6r|UQ7O|RXYyAjC#g2j?wK)mJEPK&8P`X~-dp66YBuoOto z1LAwAmTb;VMUL#6O*x7{@w0GCc3M7pwadx?Sz3^at+L^ojF!{7I zXIS$uKIj5l0*u`G*iu!)mO`bvjk!}mhP>Ee<&PtyW`iv`|DZeb(nCS)rNM*U3=Ny} zZEz*uh7GwcueO`xl>fgfl= Date: Tue, 20 Mar 2018 14:38:56 +0100 Subject: [PATCH 0105/3000] mssql: adds test for time should be ms in table mode --- pkg/tsdb/mssql/mssql_test.go | 291 +++++++++++++++++++---------------- 1 file changed, 159 insertions(+), 132 deletions(-) diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 88b35b1aa2c..7ac135ec2f5 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -41,40 +41,40 @@ func TestMSSQL(t *testing.T) { Convey("Given a table with different native data types", func() { sql := ` - IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL - DROP TABLE dbo.[mssql_types] + IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL + DROP TABLE dbo.[mssql_types] - CREATE TABLE [mssql_types] ( - c_bit bit, + CREATE TABLE [mssql_types] ( + c_bit bit, - c_tinyint tinyint, - c_smallint smallint, - c_int int, - c_bigint bigint, + c_tinyint tinyint, + c_smallint smallint, + c_int int, + c_bigint bigint, - c_money money, - c_smallmoney smallmoney, - c_numeric numeric(10,5), - c_real real, - c_decimal decimal(10,2), - c_float float, + c_money money, + c_smallmoney smallmoney, + c_numeric numeric(10,5), + c_real real, + c_decimal decimal(10,2), + c_float float, - c_char char(10), - c_varchar varchar(10), - c_text text, + c_char char(10), + c_varchar varchar(10), + c_text text, - c_nchar nchar(12), - c_nvarchar nvarchar(12), - c_ntext ntext, + c_nchar nchar(12), + c_nvarchar nvarchar(12), + c_ntext ntext, - c_datetime datetime, - c_datetime2 datetime2, - c_smalldatetime smalldatetime, - c_date date, - c_time time, - c_datetimeoffset datetimeoffset - ) - ` + c_datetime datetime, + c_datetime2 datetime2, + c_smalldatetime smalldatetime, + c_date date, + c_time time, + c_datetimeoffset datetimeoffset + ) + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) @@ -87,14 +87,14 @@ func TestMSSQL(t *testing.T) { d2 := dt2.Format(dt2Format) sql = fmt.Sprintf(` - INSERT INTO [mssql_types] - SELECT - 1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12, - 1.11, 2.22, 3.33, - 'char10', 'varchar10', 'text', - N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺', - CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00') - `, d, d2, d, d, d, d2) + INSERT INTO [mssql_types] + SELECT + 1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12, + 1.11, 2.22, 3.33, + 'char10', 'varchar10', 'text', + N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺', + CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00') + `, d, d2, d, d, d, d2) _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -151,14 +151,14 @@ func TestMSSQL(t *testing.T) { Convey("Given a table with metrics that lacks data for some series ", func() { sql := ` - IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL - DROP TABLE dbo.[metric] + IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL + DROP TABLE dbo.[metric] - CREATE TABLE [metric] ( - time datetime, - value int - ) - ` + CREATE TABLE [metric] ( + time datetime, + value int + ) + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) @@ -189,9 +189,9 @@ func TestMSSQL(t *testing.T) { dtFormat := "2006-01-02 15:04:05.999999999" for _, s := range series { sql = fmt.Sprintf(` - INSERT INTO metric (time, value) - VALUES(CAST('%s' AS DATETIME), %d) - `, s.Time.Format(dtFormat), s.Value) + INSERT INTO metric (time, value) + VALUES(CAST('%s' AS DATETIME), %d) + `, s.Time.Format(dtFormat), s.Value) _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -306,16 +306,16 @@ func TestMSSQL(t *testing.T) { Convey("Given a table with metrics having multiple values and measurements", func() { sql := ` - IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL - DROP TABLE dbo.[metric_values] + IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL + DROP TABLE dbo.[metric_values] - CREATE TABLE [metric_values] ( - time datetime, - measurement nvarchar(100), - valueOne int, - valueTwo int, - ) - ` + CREATE TABLE [metric_values] ( + time datetime, + measurement nvarchar(100), + valueOne int, + valueTwo int, + ) + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) @@ -351,9 +351,9 @@ func TestMSSQL(t *testing.T) { dtFormat := "2006-01-02 15:04:05" for _, s := range series { sql = fmt.Sprintf(` - INSERT metric_values (time, measurement, valueOne, valueTwo) - VALUES(CAST('%s' AS DATETIME), '%s', %d, %d) - `, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo) + INSERT metric_values (time, measurement, valueOne, valueTwo) + VALUES(CAST('%s' AS DATETIME), '%s', %d, %d) + `, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo) _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -407,45 +407,45 @@ func TestMSSQL(t *testing.T) { Convey("Given a stored procedure that takes @from and @to in epoch time", func() { sql := ` - IF object_id('sp_test_epoch') IS NOT NULL - DROP PROCEDURE sp_test_epoch - ` + IF object_id('sp_test_epoch') IS NOT NULL + DROP PROCEDURE sp_test_epoch + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) sql = ` - CREATE PROCEDURE sp_test_epoch( - @from int, - @to int - ) AS - BEGIN - SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, - measurement + ' - value one' as metric, - avg(valueOne) as value - FROM - metric_values - WHERE - time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') - GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), - measurement - UNION ALL - SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, - measurement + ' - value two' as metric, - avg(valueTwo) as value - FROM - metric_values - WHERE - time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') - GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), - measurement - ORDER BY 1 - END - ` + CREATE PROCEDURE sp_test_epoch( + @from int, + @to int + ) AS + BEGIN + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + measurement + ' - value one' as metric, + avg(valueOne) as value + FROM + metric_values + WHERE + time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + measurement + UNION ALL + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + measurement + ' - value two' as metric, + avg(valueTwo) as value + FROM + metric_values + WHERE + time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + measurement + ORDER BY 1 + END + ` _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -456,10 +456,10 @@ func TestMSSQL(t *testing.T) { { Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `DECLARE - @from int = $__unixEpochFrom(), - @to int = $__unixEpochTo() + @from int = $__unixEpochFrom(), + @to int = $__unixEpochTo() - EXEC dbo.sp_test_epoch @from, @to`, + EXEC dbo.sp_test_epoch @from, @to`, "format": "time_series", }), RefId: "A", @@ -486,45 +486,45 @@ func TestMSSQL(t *testing.T) { Convey("Given a stored procedure that takes @from and @to in datetime", func() { sql := ` - IF object_id('sp_test_datetime') IS NOT NULL - DROP PROCEDURE sp_test_datetime - ` + IF object_id('sp_test_datetime') IS NOT NULL + DROP PROCEDURE sp_test_datetime + ` _, err := sess.Exec(sql) So(err, ShouldBeNil) sql = ` - CREATE PROCEDURE sp_test_datetime( - @from datetime, - @to datetime - ) AS - BEGIN - SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, - measurement + ' - value one' as metric, - avg(valueOne) as value - FROM - metric_values - WHERE - time >= @from AND time <= @to - GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), - measurement - UNION ALL - SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, - measurement + ' - value two' as metric, - avg(valueTwo) as value - FROM - metric_values - WHERE - time >= @from AND time <= @to - GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), - measurement - ORDER BY 1 - END - ` + CREATE PROCEDURE sp_test_datetime( + @from datetime, + @to datetime + ) AS + BEGIN + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + measurement + ' - value one' as metric, + avg(valueOne) as value + FROM + metric_values + WHERE + time >= @from AND time <= @to + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + measurement + UNION ALL + SELECT + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + measurement + ' - value two' as metric, + avg(valueTwo) as value + FROM + metric_values + WHERE + time >= @from AND time <= @to + GROUP BY + cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + measurement + ORDER BY 1 + END + ` _, err = sess.Exec(sql) So(err, ShouldBeNil) @@ -535,10 +535,10 @@ func TestMSSQL(t *testing.T) { { Model: simplejson.NewFromAny(map[string]interface{}{ "rawSql": `DECLARE - @from int = $__unixEpochFrom(), - @to int = $__unixEpochTo() + @from int = $__unixEpochFrom(), + @to int = $__unixEpochTo() - EXEC dbo.sp_test_epoch @from, @to`, + EXEC dbo.sp_test_epoch @from, @to`, "format": "time_series", }), RefId: "A", @@ -654,6 +654,33 @@ func TestMSSQL(t *testing.T) { So(err, ShouldBeNil) So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) }) + + Convey("When doing an annotation query with a time column in datetime format", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT DATEADD(s, time_sec, {d '1970-01-01'}) AS time, description as [text], tags FROM [event] WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", + "format": "table", + }), + RefId: "Tickets", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Tickets"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(float64), ShouldBeGreaterThan, 1000000000000) + }) }) }) } From 47215098a3f3b3016ba22296c2b65520c84423bc Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 20 Mar 2018 14:43:09 +0100 Subject: [PATCH 0106/3000] changed var to const, changed to string interpolation --- .../datasource/graphite/func_editor.ts | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/datasource/graphite/func_editor.ts b/public/app/plugins/datasource/graphite/func_editor.ts index 1a4c6d4313a..86135aef343 100644 --- a/public/app/plugins/datasource/graphite/func_editor.ts +++ b/public/app/plugins/datasource/graphite/func_editor.ts @@ -4,16 +4,17 @@ import $ from 'jquery'; import rst2html from 'rst2html'; export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { - var funcSpanTemplate = '{{func.def.name}}('; - var paramTemplate = ''; + const funcSpanTemplate = '{{func.def.name}}('; + const paramTemplate = + ''; - var funcControlsTemplate = - '
    ' + - '' + - '' + - '' + - '' + - '
    '; + const funcControlsTemplate = ` +
    + + + + +
    `; return { restrict: 'A', @@ -281,13 +282,11 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { element: e.target, position: 'bottom left', classNames: 'drop-popover drop-function-def', - template: - '
    ' + - '

    ' + - funcDef.name + - '

    ' + - rst2html(funcDef.description) + - '
    ', + template: ` +
    +

    ${funcDef.name}

    + ${rst2html(funcDef.description)} +
    `, openOn: 'click', }); } else { From f9acb4157b515c9dbb9e7f2b77b362dcf491e1fa Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 20 Mar 2018 15:26:41 +0100 Subject: [PATCH 0107/3000] Expose option to disable snippets --- public/app/core/components/code_editor/code_editor.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 8cbd888bb1b..886ae2a6407 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -21,6 +21,8 @@ * data-tab-size - Tab size, default is 2. * data-behaviours-enabled - Specifies whether to use behaviors or not. "Behaviors" in this case is the auto-pairing of * special characters, like quotation marks, parenthesis, or brackets. + * data-snippets-enabled - Specifies whether to use snippets or not. "Snippets" are small pieces of code that can be + * inserted via the completion box. * * Keybindings: * Ctrl-Enter (Command-Enter): run onChange() function @@ -49,6 +51,7 @@ const DEFAULT_MODE = 'text'; const DEFAULT_MAX_LINES = 10; const DEFAULT_TAB_SIZE = 2; const DEFAULT_BEHAVIOURS = true; +const DEFAULT_SNIPPETS = true; let editorTemplate = `
    `; @@ -59,6 +62,7 @@ function link(scope, elem, attrs) { let showGutter = attrs.showGutter !== undefined; let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; + let snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; // Initialize editor let aceElem = elem.get(0); @@ -143,7 +147,7 @@ function link(scope, elem, attrs) { codeEditor.setOptions({ enableBasicAutocompletion: true, enableLiveAutocompletion: true, - enableSnippets: true, + enableSnippets: snippetsEnabled, }); if (scope.getCompleter()) { From 0e159dada1be72b121c93cb2d682b890f44968e9 Mon Sep 17 00:00:00 2001 From: ilgizar Date: Tue, 20 Mar 2018 22:44:48 +0500 Subject: [PATCH 0108/3000] Allocated to a separate alignment block. Replaced the attribute of the second axis by the attribute of the axes. --- .../app/plugins/panel/graph/axes_editor.html | 20 +++++++++---------- public/app/plugins/panel/graph/graph.ts | 4 ++-- public/app/plugins/panel/graph/module.ts | 6 ++++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/panel/graph/axes_editor.html b/public/app/plugins/panel/graph/axes_editor.html index f17c9ce105f..9020bbe4446 100644 --- a/public/app/plugins/panel/graph/axes_editor.html +++ b/public/app/plugins/panel/graph/axes_editor.html @@ -31,16 +31,6 @@
    -
    -
    - -
    -
    - - -
    -
    -
    @@ -77,6 +67,16 @@
    +
    +
    +
    Y-Axes
    + +
    + + +
    +
    +
    diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 713d7079152..dc801a1b33f 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -158,8 +158,8 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function processRangeHook(plot) { var yaxis = plot.getYAxes(); - if (yaxis.length > 1 && panel.yaxes[1].alignment) { - var align = panel.yaxes[1].align || 0; + if (yaxis.length > 1 && panel.yaxis.alignment) { + var align = panel.yaxis.align || 0; alignYLevel(yaxis, parseFloat(align)); } } diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 7e7b270bd61..2fe1ecc8684 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -46,8 +46,6 @@ class GraphCtrl extends MetricsPanelCtrl { min: null, max: null, format: 'short', - alignment: false, - align: 0, }, ], xaxis: { @@ -57,6 +55,10 @@ class GraphCtrl extends MetricsPanelCtrl { values: [], buckets: null, }, + yaxis: { + alignment: false, + align: 0, + }, // show/hide lines lines: true, // fill factor From 70630d742ef564307bc52d1aa2a4ada6f259744c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 16:40:40 +0100 Subject: [PATCH 0109/3000] snapshots: removes errors for empty values in ViewStore Occurs when opening a snapshot. --- public/app/stores/ViewStore/ViewStore.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/app/stores/ViewStore/ViewStore.ts b/public/app/stores/ViewStore/ViewStore.ts index d00bf3c429f..ba966a194d8 100644 --- a/public/app/stores/ViewStore/ViewStore.ts +++ b/public/app/stores/ViewStore/ViewStore.ts @@ -26,7 +26,9 @@ export const ViewStore = types function updateQuery(query: any) { self.query.clear(); for (let key of Object.keys(query)) { - self.query.set(key, query[key]); + if (query[key]) { + self.query.set(key, query[key]); + } } } @@ -34,7 +36,9 @@ export const ViewStore = types function updateRouteParams(routeParams: any) { self.routeParams.clear(); for (let key of Object.keys(routeParams)) { - self.routeParams.set(key, routeParams[key]); + if (routeParams[key]) { + self.routeParams.set(key, routeParams[key]); + } } } From 92388f7faf80bcc68f496a8633e8d5b4748e0cf8 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 19:31:01 +0100 Subject: [PATCH 0110/3000] session: update defaults for ConnMaxLifetime to be the same as the 5.0.3 release defaults --- conf/defaults.ini | 3 +++ pkg/setting/setting.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 557c5e49ee1..11d173d955d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -128,6 +128,9 @@ cookie_secure = false session_life_time = 86400 gc_interval_time = 86400 +# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours) +conn_max_lifetime = 14400 + #################################### Data proxy ########################### [dataproxy] diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c19043c69d0..5b79e866964 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -636,7 +636,7 @@ func readSessionConfig() { SessionOptions.CookiePath = "/" } - SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(0) + SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(14400) } func initLogging() { From a472d38fbf97dc721137ac7347c759e8e3f8db88 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 20 Mar 2018 21:33:54 +0300 Subject: [PATCH 0111/3000] snapshot: fix legend rendering bug --- public/app/features/panel/metrics_panel_ctrl.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 373211611d8..6e7f326dc08 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -78,8 +78,11 @@ class MetricsPanelCtrl extends PanelCtrl { data = data.data; } - this.events.emit('data-snapshot-load', data); - return; + // Defer panel rendering till the next digest cycle. + // For some reason snapshot panels don't init at this time, so this helps to avoid rendering issues. + return this.$timeout(() => { + this.events.emit('data-snapshot-load', data); + }); } // // ignore if we have data stream From e5df179c7cf41a986123980a594fe68f1668f86c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 20 Mar 2018 20:38:20 +0100 Subject: [PATCH 0112/3000] docs: spelling --- docs/sources/features/datasources/mssql.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index 325e1fe3596..71baf4bd050 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -86,6 +86,7 @@ We plan to add many more macros. If you have suggestions for what macros you wou The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed. ## Table queries + If the `Format as` query option is set to `Table` then you can basically do any type of SQL query. The table panel will automatically show the results of whatever columns & rows your query returns. **Example database table:** @@ -142,7 +143,7 @@ The resulting table panel: ## Time series queries -If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you ommit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. +If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. **Example database table:** @@ -377,16 +378,19 @@ ORDER BY 1 ``` ## Stored procedure support + Stored procedures have been verified to work. However, please note that we haven't done anything special to support this why there may exist edge cases where it won't work as you would expect. Stored procedures should be supported in table, time series and annotation queries as long as you use the same naming of columns and return data in the same format as describe above under respective section. Please note that any macro function will not work inside a stored procedure. ### Examples + {{< docs-imagebox img="/img/docs/v51/mssql_metrics_graph.png" class="docs-image--no-shadow docs-image--right" >}} For the following examples the database table defined in [Time series queries](#time-series-queries). Let's say that we want to visualize 4 series in a graph panel, i.e. all combinations of columns `valueOne`, `valueTwo` and `measurement`. Graph panel to the right visualizes what we want to achieve. To solve this we actually need to use two queries: **First query:** + ```sql SELECT $__timeGroup(time, '5m') as time, @@ -417,6 +421,7 @@ ORDER BY 1 ``` #### Stored procedure using time in epoch format + We can define a stored procedure that will return all data we need to render 4 series in a graph panel like above. In this case the stored procedure accepts two parameters `@from` and `@to` of `int` data types which should be a timerange (from-to) in epoch format which will be used to filter the data to return from the stored procedure. @@ -468,6 +473,7 @@ EXEC dbo.sp_test_epoch @from, @to ``` #### Stored procedure using time in datetime format + We can define a stored procedure that will return all data we need to render 4 series in a graph panel like above. In this case the stored procedure accepts two parameters `@from` and `@to` of `datetime` data types which should be a timerange (from-to) which will be used to filter the data to return from the stored procedure. @@ -521,5 +527,5 @@ EXEC dbo.sp_test_datetime @from, @to ## Alerting -Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule +Time series queries should work in alerting conditions. Table formatted queries are not yet supported in alert rule conditions. From 5d23e7710ba0be0d8b501a493d65211cdf9ea366 Mon Sep 17 00:00:00 2001 From: Thibault Chataigner Date: Fri, 16 Feb 2018 17:00:13 +0100 Subject: [PATCH 0113/3000] Alerting: Add retry mechanism and its unitests Signed-off-by: Thibault Chataigner --- pkg/services/alerting/engine.go | 89 ++++++++++++++------ pkg/services/alerting/engine_test.go | 118 +++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 pkg/services/alerting/engine_test.go diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 4448a5cb978..49a11b54c1d 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -86,17 +86,63 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error { case <-grafanaCtx.Done(): return dispatcherGroup.Wait() case job := <-e.execQueue: - dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) }) + dispatcherGroup.Go(func() error { return e.processJobWithRetry(alertCtx, job) }) } } } var ( unfinishedWorkTimeout time.Duration = time.Second * 5 - alertTimeout time.Duration = time.Second * 30 + // TODO: Make alertTimeout and alertMaxAttempts configurable in the config file. + alertTimeout time.Duration = time.Second * 30 + alertMaxAttempts int = 3 ) -func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error { +func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error { + defer func() { + if err := recover(); err != nil { + e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1)) + } + }() + + cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + attemptChan := make(chan int, 1) + + // Initialize with first attemptID=1 + attemptChan <- 1 + job.Running = true + + for { + select { + case <-grafanaCtx.Done(): + // In case grafana server context is cancel, let a chance to job processing + // to finish gracefully - by waiting a timeout duration - before forcing its end. + unfinishedWorkTimer := time.NewTimer(unfinishedWorkTimeout) + select { + case <-unfinishedWorkTimer.C: + return e.endJob(grafanaCtx.Err(), cancelChan, job) + case <-attemptChan: + return e.endJob(nil, cancelChan, job) + } + case attemptID, more := <-attemptChan: + if !more { + return e.endJob(nil, cancelChan, job) + } + go e.processJob(attemptID, attemptChan, cancelChan, job) + } + } +} + +func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error { + job.Running = false + close(cancelChan) + for cancelFn := range cancelChan { + cancelFn() + } + return err +} + +func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) { defer func() { if err := recover(); err != nil { e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1)) @@ -104,14 +150,13 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error { }() alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout) + cancelChan <- cancelFn span := opentracing.StartSpan("alert execution") alertCtx = opentracing.ContextWithSpan(alertCtx, span) - job.Running = true evalContext := NewEvalContext(alertCtx, job.Rule) evalContext.Ctx = alertCtx - done := make(chan struct{}) go func() { defer func() { if err := recover(); err != nil { @@ -122,43 +167,35 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error { tlog.String("message", "failed to execute alert rule. panic was recovered."), ) span.Finish() - close(done) + close(attemptChan) } }() e.evalHandler.Eval(evalContext) - e.resultHandler.Handle(evalContext) span.SetTag("alertId", evalContext.Rule.Id) span.SetTag("dashboardId", evalContext.Rule.DashboardId) span.SetTag("firing", evalContext.Firing) span.SetTag("nodatapoints", evalContext.NoDataFound) + span.SetTag("attemptID", attemptID) + if evalContext.Error != nil { ext.Error.Set(span, true) span.LogFields( tlog.Error(evalContext.Error), - tlog.String("message", "alerting execution failed"), + tlog.String("message", "alerting execution attempt failed"), ) + if attemptID < alertMaxAttempts { + span.Finish() + e.log.Debug("Job Execution attempt triggered retry", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) + attemptChan <- (attemptID + 1) + return + } } + e.resultHandler.Handle(evalContext) span.Finish() - close(done) + e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) + close(attemptChan) }() - - var err error = nil - select { - case <-grafanaCtx.Done(): - select { - case <-time.After(unfinishedWorkTimeout): - cancelFn() - err = grafanaCtx.Err() - case <-done: - } - case <-done: - } - - e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing) - job.Running = false - cancelFn() - return err } diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go new file mode 100644 index 00000000000..64f954c6dd5 --- /dev/null +++ b/pkg/services/alerting/engine_test.go @@ -0,0 +1,118 @@ +package alerting + +import ( + "context" + "errors" + "math" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +type FakeEvalHandler struct { + SuccessCallID int // 0 means never sucess + CallNb int +} + +func NewFakeEvalHandler(successCallID int) *FakeEvalHandler { + return &FakeEvalHandler{ + SuccessCallID: successCallID, + CallNb: 0, + } +} + +func (handler *FakeEvalHandler) Eval(evalContext *EvalContext) { + handler.CallNb++ + if handler.CallNb != handler.SuccessCallID { + evalContext.Error = errors.New("Fake evaluation failure") + } +} + +type FakeResultHandler struct{} + +func (handler *FakeResultHandler) Handle(evalContext *EvalContext) error { + return nil +} + +func TestEngineProcessJob(t *testing.T) { + Convey("Alerting engine job processing", t, func() { + engine := NewEngine() + engine.resultHandler = &FakeResultHandler{} + job := &Job{Running: true, Rule: &Rule{}} + + Convey("Should trigger retry if needed", func() { + + Convey("error + not last attempt -> retry", func() { + engine.evalHandler = NewFakeEvalHandler(0) + + for i := 1; i < alertMaxAttempts; i++ { + attemptChan := make(chan int, 1) + cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + + engine.processJob(i, attemptChan, cancelChan, job) + nextAttemptID, more := <-attemptChan + + So(nextAttemptID, ShouldEqual, i+1) + So(more, ShouldEqual, true) + So(<-cancelChan, ShouldNotBeNil) + } + }) + + Convey("error + last attempt -> no retry", func() { + engine.evalHandler = NewFakeEvalHandler(0) + attemptChan := make(chan int, 1) + cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + + engine.processJob(alertMaxAttempts, attemptChan, cancelChan, job) + nextAttemptID, more := <-attemptChan + + So(nextAttemptID, ShouldEqual, 0) + So(more, ShouldEqual, false) + So(<-cancelChan, ShouldNotBeNil) + }) + + Convey("no error -> no retry", func() { + engine.evalHandler = NewFakeEvalHandler(1) + attemptChan := make(chan int, 1) + cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + + engine.processJob(1, attemptChan, cancelChan, job) + nextAttemptID, more := <-attemptChan + + So(nextAttemptID, ShouldEqual, 0) + So(more, ShouldEqual, false) + So(<-cancelChan, ShouldNotBeNil) + }) + }) + + Convey("Should trigger as many retries as needed", func() { + + Convey("never sucess -> max retries number", func() { + expectedAttempts := alertMaxAttempts + evalHandler := NewFakeEvalHandler(0) + engine.evalHandler = evalHandler + + engine.processJobWithRetry(context.TODO(), job) + So(evalHandler.CallNb, ShouldEqual, expectedAttempts) + }) + + Convey("always sucess -> never retry", func() { + expectedAttempts := 1 + evalHandler := NewFakeEvalHandler(1) + engine.evalHandler = evalHandler + + engine.processJobWithRetry(context.TODO(), job) + So(evalHandler.CallNb, ShouldEqual, expectedAttempts) + }) + + Convey("some errors before sucess -> some retries", func() { + expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2)) + evalHandler := NewFakeEvalHandler(expectedAttempts) + engine.evalHandler = evalHandler + + engine.processJobWithRetry(context.TODO(), job) + So(evalHandler.CallNb, ShouldEqual, expectedAttempts) + }) + }) + }) +} From f142548969ca7b0e5e44dd024c3aefb27ed4983f Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 20 Mar 2018 22:21:24 +0100 Subject: [PATCH 0114/3000] dataproxy: adds dashboardid and panelid as tags closes #11315 --- pkg/api/pluginproxy/ds_proxy.go | 11 +++++++++++ public/app/features/panel/metrics_panel_ctrl.ts | 1 + public/app/plugins/datasource/graphite/datasource.ts | 2 ++ 3 files changed, 14 insertions(+) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index b861a344c75..e2f9dd7381f 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -89,6 +89,9 @@ func (proxy *DataSourceProxy) HandleRequest() { span.SetTag("user_id", proxy.ctx.SignedInUser.UserId) span.SetTag("org_id", proxy.ctx.SignedInUser.OrgId) + proxy.addTraceFromHeaderValue(span, "X-Panel-Id", "panel_id") + proxy.addTraceFromHeaderValue(span, "X-Dashboard-Id", "dashboard_id") + opentracing.GlobalTracer().Inject( span.Context(), opentracing.HTTPHeaders, @@ -98,6 +101,14 @@ func (proxy *DataSourceProxy) HandleRequest() { proxy.ctx.Resp.Header().Del("Set-Cookie") } +func (proxy *DataSourceProxy) addTraceFromHeaderValue(span opentracing.Span, headerName string, tagName string) { + panelId := proxy.ctx.Req.Header.Get(headerName) + dashId, err := strconv.Atoi(panelId) + if err == nil { + span.SetTag(tagName, dashId) + } +} + func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { return func(req *http.Request) { req.URL.Scheme = proxy.targetUrl.Scheme diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 373211611d8..3dd0406d3d3 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -222,6 +222,7 @@ class MetricsPanelCtrl extends PanelCtrl { var metricsQuery = { timezone: this.dashboard.getTimezone(), panelId: this.panel.id, + dashboardId: this.dashboard.id, range: this.range, rangeRaw: this.range.raw, interval: this.interval, diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index f02945b8969..335cb400834 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -50,6 +50,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv data: params.join('&'), headers: { 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Dashboard-Id': options.dashboardId, // enables distributed tracing in ds_proxy + 'X-Panel-Id': options.panelId, // enables distributed tracing in ds_proxy }, }; From 7a4475fbf3d7af7d2e495ecfbdc3245d08d3cfde Mon Sep 17 00:00:00 2001 From: Jordan Hamel Date: Tue, 20 Mar 2018 16:55:57 -0700 Subject: [PATCH 0115/3000] update email default year and name from 2016 grafana and raintank to 2018 Grafana Labs --- emails/templates/layouts/default.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emails/templates/layouts/default.html b/emails/templates/layouts/default.html index 07eb32874c7..ca54acdd206 100644 --- a/emails/templates/layouts/default.html +++ b/emails/templates/layouts/default.html @@ -143,7 +143,7 @@ td[class="stack-column-center"] {

    Sent by Grafana v[[.BuildVersion]] -
    © 2016 Grafana and raintank +
    © 2018 Grafana Labs

    From f41f2089a29e8f1365066155334ed90276769265 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Mar 2018 08:53:47 +0100 Subject: [PATCH 0116/3000] docs: details about provisioning elastic closes #11292 --- docs/sources/administration/provisioning.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 135973df52a..a8cb9fe3023 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -133,12 +133,18 @@ datasources: editable: false ``` +#### Extra info per datasource + +| Datasource | Misc | +| ---- | ---- | +| Elasticserach | Elasticsearch uses the `database` property to configure the index for a datasource | + #### Json data Since not all datasources have the same configuration settings we only have the most common ones as fields. The rest should be stored as a json blob in the `json_data` field. Here are the most common settings that the core datasources use. -| Name | Type | Datasource |Description | -| ----| ---- | ---- | --- | +| Name | Type | Datasource | Description | +| ---- | ---- | ---- | ---- | | tlsAuth | boolean | *All* | Enable TLS authentication using client cert configured in secure json data | | tlsAuthWithCACert | boolean | *All* | Enable TLS authtication using CA cert | | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | From 3cb0bc3da1216fd76b22c16e00b4b02d54b1a3a9 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 20 Mar 2018 19:40:10 +0100 Subject: [PATCH 0117/3000] sql datasource: extract common logic for converting time column to epoch time in ms --- pkg/tsdb/sql_engine.go | 28 ++++++++++++++++++++++ pkg/tsdb/sql_engine_test.go | 46 +++++++++++++++++++++++++++++++++++++ pkg/tsdb/time_range.go | 10 ++++++++ 3 files changed, 84 insertions(+) create mode 100644 pkg/tsdb/sql_engine_test.go diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 7ea0682235f..16370a4ea7f 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -3,6 +3,7 @@ package tsdb import ( "context" "sync" + "time" "github.com/go-xorm/core" "github.com/go-xorm/xorm" @@ -133,3 +134,30 @@ func (e *DefaultSqlEngine) Query( return result, nil } + +// ConvertTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds +// to make native datetime types and epoch dates work in annotation and table queries. +func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) { + if timeIndex >= 0 { + switch value := values[timeIndex].(type) { + case time.Time: + values[timeIndex] = EpochPrecisionToMs(float64(value.Unix())) + case *time.Time: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(float64((*value).Unix())) + } + case int64: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *int64: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case float64: + values[timeIndex] = EpochPrecisionToMs(value) + case *float64: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(*value) + } + } + } +} diff --git a/pkg/tsdb/sql_engine_test.go b/pkg/tsdb/sql_engine_test.go new file mode 100644 index 00000000000..48aac2c4d45 --- /dev/null +++ b/pkg/tsdb/sql_engine_test.go @@ -0,0 +1,46 @@ +package tsdb + +import ( + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestSqlEngine(t *testing.T) { + Convey("SqlEngine", t, func() { + Convey("Given row values with time columns when converting them", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + fixtures := make([]interface{}, 8) + fixtures[0] = dt + fixtures[1] = dt.Unix() * 1000 + fixtures[2] = dt.Unix() + fixtures[3] = float64(dt.Unix() * 1000) + fixtures[4] = float64(dt.Unix()) + + var nilDt *time.Time + var nilInt64 *int64 + var nilFloat64 *float64 + fixtures[5] = nilDt + fixtures[6] = nilInt64 + fixtures[7] = nilFloat64 + + for i := range fixtures { + ConvertSqlTimeColumnToEpochMs(fixtures, i) + } + + Convey("Should convert sql time columns to epoch time in ms ", func() { + expected := float64(dt.Unix() * 1000) + So(fixtures[0].(float64), ShouldEqual, expected) + So(fixtures[1].(int64), ShouldEqual, expected) + So(fixtures[2].(int64), ShouldEqual, expected) + So(fixtures[3].(float64), ShouldEqual, expected) + So(fixtures[4].(float64), ShouldEqual, expected) + + So(fixtures[5], ShouldBeNil) + So(fixtures[6], ShouldBeNil) + So(fixtures[7], ShouldBeNil) + }) + }) + }) +} diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go index fd797bf731a..fd0cb3f8e82 100644 --- a/pkg/tsdb/time_range.go +++ b/pkg/tsdb/time_range.go @@ -88,3 +88,13 @@ func (tr *TimeRange) ParseTo() (time.Time, error) { return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To) } + +// EpochPrecisionToMs converts epoch precision to millisecond, if needed. +// Only seconds to milliseconds supported right now +func EpochPrecisionToMs(value float64) float64 { + if int64(value)/1e10 == 0 { + return float64(value * 1e3) + } + + return float64(value) +} From 519fd8b2bacb8fdec02f47ffc97b5b12b7a74a38 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 21 Mar 2018 13:16:59 +0100 Subject: [PATCH 0118/3000] graphite: adds more traces for alerting --- pkg/tsdb/graphite/graphite.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 73b173813af..2960ba0edc4 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -28,12 +28,9 @@ func NewGraphiteExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, return &GraphiteExecutor{}, nil } -var ( - glog log.Logger -) +var glog = log.New("tsdb.graphite") func init() { - glog = log.New("tsdb.graphite") tsdb.RegisterTsdbQueryEndpoint("graphite", NewGraphiteExecutor) } @@ -52,6 +49,7 @@ func (e *GraphiteExecutor) Query(ctx context.Context, dsInfo *models.DataSource, } for _, query := range tsdbQuery.Queries { + glog.Info("graphite", "query", query.Model) if fullTarget, err := query.Model.Get("targetFull").String(); err == nil { target = fixIntervalFormat(fullTarget) } else { @@ -79,6 +77,9 @@ func (e *GraphiteExecutor) Query(ctx context.Context, dsInfo *models.DataSource, span.SetTag("target", target) span.SetTag("from", from) span.SetTag("until", until) + span.SetTag("datasource_id", dsInfo.Id) + span.SetTag("org_id", dsInfo.OrgId) + defer span.Finish() opentracing.GlobalTracer().Inject( From 624dac16fa2807ca551ba528389500ff747df72d Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 21 Mar 2018 13:23:50 +0100 Subject: [PATCH 0119/3000] docs: add variable regex examples (#11327) --- docs/sources/reference/templating.md | 69 +++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 3a15b4ed7d1..f9e16e26610 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -1,6 +1,6 @@ +++ title = "Variables" -keywords = ["grafana", "templating", "documentation", "guide"] +keywords = ["grafana", "templating", "documentation", "guide", "template", "variable"] type = "docs" [menu.docs] name = "Variables" @@ -80,6 +80,73 @@ Option | Description *Regex* | Regex to filter or capture specific parts of the names return by your data source query. Optional. *Sort* | Define sort order for options in dropdown. **Disabled** means that the order of options returned by your data source query will be used. +#### Using regex to filter/modify values in the Variable dropdown + +Using the Regex Query Option, you filter the list of options returned by the Variable query or modify the options returned. + +Examples of filtering on the following list of options: + +```text +backend_01 +backend_02 +backend_03 +backend_04 +``` + +##### Filter so that only the options that end with `01` or `02` are returned: + +Regex: + +```regex +/.*[01|02]/ +``` + +Result: + +```text +backend_01 +backend_02 +``` + +##### Filter and modify the options using a regex capture group to return part of the text: + +Regex: + +```regex +/.*(01|02)/ +``` + +Result: + +```text +01 +02 +``` + +#### Filter and modify - Prometheus Example + +List of options: + +```text +up{instance="demo.robustperception.io:9090",job="prometheus"} 1 1521630638000 +up{instance="demo.robustperception.io:9093",job="alertmanager"} 1 1521630638000 +up{instance="demo.robustperception.io:9100",job="node"} 1 1521630638000 +``` + +Regex: + +```regex +/.*instance="([^"]*).*/ +``` + +Result: + +```text +demo.robustperception.io:9090 +demo.robustperception.io:9093 +demo.robustperception.io:9100 +``` + ### Query expressions The query expressions are different for each data source. From fc2d1d6ca913df0c0a0a9d2f62f27aace755eace Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Mar 2018 17:08:25 +0300 Subject: [PATCH 0120/3000] fix dashboard version cleanup on large datasets --- pkg/services/sqlstore/dashboard_version.go | 39 ++++++++----------- .../sqlstore/dashboard_version_test.go | 2 +- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 547f62628f3..3644af77355 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -1,7 +1,7 @@ package sqlstore import ( - "strings" + "fmt" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" @@ -69,36 +69,31 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error { func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return inTransaction(func(sess *DBSession) error { - versions := []DashboardVersionExp{} versionsToKeep := setting.DashboardVersionsToKeep - if versionsToKeep < 1 { versionsToKeep = 1 } - err := sess.Table("dashboard_version"). - Select("dashboard_version.id, dashboard_version.version, dashboard_version.dashboard_id"). - Where(`dashboard_id IN ( - SELECT dashboard_id FROM dashboard_version - GROUP BY dashboard_id HAVING COUNT(dashboard_version.id) > ? - )`, versionsToKeep). - Desc("dashboard_version.dashboard_id", "dashboard_version.version"). - Find(&versions) + // Idea of this query is finding version IDs to delete based on formula: + // min_version_to_keep = min_version + (versions_count - versions_to_keep) + // where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep + // versions, but in some cases (when versions are sparse) this number may be more. + versionIdsToDeleteSybqueryTemplate := `SELECT id + FROM dashboard_version, ( + SELECT dashboard_id, count(version) as count, min(version) as min + FROM dashboard_version + GROUP BY dashboard_id + ) AS vtd + WHERE dashboard_version.dashboard_id=vtd.dashboard_id + AND version < vtd.min + vtd.count - %v` + versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSybqueryTemplate, versionsToKeep) + deleteExpiredSql := fmt.Sprintf(`DELETE FROM dashboard_version WHERE id IN (%s)`, versionIdsToDeleteSubquery) + expiredResponse, err := sess.Exec(deleteExpiredSql) if err != nil { return err } - - // Keep last versionsToKeep versions and delete other - versionIdsToDelete := getVersionIDsToDelete(versions, versionsToKeep) - if len(versionIdsToDelete) > 0 { - deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)` - expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...) - if err != nil { - return err - } - cmd.DeletedRows, _ = expiredResponse.RowsAffected() - } + cmd.DeletedRows, _ = expiredResponse.RowsAffected() return nil }) diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index 1b74e7847c4..151dc7c4be2 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -136,7 +136,7 @@ func TestDeleteExpiredVersions(t *testing.T) { err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{}) So(err, ShouldBeNil) - query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} + query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWrite} GetDashboardVersions(&query) So(len(query.Result), ShouldEqual, versionsToWrite) From f976b690ca4a208c827077eabf4d2d7e856ba076 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Mar 2018 20:26:27 +0300 Subject: [PATCH 0121/3000] limit number of rows deleted by dashboard version cleanup --- pkg/services/sqlstore/dashboard_version.go | 53 ++++++++-------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 3644af77355..754bcd504a7 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -2,6 +2,7 @@ package sqlstore import ( "fmt" + "strings" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" @@ -69,6 +70,8 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error { func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return inTransaction(func(sess *DBSession) error { + const MAX_VERSIONS_TO_DELETE = 100 + versionsToKeep := setting.DashboardVersionsToKeep if versionsToKeep < 1 { versionsToKeep = 1 @@ -83,12 +86,25 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { SELECT dashboard_id, count(version) as count, min(version) as min FROM dashboard_version GROUP BY dashboard_id - ) AS vtd - WHERE dashboard_version.dashboard_id=vtd.dashboard_id + ) AS vtd + WHERE dashboard_version.dashboard_id=vtd.dashboard_id AND version < vtd.min + vtd.count - %v` versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSybqueryTemplate, versionsToKeep) - deleteExpiredSql := fmt.Sprintf(`DELETE FROM dashboard_version WHERE id IN (%s)`, versionIdsToDeleteSubquery) + versions := []string{} + err := sess.SQL(versionIdsToDeleteSubquery).Find(&versions) + if err != nil { + return err + } + + // Don't delete more than MAX_VERSIONS_TO_DELETE version per time + limit := MAX_VERSIONS_TO_DELETE + if len(versions) < MAX_VERSIONS_TO_DELETE { + limit = len(versions) + } + versions = versions[:limit] + + deleteExpiredSql := fmt.Sprintf(`DELETE FROM dashboard_version WHERE id IN (%s)`, strings.Join(versions, `,`)) expiredResponse, err := sess.Exec(deleteExpiredSql) if err != nil { return err @@ -98,34 +114,3 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return nil }) } - -// Short version of DashboardVersion for getting expired versions -type DashboardVersionExp struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - Version int `json:"version"` -} - -func getVersionIDsToDelete(versions []DashboardVersionExp, versionsToKeep int) []interface{} { - versionIds := make([]interface{}, 0) - - if len(versions) == 0 { - return versionIds - } - - currentDashboard := versions[0].DashboardId - count := 0 - for _, v := range versions { - if v.DashboardId == currentDashboard { - count++ - } else { - count = 1 - currentDashboard = v.DashboardId - } - if count > versionsToKeep { - versionIds = append(versionIds, v.Id) - } - } - - return versionIds -} From 3f85fcce2ddc0ff81f83565373d218fdef2dfb3f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Mar 2018 21:01:15 +0300 Subject: [PATCH 0122/3000] refactor: dashboard version cleanup --- pkg/services/sqlstore/dashboard_version.go | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 754bcd504a7..54c858df7de 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -81,7 +81,7 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { // min_version_to_keep = min_version + (versions_count - versions_to_keep) // where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep // versions, but in some cases (when versions are sparse) this number may be more. - versionIdsToDeleteSybqueryTemplate := `SELECT id + versionIdsToDeleteSubqueryTemplate := `SELECT id FROM dashboard_version, ( SELECT dashboard_id, count(version) as count, min(version) as min FROM dashboard_version @@ -90,26 +90,28 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { WHERE dashboard_version.dashboard_id=vtd.dashboard_id AND version < vtd.min + vtd.count - %v` - versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSybqueryTemplate, versionsToKeep) - versions := []string{} - err := sess.SQL(versionIdsToDeleteSubquery).Find(&versions) + versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSubqueryTemplate, versionsToKeep) + var versionIdsToDelete []interface{} + err := sess.SQL(versionIdsToDeleteSubquery).Find(&versionIdsToDelete) if err != nil { return err } // Don't delete more than MAX_VERSIONS_TO_DELETE version per time limit := MAX_VERSIONS_TO_DELETE - if len(versions) < MAX_VERSIONS_TO_DELETE { - limit = len(versions) + if len(versionIdsToDelete) < MAX_VERSIONS_TO_DELETE { + limit = len(versionIdsToDelete) } - versions = versions[:limit] + versionIdsToDelete = versionIdsToDelete[:limit] - deleteExpiredSql := fmt.Sprintf(`DELETE FROM dashboard_version WHERE id IN (%s)`, strings.Join(versions, `,`)) - expiredResponse, err := sess.Exec(deleteExpiredSql) - if err != nil { - return err + if len(versionIdsToDelete) > 0 { + deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)` + expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...) + if err != nil { + return err + } + cmd.DeletedRows, _ = expiredResponse.RowsAffected() } - cmd.DeletedRows, _ = expiredResponse.RowsAffected() return nil }) From 2ade0881b164437fb68870fdd1ae43fa66612923 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 21 Mar 2018 22:48:17 +0300 Subject: [PATCH 0123/3000] minor refactor of dashboard version cleanup --- pkg/services/sqlstore/dashboard_version.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index 54c858df7de..d91b5727545 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -1,7 +1,6 @@ package sqlstore import ( - "fmt" "strings" "github.com/grafana/grafana/pkg/bus" @@ -81,18 +80,17 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { // min_version_to_keep = min_version + (versions_count - versions_to_keep) // where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep // versions, but in some cases (when versions are sparse) this number may be more. - versionIdsToDeleteSubqueryTemplate := `SELECT id + versionIdsToDeleteSubquery := `SELECT id FROM dashboard_version, ( SELECT dashboard_id, count(version) as count, min(version) as min FROM dashboard_version GROUP BY dashboard_id ) AS vtd WHERE dashboard_version.dashboard_id=vtd.dashboard_id - AND version < vtd.min + vtd.count - %v` + AND version < vtd.min + vtd.count - ?` - versionIdsToDeleteSubquery := fmt.Sprintf(versionIdsToDeleteSubqueryTemplate, versionsToKeep) var versionIdsToDelete []interface{} - err := sess.SQL(versionIdsToDeleteSubquery).Find(&versionIdsToDelete) + err := sess.SQL(versionIdsToDeleteSubquery, versionsToKeep).Find(&versionIdsToDelete) if err != nil { return err } From 38bdb8dfb38b0cfc42c5f28aef0dbd5f4fb0bb5a Mon Sep 17 00:00:00 2001 From: Thibault Chataigner Date: Wed, 21 Mar 2018 20:48:29 +0100 Subject: [PATCH 0124/3000] Alerting: move getNewState to EvalContext This fix alert state update when several evaluation attempts are needed Signed-off-by: Thibault Chataigner --- pkg/services/alerting/engine.go | 1 + pkg/services/alerting/eval_context.go | 31 ++++++++++ pkg/services/alerting/eval_context_test.go | 69 ++++++++++++++++++++- pkg/services/alerting/eval_handler.go | 34 ----------- pkg/services/alerting/eval_handler_test.go | 70 ---------------------- pkg/services/alerting/test_rule.go | 1 + 6 files changed, 101 insertions(+), 105 deletions(-) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 49a11b54c1d..a6f97333d1b 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -193,6 +193,7 @@ func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan } } + evalContext.Rule.State = evalContext.GetNewState() e.resultHandler.Handle(evalContext) span.Finish() e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d598203d675..91d0e179a14 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -112,3 +112,34 @@ func (c *EvalContext) GetRuleUrl() (string, error) { return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil } } + +func (c *EvalContext) GetNewState() m.AlertStateType { + if c.Error != nil { + c.log.Error("Alert Rule Result Error", + "ruleId", c.Rule.Id, + "name", c.Rule.Name, + "error", c.Error, + "changing state to", c.Rule.ExecutionErrorState.ToAlertState()) + + if c.Rule.ExecutionErrorState == m.ExecutionErrorKeepState { + return c.PrevAlertState + } + return c.Rule.ExecutionErrorState.ToAlertState() + + } else if c.Firing { + return m.AlertStateAlerting + + } else if c.NoDataFound { + c.log.Info("Alert Rule returned no data", + "ruleId", c.Rule.Id, + "name", c.Rule.Name, + "changing state to", c.Rule.NoDataState.ToAlertState()) + + if c.Rule.NoDataState == m.NoDataKeepState { + return c.PrevAlertState + } + return c.Rule.NoDataState.ToAlertState() + } + + return m.AlertStateOK +} diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 019ca1ed01f..709eeee4e5e 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -2,6 +2,7 @@ package alerting import ( "context" + "fmt" "testing" "github.com/grafana/grafana/pkg/models" @@ -12,7 +13,7 @@ func TestAlertingEvalContext(t *testing.T) { Convey("Eval context", t, func() { ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}) - Convey("Should update alert state", func() { + Convey("Should update alert state when needed", func() { Convey("ok -> alerting", func() { ctx.PrevAlertState = models.AlertStateOK @@ -28,5 +29,71 @@ func TestAlertingEvalContext(t *testing.T) { So(ctx.ShouldUpdateAlertState(), ShouldBeFalse) }) }) + + Convey("Should compute and replace properly new rule state", func() { + dummieError := fmt.Errorf("dummie error") + + Convey("ok -> alerting", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Firing = true + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) + }) + + Convey("ok -> error(alerting)", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Error = dummieError + ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) + }) + + Convey("ok -> error(keep_last)", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Error = dummieError + ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) + }) + + Convey("pending -> error(keep_last)", func() { + ctx.PrevAlertState = models.AlertStatePending + ctx.Error = dummieError + ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) + }) + + Convey("ok -> no_data(alerting)", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Rule.NoDataState = models.NoDataSetAlerting + ctx.NoDataFound = true + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) + }) + + Convey("ok -> no_data(keep_last)", func() { + ctx.PrevAlertState = models.AlertStateOK + ctx.Rule.NoDataState = models.NoDataKeepState + ctx.NoDataFound = true + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) + }) + + Convey("pending -> no_data(keep_last)", func() { + ctx.PrevAlertState = models.AlertStatePending + ctx.Rule.NoDataState = models.NoDataKeepState + ctx.NoDataFound = true + + ctx.Rule.State = ctx.GetNewState() + So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) + }) + }) }) } diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 457e02000fa..aa24efa77cd 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/models" ) type DefaultEvalHandler struct { @@ -66,40 +65,7 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { context.Firing = firing context.NoDataFound = noDataFound context.EndTime = time.Now() - context.Rule.State = e.getNewState(context) elapsedTime := context.EndTime.Sub(context.StartTime).Nanoseconds() / int64(time.Millisecond) metrics.M_Alerting_Execution_Time.Observe(float64(elapsedTime)) } - -// This should be move into evalContext once its been refactored. (Carl Bergquist) -func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType { - if evalContext.Error != nil { - handler.log.Error("Alert Rule Result Error", - "ruleId", evalContext.Rule.Id, - "name", evalContext.Rule.Name, - "error", evalContext.Error, - "changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState()) - - if evalContext.Rule.ExecutionErrorState == models.ExecutionErrorKeepState { - return evalContext.PrevAlertState - } else { - return evalContext.Rule.ExecutionErrorState.ToAlertState() - } - } else if evalContext.Firing { - return models.AlertStateAlerting - } else if evalContext.NoDataFound { - handler.log.Info("Alert Rule returned no data", - "ruleId", evalContext.Rule.Id, - "name", evalContext.Rule.Name, - "changing state to", evalContext.Rule.NoDataState.ToAlertState()) - - if evalContext.Rule.NoDataState == models.NoDataKeepState { - return evalContext.PrevAlertState - } else { - return evalContext.Rule.NoDataState.ToAlertState() - } - } - - return models.AlertStateOK -} diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index c942e24818f..a7c1f1e67fa 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -2,10 +2,8 @@ package alerting import ( "context" - "fmt" "testing" - "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -203,73 +201,5 @@ func TestAlertingEvaluationHandler(t *testing.T) { handler.Eval(context) So(context.NoDataFound, ShouldBeTrue) }) - - Convey("EvalHandler can replace alert state based for errors and no_data", func() { - ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}) - dummieError := fmt.Errorf("dummie error") - Convey("Should update alert state", func() { - - Convey("ok -> alerting", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Firing = true - - So(handler.getNewState(ctx), ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - - Convey("ok -> no_data(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataSetAlerting - ctx.NoDataFound = true - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = handler.getNewState(ctx) - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - }) - }) }) } diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index e3aa95e0ede..88418bff14e 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -53,6 +53,7 @@ func testAlertRule(rule *Rule) *EvalContext { context.IsTestRun = true handler.Eval(context) + context.Rule.State = context.GetNewState() return context } From 3898ea02e60c2811feec65e8ed4c32fea862b632 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 22 Mar 2018 02:22:58 +0100 Subject: [PATCH 0125/3000] adding created column --- pkg/api/annotations.go | 1 + pkg/services/annotations/annotations.go | 3 +++ pkg/services/sqlstore/annotation.go | 15 ++++++++++++++- .../sqlstore/migrations/annotation_mig.go | 10 ++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index fb75e0bf129..e5a97f340bf 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -24,6 +24,7 @@ func GetAnnotations(c *m.ReqContext) Response { Limit: c.QueryInt64("limit"), Tags: c.QueryStrings("tags"), Type: c.Query("type"), + Sort: c.Query("sort"), } repo := annotations.GetRepository() diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index a6cd7a33318..fd178176ef1 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -20,6 +20,7 @@ type ItemQuery struct { RegionId int64 `json:"regionId"` Tags []string `json:"tags"` Type string `json:"type"` + Sort string `json:"sort"` Limit int64 `json:"limit"` } @@ -63,6 +64,7 @@ type Item struct { PrevState string `json:"prevState"` NewState string `json:"newState"` Epoch int64 `json:"epoch"` + Created int64 `json:"created"` Tags []string `json:"tags"` Data *simplejson.Json `json:"data"` @@ -80,6 +82,7 @@ type ItemDTO struct { UserId int64 `json:"userId"` NewState string `json:"newState"` PrevState string `json:"prevState"` + Created int64 `json:"created"` Time int64 `json:"time"` Text string `json:"text"` RegionId int64 `json:"regionId"` diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 76f1819a18c..65f2abd9a54 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" @@ -17,6 +18,7 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error { return inTransaction(func(sess *DBSession) error { tags := models.ParseTagPairs(item.Tags) item.Tags = models.JoinTagPairs(tags) + item.Created = time.Now().UnixNano() / int64(time.Millisecond) if _, err := sess.Table("annotation").Insert(item); err != nil { return err } @@ -127,6 +129,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I annotation.text, annotation.tags, annotation.data, + annotation.created, usr.email, usr.login, alert.name as alert_name @@ -205,7 +208,17 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I query.Limit = 10 } - sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit)) + var sort string = "epoch DESC" + switch query.Sort { + case "time.asc": + sort = "epoch ASC" + case "created": + sort = "annotation.created DESC" + case "created.asc": + sort = "annotation.created ASC" + } + + sql.WriteString(fmt.Sprintf(" ORDER BY %s LIMIT %v", sort, query.Limit)) items := make([]*annotations.ItemDTO, 0) diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go index 8d2bf94bc42..24e2beb2eda 100644 --- a/pkg/services/sqlstore/migrations/annotation_mig.go +++ b/pkg/services/sqlstore/migrations/annotation_mig.go @@ -90,4 +90,14 @@ func addAnnotationMig(mg *Migrator) { Sqlite(updateTextFieldSql). Postgres(updateTextFieldSql). Mysql(updateTextFieldSql)) + + // + // Add a 'created' column + // + mg.AddMigration("Add created time to annotation table", NewAddColumnMigration(table, &Column{ + Name: "created", Type: DB_BigInt, Nullable: true, Default: "0", + })) + mg.AddMigration("Add index for created in annotation table", NewAddIndexMigration(table, &Index{ + Cols: []string{"org_id", "created"}, Type: IndexType, + })) } From f2f709989fae4920deeb1006526cec1172db98d9 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 22 Mar 2018 09:41:05 +0100 Subject: [PATCH 0126/3000] fixed so legend right works like legend under on small screens --- public/app/plugins/panel/graph/legend.ts | 4 +++- public/sass/components/_panel_graph.scss | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 0c8852bf55a..7a9c75d4f1d 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -227,6 +227,8 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function renderLegendElement(tableHeaderElem) { + let legendWidth = elem.width(); + var seriesElements = renderSeriesLegendElements(); if (panel.legend.alignAsTable) { @@ -238,7 +240,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { elem.append(seriesElements); } - if (!panel.legend.rightSide) { + if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== 10)) { addScrollbar(); } else { destroyScrollbar(); diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index c00af05140a..48d88872074 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -6,11 +6,11 @@ &--legend-right { @include media-breakpoint-up(sm) { flex-direction: row; - } - .graph-legend { - flex: 0 1 10px; - max-height: 100%; + .graph-legend { + flex: 0 1 10px; + max-height: 100%; + } } .graph-legend-series { From 7aab6a88873f5516636b1d710736d47cc33e84d8 Mon Sep 17 00:00:00 2001 From: Julian Kornberger Date: Thu, 22 Mar 2018 12:37:35 +0100 Subject: [PATCH 0127/3000] Make golint happier --- pkg/api/admin_users.go | 14 ++-- pkg/api/alerting.go | 6 +- pkg/api/annotations.go | 34 ++++----- pkg/api/api.go | 38 +++++----- pkg/api/apikey.go | 6 +- pkg/api/app_routes.go | 4 +- pkg/api/dashboard.go | 39 +++++----- pkg/api/dashboard_permission.go | 16 ++-- pkg/api/dashboard_snapshot.go | 4 +- pkg/api/dashboard_test.go | 20 ++--- pkg/api/dataproxy.go | 8 +- pkg/api/datasources.go | 25 +++---- pkg/api/folder.go | 4 +- pkg/api/folder_test.go | 4 +- pkg/api/http_server.go | 2 +- pkg/api/index.go | 29 ++++---- pkg/api/login.go | 6 +- pkg/api/metrics.go | 6 +- pkg/api/org.go | 12 +-- pkg/api/org_invite.go | 31 ++++---- pkg/api/org_users.go | 18 ++--- pkg/api/playlist.go | 4 +- pkg/api/playlist_play.go | 56 +++++++------- pkg/api/plugins.go | 90 ++++++++++++----------- pkg/api/preferences.go | 10 +-- pkg/api/search.go | 16 ++-- pkg/api/team.go | 4 +- pkg/api/user.go | 36 ++++----- pkg/cmd/grafana-server/server.go | 2 +- pkg/middleware/auth.go | 6 +- pkg/middleware/dashboard_redirect.go | 2 +- pkg/middleware/dashboard_redirect_test.go | 18 ++--- pkg/middleware/recovery_test.go | 12 +-- 33 files changed, 289 insertions(+), 293 deletions(-) diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 4cf7f4db4ec..dc3d390dda9 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -47,14 +47,14 @@ func AdminCreateUser(c *m.ReqContext, form dtos.AdminCreateUserForm) { } func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordForm) { - userId := c.ParamsInt64(":id") + userID := c.ParamsInt64(":id") if len(form.Password) < 4 { c.JsonApiErr(400, "New password too short", nil) return } - userQuery := m.GetUserByIdQuery{Id: userId} + userQuery := m.GetUserByIdQuery{Id: userID} if err := bus.Dispatch(&userQuery); err != nil { c.JsonApiErr(500, "Could not read user from database", err) @@ -64,7 +64,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF passwordHashed := util.EncodePassword(form.Password, userQuery.Result.Salt) cmd := m.ChangeUserPasswordCommand{ - UserId: userId, + UserId: userID, NewPassword: passwordHashed, } @@ -77,10 +77,10 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF } func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) { - userId := c.ParamsInt64(":id") + userID := c.ParamsInt64(":id") cmd := m.UpdateUserPermissionsCommand{ - UserId: userId, + UserId: userID, IsGrafanaAdmin: form.IsGrafanaAdmin, } @@ -93,9 +93,9 @@ func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermis } func AdminDeleteUser(c *m.ReqContext) { - userId := c.ParamsInt64(":id") + userID := c.ParamsInt64(":id") - cmd := m.DeleteUserCommand{UserId: userId} + cmd := m.DeleteUserCommand{UserId: userID} if err := bus.Dispatch(&cmd); err != nil { c.JsonApiErr(500, "Failed to delete user", err) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index eea4ef90c05..803823f6a94 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -26,9 +26,9 @@ func ValidateOrgAlert(c *m.ReqContext) { } func GetAlertStatesForDashboard(c *m.ReqContext) Response { - dashboardId := c.QueryInt64("dashboardId") + dashboardID := c.QueryInt64("dashboardId") - if dashboardId == 0 { + if dashboardID == 0 { return ApiError(400, "Missing query parameter dashboardId", nil) } @@ -151,7 +151,7 @@ func GetAlertNotifications(c *m.ReqContext) Response { return Json(200, result) } -func GetAlertNotificationById(c *m.ReqContext) Response { +func GetAlertNotificationByID(c *m.ReqContext) Response { query := &m.GetAlertNotificationsQuery{ OrgId: c.OrgId, Id: c.ParamsInt64("notificationId"), diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index fb75e0bf129..b4e328793cc 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -52,7 +52,7 @@ func (e *CreateAnnotationError) Error() string { } func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response { - if canSave, err := canSaveByDashboardId(c, cmd.DashboardId); err != nil || !canSave { + if canSave, err := canSaveByDashboardID(c, cmd.DashboardId); err != nil || !canSave { return dashboardGuardianResponse(err) } @@ -179,18 +179,18 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd } func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response { - annotationId := c.ParamsInt64(":annotationId") + annotationID := c.ParamsInt64(":annotationId") repo := annotations.GetRepository() - if resp := canSave(c, repo, annotationId); resp != nil { + if resp := canSave(c, repo, annotationID); resp != nil { return resp } item := annotations.Item{ OrgId: c.OrgId, UserId: c.UserId, - Id: annotationId, + Id: annotationID, Epoch: cmd.Time / 1000, Text: cmd.Text, Tags: cmd.Tags, @@ -254,14 +254,14 @@ func DeleteAnnotationById(c *m.ReqContext) Response { func DeleteAnnotationRegion(c *m.ReqContext) Response { repo := annotations.GetRepository() - regionId := c.ParamsInt64(":regionId") + regionID := c.ParamsInt64(":regionId") - if resp := canSave(c, repo, regionId); resp != nil { + if resp := canSave(c, repo, regionID); resp != nil { return resp } err := repo.Delete(&annotations.DeleteParams{ - RegionId: regionId, + RegionId: regionID, }) if err != nil { @@ -271,13 +271,13 @@ func DeleteAnnotationRegion(c *m.ReqContext) Response { return ApiSuccess("Annotation region deleted") } -func canSaveByDashboardId(c *m.ReqContext, dashboardId int64) (bool, error) { - if dashboardId == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) { +func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) { + if dashboardID == 0 && !c.SignedInUser.HasRole(m.ROLE_EDITOR) { return false, nil } - if dashboardId > 0 { - guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser) + if dashboardID > 0 { + guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser) if canEdit, err := guardian.CanEdit(); err != nil || !canEdit { return false, err } @@ -293,25 +293,25 @@ func canSave(c *m.ReqContext, repo annotations.Repository, annotationId int64) R return ApiError(500, "Could not find annotation to update", err) } - dashboardId := items[0].DashboardId + dashboardID := items[0].DashboardId - if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave { + if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave { return dashboardGuardianResponse(err) } return nil } -func canSaveByRegionId(c *m.ReqContext, repo annotations.Repository, regionId int64) Response { - items, err := repo.Find(&annotations.ItemQuery{RegionId: regionId, OrgId: c.OrgId}) +func canSaveByRegionID(c *m.ReqContext, repo annotations.Repository, regionID int64) Response { + items, err := repo.Find(&annotations.ItemQuery{RegionId: regionID, OrgId: c.OrgId}) if err != nil || len(items) == 0 { return ApiError(500, "Could not find annotation to update", err) } - dashboardId := items[0].DashboardId + dashboardID := items[0].DashboardId - if canSave, err := canSaveByDashboardId(c, dashboardId); err != nil || !canSave { + if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave { return dashboardGuardianResponse(err) } diff --git a/pkg/api/api.go b/pkg/api/api.go index 5b3cde09fd5..22d5d773d2a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -15,7 +15,7 @@ func (hs *HttpServer) registerRoutes() { reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}) reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN) reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN) - redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl() quota := middleware.Quota bind := binding.Bind @@ -110,7 +110,7 @@ func (hs *HttpServer) registerRoutes() { r.Get("/api/snapshots-delete/:key", reqEditorRole, wrap(DeleteDashboardSnapshot)) // api renew session based on remember cookie - r.Get("/api/login/ping", quota("session"), LoginApiPing) + r.Get("/api/login/ping", quota("session"), LoginAPIPing) // authed api r.Group("/api", func(apiRoute RouteRegister) { @@ -139,7 +139,7 @@ func (hs *HttpServer) registerRoutes() { apiRoute.Group("/users", func(usersRoute RouteRegister) { usersRoute.Get("/", wrap(SearchUsers)) usersRoute.Get("/search", wrap(SearchUsersWithPaging)) - usersRoute.Get("/:id", wrap(GetUserById)) + usersRoute.Get("/:id", wrap(GetUserByID)) usersRoute.Get("/:id/orgs", wrap(GetUserOrgList)) // query parameters /users/lookup?loginOrEmail=admin@example.com usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail)) @@ -149,11 +149,11 @@ func (hs *HttpServer) registerRoutes() { // team (admin permission required) apiRoute.Group("/teams", func(teamsRoute RouteRegister) { - teamsRoute.Get("/:teamId", wrap(GetTeamById)) + teamsRoute.Get("/:teamId", wrap(GetTeamByID)) teamsRoute.Get("/search", wrap(SearchTeams)) teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam)) teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) - teamsRoute.Delete("/:teamId", wrap(DeleteTeamById)) + teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID)) teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers)) teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember)) @@ -192,10 +192,10 @@ func (hs *HttpServer) registerRoutes() { // orgs (admin routes) apiRoute.Group("/orgs/:orgId", func(orgsRoute RouteRegister) { - orgsRoute.Get("/", wrap(GetOrgById)) + orgsRoute.Get("/", wrap(GetOrgByID)) orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg)) orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress)) - orgsRoute.Delete("/", wrap(DeleteOrgById)) + orgsRoute.Delete("/", wrap(DeleteOrgByID)) orgsRoute.Get("/users", wrap(GetOrgUsers)) orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser)) orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser)) @@ -211,9 +211,9 @@ func (hs *HttpServer) registerRoutes() { // auth api keys apiRoute.Group("/auth/keys", func(keysRoute RouteRegister) { - keysRoute.Get("/", wrap(GetApiKeys)) - keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddApiKey)) - keysRoute.Delete("/:id", wrap(DeleteApiKey)) + keysRoute.Get("/", wrap(GetAPIKeys)) + keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddAPIKey)) + keysRoute.Delete("/:id", wrap(DeleteAPIKey)) }, reqOrgAdmin) // Preferences @@ -226,16 +226,16 @@ func (hs *HttpServer) registerRoutes() { datasourceRoute.Get("/", wrap(GetDataSources)) datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource)) datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource)) - datasourceRoute.Delete("/:id", wrap(DeleteDataSourceById)) + datasourceRoute.Delete("/:id", wrap(DeleteDataSourceByID)) datasourceRoute.Delete("/name/:name", wrap(DeleteDataSourceByName)) - datasourceRoute.Get("/:id", wrap(GetDataSourceById)) + datasourceRoute.Get("/:id", wrap(GetDataSourceByID)) datasourceRoute.Get("/name/:name", wrap(GetDataSourceByName)) }, reqOrgAdmin) - apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn) + apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIDByName), reqSignedIn) apiRoute.Get("/plugins", wrap(GetPluginList)) - apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById)) + apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingByID)) apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown)) apiRoute.Group("/plugins", func(pluginRoute RouteRegister) { @@ -250,11 +250,11 @@ func (hs *HttpServer) registerRoutes() { // Folders apiRoute.Group("/folders", func(folderRoute RouteRegister) { folderRoute.Get("/", wrap(GetFolders)) - folderRoute.Get("/id/:id", wrap(GetFolderById)) + folderRoute.Get("/id/:id", wrap(GetFolderByID)) folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder)) folderRoute.Group("/:uid", func(folderUidRoute RouteRegister) { - folderUidRoute.Get("/", wrap(GetFolderByUid)) + folderUidRoute.Get("/", wrap(GetFolderByUID)) folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder)) folderUidRoute.Delete("/", wrap(DeleteFolder)) @@ -268,7 +268,7 @@ func (hs *HttpServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) { dashboardRoute.Get("/uid/:uid", wrap(GetDashboard)) - dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUid)) + dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUID)) dashboardRoute.Get("/db/:slug", wrap(GetDashboard)) dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard)) @@ -314,7 +314,7 @@ func (hs *HttpServer) registerRoutes() { // metrics apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics)) apiRoute.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios)) - apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSqlTestData)) + apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSQLTestData)) apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk)) apiRoute.Group("/alerts", func(alertsRoute RouteRegister) { @@ -332,7 +332,7 @@ func (hs *HttpServer) registerRoutes() { alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest)) alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification)) alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification)) - alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationById)) + alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationByID)) alertNotifications.Delete("/:notificationId", wrap(DeleteAlertNotification)) }, reqEditorRole) diff --git a/pkg/api/apikey.go b/pkg/api/apikey.go index 24ed69ec691..7195c7453f6 100644 --- a/pkg/api/apikey.go +++ b/pkg/api/apikey.go @@ -7,7 +7,7 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func GetApiKeys(c *m.ReqContext) Response { +func GetAPIKeys(c *m.ReqContext) Response { query := m.GetApiKeysQuery{OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { @@ -26,7 +26,7 @@ func GetApiKeys(c *m.ReqContext) Response { return Json(200, result) } -func DeleteApiKey(c *m.ReqContext) Response { +func DeleteAPIKey(c *m.ReqContext) Response { id := c.ParamsInt64(":id") cmd := &m.DeleteApiKeyCommand{Id: id, OrgId: c.OrgId} @@ -39,7 +39,7 @@ func DeleteApiKey(c *m.ReqContext) Response { return ApiSuccess("API key deleted") } -func AddApiKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response { +func AddAPIKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response { if !cmd.Role.IsValid() { return ApiError(400, "Invalid role specified", nil) } diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 8d74d96396b..0b7dcd32ce3 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -55,11 +55,11 @@ func InitAppPluginRoutes(r *macaron.Macaron) { } } -func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler { +func AppPluginRoute(route *plugins.AppPluginRoute, appID string) macaron.Handler { return func(c *m.ReqContext) { path := c.Params("*") - proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId) + proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID) proxy.Transport = pluginProxyTransport proxy.ServeHTTP(c.Resp, c.Req.Request) } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 877524ad5dd..25c89238933 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -22,12 +22,12 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func isDashboardStarredByUser(c *m.ReqContext, dashId int64) (bool, error) { +func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) { if !c.IsSignedIn { return false, nil } - query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashId} + query := m.IsStarredByUserQuery{UserId: c.UserId, DashboardId: dashID} if err := bus.Dispatch(&query); err != nil { return false, err } @@ -114,24 +114,22 @@ func GetDashboard(c *m.ReqContext) Response { return Json(200, dto) } -func getUserLogin(userId int64) string { - query := m.GetUserByIdQuery{Id: userId} +func getUserLogin(userID int64) string { + query := m.GetUserByIdQuery{Id: userID} err := bus.Dispatch(&query) if err != nil { return "Anonymous" - } else { - user := query.Result - return user.Login } + return query.Result.Login } -func getDashboardHelper(orgId int64, slug string, id int64, uid string) (*m.Dashboard, Response) { +func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*m.Dashboard, Response) { var query m.GetDashboardQuery if len(uid) > 0 { - query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgId} + query = m.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID} } else { - query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId} + query = m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgID} } if err := bus.Dispatch(&query); err != nil { @@ -173,7 +171,7 @@ func DeleteDashboard(c *m.ReqContext) Response { }) } -func DeleteDashboardByUid(c *m.ReqContext) Response { +func DeleteDashboardByUID(c *m.ReqContext) Response { dash, rsp := getDashboardHelper(c.OrgId, "", 0, c.Params(":uid")) if rsp != nil { return rsp @@ -291,9 +289,8 @@ func GetHomeDashboard(c *m.ReqContext) Response { url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) dashRedirect := dtos.DashboardRedirect{RedirectUri: url} return Json(200, &dashRedirect) - } else { - log.Warn("Failed to get slug from database, %s", err.Error()) } + log.Warn("Failed to get slug from database, %s", err.Error()) } filePath := path.Join(setting.StaticRootPath, "dashboards/home.json") @@ -339,22 +336,22 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) { // GetDashboardVersions returns all dashboard versions as JSON func GetDashboardVersions(c *m.ReqContext) Response { - dashId := c.ParamsInt64(":dashboardId") + dashID := c.ParamsInt64(":dashboardId") - guardian := guardian.New(dashId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashID, c.OrgId, c.SignedInUser) if canSave, err := guardian.CanSave(); err != nil || !canSave { return dashboardGuardianResponse(err) } query := m.GetDashboardVersionsQuery{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, Limit: c.QueryInt("limit"), Start: c.QueryInt("start"), } if err := bus.Dispatch(&query); err != nil { - return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err) + return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err) } for _, version := range query.Result { @@ -378,21 +375,21 @@ func GetDashboardVersions(c *m.ReqContext) Response { // GetDashboardVersion returns the dashboard version with the given ID. func GetDashboardVersion(c *m.ReqContext) Response { - dashId := c.ParamsInt64(":dashboardId") + dashID := c.ParamsInt64(":dashboardId") - guardian := guardian.New(dashId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashID, c.OrgId, c.SignedInUser) if canSave, err := guardian.CanSave(); err != nil || !canSave { return dashboardGuardianResponse(err) } query := m.GetDashboardVersionQuery{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, Version: c.ParamsInt(":id"), } if err := bus.Dispatch(&query); err != nil { - return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err) + return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err) } creator := "Anonymous" diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index a62c27ab320..c852033829a 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -10,14 +10,14 @@ import ( ) func GetDashboardPermissionList(c *m.ReqContext) Response { - dashId := c.ParamsInt64(":dashboardId") + dashID := c.ParamsInt64(":dashboardId") - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + _, rsp := getDashboardHelper(c.OrgId, "", dashID, "") if rsp != nil { return rsp } - g := guardian.New(dashId, c.OrgId, c.SignedInUser) + g := guardian.New(dashID, c.OrgId, c.SignedInUser) if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) @@ -38,25 +38,25 @@ func GetDashboardPermissionList(c *m.ReqContext) Response { } func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response { - dashId := c.ParamsInt64(":dashboardId") + dashID := c.ParamsInt64(":dashboardId") - _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + _, rsp := getDashboardHelper(c.OrgId, "", dashID, "") if rsp != nil { return rsp } - g := guardian.New(dashId, c.OrgId, c.SignedInUser) + g := guardian.New(dashID, c.OrgId, c.SignedInUser) if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) } cmd := m.UpdateDashboardAclCommand{} - cmd.DashboardId = dashId + cmd.DashboardId = dashID for _, item := range apiCmd.Items { cmd.Items = append(cmd.Items, &m.DashboardAcl{ OrgId: c.OrgId, - DashboardId: dashId, + DashboardId: dashID, UserId: item.UserId, TeamId: item.TeamId, Role: item.Role, diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 4656940d2bb..b74dfe5fd34 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -106,9 +106,9 @@ func DeleteDashboardSnapshot(c *m.ReqContext) Response { return ApiError(404, "Failed to get dashboard snapshot", nil) } dashboard := query.Result.Dashboard - dashboardId := dashboard.Get("id").MustInt64() + dashboardID := dashboard.Get("id").MustInt64() - guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser) + guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser) canEdit, err := guardian.CanEdit() if err != nil { return ApiError(500, "Error while checking permissions for snapshot", err) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 6c5b4e4c102..d940c5406be 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -105,7 +105,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -165,7 +165,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -271,7 +271,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -329,7 +329,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -398,7 +398,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -468,7 +468,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -527,7 +527,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 200) Convey("Should lookup dashboard by uid", func() { @@ -594,7 +594,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUid(sc) + CallDeleteDashboardByUID(sc) So(sc.resp.Code, ShouldEqual, 403) Convey("Should lookup dashboard by uid", func() { @@ -837,12 +837,12 @@ func CallDeleteDashboard(sc *scenarioContext) { sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } -func CallDeleteDashboardByUid(sc *scenarioContext) { +func CallDeleteDashboardByUID(sc *scenarioContext) { bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error { return nil }) - sc.handlerFunc = DeleteDashboardByUid + sc.handlerFunc = DeleteDashboardByUID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index c6fe8b6cd8c..2e97e5d2d6f 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -13,19 +13,19 @@ import ( const HeaderNameNoBackendCache = "X-Grafana-NoCache" -func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m.DataSource, error) { +func (hs *HttpServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) { cacheKey := fmt.Sprintf("ds-%d", id) if !nocache { if cached, found := hs.cache.Get(cacheKey); found { ds := cached.(*m.DataSource) - if ds.OrgId == orgId { + if ds.OrgId == orgID { return ds, nil } } } - query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgId} + query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgID} if err := bus.Dispatch(&query); err != nil { return nil, err } @@ -39,7 +39,7 @@ func (hs *HttpServer) ProxyDataSourceRequest(c *m.ReqContext) { nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" - ds, err := hs.getDatasourceById(c.ParamsInt64(":id"), c.OrgId, nocache) + ds, err := hs.getDatasourceByID(c.ParamsInt64(":id"), c.OrgId, nocache) if err != nil { c.JsonApiErr(500, "Unable to load datasource meta data", err) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index ed8fc5d2a66..4b5aa56d6e7 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -49,7 +49,7 @@ func GetDataSources(c *m.ReqContext) Response { return Json(200, &result) } -func GetDataSourceById(c *m.ReqContext) Response { +func GetDataSourceByID(c *m.ReqContext) Response { query := m.GetDataSourceByIdQuery{ Id: c.ParamsInt64(":id"), OrgId: c.OrgId, @@ -68,14 +68,14 @@ func GetDataSourceById(c *m.ReqContext) Response { return Json(200, &dtos) } -func DeleteDataSourceById(c *m.ReqContext) Response { +func DeleteDataSourceByID(c *m.ReqContext) Response { id := c.ParamsInt64(":id") if id <= 0 { return ApiError(400, "Missing valid datasource id", nil) } - ds, err := getRawDataSourceById(id, c.OrgId) + ds, err := getRawDataSourceByID(id, c.OrgId) if err != nil { return ApiError(400, "Failed to delete datasource", nil) } @@ -143,7 +143,7 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":id") - err := fillWithSecureJsonData(&cmd) + err := fillWithSecureJSONData(&cmd) if err != nil { return ApiError(500, "Failed to update datasource", err) } @@ -152,9 +152,8 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { if err != nil { if err == m.ErrDataSourceUpdatingOldVersion { return ApiError(500, "Failed to update datasource. Reload new version and try again", err) - } else { - return ApiError(500, "Failed to update datasource", err) } + return ApiError(500, "Failed to update datasource", err) } ds := convertModelToDtos(cmd.Result) return Json(200, util.DynMap{ @@ -165,12 +164,12 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { }) } -func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { +func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error { if len(cmd.SecureJsonData) == 0 { return nil } - ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId) + ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId) if err != nil { return err } @@ -179,8 +178,8 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { return m.ErrDatasourceIsReadOnly } - secureJsonData := ds.SecureJsonData.Decrypt() - for k, v := range secureJsonData { + secureJSONData := ds.SecureJsonData.Decrypt() + for k, v := range secureJSONData { if _, ok := cmd.SecureJsonData[k]; !ok { cmd.SecureJsonData[k] = v @@ -190,10 +189,10 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { return nil } -func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) { +func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) { query := m.GetDataSourceByIdQuery{ Id: id, - OrgId: orgId, + OrgId: orgID, } if err := bus.Dispatch(&query); err != nil { @@ -220,7 +219,7 @@ func GetDataSourceByName(c *m.ReqContext) Response { } // Get /api/datasources/id/:name -func GetDataSourceIdByName(c *m.ReqContext) Response { +func GetDataSourceIDByName(c *m.ReqContext) Response { query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 143892fa6e8..e88f7fc2c3b 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -31,7 +31,7 @@ func GetFolders(c *m.ReqContext) Response { return Json(200, result) } -func GetFolderByUid(c *m.ReqContext) Response { +func GetFolderByUID(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folder, err := s.GetFolderByUid(c.Params(":uid")) @@ -43,7 +43,7 @@ func GetFolderByUid(c *m.ReqContext) Response { return Json(200, toFolderDto(g, folder)) } -func GetFolderById(c *m.ReqContext) Response { +func GetFolderByID(c *m.ReqContext) Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) folder, err := s.GetFolderById(c.ParamsInt64(":id")) if err != nil { diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 7cefdcf8544..e3c63ac0745 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -133,8 +133,8 @@ func TestFoldersApiEndpoint(t *testing.T) { }) } -func callGetFolderByUid(sc *scenarioContext) { - sc.handlerFunc = GetFolderByUid +func callGetFolderByUID(sc *scenarioContext) { + sc.handlerFunc = GetFolderByUID sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index c6a9286a5d8..772a27bd4cd 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -39,7 +39,7 @@ type HttpServer struct { httpSrv *http.Server } -func NewHttpServer() *HttpServer { +func NewHTTPServer() *HttpServer { return &HttpServer{ log: log.New("http.server"), cache: gocache.New(5*time.Minute, 10*time.Minute), diff --git a/pkg/api/index.go b/pkg/api/index.go index e50c59e082a..a1d21d1c686 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -32,13 +32,13 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { locale = parts[0] } - appUrl := setting.AppUrl - appSubUrl := setting.AppSubUrl + appURL := setting.AppUrl + appSubURL := setting.AppSubUrl // special case when doing localhost call from phantomjs if c.IsRenderCall { - appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort) - appSubUrl = "" + appURL = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort) + appSubURL = "" settings["appSubUrl"] = "" } @@ -62,8 +62,8 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { }, Settings: settings, Theme: prefs.Theme, - AppUrl: appUrl, - AppSubUrl: appSubUrl, + AppUrl: appURL, + AppSubUrl: appSubURL, GoogleAnalyticsId: setting.GoogleAnalyticsId, GoogleTagManagerId: setting.GoogleTagManagerId, BuildVersion: setting.BuildVersion, @@ -80,8 +80,8 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { data.User.Name = data.User.Login } - themeUrlParam := c.Query("theme") - if themeUrlParam == "light" { + themeURLParam := c.Query("theme") + if themeURLParam == "light" { data.User.LightTheme = true data.Theme = "light" } @@ -299,12 +299,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { } func Index(c *m.ReqContext) { - if data, err := setIndexViewData(c); err != nil { + data, err := setIndexViewData(c) + if err != nil { c.Handle(500, "Failed to get settings", err) return - } else { - c.HTML(200, "index", data) } + c.HTML(200, "index", data) } func NotFoundHandler(c *m.ReqContext) { @@ -313,10 +313,11 @@ func NotFoundHandler(c *m.ReqContext) { return } - if data, err := setIndexViewData(c); err != nil { + data, err := setIndexViewData(c) + if err != nil { c.Handle(500, "Failed to get settings", err) return - } else { - c.HTML(404, "index", data) } + + c.HTML(404, "index", data) } diff --git a/pkg/api/login.go b/pkg/api/login.go index 2ca2ce5a3e2..dc5ae730721 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -14,7 +14,7 @@ import ( ) const ( - VIEW_INDEX = "index" + ViewIndex = "index" ) func LoginView(c *m.ReqContext) { @@ -40,7 +40,7 @@ func LoginView(c *m.ReqContext) { } if !tryLoginUsingRememberCookie(c) { - c.HTML(200, VIEW_INDEX, viewData) + c.HTML(200, ViewIndex, viewData) return } @@ -87,7 +87,7 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool { return true } -func LoginApiPing(c *m.ReqContext) { +func LoginAPIPing(c *m.ReqContext) { if !tryLoginUsingRememberCookie(c) { c.JsonApiErr(401, "Unauthorized", nil) return diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 5d395d655a9..38bb8dc0688 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -20,12 +20,12 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { return ApiError(400, "No queries found in query", nil) } - dsId, err := reqDto.Queries[0].Get("datasourceId").Int64() + dsID, err := reqDto.Queries[0].Get("datasourceId").Int64() if err != nil { return ApiError(400, "Query missing datasourceId", nil) } - dsQuery := m.GetDataSourceByIdQuery{Id: dsId, OrgId: c.OrgId} + dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId} if err := bus.Dispatch(&dsQuery); err != nil { return ApiError(500, "failed to fetch data source", err) } @@ -82,7 +82,7 @@ func GenerateError(c *m.ReqContext) Response { } // GET /api/tsdb/testdata/gensql -func GenerateSqlTestData(c *m.ReqContext) Response { +func GenerateSQLTestData(c *m.ReqContext) Response { if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil { return ApiError(500, "Failed to insert test data", err) } diff --git a/pkg/api/org.go b/pkg/api/org.go index 5f20559dbbe..7735bd6a7eb 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -15,7 +15,7 @@ func GetOrgCurrent(c *m.ReqContext) Response { } // GET /api/orgs/:orgId -func GetOrgById(c *m.ReqContext) Response { +func GetOrgByID(c *m.ReqContext) Response { return getOrgHelper(c.ParamsInt64(":orgId")) } @@ -106,8 +106,8 @@ func UpdateOrg(c *m.ReqContext, form dtos.UpdateOrgForm) Response { return updateOrgHelper(form, c.ParamsInt64(":orgId")) } -func updateOrgHelper(form dtos.UpdateOrgForm, orgId int64) Response { - cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgId} +func updateOrgHelper(form dtos.UpdateOrgForm, orgID int64) Response { + cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrOrgNameTaken { return ApiError(400, "Organization name taken", err) @@ -128,9 +128,9 @@ func UpdateOrgAddress(c *m.ReqContext, form dtos.UpdateOrgAddressForm) Response return updateOrgAddressHelper(form, c.ParamsInt64(":orgId")) } -func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Response { +func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgID int64) Response { cmd := m.UpdateOrgAddressCommand{ - OrgId: orgId, + OrgId: orgID, Address: m.Address{ Address1: form.Address1, Address2: form.Address2, @@ -149,7 +149,7 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Respons } // GET /api/orgs/:orgId -func DeleteOrgById(c *m.ReqContext) Response { +func DeleteOrgByID(c *m.ReqContext) Response { if err := bus.Dispatch(&m.DeleteOrgCommand{Id: c.ParamsInt64(":orgId")}); err != nil { if err == m.ErrOrgNotFound { return ApiError(404, "Failed to delete organization. ID not found", nil) diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 6a727dd95cc..0486287a31b 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -96,26 +96,25 @@ func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddI return ApiError(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err) } return ApiError(500, "Error while trying to create org user", err) - } else { + } - if inviteDto.SendEmail && util.IsEmail(user.Email) { - emailCmd := m.SendEmailCommand{ - To: []string{user.Email}, - Template: "invited_to_org.html", - Data: map[string]interface{}{ - "Name": user.NameOrFallback(), - "OrgName": c.OrgName, - "InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login), - }, - } - - if err := bus.Dispatch(&emailCmd); err != nil { - return ApiError(500, "Failed to send email invited_to_org", err) - } + if inviteDto.SendEmail && util.IsEmail(user.Email) { + emailCmd := m.SendEmailCommand{ + To: []string{user.Email}, + Template: "invited_to_org.html", + Data: map[string]interface{}{ + "Name": user.NameOrFallback(), + "OrgName": c.OrgName, + "InvitedBy": util.StringsFallback3(c.Name, c.Email, c.Login), + }, } - return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName)) + if err := bus.Dispatch(&emailCmd); err != nil { + return ApiError(500, "Failed to send email invited_to_org", err) + } } + + return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName)) } func RevokeInvite(c *m.ReqContext) Response { diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 6d7c2bb94bd..cd1430d22fb 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -53,9 +53,9 @@ func GetOrgUsers(c *m.ReqContext) Response { return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0) } -func getOrgUsersHelper(orgId int64, query string, limit int) Response { +func getOrgUsersHelper(orgID int64, query string, limit int) Response { q := m.GetOrgUsersQuery{ - OrgId: orgId, + OrgId: orgID, Query: query, Limit: limit, } @@ -102,19 +102,19 @@ func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response { // DELETE /api/org/users/:userId func RemoveOrgUserForCurrentOrg(c *m.ReqContext) Response { - userId := c.ParamsInt64(":userId") - return removeOrgUserHelper(c.OrgId, userId) + userID := c.ParamsInt64(":userId") + return removeOrgUserHelper(c.OrgId, userID) } // DELETE /api/orgs/:orgId/users/:userId func RemoveOrgUser(c *m.ReqContext) Response { - userId := c.ParamsInt64(":userId") - orgId := c.ParamsInt64(":orgId") - return removeOrgUserHelper(orgId, userId) + userID := c.ParamsInt64(":userId") + orgID := c.ParamsInt64(":orgId") + return removeOrgUserHelper(orgID, userID) } -func removeOrgUserHelper(orgId int64, userId int64) Response { - cmd := m.RemoveOrgUserCommand{OrgId: orgId, UserId: userId} +func removeOrgUserHelper(orgID int64, userID int64) Response { + cmd := m.RemoveOrgUserCommand{OrgId: orgID, UserId: userID} if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrLastOrgAdmin { diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 45de40ce337..0198850252d 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -127,9 +127,9 @@ func GetPlaylistItems(c *m.ReqContext) Response { } func GetPlaylistDashboards(c *m.ReqContext) Response { - playlistId := c.ParamsInt64(":id") + playlistID := c.ParamsInt64(":id") - playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId) + playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistID) if err != nil { return ApiError(500, "Could not load dashboards", err) } diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 1d059e06be5..69a2caef7e0 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -34,29 +34,27 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i return result, nil } -func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { +func populateDashboardsByTag(orgID int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice { result := make(dtos.PlaylistDashboardsSlice, 0) - if len(dashboardByTag) > 0 { - for _, tag := range dashboardByTag { - searchQuery := search.Query{ - Title: "", - Tags: []string{tag}, - SignedInUser: signedInUser, - Limit: 100, - IsStarred: false, - OrgId: orgId, - } + for _, tag := range dashboardByTag { + searchQuery := search.Query{ + Title: "", + Tags: []string{tag}, + SignedInUser: signedInUser, + Limit: 100, + IsStarred: false, + OrgId: orgID, + } - if err := bus.Dispatch(&searchQuery); err == nil { - for _, item := range searchQuery.Result { - result = append(result, dtos.PlaylistDashboard{ - Id: item.Id, - Title: item.Title, - Uri: item.Uri, - Order: dashboardTagOrder[tag], - }) - } + if err := bus.Dispatch(&searchQuery); err == nil { + for _, item := range searchQuery.Result { + result = append(result, dtos.PlaylistDashboard{ + Id: item.Id, + Title: item.Title, + Uri: item.Uri, + Order: dashboardTagOrder[tag], + }) } } } @@ -64,19 +62,19 @@ func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboar return result } -func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) { - playlistItems, _ := LoadPlaylistItems(playlistId) +func LoadPlaylistDashboards(orgID int64, signedInUser *m.SignedInUser, playlistID int64) (dtos.PlaylistDashboardsSlice, error) { + playlistItems, _ := LoadPlaylistItems(playlistID) - dashboardByIds := make([]int64, 0) + dashboardByIDs := make([]int64, 0) dashboardByTag := make([]string, 0) - dashboardIdOrder := make(map[int64]int) + dashboardIDOrder := make(map[int64]int) dashboardTagOrder := make(map[string]int) for _, i := range playlistItems { if i.Type == "dashboard_by_id" { - dashboardId, _ := strconv.ParseInt(i.Value, 10, 64) - dashboardByIds = append(dashboardByIds, dashboardId) - dashboardIdOrder[dashboardId] = i.Order + dashboardID, _ := strconv.ParseInt(i.Value, 10, 64) + dashboardByIDs = append(dashboardByIDs, dashboardID) + dashboardIDOrder[dashboardID] = i.Order } if i.Type == "dashboard_by_tag" { @@ -87,9 +85,9 @@ func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistI result := make(dtos.PlaylistDashboardsSlice, 0) - var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder) + var k, _ = populateDashboardsById(dashboardByIDs, dashboardIDOrder) result = append(result, k...) - result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...) + result = append(result, populateDashboardsByTag(orgID, signedInUser, dashboardByTag, dashboardTagOrder)...) sort.Sort(result) return result, nil diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index bc38f4a7775..81b3ac10cef 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -78,48 +78,48 @@ func GetPluginList(c *m.ReqContext) Response { return Json(200, result) } -func GetPluginSettingById(c *m.ReqContext) Response { - pluginId := c.Params(":pluginId") +func GetPluginSettingByID(c *m.ReqContext) Response { + pluginID := c.Params(":pluginId") - if def, exists := plugins.Plugins[pluginId]; !exists { + def, exists := plugins.Plugins[pluginID] + if !exists { return ApiError(404, "Plugin not found, no installed plugin with that id", nil) - } else { - - dto := &dtos.PluginSetting{ - Type: def.Type, - Id: def.Id, - Name: def.Name, - Info: &def.Info, - Dependencies: &def.Dependencies, - Includes: def.Includes, - BaseUrl: def.BaseUrl, - Module: def.Module, - DefaultNavUrl: def.DefaultNavUrl, - LatestVersion: def.GrafanaNetVersion, - HasUpdate: def.GrafanaNetHasUpdate, - State: def.State, - } - - query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId} - if err := bus.Dispatch(&query); err != nil { - if err != m.ErrPluginSettingNotFound { - return ApiError(500, "Failed to get login settings", nil) - } - } else { - dto.Enabled = query.Result.Enabled - dto.Pinned = query.Result.Pinned - dto.JsonData = query.Result.JsonData - } - - return Json(200, dto) } + + dto := &dtos.PluginSetting{ + Type: def.Type, + Id: def.Id, + Name: def.Name, + Info: &def.Info, + Dependencies: &def.Dependencies, + Includes: def.Includes, + BaseUrl: def.BaseUrl, + Module: def.Module, + DefaultNavUrl: def.DefaultNavUrl, + LatestVersion: def.GrafanaNetVersion, + HasUpdate: def.GrafanaNetHasUpdate, + State: def.State, + } + + query := m.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId} + if err := bus.Dispatch(&query); err != nil { + if err != m.ErrPluginSettingNotFound { + return ApiError(500, "Failed to get login settings", nil) + } + } else { + dto.Enabled = query.Result.Enabled + dto.Pinned = query.Result.Pinned + dto.JsonData = query.Result.JsonData + } + + return Json(200, dto) } func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response { - pluginId := c.Params(":pluginId") + pluginID := c.Params(":pluginId") cmd.OrgId = c.OrgId - cmd.PluginId = pluginId + cmd.PluginId = pluginID if _, ok := plugins.Apps[cmd.PluginId]; !ok { return ApiError(404, "Plugin not installed.", nil) @@ -133,34 +133,36 @@ func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response } func GetPluginDashboards(c *m.ReqContext) Response { - pluginId := c.Params(":pluginId") + pluginID := c.Params(":pluginId") - if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil { + list, err := plugins.GetPluginDashboards(c.OrgId, pluginID) + if err != nil { if notfound, ok := err.(plugins.PluginNotFoundError); ok { return ApiError(404, notfound.Error(), nil) } return ApiError(500, "Failed to get plugin dashboards", err) - } else { - return Json(200, list) } + + return Json(200, list) } func GetPluginMarkdown(c *m.ReqContext) Response { - pluginId := c.Params(":pluginId") + pluginID := c.Params(":pluginId") name := c.Params(":name") - if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil { + content, err := plugins.GetPluginMarkdown(pluginID, name) + if err != nil { if notfound, ok := err.(plugins.PluginNotFoundError); ok { return ApiError(404, notfound.Error(), nil) } return ApiError(500, "Could not get markdown file", err) - } else { - resp := Respond(200, content) - resp.Header("Content-Type", "text/plain; charset=utf-8") - return resp } + + resp := Respond(200, content) + resp.Header("Content-Type", "text/plain; charset=utf-8") + return resp } func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Response { diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index eb0ffa14b39..2b85c3dcee1 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -24,8 +24,8 @@ func GetUserPreferences(c *m.ReqContext) Response { return getPreferencesFor(c.OrgId, c.UserId) } -func getPreferencesFor(orgId int64, userId int64) Response { - prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId} +func getPreferencesFor(orgID int64, userID int64) Response { + prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID} if err := bus.Dispatch(&prefsQuery); err != nil { return ApiError(500, "Failed to get preferences", err) @@ -45,10 +45,10 @@ func UpdateUserPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd) } -func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response { +func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd) Response { saveCmd := m.SavePreferencesCommand{ - UserId: userId, - OrgId: orgId, + UserId: userID, + OrgId: orgID, Theme: dtoCmd.Theme, Timezone: dtoCmd.Timezone, HomeDashboardId: dtoCmd.HomeDashboardId, diff --git a/pkg/api/search.go b/pkg/api/search.go index c8a0a5592bb..8c2b708d5a2 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -25,19 +25,19 @@ func Search(c *m.ReqContext) { permission = m.PERMISSION_EDIT } - dbids := make([]int64, 0) + dbIDs := make([]int64, 0) for _, id := range c.QueryStrings("dashboardIds") { - dashboardId, err := strconv.ParseInt(id, 10, 64) + dashboardID, err := strconv.ParseInt(id, 10, 64) if err == nil { - dbids = append(dbids, dashboardId) + dbIDs = append(dbIDs, dashboardID) } } - folderIds := make([]int64, 0) + folderIDs := make([]int64, 0) for _, id := range c.QueryStrings("folderIds") { - folderId, err := strconv.ParseInt(id, 10, 64) + folderID, err := strconv.ParseInt(id, 10, 64) if err == nil { - folderIds = append(folderIds, folderId) + folderIDs = append(folderIDs, folderID) } } @@ -48,9 +48,9 @@ func Search(c *m.ReqContext) { Limit: limit, IsStarred: starred == "true", OrgId: c.OrgId, - DashboardIds: dbids, + DashboardIds: dbIDs, Type: dashboardType, - FolderIds: folderIds, + FolderIds: folderIDs, Permission: permission, } diff --git a/pkg/api/team.go b/pkg/api/team.go index 316adfc4e7c..13e23df27c8 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -38,7 +38,7 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response { } // DELETE /api/teams/:teamId -func DeleteTeamById(c *m.ReqContext) Response { +func DeleteTeamByID(c *m.ReqContext) Response { if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil { if err == m.ErrTeamNotFound { return ApiError(404, "Failed to delete Team. ID not found", nil) @@ -82,7 +82,7 @@ func SearchTeams(c *m.ReqContext) Response { } // GET /api/teams/:teamId -func GetTeamById(c *m.ReqContext) Response { +func GetTeamByID(c *m.ReqContext) Response { query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/api/user.go b/pkg/api/user.go index b8483316b9d..4469fab2d29 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -14,12 +14,12 @@ func GetSignedInUser(c *m.ReqContext) Response { } // GET /api/users/:id -func GetUserById(c *m.ReqContext) Response { +func GetUserByID(c *m.ReqContext) Response { return getUserUserProfile(c.ParamsInt64(":id")) } -func getUserUserProfile(userId int64) Response { - query := m.GetUserProfileQuery{UserId: userId} +func getUserUserProfile(userID int64) Response { + query := m.GetUserProfileQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { if err == m.ErrUserNotFound { @@ -75,14 +75,14 @@ func UpdateUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response { //POST /api/users/:id/using/:orgId func UpdateUserActiveOrg(c *m.ReqContext) Response { - userId := c.ParamsInt64(":id") - orgId := c.ParamsInt64(":orgId") + userID := c.ParamsInt64(":id") + orgID := c.ParamsInt64(":orgId") - if !validateUsingOrg(userId, orgId) { + if !validateUsingOrg(userID, orgID) { return ApiError(401, "Not a valid organization", nil) } - cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: userID, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to change active organization", err) @@ -116,8 +116,8 @@ func GetUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.ParamsInt64(":id")) } -func getUserOrgList(userId int64) Response { - query := m.GetUserOrgListQuery{UserId: userId} +func getUserOrgList(userID int64) Response { + query := m.GetUserOrgListQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { return ApiError(500, "Failed to get user organizations", err) @@ -126,8 +126,8 @@ func getUserOrgList(userId int64) Response { return Json(200, query.Result) } -func validateUsingOrg(userId int64, orgId int64) bool { - query := m.GetUserOrgListQuery{UserId: userId} +func validateUsingOrg(userID int64, orgID int64) bool { + query := m.GetUserOrgListQuery{UserId: userID} if err := bus.Dispatch(&query); err != nil { return false @@ -136,7 +136,7 @@ func validateUsingOrg(userId int64, orgId int64) bool { // validate that the org id in the list valid := false for _, other := range query.Result { - if other.OrgId == orgId { + if other.OrgId == orgID { valid = true } } @@ -146,13 +146,13 @@ func validateUsingOrg(userId int64, orgId int64) bool { // POST /api/user/using/:id func UserSetUsingOrg(c *m.ReqContext) Response { - orgId := c.ParamsInt64(":id") + orgID := c.ParamsInt64(":id") - if !validateUsingOrg(c.UserId, orgId) { + if !validateUsingOrg(c.UserId, orgID) { return ApiError(401, "Not a valid organization", nil) } - cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to change active organization", err) @@ -163,13 +163,13 @@ func UserSetUsingOrg(c *m.ReqContext) Response { // GET /profile/switch-org/:id func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) { - orgId := c.ParamsInt64(":id") + orgID := c.ParamsInt64(":id") - if !validateUsingOrg(c.UserId, orgId) { + if !validateUsingOrg(c.UserId, orgID) { NotFoundHandler(c) } - cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId} + cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { NotFoundHandler(c) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 8ed3196e4ad..0338b3d6aa2 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -120,7 +120,7 @@ func (g *GrafanaServerImpl) initLogging() { } func (g *GrafanaServerImpl) startHttpServer() error { - g.httpServer = api.NewHttpServer() + g.httpServer = api.NewHTTPServer() err := g.httpServer.Start(g.context) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index d6c377bc9ac..37e79c01071 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -17,10 +17,10 @@ type AuthOptions struct { } func getRequestUserId(c *m.ReqContext) int64 { - userId := c.Session.Get(session.SESS_KEY_USERID) + userID := c.Session.Get(session.SESS_KEY_USERID) - if userId != nil { - return userId.(int64) + if userID != nil { + return userID.(int64) } return 0 diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 7c2af548a8f..024b112e154 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -20,7 +20,7 @@ func getDashboardUrlBySlug(orgId int64, slug string) (string, error) { return m.GetDashboardUrl(query.Result.Uid, query.Result.Slug), nil } -func RedirectFromLegacyDashboardUrl() macaron.Handler { +func RedirectFromLegacyDashboardURL() macaron.Handler { return func(c *m.ReqContext) { slug := c.Params("slug") diff --git a/pkg/middleware/dashboard_redirect_test.go b/pkg/middleware/dashboard_redirect_test.go index 0af06347ed0..24eab2d7b79 100644 --- a/pkg/middleware/dashboard_redirect_test.go +++ b/pkg/middleware/dashboard_redirect_test.go @@ -13,7 +13,7 @@ import ( func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Given the dashboard redirect middleware", t, func() { bus.ClearBusHandlers() - redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardUrl() + redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardURL() redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl() fakeDash := m.NewDashboard("Child dash") @@ -34,9 +34,9 @@ func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { So(sc.resp.Code, ShouldEqual, 301) - redirectUrl, _ := sc.resp.Result().Location() - So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) - So(len(redirectUrl.Query()), ShouldEqual, 2) + redirectURL, _ := sc.resp.Result().Location() + So(redirectURL.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)) + So(len(redirectURL.Query()), ShouldEqual, 2) }) }) @@ -47,11 +47,11 @@ func TestMiddlewareDashboardRedirect(t *testing.T) { Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() { So(sc.resp.Code, ShouldEqual, 301) - redirectUrl, _ := sc.resp.Result().Location() - expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) - expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1) - So(redirectUrl.Path, ShouldEqual, expectedUrl) - So(len(redirectUrl.Query()), ShouldEqual, 2) + redirectURL, _ := sc.resp.Result().Location() + expectedURL := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug) + expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1) + So(redirectURL.Path, ShouldEqual, expectedURL) + So(len(redirectURL.Query()), ShouldEqual, 2) }) }) }) diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index c63a0e81e57..4bbedbc3b21 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -14,10 +14,10 @@ import ( func TestRecoveryMiddleware(t *testing.T) { Convey("Given an api route that panics", t, func() { - apiUrl := "/api/whatever" - recoveryScenario("recovery middleware should return json", apiUrl, func(sc *scenarioContext) { + apiURL := "/api/whatever" + recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) { sc.handlerFunc = PanicHandler - sc.fakeReq("GET", apiUrl).exec() + sc.fakeReq("GET", apiURL).exec() sc.req.Header.Add("content-type", "application/json") So(sc.resp.Code, ShouldEqual, 500) @@ -27,10 +27,10 @@ func TestRecoveryMiddleware(t *testing.T) { }) Convey("Given a non-api route that panics", t, func() { - apiUrl := "/whatever" - recoveryScenario("recovery middleware should return html", apiUrl, func(sc *scenarioContext) { + apiURL := "/whatever" + recoveryScenario("recovery middleware should return html", apiURL, func(sc *scenarioContext) { sc.handlerFunc = PanicHandler - sc.fakeReq("GET", apiUrl).exec() + sc.fakeReq("GET", apiURL).exec() So(sc.resp.Code, ShouldEqual, 500) So(sc.resp.Header().Get("content-type"), ShouldEqual, "text/html; charset=UTF-8") From 0ffcea08c7188c6760322160b0bcfea1374a086f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 22 Mar 2018 15:18:05 +0300 Subject: [PATCH 0128/3000] dashboard version cleanup: more tests and refactor --- pkg/services/sqlstore/dashboard_version.go | 20 +++++++++---------- .../sqlstore/dashboard_version_test.go | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/pkg/services/sqlstore/dashboard_version.go b/pkg/services/sqlstore/dashboard_version.go index d91b5727545..1f2850b2021 100644 --- a/pkg/services/sqlstore/dashboard_version.go +++ b/pkg/services/sqlstore/dashboard_version.go @@ -67,10 +67,10 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error { return nil } +const MAX_VERSIONS_TO_DELETE = 100 + func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { return inTransaction(func(sess *DBSession) error { - const MAX_VERSIONS_TO_DELETE = 100 - versionsToKeep := setting.DashboardVersionsToKeep if versionsToKeep < 1 { versionsToKeep = 1 @@ -80,27 +80,25 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error { // min_version_to_keep = min_version + (versions_count - versions_to_keep) // where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep // versions, but in some cases (when versions are sparse) this number may be more. - versionIdsToDeleteSubquery := `SELECT id + versionIdsToDeleteQuery := `SELECT id FROM dashboard_version, ( SELECT dashboard_id, count(version) as count, min(version) as min FROM dashboard_version GROUP BY dashboard_id - ) AS vtd - WHERE dashboard_version.dashboard_id=vtd.dashboard_id - AND version < vtd.min + vtd.count - ?` + ) AS vtd + WHERE dashboard_version.dashboard_id=vtd.dashboard_id + AND version < vtd.min + vtd.count - ?` var versionIdsToDelete []interface{} - err := sess.SQL(versionIdsToDeleteSubquery, versionsToKeep).Find(&versionIdsToDelete) + err := sess.SQL(versionIdsToDeleteQuery, versionsToKeep).Find(&versionIdsToDelete) if err != nil { return err } // Don't delete more than MAX_VERSIONS_TO_DELETE version per time - limit := MAX_VERSIONS_TO_DELETE - if len(versionIdsToDelete) < MAX_VERSIONS_TO_DELETE { - limit = len(versionIdsToDelete) + if len(versionIdsToDelete) > MAX_VERSIONS_TO_DELETE { + versionIdsToDelete = versionIdsToDelete[:MAX_VERSIONS_TO_DELETE] } - versionIdsToDelete = versionIdsToDelete[:limit] if len(versionIdsToDelete) > 0 { deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)` diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index 151dc7c4be2..a6403755d05 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -141,5 +141,25 @@ func TestDeleteExpiredVersions(t *testing.T) { So(len(query.Result), ShouldEqual, versionsToWrite) }) + + Convey("Don't delete more than MAX_VERSIONS_TO_DELETE per iteration", func() { + versionsToWriteBigNumber := MAX_VERSIONS_TO_DELETE + versionsToWrite + for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ { + updateTestDashboard(savedDash, map[string]interface{}{ + "tags": "different-tag", + }) + } + + err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{}) + So(err, ShouldBeNil) + + query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWriteBigNumber} + GetDashboardVersions(&query) + + // Ensure we have at least versionsToKeep versions + So(len(query.Result), ShouldBeGreaterThanOrEqualTo, versionsToKeep) + // Ensure we haven't deleted more than MAX_VERSIONS_TO_DELETE rows + So(versionsToWriteBigNumber-len(query.Result), ShouldBeLessThanOrEqualTo, MAX_VERSIONS_TO_DELETE) + }) }) } From 4916826364691ac5bd8332651c22c2f1d069e96c Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 22 Mar 2018 14:39:13 +0100 Subject: [PATCH 0129/3000] small screen legend right also work like legend under in render + set scrollbar to undefined in destroyScrollbar so it doesnt become disabled when toggeling between right and under --- public/app/plugins/panel/graph/legend.ts | 4 +++- public/sass/components/_panel_graph.scss | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 7a9c75d4f1d..8a7248fea7f 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -111,6 +111,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } function render() { + let legendWidth = elem.width(); if (!ctrl.panel.legend.show) { elem.empty(); firstRender = true; @@ -163,7 +164,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } // render first time for getting proper legend height - if (!panel.legend.rightSide) { + if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== 10)) { renderLegendElement(tableHeaderElem); elem.empty(); } @@ -265,6 +266,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { function destroyScrollbar() { if (legendScrollbar) { legendScrollbar.destroy(); + legendScrollbar = undefined; } } }, diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 48d88872074..e15cd576367 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -11,19 +11,19 @@ flex: 0 1 10px; max-height: 100%; } - } - .graph-legend-series { - display: block; - padding-left: 0px; - } + .graph-legend-series { + display: block; + padding-left: 0px; + } - .graph-legend-table { - width: auto; - } + .graph-legend-table { + width: auto; + } - .graph-legend-table .graph-legend-series { - display: table-row; + .graph-legend-table .graph-legend-series { + display: table-row; + } } } } From 3ccadff800b350940cca0aae72cf35f2822bcc58 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 14:49:40 +0100 Subject: [PATCH 0130/3000] mssql: fix timeGroup macro so that it properly creates correct groups Earlier the division of interval was done using whole numbers resulting in that important information was lost/too many time series merged to the same group. Now using division of floating point and rounding up to solve the problem --- pkg/tsdb/mssql/macros.go | 2 +- pkg/tsdb/mssql/macros_test.go | 4 +- pkg/tsdb/mssql/mssql_test.go | 72 ++++++++++++++++++++--------------- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 92c6ede148e..108faee85b4 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -113,7 +113,7 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er m.Query.Model.Set("fillValue", floatVal) } } - return fmt.Sprintf("cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s))/%.0f as int)*%.0f as int)", args[0], interval.Seconds(), interval.Seconds()), nil + return fmt.Sprintf("CAST(ROUND(DATEDIFF(second, '1970-01-01', %s)/%.1f, 0) as bigint)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index db1f5670924..c07bcbf498c 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -57,14 +57,14 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)") + So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300") }) Convey("interpolate __timeGroup function with spaces around arguments", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)") + So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300") }) Convey("interpolate __timeGroup function with fill (value = NULL)", func() { diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 7ac135ec2f5..8e8b22d254f 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -211,22 +211,32 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 6) - So(len(points), ShouldEqual, 4) - actualValueFirst := points[0][0].Float64 - actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0) - So(actualValueFirst, ShouldEqual, 15) - So(actualTimeFirst, ShouldEqual, fromStart) + dt := fromStart - actualValueLast := points[3][0].Float64 - actualTimeLast := time.Unix(int64(points[3][1].Float64)/1000, 0) - So(actualValueLast, ShouldEqual, 20) - So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute)) + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 3; i < 6; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } }) Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { @@ -247,33 +257,34 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) points := queryResult.Series[0].Points - So(len(points), ShouldEqual, 7) - actualValueFirst := points[0][0].Float64 - actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0) - So(actualValueFirst, ShouldEqual, 15) - So(actualTimeFirst, ShouldEqual, fromStart) - actualNullPoint := points[3][0] - actualNullTime := time.Unix(int64(points[3][1].Float64)/1000, 0) - So(actualNullPoint.Valid, ShouldBeFalse) - So(actualNullTime, ShouldEqual, fromStart.Add(15*time.Minute)) + dt := fromStart - actualValueLast := points[5][0].Float64 - actualTimeLast := time.Unix(int64(points[5][1].Float64)/1000, 0) - So(actualValueLast, ShouldEqual, 20) - So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute)) + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } - actualLastNullPoint := points[6][0] - actualLastNullTime := time.Unix(int64(points[6][1].Float64)/1000, 0) - So(actualLastNullPoint.Valid, ShouldBeFalse) - So(actualLastNullTime, ShouldEqual, fromStart.Add(30*time.Minute)) + So(points[3][0].Valid, ShouldBeFalse) + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 4; i < 7; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } }) Convey("When doing a metric query using timeGroup with float fill enabled", func() { @@ -294,13 +305,12 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) points := queryResult.Series[0].Points - - So(points[6][0].Float64, ShouldEqual, 1.5) + So(points[3][0].Float64, ShouldEqual, 1.5) }) }) From b0076d4f6500b7fd02c0d862fc5de586154ff076 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 14:55:44 +0100 Subject: [PATCH 0131/3000] mssql: remove UTC conversion in macro functions Removes the macro function . Macro functions should not do UTC/timezone conversion - they should work in the same way as postgres and mysql datasource implementations. Grafana and Microsft SQL Server should be run on servers with UTC timezones. --- pkg/tsdb/mssql/macros.go | 13 ++++-------- pkg/tsdb/mssql/macros_test.go | 21 +++++++------------ .../mssql/partials/annotations.editor.html | 15 +++++++------ .../mssql/partials/query.editor.html | 13 ++++++------ 4 files changed, 24 insertions(+), 38 deletions(-) diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index 108faee85b4..9d41cd03255 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -73,25 +73,20 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("missing time column argument for macro %v", name) } return fmt.Sprintf("%s AS time", args[0]), nil - case "__utcTime": - if len(args) == 0 { - return "", fmt.Errorf("missing time column argument for macro %v", name) - } - return fmt.Sprintf("DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) AS time", args[0]), nil case "__timeEpoch": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) ) AS time", args[0]), nil + return fmt.Sprintf("DATEDIFF(second, '1970-01-01', %s) AS time", args[0]), nil case "__timeFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND %s <= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + return fmt.Sprintf("%s >= DATEADD(s, %d, '1970-01-01') AND %s <= DATEADD(s, %d, '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil case "__timeFrom": - return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil + return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil case "__timeTo": - return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval", name) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index c07bcbf498c..12a9b0d82be 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -25,32 +25,25 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, "select time_column AS time") }) - Convey("interpolate __utcTime function", func() { - sql, err := engine.Interpolate(query, nil, "select $__utcTime(time_column)") - So(err, ShouldBeNil) - - So(sql, ShouldEqual, "select DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) AS time") - }) - Convey("interpolate __timeEpoch function", func() { sql, err := engine.Interpolate(query, nil, "select $__timeEpoch(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time") + So(sql, ShouldEqual, "select DATEDIFF(second, '1970-01-01', time_column) AS time") }) Convey("interpolate __timeEpoch function wrapped in aggregation", func() { sql, err := engine.Interpolate(query, nil, "select min($__timeEpoch(time_column))") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select min(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time)") + So(sql, ShouldEqual, "select min(DATEDIFF(second, '1970-01-01', time_column) AS time)") }) Convey("interpolate __timeFilter function", func() { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038, '1970-01-01')") }) Convey("interpolate __timeGroup function", func() { @@ -97,21 +90,21 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738, '1970-01-01')") }) Convey("interpolate __timeTo function", func() { sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')") + So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038, '1970-01-01')") }) Convey("interpolate __unixEpochFilter function", func() { - sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(18446744066914186738)") + sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "select 18446744066914186738 >= 18446744066914186738 AND 18446744066914186738 <= 18446744066914187038") + So(sql, ShouldEqual, "select time_column >= 18446744066914186738 AND time_column <= 18446744066914187038") }) Convey("interpolate __unixEpochFrom function", func() { diff --git a/public/app/plugins/datasource/mssql/partials/annotations.editor.html b/public/app/plugins/datasource/mssql/partials/annotations.editor.html index ecdffd92d1e..185785ced4a 100644 --- a/public/app/plugins/datasource/mssql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mssql/partials/annotations.editor.html @@ -27,16 +27,15 @@ An annotation is an event that is overlayed on top of graphs. The query can have Macros: - $__time(column) -> column AS time -- $__utcTime(column) -> DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column) AS time -- $__timeEpoch(column) -> DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column) ) AS time -- $__timeFilter(column) -> column > DATEADD(s, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') AND column < DATEADD(s, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 +- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time +- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01') +- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 Or build your own conditionals using these macros which just return the values: -- $__timeFrom() -> DATEADD(second, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__timeTo() -> DATEADD(second, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__unixEpochFrom() -> 1492750877 -- $__unixEpochTo() -> 1492750877 +- $__timeFrom() -> DATEADD(second, 1492750877, '1970-01-01') +- $__timeTo() -> DATEADD(second, 1492750877, '1970-01-01') +- $__unixEpochFrom() -> 1492750877 +- $__unixEpochTo() -> 1492750877
    diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html index e8f44c8c9f8..f8c7effb827 100644 --- a/public/app/plugins/datasource/mssql/partials/query.editor.html +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html @@ -48,15 +48,14 @@ Table: Macros: - $__time(column) -> column AS time -- $__utcTime(column) -> DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column) AS time -- $__timeEpoch(column) -> DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column) ) AS time -- $__timeFilter(column) -> column > DATEADD(s, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') AND column < DATEADD(s, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 -- $__timeGroup(column, '5m'[, fillvalue]) -> cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second, GETDATE(), GETUTCDATE()), column))/300 as int)*300 as int). Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. +- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time +- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01') +- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 +- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value. Or build your own conditionals using these macros which just return the values: -- $__timeFrom() -> DATEADD(second, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') -- $__timeTo() -> DATEADD(second, 1492750877+DATEDIFF(second, GETUTCDATE(), GETDATE()), '1970-01-01') +- $__timeFrom() -> DATEADD(second, 1492750877, '1970-01-01') +- $__timeTo() -> DATEADD(second, 1492750877, '1970-01-01') - $__unixEpochFrom() -> 1492750877 - $__unixEpochTo() -> 1492750877 From b69ebee066ae3eea79a79213a2360e55be837726 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 15:23:12 +0100 Subject: [PATCH 0132/3000] mssql: fix precision for the time column in table/annotation query mode Use the ConvertSqlTimeColumnToEpochMs function to convert any native datetime data type or epoch time (millisecond precision). Additional tests and update of existing due to timezone issues running MSSQL on UTC and dev environment on non-utc. Update stored procedures test to handle more parameters. Update test dashboard. --- docker/blocks/mssql_tests/dashboard.json | 380 +++++++++++++++--- pkg/tsdb/mssql/mssql.go | 13 +- pkg/tsdb/mssql/mssql_test.go | 285 ++++++++++--- .../mssql/partials/annotations.editor.html | 2 +- .../datasource/mssql/response_parser.ts | 2 +- 5 files changed, 548 insertions(+), 134 deletions(-) diff --git a/docker/blocks/mssql_tests/dashboard.json b/docker/blocks/mssql_tests/dashboard.json index 323a61bb49a..20e3907b48b 100644 --- a/docker/blocks/mssql_tests/dashboard.json +++ b/docker/blocks/mssql_tests/dashboard.json @@ -53,7 +53,7 @@ "iconColor": "#6ed0e0", "limit": 100, "name": "Deploys", - "rawQuery": "SELECT\n time_sec as time,\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", + "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", "showIn": 0, "tags": [], "type": "tags" @@ -65,7 +65,7 @@ "iconColor": "rgba(255, 96, 96, 1)", "limit": 100, "name": "Tickets", - "rawQuery": "SELECT\n time_sec as time,\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", + "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", "showIn": 0, "tags": [], "type": "tags" @@ -76,8 +76,20 @@ "hide": false, "iconColor": "#7eb26d", "limit": 100, - "name": "Metric Values", - "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nORDER BY 1", + "name": "Metric Values timeEpoch macro", + "rawQuery": "SELECT \n $__timeEpoch(time), \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MSSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#1f78c1", + "limit": 100, + "name": "Metric Values native time", + "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", "showIn": 0, "tags": [], "type": "tags" @@ -88,7 +100,7 @@ "gnetId": null, "graphTooltip": 0, "id": null, - "iteration": 1521481503341, + "iteration": 1521715844826, "links": [], "panels": [ { @@ -138,6 +150,222 @@ "transform": "table", "type": "table" }, + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 32, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as bigint) as time", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as bigint) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 33, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as datetime) as time", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as datetime) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 34, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT GETDATE() as time", + "refId": "A", + "target": "" + } + ], + "title": "GETDATE() as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MSSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 35, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT GETUTCDATE() as time", + "refId": "A", + "target": "" + } + ], + "title": "GETUTCDATE() as time", + "transform": "table", + "type": "table" + }, { "aliasColors": {}, "bars": false, @@ -149,7 +377,7 @@ "h": 9, "w": 8, "x": 0, - "y": 4 + "y": 7 }, "id": 7, "legend": { @@ -228,7 +456,7 @@ "h": 9, "w": 8, "x": 8, - "y": 4 + "y": 7 }, "id": 9, "legend": { @@ -307,7 +535,7 @@ "h": 9, "w": 8, "x": 16, - "y": 4 + "y": 7 }, "id": 10, "legend": { @@ -386,7 +614,7 @@ "h": 9, "w": 8, "x": 0, - "y": 13 + "y": 16 }, "id": 16, "legend": { @@ -465,7 +693,7 @@ "h": 9, "w": 8, "x": 8, - "y": 13 + "y": 16 }, "id": 12, "legend": { @@ -544,7 +772,7 @@ "h": 9, "w": 8, "x": 16, - "y": 13 + "y": 16 }, "id": 13, "legend": { @@ -623,7 +851,7 @@ "h": 8, "w": 12, "x": 0, - "y": 22 + "y": 25 }, "id": 27, "legend": { @@ -655,13 +883,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value one' as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value one' as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value two' as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value two' as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", "refId": "B" } ], @@ -712,7 +940,7 @@ "h": 8, "w": 12, "x": 12, - "y": 22 + "y": 25 }, "id": 5, "legend": { @@ -734,7 +962,19 @@ "pointradius": 3, "points": false, "renderer": "flot", - "seriesOverrides": [], + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], "spaceLength": 10, "stack": false, "steppedLine": false, @@ -742,8 +982,14 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n time,\n avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1", + "refId": "B" } ], "thresholds": [], @@ -793,7 +1039,7 @@ "h": 8, "w": 12, "x": 0, - "y": 30 + "y": 33 }, "id": 4, "legend": { @@ -825,13 +1071,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -882,7 +1128,7 @@ "h": 8, "w": 12, "x": 12, - "y": 30 + "y": 33 }, "id": 28, "legend": { @@ -963,7 +1209,7 @@ "h": 8, "w": 12, "x": 0, - "y": 38 + "y": 41 }, "id": 19, "legend": { @@ -995,13 +1241,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1052,7 +1298,7 @@ "h": 8, "w": 12, "x": 12, - "y": 38 + "y": 41 }, "id": 18, "legend": { @@ -1082,7 +1328,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1133,7 +1379,7 @@ "h": 8, "w": 12, "x": 0, - "y": 46 + "y": 49 }, "id": 17, "legend": { @@ -1165,13 +1411,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1222,7 +1468,7 @@ "h": 8, "w": 12, "x": 12, - "y": 46 + "y": 49 }, "id": 20, "legend": { @@ -1252,7 +1498,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1303,7 +1549,7 @@ "h": 8, "w": 12, "x": 0, - "y": 54 + "y": 57 }, "id": 29, "legend": { @@ -1335,7 +1581,7 @@ { "alias": "", "format": "time_series", - "rawSql": "DECLARE \n @from int = $__unixEpochFrom(),\n @to int = $__unixEpochTo()\n \nEXEC dbo.sp_test_epoch @from, @to", + "rawSql": "DECLARE\n @from int = $__unixEpochFrom(), \n @to int = $__unixEpochTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_epoch @from, @to, @interval, @metric", "refId": "A" } ], @@ -1386,7 +1632,7 @@ "h": 8, "w": 12, "x": 12, - "y": 54 + "y": 57 }, "id": 30, "legend": { @@ -1418,7 +1664,7 @@ { "alias": "", "format": "time_series", - "rawSql": "DECLARE \n @from datetime = $__timeFrom(),\n @to datetime = $__timeTo()\n \nEXEC dbo.sp_test_datetime @from, @to", + "rawSql": "DECLARE\n @from datetime = $__timeFrom(), \n @to datetime = $__timeTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_datetime @from, @to, @interval, @metric", "refId": "A" } ], @@ -1469,7 +1715,7 @@ "h": 8, "w": 12, "x": 0, - "y": 62 + "y": 65 }, "id": 14, "legend": { @@ -1499,13 +1745,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1559,7 +1805,7 @@ "h": 8, "w": 12, "x": 12, - "y": 62 + "y": 65 }, "id": 15, "legend": { @@ -1589,7 +1835,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1642,7 +1888,7 @@ "h": 8, "w": 12, "x": 0, - "y": 70 + "y": 73 }, "id": 25, "legend": { @@ -1672,13 +1918,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1732,7 +1978,7 @@ "h": 8, "w": 12, "x": 12, - "y": 70 + "y": 73 }, "id": 22, "legend": { @@ -1762,7 +2008,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1815,7 +2061,7 @@ "h": 8, "w": 12, "x": 0, - "y": 78 + "y": 81 }, "id": 21, "legend": { @@ -1845,13 +2091,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -1905,7 +2151,7 @@ "h": 8, "w": 12, "x": 12, - "y": 78 + "y": 81 }, "id": 26, "legend": { @@ -1935,7 +2181,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1988,7 +2234,7 @@ "h": 8, "w": 12, "x": 0, - "y": 86 + "y": 89 }, "id": 23, "legend": { @@ -2018,13 +2264,13 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" }, { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "B" } ], @@ -2078,7 +2324,7 @@ "h": 8, "w": 12, "x": 12, - "y": 86 + "y": 89 }, "id": 24, "legend": { @@ -2108,7 +2354,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -2157,6 +2403,26 @@ "tags": [], "templating": { "list": [ + { + "allValue": "'ALL'", + "current": {}, + "datasource": "${DS_MSSQL_TEST}", + "hide": 0, + "includeAll": true, + "label": "Metric", + "multi": false, + "name": "metric", + "options": [], + "query": "SELECT DISTINCT measurement FROM metric_values", + "refresh": 1, + "regex": "", + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, { "auto": false, "auto_count": 30, @@ -2208,7 +2474,7 @@ }, "time": { "from": "2018-03-15T12:30:00.000Z", - "to": "2018-03-15T13:55:00.000Z" + "to": "2018-03-15T13:55:01.000Z" }, "timepicker": { "refresh_intervals": [ @@ -2238,5 +2504,5 @@ "timezone": "", "title": "Microsoft SQL Server Data Source Test", "uid": "GlAqcPgmz", - "version": 37 + "version": 57 } \ No newline at end of file diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index 6da61d63e42..2638fd8bb40 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -119,15 +119,10 @@ func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, return err } - // convert column named time to unix timestamp to make - // native datetime mssql types work in annotation queries - if timeIndex != -1 { - switch value := values[timeIndex].(type) { - case time.Time: - values[timeIndex] = (float64(value.Unix()) * 1000) + float64(value.Nanosecond()/1e6) // in case someone is trying to map times beyond 2262 :D - } - } - + // converts column named time to unix timestamp in milliseconds + // to make native mssql datetime types and epoch dates work in + // annotation and table queries. + tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) table.Rows = append(table.Rows, values) } diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 8e8b22d254f..4bd1e3a8ad7 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -19,6 +19,8 @@ import ( // and set up a MSSQL db named grafanatest and a user/password grafana/Password! // Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a // preconfigured MSSQL server suitable for running these tests. +// Thers's also a dashboard.json in same directory that you can import to Grafana +// once you've created a datasource for the test server/database. // If needed, change the variable below to the IP address of the database. var serverIP string = "localhost" @@ -37,7 +39,7 @@ func TestMSSQL(t *testing.T) { sess := x.NewSession() defer sess.Close() - fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC) + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) Convey("Given a table with different native data types", func() { sql := ` @@ -186,14 +188,8 @@ func TestMSSQL(t *testing.T) { }) } - dtFormat := "2006-01-02 15:04:05.999999999" for _, s := range series { - sql = fmt.Sprintf(` - INSERT INTO metric (time, value) - VALUES(CAST('%s' AS DATETIME), %d) - `, s.Time.Format(dtFormat), s.Value) - - _, err = sess.Exec(sql) + _, err = sess.Insert(s) So(err, ShouldBeNil) } @@ -315,42 +311,34 @@ func TestMSSQL(t *testing.T) { }) Convey("Given a table with metrics having multiple values and measurements", func() { - sql := ` - IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL - DROP TABLE dbo.[metric_values] - - CREATE TABLE [metric_values] ( - time datetime, - measurement nvarchar(100), - valueOne int, - valueTwo int, - ) - ` - - _, err := sess.Exec(sql) - So(err, ShouldBeNil) - - type metricValues struct { + type metric_values struct { Time time.Time Measurement string - ValueOne int64 - ValueTwo int64 + ValueOne int64 `xorm:"integer 'valueOne'"` + ValueTwo int64 `xorm:"integer 'valueTwo'"` } + if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(metric_values{}) + } + err := sess.CreateTable(metric_values{}) + So(err, ShouldBeNil) + rand.Seed(time.Now().Unix()) rnd := func(min, max int64) int64 { return rand.Int63n(max-min) + min } - series := []*metricValues{} + series := []*metric_values{} for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { - series = append(series, &metricValues{ + series = append(series, &metric_values{ Time: t, Measurement: "Metric A", ValueOne: rnd(0, 100), ValueTwo: rnd(0, 100), }) - series = append(series, &metricValues{ + series = append(series, &metric_values{ Time: t, Measurement: "Metric B", ValueOne: rnd(0, 100), @@ -358,14 +346,8 @@ func TestMSSQL(t *testing.T) { }) } - dtFormat := "2006-01-02 15:04:05" for _, s := range series { - sql = fmt.Sprintf(` - INSERT metric_values (time, measurement, valueOne, valueTwo) - VALUES(CAST('%s' AS DATETIME), '%s', %d, %d) - `, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo) - - _, err = sess.Exec(sql) + _, err = sess.Insert(s) So(err, ShouldBeNil) } @@ -383,8 +365,8 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 2) @@ -406,8 +388,8 @@ func TestMSSQL(t *testing.T) { } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] So(err, ShouldBeNil) + queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 2) @@ -426,32 +408,42 @@ func TestMSSQL(t *testing.T) { sql = ` CREATE PROCEDURE sp_test_epoch( - @from int, - @to int + @from int, + @to int, + @interval nvarchar(50) = '5m', + @metric nvarchar(200) = 'ALL' ) AS BEGIN + DECLARE @dInterval int + SELECT @dInterval = 300 + + IF @interval = '10m' + SELECT @dInterval = 600 + SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, measurement + ' - value one' as metric, avg(valueOne) as value FROM metric_values WHERE - time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND + (@metric = 'ALL' OR measurement = @metric) GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, measurement UNION ALL SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time, + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, measurement + ' - value two' as metric, avg(valueTwo) as value FROM metric_values WHERE - time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01') + time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND + (@metric = 'ALL' OR measurement = @metric) GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int), + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, measurement ORDER BY 1 END @@ -484,6 +476,7 @@ func TestMSSQL(t *testing.T) { resp, err := endpoint.Query(nil, nil, query) queryResult := resp.Results["A"] So(err, ShouldBeNil) + fmt.Println("query", "sql", queryResult.Meta) So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 4) @@ -505,32 +498,42 @@ func TestMSSQL(t *testing.T) { sql = ` CREATE PROCEDURE sp_test_datetime( - @from datetime, - @to datetime + @from datetime, + @to datetime, + @interval nvarchar(50) = '5m', + @metric nvarchar(200) = 'ALL' ) AS BEGIN + DECLARE @dInterval int + SELECT @dInterval = 300 + + IF @interval = '10m' + SELECT @dInterval = 600 + SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, measurement + ' - value one' as metric, avg(valueOne) as value FROM metric_values WHERE - time >= @from AND time <= @to + time BETWEEN @from AND @to AND + (@metric = 'ALL' OR measurement = @metric) GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, measurement UNION ALL SELECT - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time, + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, measurement + ' - value two' as metric, avg(valueTwo) as value FROM metric_values WHERE - time >= @from AND time <= @to + time BETWEEN @from AND @to AND + (@metric = 'ALL' OR measurement = @metric) GROUP BY - cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int), + CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, measurement ORDER BY 1 END @@ -580,7 +583,7 @@ func TestMSSQL(t *testing.T) { DROP TABLE dbo.[event] CREATE TABLE [event] ( - time_sec bigint, + time_sec int, description nvarchar(100), tags nvarchar(100), ) @@ -666,30 +669,180 @@ func TestMSSQL(t *testing.T) { }) Convey("When doing an annotation query with a time column in datetime format", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + dtFormat := "2006-01-02 15:04:05.999999999" + query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT DATEADD(s, time_sec, {d '1970-01-01'}) AS time, description as [text], tags FROM [event] WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", + "rawSql": fmt.Sprintf(`SELECT + CAST('%s' AS DATETIME) as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Format(dtFormat)), "format": "table", }), - RefId: "Tickets", + RefId: "A", }, }, - TimeRange: &tsdb.TimeRange{ - From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), - To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), - }, } resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["Tickets"] So(err, ShouldBeNil) - So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) columns := queryResult.Tables[0].Rows[0] //Should be in milliseconds - So(columns[0].(float64), ShouldBeGreaterThan, 1000000000000) + So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + cast(%d as int) as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()*1000), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column holding a bigint null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as bigint) as time, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) + + Convey("When doing an annotation query with a time column holding a datetime null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as datetime) as time, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) }) }) }) @@ -697,6 +850,8 @@ func TestMSSQL(t *testing.T) { func InitMSSQLTestDB(t *testing.T) *xorm.Engine { x, err := xorm.NewEngine(sqlutil.TestDB_Mssql.DriverName, strings.Replace(sqlutil.TestDB_Mssql.ConnStr, "localhost", serverIP, 1)) + x.DatabaseTZ = time.UTC + x.TZLocation = time.UTC // x.ShowSQL() @@ -704,8 +859,6 @@ func InitMSSQLTestDB(t *testing.T) *xorm.Engine { t.Fatalf("Failed to init mssql db %v", err) } - sqlutil.CleanDB(x) - return x } diff --git a/public/app/plugins/datasource/mssql/partials/annotations.editor.html b/public/app/plugins/datasource/mssql/partials/annotations.editor.html index 185785ced4a..75eaa3ed1d9 100644 --- a/public/app/plugins/datasource/mssql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mssql/partials/annotations.editor.html @@ -20,7 +20,7 @@
    Annotation Query Format
    An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. -- column with alias: time for the annotation event time (in UTC). Use unix timestamp in seconds or any native date data type. +- column with alias: time for the annotation event time. Use epoch time or any native date data type. - column with alias: text for the annotation text. - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'. diff --git a/public/app/plugins/datasource/mssql/response_parser.ts b/public/app/plugins/datasource/mssql/response_parser.ts index c98a9652b0e..b6f538707b0 100644 --- a/public/app/plugins/datasource/mssql/response_parser.ts +++ b/public/app/plugins/datasource/mssql/response_parser.ts @@ -128,7 +128,7 @@ export default class ResponseParser { const row = table.rows[i]; list.push({ annotation: options.annotation, - time: row[timeColumnIndex], + time: Math.floor(row[timeColumnIndex]), text: row[textColumnIndex], tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [], }); From 66c03f84f59a493dd5110982070dbddb91ae48c2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 15:27:12 +0100 Subject: [PATCH 0133/3000] postgres: fix precision for the time column in table/annotation query mode Use the ConvertSqlTimeColumnToEpochMs function to convert any native datetime data type or epoch time (millisecond precision). Additional tests and update of existing due to timezone issues running postgres on UTC and dev environment on non-utc. Added test dashboard. --- docker/blocks/postgres_tests/dashboard.json | 2324 +++++++++++++++++ pkg/tsdb/postgres/postgres.go | 15 +- pkg/tsdb/postgres/postgres_test.go | 668 ++++- .../postgres/partials/annotations.editor.html | 5 +- .../datasource/postgres/response_parser.ts | 2 +- 5 files changed, 2930 insertions(+), 84 deletions(-) create mode 100644 docker/blocks/postgres_tests/dashboard.json diff --git a/docker/blocks/postgres_tests/dashboard.json b/docker/blocks/postgres_tests/dashboard.json new file mode 100644 index 00000000000..eea95863716 --- /dev/null +++ b/docker/blocks/postgres_tests/dashboard.json @@ -0,0 +1,2324 @@ +{ + "__inputs": [ + { + "name": "DS_POSTGRES_TEST", + "label": "Postgres TEST", + "description": "", + "type": "datasource", + "pluginId": "postgres", + "pluginName": "PostgreSQL" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "5.0.0" + }, + { + "type": "datasource", + "id": "postgres", + "name": "PostgreSQL", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "5.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + }, + { + "datasource": "${DS_POSTGRES_TEST}", + "enable": false, + "hide": false, + "iconColor": "#6ed0e0", + "limit": 100, + "name": "Deploys", + "rawQuery": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_POSTGRES_TEST}", + "enable": false, + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "limit": 100, + "name": "Tickets", + "rawQuery": "SELECT \"time_sec\" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_POSTGRES_TEST}", + "enable": false, + "hide": false, + "iconColor": "#7eb26d", + "limit": 100, + "name": "Metric Values timeEpoch macro", + "rawQuery": "SELECT \n $__timeEpoch(time), \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_POSTGRES_TEST}", + "enable": false, + "hide": false, + "iconColor": "#1f78c1", + "limit": 100, + "name": "Metric Values native time", + "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1521725946837, + "links": [], + "panels": [ + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 1, + "desc": false + }, + "styles": [ + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT * FROM postgres_types", + "refId": "A" + } + ], + "title": "Data types", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 32, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as bigint) as time", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as bigint) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 33, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as timestamp) as time", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as datetime) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 34, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT localtimestamp as time", + "refId": "A", + "target": "" + } + ], + "title": "localtimestamp as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_POSTGRES_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 35, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT NOW() as time", + "refId": "A", + "target": "" + } + ], + "title": "NOW() as time", + "transform": "table", + "type": "table" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 7 + }, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m without fill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 7 + }, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(NULL) and null as zero", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(10.0)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize without fill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(NULL)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(100.0)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value one' as metric, \n avg(\"valueOne\") as \"valueOne\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value two' as metric, \n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 5, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 28, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 19, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "id": 18, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 49 + }, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 14, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 15, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 65 + }, + "id": 25, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 50, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 65 + }, + "id": 22, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values\nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 100, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 21, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 26, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 23, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_POSTGRES_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 81 + }, + "id": 24, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": {}, + "datasource": "${DS_POSTGRES_TEST}", + "hide": 0, + "includeAll": true, + "label": "Metric", + "multi": true, + "name": "metric", + "options": [], + "query": "SELECT DISTINCT measurement FROM metric_values", + "refresh": 1, + "regex": "", + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "10m", + "value": "10m" + }, + "hide": 0, + "label": "Interval", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": true, + "text": "10m", + "value": "10m" + } + ], + "query": "1s,10s,30s,1m,5m,10m", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "2018-03-15T12:30:00.000Z", + "to": "2018-03-15T13:55:01.000Z" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Postgres Data Source Test", + "uid": "vHQdlVziz", + "version": 14 +} \ No newline at end of file diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 6a084ad1237..5f6b56ebcf1 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -63,7 +63,6 @@ func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSo } func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - columnNames, err := rows.Columns() if err != nil { return err @@ -100,14 +99,10 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro return err } - // convert column named time to unix timestamp to make - // native datetime postgres types work in annotation queries - if timeIndex != -1 { - switch value := values[timeIndex].(type) { - case time.Time: - values[timeIndex] = float64(value.UnixNano() / 1e9) - } - } + // converts column named time to unix timestamp in milliseconds to make + // native postgres datetime types and epoch dates work in + // annotation and table queries. + tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) table.Rows = append(table.Rows, values) } @@ -118,7 +113,6 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro } func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) { - types, err := rows.ColumnTypes() if err != nil { return nil, err @@ -209,7 +203,6 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() fillValue.Valid = true } - } for rows.Next() { diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 75e8cb77f2e..3f2203ac7a4 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -1,6 +1,8 @@ package postgres import ( + "fmt" + "math/rand" "testing" "time" @@ -14,7 +16,11 @@ import ( ) // To run this test, remove the Skip from SkipConvey -// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest +// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest! +// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a +// preconfigured Postgres server suitable for running these tests. +// Thers's also a dashboard.json in same directory that you can import to Grafana +// once you've created a datasource for the test server/database. func TestPostgres(t *testing.T) { SkipConvey("PostgreSQL", t, func() { x := InitPostgresTestDB(t) @@ -30,88 +36,599 @@ func TestPostgres(t *testing.T) { sess := x.NewSession() defer sess.Close() - sql := ` - CREATE TABLE postgres_types( - c00_smallint smallint, - c01_integer integer, - c02_bigint bigint, + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) - c03_real real, - c04_double double precision, - c05_decimal decimal(10,2), - c06_numeric numeric(10,2), + Convey("Given a table with different native data types", func() { + sql := ` + DROP TABLE IF EXISTS postgres_types; + CREATE TABLE postgres_types( + c00_smallint smallint, + c01_integer integer, + c02_bigint bigint, - c07_char char(10), - c08_varchar varchar(10), - c09_text text, + c03_real real, + c04_double double precision, + c05_decimal decimal(10,2), + c06_numeric numeric(10,2), - c10_timestamp timestamp without time zone, - c11_timestamptz timestamp with time zone, - c12_date date, - c13_time time without time zone, - c14_timetz time with time zone, - c15_interval interval - ); - ` - _, err := sess.Exec(sql) - So(err, ShouldBeNil) + c07_char char(10), + c08_varchar varchar(10), + c09_text text, - sql = ` - INSERT INTO postgres_types VALUES( - 1,2,3, - 4.5,6.7,1.1,1.2, - 'char10','varchar10','text', + c10_timestamp timestamp without time zone, + c11_timestamptz timestamp with time zone, + c12_date date, + c13_time time without time zone, + c14_timetz time with time zone, - now(),now(),now(),now(),now(),'15m'::interval - ); - ` - _, err = sess.Exec(sql) - So(err, ShouldBeNil) - - Convey("Query with Table format should map PostgreSQL column types to Go types", func() { - query := &tsdb.TsdbQuery{ - Queries: []*tsdb.Query{ - { - Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT * FROM postgres_types", - "format": "table", - }), - RefId: "A", - }, - }, - } - - resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] + c15_interval interval + ); + ` + _, err := sess.Exec(sql) So(err, ShouldBeNil) - column := queryResult.Tables[0].Rows[0] - So(column[0].(int64), ShouldEqual, 1) - So(column[1].(int64), ShouldEqual, 2) - So(column[2].(int64), ShouldEqual, 3) - So(column[3].(float64), ShouldEqual, 4.5) - So(column[4].(float64), ShouldEqual, 6.7) - // libpq doesnt properly convert decimal, numeric and char to go types but returns []uint8 instead - // So(column[5].(float64), ShouldEqual, 1.1) - // So(column[6].(float64), ShouldEqual, 1.2) - // So(column[7].(string), ShouldEqual, "char") - So(column[8].(string), ShouldEqual, "varchar10") - So(column[9].(string), ShouldEqual, "text") + sql = ` + INSERT INTO postgres_types VALUES( + 1,2,3, + 4.5,6.7,1.1,1.2, + 'char10','varchar10','text', - So(column[10].(time.Time), ShouldHaveSameTypeAs, time.Now()) - So(column[11].(time.Time), ShouldHaveSameTypeAs, time.Now()) - So(column[12].(time.Time), ShouldHaveSameTypeAs, time.Now()) - So(column[13].(time.Time), ShouldHaveSameTypeAs, time.Now()) - So(column[14].(time.Time), ShouldHaveSameTypeAs, time.Now()) + now(),now(),now(),now(),now(),'15m'::interval + ); + ` + _, err = sess.Exec(sql) + So(err, ShouldBeNil) - // libpq doesnt properly convert interval to go types but returns []uint8 instead - // So(column[15].(time.Time), ShouldHaveSameTypeAs, time.Now()) + Convey("When doing a table query should map Postgres column types to Go types", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT * FROM postgres_types", + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + column := queryResult.Tables[0].Rows[0] + So(column[0].(int64), ShouldEqual, 1) + So(column[1].(int64), ShouldEqual, 2) + So(column[2].(int64), ShouldEqual, 3) + + So(column[3].(float64), ShouldEqual, 4.5) + So(column[4].(float64), ShouldEqual, 6.7) + So(column[5].(float64), ShouldEqual, 1.1) + So(column[6].(float64), ShouldEqual, 1.2) + + So(column[7].(string), ShouldEqual, "char10 ") + So(column[8].(string), ShouldEqual, "varchar10") + So(column[9].(string), ShouldEqual, "text") + + So(column[10].(time.Time), ShouldHaveSameTypeAs, time.Now()) + So(column[11].(time.Time), ShouldHaveSameTypeAs, time.Now()) + So(column[12].(time.Time), ShouldHaveSameTypeAs, time.Now()) + So(column[13].(time.Time), ShouldHaveSameTypeAs, time.Now()) + So(column[14].(time.Time), ShouldHaveSameTypeAs, time.Now()) + + So(column[15].(string), ShouldEqual, "00:15:00") + }) + }) + + Convey("Given a table with metrics that lacks data for some series ", func() { + sql := ` + DROP TABLE IF EXISTS metric; + CREATE TABLE metric ( + time timestamp, + value integer + ) + ` + + _, err := sess.Exec(sql) + So(err, ShouldBeNil) + + type metric struct { + Time time.Time + Value int64 + } + + series := []*metric{} + firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second) + secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second) + + for _, t := range firstRange { + series = append(series, &metric{ + Time: t, + Value: 15, + }) + } + + for _, t := range secondRange { + series = append(series, &metric{ + Time: t, + Value: 20, + }) + } + + for _, s := range series { + _, err = sess.Insert(s) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query using timeGroup", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 6) + + dt := fromStart + + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 3; i < 6; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + }) + + Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 7) + + dt := fromStart + + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + So(points[3][0].Valid, ShouldBeFalse) + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 4; i < 7; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + }) + + Convey("When doing a metric query using timeGroup with float fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(points[3][0].Float64, ShouldEqual, 1.5) + }) + }) + + Convey("Given a table with metrics having multiple values and measurements", func() { + type metric_values struct { + Time time.Time + Measurement string + ValueOne int64 `xorm:"integer 'valueOne'"` + ValueTwo int64 `xorm:"integer 'valueTwo'"` + } + + if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(metric_values{}) + } + err := sess.CreateTable(metric_values{}) + So(err, ShouldBeNil) + + rand.Seed(time.Now().Unix()) + rnd := func(min, max int64) int64 { + return rand.Int63n(max-min) + min + } + + series := []*metric_values{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { + series = append(series, &metric_values{ + Time: t, + Measurement: "Metric A", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + series = append(series, &metric_values{ + Time: t, + Measurement: "Metric B", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + } + + for _, s := range series { + _, err := sess.Insert(s) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query grouping by time and select metric column should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__timeEpoch(time), measurement || ' - value one' as metric, "valueOne" FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") + So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") + }) + + Convey("When doing a metric query grouping by time should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__timeEpoch(time), "valueOne", "valueTwo" FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "valueTwo") + }) + }) + + Convey("Given a table with event data", func() { + type event struct { + TimeSec int64 + Description string + Tags string + } + + if exist, err := sess.IsTableExist(event{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(event{}) + } + err := sess.CreateTable(event{}) + So(err, ShouldBeNil) + + events := []*event{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) { + events = append(events, &event{ + TimeSec: t.Unix(), + Description: "Someone deployed something", + Tags: "deploy", + }) + events = append(events, &event{ + TimeSec: t.Add(5 * time.Minute).Unix(), + Description: "New support ticket registered", + Tags: "ticket", + }) + } + + for _, e := range events { + _, err = sess.Insert(e) + So(err, ShouldBeNil) + } + + Convey("When doing an annotation query of deploy events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`, + "format": "table", + }), + RefId: "Deploys", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Deploys"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query of ticket events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`, + "format": "table", + }), + RefId: "Tickets", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Tickets"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query with a time column in datetime format", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + dtFormat := "2006-01-02 15:04:05.999999999" + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + CAST('%s' AS TIMESTAMP) as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Format(dtFormat)), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + cast(%d as bigint) as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()*1000), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, dt.Unix()*1000) + }) + + Convey("When doing an annotation query with a time column holding a bigint null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as bigint) as time, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) + + Convey("When doing an annotation query with a time column holding a timestamp null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as timestamp) as time, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) }) }) } func InitPostgresTestDB(t *testing.T) *xorm.Engine { x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr) + x.DatabaseTZ = time.UTC + x.TZLocation = time.UTC // x.ShowSQL() @@ -119,7 +636,18 @@ func InitPostgresTestDB(t *testing.T) *xorm.Engine { t.Fatalf("Failed to init postgres db %v", err) } - sqlutil.CleanDB(x) - return x } + +func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time { + durationSec := int64(duration.Seconds()) + intervalSec := int64(interval.Seconds()) + timeRange := []time.Time{} + + for i := int64(0); i < durationSec; i += intervalSec { + timeRange = append(timeRange, from) + from = from.Add(time.Duration(int64(time.Second) * intervalSec)) + } + + return timeRange +} diff --git a/public/app/plugins/datasource/postgres/partials/annotations.editor.html b/public/app/plugins/datasource/postgres/partials/annotations.editor.html index b56f7523087..09232d6f8ed 100644 --- a/public/app/plugins/datasource/postgres/partials/annotations.editor.html +++ b/public/app/plugins/datasource/postgres/partials/annotations.editor.html @@ -18,15 +18,16 @@
    Annotation Query Format
    -An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. +An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. -- column with alias: time for the annotation event. Format is UTC in seconds, use extract(epoch from column) as "time" +- column with alias: time for the annotation event time. Use epoch time or any native date data type. - column with alias: text for the annotation text - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' Macros: - $__time(column) -> column as "time" +- $__timeEpoch -> extract(epoch from column) as "time" - $__timeFilter(column) -> column ≥ to_timestamp(1492750877) AND column ≤ to_timestamp(1492750877) - $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 diff --git a/public/app/plugins/datasource/postgres/response_parser.ts b/public/app/plugins/datasource/postgres/response_parser.ts index 620aba5fa7e..ebc9598468b 100644 --- a/public/app/plugins/datasource/postgres/response_parser.ts +++ b/public/app/plugins/datasource/postgres/response_parser.ts @@ -134,7 +134,7 @@ export default class ResponseParser { const row = table.rows[i]; list.push({ annotation: options.annotation, - time: Math.floor(row[timeColumnIndex]) * 1000, + time: Math.floor(row[timeColumnIndex]), title: row[titleColumnIndex], text: row[textColumnIndex], tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [], From f5654f88e21eba4b5418151737367e969f04025d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 22 Mar 2018 15:40:46 +0100 Subject: [PATCH 0134/3000] mysql: fix precision for the time column in table/annotation query mode Use the ConvertSqlTimeColumnToEpochMs function to convert any native datetime data type or epoch time (millisecond precision). Refactored mysql implementation to make it more similar to postgres and mssql implementations. Added $__timeEpoch macro function with same implementation as $__time. Added possibility to use a time column named time in addition to the currectly supported time_sec. Additional tests and update of existing. Added test dashboard. --- docker/blocks/mysql_tests/dashboard.json | 2350 +++++++++++++++++ pkg/tsdb/mysql/macros.go | 2 +- pkg/tsdb/mysql/mysql.go | 205 +- pkg/tsdb/mysql/mysql_test.go | 718 ++++- .../mysql/partials/annotations.editor.html | 7 +- .../mysql/partials/query.editor.html | 7 +- .../datasource/mysql/response_parser.ts | 4 +- 7 files changed, 3088 insertions(+), 205 deletions(-) create mode 100644 docker/blocks/mysql_tests/dashboard.json diff --git a/docker/blocks/mysql_tests/dashboard.json b/docker/blocks/mysql_tests/dashboard.json new file mode 100644 index 00000000000..3ab08a7da35 --- /dev/null +++ b/docker/blocks/mysql_tests/dashboard.json @@ -0,0 +1,2350 @@ +{ + "__inputs": [ + { + "name": "DS_MYSQL_TEST", + "label": "MySQL TEST", + "description": "", + "type": "datasource", + "pluginId": "mysql", + "pluginName": "MySQL" + }, + { + "name": "DS_MSSQL_TEST", + "label": "MSSQL Test", + "description": "", + "type": "datasource", + "pluginId": "mssql", + "pluginName": "Microsoft SQL Server" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "graph", + "name": "Graph", + "version": "5.0.0" + }, + { + "type": "datasource", + "id": "mssql", + "name": "Microsoft SQL Server", + "version": "1.0.0" + }, + { + "type": "datasource", + "id": "mysql", + "name": "MySQL", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "5.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + }, + { + "datasource": "${DS_MYSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#6ed0e0", + "limit": 100, + "name": "Deploys", + "rawQuery": "SELECT\n time_sec,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MYSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "rgba(255, 96, 96, 1)", + "limit": 100, + "name": "Tickets", + "rawQuery": "SELECT\n time_sec as time,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MYSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#7eb26d", + "limit": 100, + "name": "Metric Values timeEpoch macro", + "rawQuery": "SELECT \n $__timeEpoch(time), \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + }, + { + "datasource": "${DS_MYSQL_TEST}", + "enable": false, + "hide": false, + "iconColor": "#1f78c1", + "limit": 100, + "name": "Metric Values native time", + "rawQuery": "SELECT \n time, \n measurement as text, \n '' as tags\nFROM\n metric_values \nWHERE\n $__timeFilter(time)\nORDER BY 1", + "showIn": 0, + "tags": [], + "type": "tags" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1521715720483, + "links": [], + "panels": [ + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "string", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT * from mysql_types", + "refId": "A" + } + ], + "title": "Data types", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 32, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as unsigned integer) as time_sec", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as unsigned integer) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "id": 33, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(null as datetime) as time_sec", + "refId": "A", + "target": "" + } + ], + "title": "cast(null as datetime) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 34, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT cast(NOW() as datetime) as time_sec", + "refId": "A", + "target": "" + } + ], + "title": "cast()NOW() as datetime) as time", + "transform": "table", + "type": "table" + }, + { + "columns": [], + "datasource": "${DS_MYSQL_TEST}", + "fontSize": "100%", + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 35, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "time_sec", + "type": "date" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "", + "format": "table", + "rawSql": "SELECT NOW() as time", + "refId": "A", + "target": "" + } + ], + "title": "NOW() as time", + "transform": "table", + "type": "table" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 7 + }, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m without fill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 7 + }, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(NULL) and null as zero", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(10.0)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize without fill", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(NULL)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(100.0)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 25 + }, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value one') as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value two') as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1,2\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 5, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n measurement IN($metric)\nGROUP BY 1\nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 28, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 19, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "id": 18, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 49 + }, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 14, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MSSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 15, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - series mode", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "series", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 65 + }, + "id": 25, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 50, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 65 + }, + "id": 22, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 100, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 21, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 26, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 23, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "current" + ] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_MYSQL_TEST}", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 81 + }, + "id": 24, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": false, + "total": true, + "values": true + }, + "lines": false, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column - histogram stacked percent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": 20, + "mode": "histogram", + "name": null, + "show": true, + "values": [ + "total" + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": "", + "current": {}, + "datasource": "${DS_MYSQL_TEST}", + "hide": 0, + "includeAll": true, + "label": "Metric", + "multi": true, + "name": "metric", + "options": [], + "query": "SELECT DISTINCT measurement FROM metric_values", + "refresh": 1, + "regex": "", + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "text": "10m", + "value": "10m" + }, + "hide": 0, + "label": "Interval", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": true, + "text": "10m", + "value": "10m" + } + ], + "query": "1s,10s,30s,1m,5m,10m", + "refresh": 2, + "type": "interval" + } + ] + }, + "time": { + "from": "2018-03-15T11:30:00.000Z", + "to": "2018-03-15T12:55:01.000Z" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "MySQL Data Source Test", + "uid": "Hmf8FDkmz", + "version": 9 +} \ No newline at end of file diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index b0170070dcf..a292f209429 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -68,7 +68,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { - case "__time": + case "__timeEpoch", "__time": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index f3060e235e5..483974c55a4 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -81,7 +81,7 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, // check if there is a column named time for i, col := range columnNames { switch col { - case "time_sec": + case "time", "time_sec": timeIndex = i } } @@ -96,13 +96,10 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, return err } - // for annotations, convert to epoch - if timeIndex != -1 { - switch value := values[timeIndex].(type) { - case time.Time: - values[timeIndex] = float64(value.UnixNano() / 1e9) - } - } + // converts column named time to unix timestamp in milliseconds to make + // native mysql datetime types and epoch dates work in + // annotation and table queries. + tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) table.Rows = append(table.Rows, values) } @@ -185,9 +182,37 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. return err } - rowData := NewStringStringScan(columnNames) + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + rowLimit := 1000000 rowCount := 0 + timeIndex := -1 + metricIndex := -1 + + // check columns of resultset: a column named time is mandatory + // the first text column is treated as metric name unless a column named metric is present + for i, col := range columnNames { + switch col { + case "time", "time_sec": + timeIndex = i + case "metric": + metricIndex = i + default: + if metricIndex == -1 { + switch columnTypes[i].DatabaseTypeName() { + case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT": + metricIndex = i + } + } + } + } + + if timeIndex == -1 { + return fmt.Errorf("Found no column named time or time_sec") + } fillMissing := query.Model.Get("fill").MustBool(false) var fillInterval float64 @@ -198,53 +223,90 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() fillValue.Valid = true } - } - for ; rows.Next(); rowCount++ { + for rows.Next() { + var timestamp float64 + var value null.Float + var metric string + if rowCount > rowLimit { - return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit) + return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit) } - err := rowData.Update(rows.Rows) + values, err := e.getTypedRowData(rows) if err != nil { - e.log.Error("MySQL response parsing", "error", err) - return fmt.Errorf("MySQL response parsing error %v", err) + return err } - if rowData.metric == "" { - rowData.metric = "Unknown" + switch columnValue := values[timeIndex].(type) { + case int64: + timestamp = float64(columnValue * 1000) + case float64: + timestamp = columnValue * 1000 + case time.Time: + timestamp = float64(columnValue.UnixNano() / 1e6) + default: + return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) } - if !rowData.time.Valid { - return fmt.Errorf("Found row with no time value") - } - - series, exist := pointsBySeries[rowData.metric] - if exist == false { - series = &tsdb.TimeSeries{Name: rowData.metric} - pointsBySeries[rowData.metric] = series - seriesByQueryOrder.PushBack(rowData.metric) - } - - if fillMissing { - var intervalStart float64 - if exist == false { - intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) + if metricIndex >= 0 { + if columnValue, ok := values[metricIndex].(string); ok == true { + metric = columnValue } else { - intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval - } - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - - for i := intervalStart; i < rowData.time.Float64; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ + return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex]) } } - series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time}) + for i, col := range columnNames { + if i == timeIndex || i == metricIndex { + continue + } + + switch columnValue := values[i].(type) { + case int64: + value = null.FloatFrom(float64(columnValue)) + case float64: + value = null.FloatFrom(columnValue) + case nil: + value.Valid = false + default: + return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + } + if metricIndex == -1 { + metric = col + } + + series, exist := pointsBySeries[metric] + if exist == false { + series = &tsdb.TimeSeries{Name: metric} + pointsBySeries[metric] = series + seriesByQueryOrder.PushBack(metric) + } + + if fillMissing { + var intervalStart float64 + if exist == false { + intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) + } else { + intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval + } + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + + for i := intervalStart; i < timestamp; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + + series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) + + e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) + rowCount++ + + } } for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { @@ -269,62 +331,3 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. result.Meta.Set("rowCount", rowCount) return nil } - -type stringStringScan struct { - rowPtrs []interface{} - rowValues []string - columnNames []string - columnCount int - - time null.Float - value null.Float - metric string -} - -func NewStringStringScan(columnNames []string) *stringStringScan { - s := &stringStringScan{ - columnCount: len(columnNames), - columnNames: columnNames, - rowPtrs: make([]interface{}, len(columnNames)), - rowValues: make([]string, len(columnNames)), - } - - for i := 0; i < s.columnCount; i++ { - s.rowPtrs[i] = new(sql.RawBytes) - } - - return s -} - -func (s *stringStringScan) Update(rows *sql.Rows) error { - if err := rows.Scan(s.rowPtrs...); err != nil { - return err - } - - s.time = null.FloatFromPtr(nil) - s.value = null.FloatFromPtr(nil) - - for i := 0; i < s.columnCount; i++ { - if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok { - s.rowValues[i] = string(*rb) - - switch s.columnNames[i] { - case "time_sec": - if sec, err := strconv.ParseInt(s.rowValues[i], 10, 64); err == nil { - s.time = null.FloatFrom(float64(sec * 1000)) - } - case "value": - if value, err := strconv.ParseFloat(s.rowValues[i], 64); err == nil { - s.value = null.FloatFrom(value) - } - case "metric": - s.metric = s.rowValues[i] - } - - *rb = nil // reset pointer to discard current value to avoid a bug - } else { - return fmt.Errorf("Cannot convert index %d column %s to type *sql.RawBytes", i, s.columnNames[i]) - } - } - return nil -} diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index fe2c82223d2..668babd5ddf 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -1,6 +1,8 @@ package mysql import ( + "fmt" + "math/rand" "testing" "time" @@ -14,8 +16,12 @@ import ( // To run this test, remove the Skip from SkipConvey // and set up a MySQL db named grafana_tests and a user/password grafana/password +// Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a +// preconfigured MySQL server suitable for running these tests. +// Thers's also a dashboard.json in same directory that you can import to Grafana +// once you've created a datasource for the test server/database. func TestMySQL(t *testing.T) { - SkipConvey("MySQL", t, func() { + Convey("MySQL", t, func() { x := InitMySQLTestDB(t) endpoint := &MysqlQueryEndpoint{ @@ -29,110 +35,621 @@ func TestMySQL(t *testing.T) { sess := x.NewSession() defer sess.Close() - sql := "CREATE TABLE `mysql_types` (" - sql += "`atinyint` tinyint(1) NOT NULL," - sql += "`avarchar` varchar(3) NOT NULL," - sql += "`achar` char(3)," - sql += "`amediumint` mediumint NOT NULL," - sql += "`asmallint` smallint NOT NULL," - sql += "`abigint` bigint NOT NULL," - sql += "`aint` int(11) NOT NULL," - sql += "`adouble` double(10,2)," - sql += "`anewdecimal` decimal(10,2)," - sql += "`afloat` float(10,2) NOT NULL," - sql += "`atimestamp` timestamp NOT NULL," - sql += "`adatetime` datetime NOT NULL," - sql += "`atime` time NOT NULL," - // sql += "`ayear` year," // Crashes xorm when running cleandb - sql += "`abit` bit(1)," - sql += "`atinytext` tinytext," - sql += "`atinyblob` tinyblob," - sql += "`atext` text," - sql += "`ablob` blob," - sql += "`amediumtext` mediumtext," - sql += "`amediumblob` mediumblob," - sql += "`alongtext` longtext," - sql += "`alongblob` longblob," - sql += "`aenum` enum('val1', 'val2')," - sql += "`aset` set('a', 'b', 'c', 'd')," - sql += "`adate` date," - sql += "`time_sec` datetime(6)," - sql += "`aintnull` int(11)," - sql += "`afloatnull` float(10,2)," - sql += "`avarcharnull` varchar(3)," - sql += "`adecimalnull` decimal(10,2)" - sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;" - _, err := sess.Exec(sql) - So(err, ShouldBeNil) + fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.Local) - sql = "INSERT INTO `mysql_types` " - sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, " - sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `abit`, `atinytext`, " - sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, " - sql += "`aenum`, `aset`, `adate`, `time_sec`) " - sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, " - sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', 1, 'tinytext', " - sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', " - sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');" - _, err = sess.Exec(sql) - So(err, ShouldBeNil) - - Convey("Query with Table format should map MySQL column types to Go types", func() { - query := &tsdb.TsdbQuery{ - Queries: []*tsdb.Query{ - { - Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT * FROM mysql_types", - "format": "table", - }), - RefId: "A", - }, - }, + Convey("Given a table with different native data types", func() { + if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists { + So(err, ShouldBeNil) + sess.DropTable("mysql_types") } - resp, err := endpoint.Query(nil, nil, query) - queryResult := resp.Results["A"] + sql := "CREATE TABLE `mysql_types` (" + sql += "`atinyint` tinyint(1) NOT NULL," + sql += "`avarchar` varchar(3) NOT NULL," + sql += "`achar` char(3)," + sql += "`amediumint` mediumint NOT NULL," + sql += "`asmallint` smallint NOT NULL," + sql += "`abigint` bigint NOT NULL," + sql += "`aint` int(11) NOT NULL," + sql += "`adouble` double(10,2)," + sql += "`anewdecimal` decimal(10,2)," + sql += "`afloat` float(10,2) NOT NULL," + sql += "`atimestamp` timestamp NOT NULL," + sql += "`adatetime` datetime NOT NULL," + sql += "`atime` time NOT NULL," + sql += "`ayear` year," // Crashes xorm when running cleandb + sql += "`abit` bit(1)," + sql += "`atinytext` tinytext," + sql += "`atinyblob` tinyblob," + sql += "`atext` text," + sql += "`ablob` blob," + sql += "`amediumtext` mediumtext," + sql += "`amediumblob` mediumblob," + sql += "`alongtext` longtext," + sql += "`alongblob` longblob," + sql += "`aenum` enum('val1', 'val2')," + sql += "`aset` set('a', 'b', 'c', 'd')," + sql += "`adate` date," + sql += "`time_sec` datetime(6)," + sql += "`aintnull` int(11)," + sql += "`afloatnull` float(10,2)," + sql += "`avarcharnull` varchar(3)," + sql += "`adecimalnull` decimal(10,2)" + sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;" + _, err := sess.Exec(sql) So(err, ShouldBeNil) - column := queryResult.Tables[0].Rows[0] + sql = "INSERT INTO `mysql_types` " + sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, " + sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `ayear`, `abit`, `atinytext`, " + sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, " + sql += "`aenum`, `aset`, `adate`, `time_sec`) " + sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, " + sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', '2018', 1, 'tinytext', " + sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', " + sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');" + _, err = sess.Exec(sql) + So(err, ShouldBeNil) - So(*column[0].(*int8), ShouldEqual, 1) - So(column[1].(string), ShouldEqual, "abc") - So(column[2].(string), ShouldEqual, "def") - So(*column[3].(*int32), ShouldEqual, 1) - So(*column[4].(*int16), ShouldEqual, 10) - So(*column[5].(*int64), ShouldEqual, 100) - So(*column[6].(*int32), ShouldEqual, 1420070400) - So(column[7].(float64), ShouldEqual, 1.11) - So(column[8].(float64), ShouldEqual, 2.22) - So(*column[9].(*float32), ShouldEqual, 3.33) - _, offset := time.Now().Zone() - So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) - So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) - So(column[12].(string), ShouldEqual, "11:11:11") - So(*column[13].(*[]byte), ShouldHaveSameTypeAs, []byte{1}) - So(column[14].(string), ShouldEqual, "tinytext") - So(column[15].(string), ShouldEqual, "tinyblob") - So(column[16].(string), ShouldEqual, "text") - So(column[17].(string), ShouldEqual, "blob") - So(column[18].(string), ShouldEqual, "mediumtext") - So(column[19].(string), ShouldEqual, "mediumblob") - So(column[20].(string), ShouldEqual, "longtext") - So(column[21].(string), ShouldEqual, "longblob") - So(column[22].(string), ShouldEqual, "val2") - So(column[23].(string), ShouldEqual, "a,b") - So(column[24].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z")) - So(column[25].(float64), ShouldEqual, 1514764861) - So(column[26], ShouldEqual, nil) - So(column[27], ShouldEqual, nil) - So(column[28], ShouldEqual, "") - So(column[29], ShouldEqual, nil) + Convey("Query with Table format should map MySQL column types to Go types", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT * FROM mysql_types", + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + column := queryResult.Tables[0].Rows[0] + + So(*column[0].(*int8), ShouldEqual, 1) + So(column[1].(string), ShouldEqual, "abc") + So(column[2].(string), ShouldEqual, "def") + So(*column[3].(*int32), ShouldEqual, 1) + So(*column[4].(*int16), ShouldEqual, 10) + So(*column[5].(*int64), ShouldEqual, 100) + So(*column[6].(*int32), ShouldEqual, 1420070400) + So(column[7].(float64), ShouldEqual, 1.11) + So(column[8].(float64), ShouldEqual, 2.22) + So(*column[9].(*float32), ShouldEqual, 3.33) + _, offset := time.Now().Zone() + So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) + So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second)) + So(column[12].(string), ShouldEqual, "11:11:11") + So(column[13].(int64), ShouldEqual, 2018) + So(*column[14].(*[]byte), ShouldHaveSameTypeAs, []byte{1}) + So(column[15].(string), ShouldEqual, "tinytext") + So(column[16].(string), ShouldEqual, "tinyblob") + So(column[17].(string), ShouldEqual, "text") + So(column[18].(string), ShouldEqual, "blob") + So(column[19].(string), ShouldEqual, "mediumtext") + So(column[20].(string), ShouldEqual, "mediumblob") + So(column[21].(string), ShouldEqual, "longtext") + So(column[22].(string), ShouldEqual, "longblob") + So(column[23].(string), ShouldEqual, "val2") + So(column[24].(string), ShouldEqual, "a,b") + So(column[25].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z")) + So(column[26].(float64), ShouldEqual, float64(1514764861000)) + So(column[27], ShouldEqual, nil) + So(column[28], ShouldEqual, nil) + So(column[29], ShouldEqual, "") + So(column[30], ShouldEqual, nil) + }) + }) + + Convey("Given a table with metrics that lacks data for some series ", func() { + type metric struct { + Time time.Time + Value int64 + } + + if exist, err := sess.IsTableExist(metric{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(metric{}) + } + err := sess.CreateTable(metric{}) + So(err, ShouldBeNil) + + series := []*metric{} + firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second) + secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second) + + for _, t := range firstRange { + series = append(series, &metric{ + Time: t, + Value: 15, + }) + } + + for _, t := range secondRange { + series = append(series, &metric{ + Time: t, + Value: 20, + }) + } + + for _, s := range series { + _, err = sess.Insert(s) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query using timeGroup", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m') as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 6) + + dt := fromStart + + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 3; i < 6; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + }) + + Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(len(points), ShouldEqual, 7) + + dt := fromStart + + for i := 0; i < 3; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 15) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + + So(points[3][0].Valid, ShouldBeFalse) + + // adjust for 5 minute gap + dt = dt.Add(5 * time.Minute) + for i := 4; i < 7; i++ { + aValue := points[i][0].Float64 + aTime := time.Unix(int64(points[i][1].Float64)/1000, 0) + So(aValue, ShouldEqual, 20) + So(aTime, ShouldEqual, dt) + dt = dt.Add(5 * time.Minute) + } + }) + + Convey("When doing a metric query using timeGroup with float fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(points[3][0].Float64, ShouldEqual, 1.5) + }) + }) + + Convey("Given a table with metrics having multiple values and measurements", func() { + type metric_values struct { + Time time.Time + Measurement string + ValueOne int64 `xorm:"integer 'valueOne'"` + ValueTwo int64 `xorm:"integer 'valueTwo'"` + } + + if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(metric_values{}) + } + err := sess.CreateTable(metric_values{}) + So(err, ShouldBeNil) + + rand.Seed(time.Now().Unix()) + rnd := func(min, max int64) int64 { + return rand.Int63n(max-min) + min + } + + series := []*metric_values{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) { + series = append(series, &metric_values{ + Time: t, + Measurement: "Metric A", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + series = append(series, &metric_values{ + Time: t, + Measurement: "Metric B", + ValueOne: rnd(0, 100), + ValueTwo: rnd(0, 100), + }) + } + + for _, s := range series { + _, err := sess.Insert(s) + So(err, ShouldBeNil) + } + + Convey("When doing a metric query grouping by time and select metric column should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "Metric B - value one") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A - value one") + }) + + Convey("When doing a metric query grouping by time should return correct series", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 2) + So(queryResult.Series[0].Name, ShouldEqual, "valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "valueTwo") + }) + }) + + Convey("Given a table with event data", func() { + type event struct { + TimeSec int64 + Description string + Tags string + } + + if exist, err := sess.IsTableExist(event{}); err != nil || exist { + So(err, ShouldBeNil) + sess.DropTable(event{}) + } + err := sess.CreateTable(event{}) + So(err, ShouldBeNil) + + events := []*event{} + for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) { + events = append(events, &event{ + TimeSec: t.Unix(), + Description: "Someone deployed something", + Tags: "deploy", + }) + events = append(events, &event{ + TimeSec: t.Add(5 * time.Minute).Unix(), + Description: "New support ticket registered", + Tags: "ticket", + }) + } + + for _, e := range events { + _, err = sess.Insert(e) + So(err, ShouldBeNil) + } + + Convey("When doing an annotation query of deploy events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT time_sec, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`, + "format": "table", + }), + RefId: "Deploys", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Deploys"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query of ticket events should return expected result", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT time_sec, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`, + "format": "table", + }), + RefId: "Tickets", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + queryResult := resp.Results["Tickets"] + So(err, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 3) + }) + + Convey("When doing an annotation query with a time column in datetime format", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 0, time.UTC) + dtFormat := "2006-01-02 15:04:05.999999999" + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + CAST('%s' as datetime) as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Format(dtFormat)), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch second format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, dt.Unix()*1000) + }) + + Convey("When doing an annotation query with a time column in epoch second format (signed integer) should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 0, time.Local) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + CAST('%d' as signed integer) as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000)) + }) + + Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() { + dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC) + + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": fmt.Sprintf(`SELECT + %d as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, dt.Unix()*1000), + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0].(int64), ShouldEqual, dt.Unix()*1000) + }) + + Convey("When doing an annotation query with a time column holding a unsigned integer null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as unsigned integer) as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) + + Convey("When doing an annotation query with a time column holding a DATETIME null value should return nil", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT + cast(null as DATETIME) as time_sec, + 'message' as text, + 'tag1,tag2' as tags + `, + "format": "table", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(len(queryResult.Tables[0].Rows), ShouldEqual, 1) + columns := queryResult.Tables[0].Rows[0] + + //Should be in milliseconds + So(columns[0], ShouldBeNil) + }) }) }) } func InitMySQLTestDB(t *testing.T) *xorm.Engine { x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr+"&parseTime=true") + x.DatabaseTZ = time.Local + x.TZLocation = time.Local // x.ShowSQL() @@ -140,7 +657,18 @@ func InitMySQLTestDB(t *testing.T) *xorm.Engine { t.Fatalf("Failed to init mysql db %v", err) } - sqlutil.CleanDB(x) - return x } + +func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time { + durationSec := int64(duration.Seconds()) + intervalSec := int64(interval.Seconds()) + timeRange := []time.Time{} + + for i := int64(0); i < durationSec; i += intervalSec { + timeRange = append(timeRange, from) + from = from.Add(time.Duration(int64(time.Second) * intervalSec)) + } + + return timeRange +} diff --git a/public/app/plugins/datasource/mysql/partials/annotations.editor.html b/public/app/plugins/datasource/mysql/partials/annotations.editor.html index b34eff5b011..d142e091fed 100644 --- a/public/app/plugins/datasource/mysql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mysql/partials/annotations.editor.html @@ -18,15 +18,16 @@
    Annotation Query Format
    -An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. +An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time or time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned. -- column with alias: time_sec for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column) +- column with alias: time or time_sec for the annotation event time. Use epoch time or any native date data type. - column with alias: text for the annotation text - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' Macros: -- $__time(column) -> UNIX_TIMESTAMP(column) as time_sec +- $__time(column) -> UNIX_TIMESTAMP(column) as time (or as time_sec) +- $__timeEpoch(column) -> UNIX_TIMESTAMP(column) as time (or as time_sec) - $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) > 1492750877 AND UNIX_TIMESTAMP(time_date_time) < 1492750877 - $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877 diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index 22d64c9190f..9acf32405c1 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -38,15 +38,16 @@
    Time series:
    -- return column named time_sec (UTC in seconds), use UNIX_TIMESTAMP(column)
    -- return column named value for the time point value
    -- return column named metric to represent the series name
    +- return column named time or time_sec (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
    +- return column(s) with numeric datatype as values
    +- (Optional: return column named metric to represent the series name. If no column named metric is found the column name of the value column is used as series name)
     
     Table:
     - return any set of columns
     
     Macros:
     - $__time(column) -> UNIX_TIMESTAMP(column) as time_sec
    +- $__timeEpoch(column) -> UNIX_TIMESTAMP(column) as time_sec
     - $__timeFilter(column) ->  UNIX_TIMESTAMP(time_date_time) ≥ 1492750877 AND UNIX_TIMESTAMP(time_date_time) ≤ 1492750877
     - $__unixEpochFilter(column) ->  time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877
     - $__timeGroup(column,'5m') -> cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
    diff --git a/public/app/plugins/datasource/mysql/response_parser.ts b/public/app/plugins/datasource/mysql/response_parser.ts
    index 50683578035..e5d8ab79f2a 100644
    --- a/public/app/plugins/datasource/mysql/response_parser.ts
    +++ b/public/app/plugins/datasource/mysql/response_parser.ts
    @@ -113,7 +113,7 @@ export default class ResponseParser {
         let tagsColumnIndex = -1;
     
         for (let i = 0; i < table.columns.length; i++) {
    -      if (table.columns[i].text === 'time_sec') {
    +      if (table.columns[i].text === 'time_sec' || table.columns[i].text === 'time') {
             timeColumnIndex = i;
           } else if (table.columns[i].text === 'title') {
             return this.$q.reject({
    @@ -137,7 +137,7 @@ export default class ResponseParser {
           const row = table.rows[i];
           list.push({
             annotation: options.annotation,
    -        time: Math.floor(row[timeColumnIndex]) * 1000,
    +        time: Math.floor(row[timeColumnIndex]),
             text: row[textColumnIndex] ? row[textColumnIndex].toString() : '',
             tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [],
           });
    
    From bd4ecaeac6e6737ac44043c90ba9a574cfea433f Mon Sep 17 00:00:00 2001
    From: Marcus Efraimsson 
    Date: Thu, 22 Mar 2018 15:46:40 +0100
    Subject: [PATCH 0135/3000] mssql: update query editor help
    
    ---
     .../plugins/datasource/mssql/partials/query.editor.html   | 8 ++++++++
     1 file changed, 8 insertions(+)
    
    diff --git a/public/app/plugins/datasource/mssql/partials/query.editor.html b/public/app/plugins/datasource/mssql/partials/query.editor.html
    index f8c7effb827..c7dc030be6e 100644
    --- a/public/app/plugins/datasource/mssql/partials/query.editor.html
    +++ b/public/app/plugins/datasource/mssql/partials/query.editor.html
    @@ -53,6 +53,14 @@ Macros:
     - $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
     - $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a fillValue of NULL or floating value will automatically fill empty series in timerange with that value.
     
    +Example of group by and order by with $__timeGroup:
    +SELECT
    +  $__timeGroup(date_time_col, '1h') AS time,
    +  sum(value) as value
    +FROM yourtable
    +GROUP BY $__timeGroup(date_time_col, '1h')
    +ORDER BY 1
    +
     Or build your own conditionals using these macros which just return the values:
     - $__timeFrom() -> DATEADD(second, 1492750877, '1970-01-01')
     - $__timeTo() -> DATEADD(second, 1492750877, '1970-01-01')
    
    From a2bbd89a9ebb73cd445bc920b0dbda02aa2cb31d Mon Sep 17 00:00:00 2001
    From: ryan 
    Date: Thu, 22 Mar 2018 15:52:09 +0100
    Subject: [PATCH 0136/3000] adding updated column
    
    ---
     CHANGELOG.md                                  |  1 +
     docs/sources/http_api/annotations.md          |  2 ++
     pkg/api/annotations.go                        |  2 +-
     pkg/services/annotations/annotations.go       |  4 ++-
     pkg/services/sqlstore/annotation.go           | 26 +++++++++++--------
     .../sqlstore/migrations/annotation_mig.go     |  8 +++++-
     6 files changed, 29 insertions(+), 14 deletions(-)
    
    diff --git a/CHANGELOG.md b/CHANGELOG.md
    index 304b1ba6d0b..001433fa652 100644
    --- a/CHANGELOG.md
    +++ b/CHANGELOG.md
    @@ -9,6 +9,7 @@
     * **Alerting**: Support Pagerduty notification channel using Pagerduty V2 API [#10531](https://github.com/grafana/grafana/issues/10531), thx [@jbaublitz](https://github.com/jbaublitz)
     * **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda)
     * **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
    +* **Annotations API**: Record creation/update times and add more query options [#11333](https://github.com/grafana/grafana/pull/11333), thx [@mtanda](https://github.com/ryantxu)
     
     ### Minor
     * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
    diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md
    index 19c2a5c386c..c26b7d72a4b 100644
    --- a/docs/sources/http_api/annotations.md
    +++ b/docs/sources/http_api/annotations.md
    @@ -36,6 +36,8 @@ Query Parameters:
     - `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
    +- `userId`: number. Optional. Find annotations created by a specific user
    +- `type`: string. Optional. `alert`|`annotation` Return alerts or user created annotations
     - `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**:
    diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go
    index 123a8432f13..5762d56548a 100644
    --- a/pkg/api/annotations.go
    +++ b/pkg/api/annotations.go
    @@ -18,13 +18,13 @@ func GetAnnotations(c *m.ReqContext) Response {
     		From:        c.QueryInt64("from") / 1000,
     		To:          c.QueryInt64("to") / 1000,
     		OrgId:       c.OrgId,
    +		UserId:      c.QueryInt64("userId"),
     		AlertId:     c.QueryInt64("alertId"),
     		DashboardId: c.QueryInt64("dashboardId"),
     		PanelId:     c.QueryInt64("panelId"),
     		Limit:       c.QueryInt64("limit"),
     		Tags:        c.QueryStrings("tags"),
     		Type:        c.Query("type"),
    -		Sort:        c.Query("sort"),
     	}
     
     	repo := annotations.GetRepository()
    diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go
    index fd178176ef1..5cebb3d2df9 100644
    --- a/pkg/services/annotations/annotations.go
    +++ b/pkg/services/annotations/annotations.go
    @@ -13,6 +13,7 @@ type ItemQuery struct {
     	OrgId        int64    `json:"orgId"`
     	From         int64    `json:"from"`
     	To           int64    `json:"to"`
    +	UserId       int64    `json:"userId"`
     	AlertId      int64    `json:"alertId"`
     	DashboardId  int64    `json:"dashboardId"`
     	PanelId      int64    `json:"panelId"`
    @@ -20,7 +21,6 @@ type ItemQuery struct {
     	RegionId     int64    `json:"regionId"`
     	Tags         []string `json:"tags"`
     	Type         string   `json:"type"`
    -	Sort         string   `json:"sort"`
     
     	Limit int64 `json:"limit"`
     }
    @@ -65,6 +65,7 @@ type Item struct {
     	NewState    string           `json:"newState"`
     	Epoch       int64            `json:"epoch"`
     	Created     int64            `json:"created"`
    +	Updated     int64            `json:"updated"`
     	Tags        []string         `json:"tags"`
     	Data        *simplejson.Json `json:"data"`
     
    @@ -83,6 +84,7 @@ type ItemDTO struct {
     	NewState    string           `json:"newState"`
     	PrevState   string           `json:"prevState"`
     	Created     int64            `json:"created"`
    +	Updated     int64            `json:"updated"`
     	Time        int64            `json:"time"`
     	Text        string           `json:"text"`
     	RegionId    int64            `json:"regionId"`
    diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go
    index 65f2abd9a54..ebba2083576 100644
    --- a/pkg/services/sqlstore/annotation.go
    +++ b/pkg/services/sqlstore/annotation.go
    @@ -15,10 +15,14 @@ type SqlAnnotationRepo struct {
     }
     
     func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
    +	if item.DashboardId == 0 {
    +		return errors.New("Annotation is missing dashboard_id")
    +	}
     	return inTransaction(func(sess *DBSession) error {
     		tags := models.ParseTagPairs(item.Tags)
     		item.Tags = models.JoinTagPairs(tags)
     		item.Created = time.Now().UnixNano() / int64(time.Millisecond)
    +		item.Updated = item.Created
     		if _, err := sess.Table("annotation").Insert(item); err != nil {
     			return err
     		}
    @@ -66,6 +70,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
     			err     error
     		)
     		existing := new(annotations.Item)
    +		item.Updated = time.Now().UnixNano() / int64(time.Millisecond)
     
     		if item.Id == 0 && item.RegionId != 0 {
     			// Update region end time
    @@ -130,6 +135,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
     			annotation.tags,
     			annotation.data,
     			annotation.created,
    +			annotation.updated,
     			usr.email,
     			usr.login,
     			alert.name as alert_name
    @@ -167,6 +173,11 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
     		params = append(params, query.PanelId)
     	}
     
    +	if query.UserId != 0 {
    +		sql.WriteString(` AND annotation.user_id = ?`)
    +		params = append(params, query.UserId)
    +	}
    +
     	if query.From > 0 && query.To > 0 {
     		sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`)
     		params = append(params, query.From, query.To)
    @@ -175,6 +186,9 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
     	if query.Type == "alert" {
     		sql.WriteString(` AND annotation.alert_id > 0`)
     	}
    +	if query.Type == "annotation" {
    +		sql.WriteString(` AND annotation.alert_id = 0`)
    +	}
     
     	if len(query.Tags) > 0 {
     		keyValueFilters := []string{}
    @@ -208,17 +222,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
     		query.Limit = 10
     	}
     
    -	var sort string = "epoch DESC"
    -	switch query.Sort {
    -	case "time.asc":
    -		sort = "epoch ASC"
    -	case "created":
    -		sort = "annotation.created DESC"
    -	case "created.asc":
    -		sort = "annotation.created ASC"
    -	}
    -
    -	sql.WriteString(fmt.Sprintf(" ORDER BY %s LIMIT %v", sort, query.Limit))
    +	sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
     
     	items := make([]*annotations.ItemDTO, 0)
     
    diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go
    index 24e2beb2eda..11cc986d669 100644
    --- a/pkg/services/sqlstore/migrations/annotation_mig.go
    +++ b/pkg/services/sqlstore/migrations/annotation_mig.go
    @@ -92,12 +92,18 @@ func addAnnotationMig(mg *Migrator) {
     		Mysql(updateTextFieldSql))
     
     	//
    -	// Add a 'created' column
    +	// Add a 'created' & 'updated' column
     	//
     	mg.AddMigration("Add created time to annotation table", NewAddColumnMigration(table, &Column{
     		Name: "created", Type: DB_BigInt, Nullable: true, Default: "0",
     	}))
    +	mg.AddMigration("Add updated time to annotation table", NewAddColumnMigration(table, &Column{
    +		Name: "updated", Type: DB_BigInt, Nullable: true, Default: "0",
    +	}))
     	mg.AddMigration("Add index for created in annotation table", NewAddIndexMigration(table, &Index{
     		Cols: []string{"org_id", "created"}, Type: IndexType,
     	}))
    +	mg.AddMigration("Add index for updated in annotation table", NewAddIndexMigration(table, &Index{
    +		Cols: []string{"org_id", "updated"}, Type: IndexType,
    +	}))
     }
    
    From 20353db9660fdc3df31bd85041f87f2c954dd8dd Mon Sep 17 00:00:00 2001
    From: ryan 
    Date: Thu, 22 Mar 2018 16:21:47 +0100
    Subject: [PATCH 0137/3000] convert epoch to milliseconds
    
    ---
     pkg/api/annotations.go                        | 22 ++++++-------------
     pkg/services/sqlstore/annotation.go           |  9 +++++---
     .../sqlstore/migrations/annotation_mig.go     |  9 ++++++++
     3 files changed, 22 insertions(+), 18 deletions(-)
    
    diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go
    index 5762d56548a..e17cabb01a1 100644
    --- a/pkg/api/annotations.go
    +++ b/pkg/api/annotations.go
    @@ -2,7 +2,6 @@ package api
     
     import (
     	"strings"
    -	"time"
     
     	"github.com/grafana/grafana/pkg/api/dtos"
     	"github.com/grafana/grafana/pkg/components/simplejson"
    @@ -15,8 +14,8 @@ import (
     func GetAnnotations(c *m.ReqContext) Response {
     
     	query := &annotations.ItemQuery{
    -		From:        c.QueryInt64("from") / 1000,
    -		To:          c.QueryInt64("to") / 1000,
    +		From:        c.QueryInt64("from"),
    +		To:          c.QueryInt64("to"),
     		OrgId:       c.OrgId,
     		UserId:      c.QueryInt64("userId"),
     		AlertId:     c.QueryInt64("alertId"),
    @@ -38,7 +37,7 @@ func GetAnnotations(c *m.ReqContext) Response {
     		if item.Email != "" {
     			item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
     		}
    -		item.Time = item.Time * 1000
    +		item.Time = item.Time
     	}
     
     	return Json(200, items)
    @@ -69,16 +68,12 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
     		UserId:      c.UserId,
     		DashboardId: cmd.DashboardId,
     		PanelId:     cmd.PanelId,
    -		Epoch:       cmd.Time / 1000,
    +		Epoch:       cmd.Time,
     		Text:        cmd.Text,
     		Data:        cmd.Data,
     		Tags:        cmd.Tags,
     	}
     
    -	if item.Epoch == 0 {
    -		item.Epoch = time.Now().Unix()
    -	}
    -
     	if err := repo.Save(&item); err != nil {
     		return ApiError(500, "Failed to save annotation", err)
     	}
    @@ -98,7 +93,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
     		}
     
     		item.Id = 0
    -		item.Epoch = cmd.TimeEnd / 1000
    +		item.Epoch = cmd.TimeEnd
     
     		if err := repo.Save(&item); err != nil {
     			return ApiError(500, "Failed save annotation for region end time", err)
    @@ -133,9 +128,6 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
     		return ApiError(500, "Failed to save Graphite annotation", err)
     	}
     
    -	if cmd.When == 0 {
    -		cmd.When = time.Now().Unix()
    -	}
     	text := formatGraphiteAnnotation(cmd.What, cmd.Data)
     
     	// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
    @@ -192,7 +184,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
     		OrgId:  c.OrgId,
     		UserId: c.UserId,
     		Id:     annotationID,
    -		Epoch:  cmd.Time / 1000,
    +		Epoch:  cmd.Time,
     		Text:   cmd.Text,
     		Tags:   cmd.Tags,
     	}
    @@ -204,7 +196,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
     	if cmd.IsRegion {
     		itemRight := item
     		itemRight.RegionId = item.Id
    -		itemRight.Epoch = cmd.TimeEnd / 1000
    +		itemRight.Epoch = cmd.TimeEnd
     
     		// 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 !=  ...
    diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go
    index ebba2083576..5906be3736b 100644
    --- a/pkg/services/sqlstore/annotation.go
    +++ b/pkg/services/sqlstore/annotation.go
    @@ -23,6 +23,10 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
     		item.Tags = models.JoinTagPairs(tags)
     		item.Created = time.Now().UnixNano() / int64(time.Millisecond)
     		item.Updated = item.Created
    +		if item.Epoch == 0 {
    +			item.Epoch = item.Created
    +		}
    +
     		if _, err := sess.Table("annotation").Insert(item); err != nil {
     			return err
     		}
    @@ -70,7 +74,6 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
     			err     error
     		)
     		existing := new(annotations.Item)
    -		item.Updated = time.Now().UnixNano() / int64(time.Millisecond)
     
     		if item.Id == 0 && item.RegionId != 0 {
     			// Update region end time
    @@ -86,6 +89,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
     			return errors.New("Annotation not found")
     		}
     
    +		existing.Updated = time.Now().UnixNano() / int64(time.Millisecond)
     		existing.Epoch = item.Epoch
     		existing.Text = item.Text
     		if item.RegionId != 0 {
    @@ -185,8 +189,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
     
     	if query.Type == "alert" {
     		sql.WriteString(` AND annotation.alert_id > 0`)
    -	}
    -	if query.Type == "annotation" {
    +	} else if query.Type == "annotation" {
     		sql.WriteString(` AND annotation.alert_id = 0`)
     	}
     
    diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go
    index 11cc986d669..89fccad0d09 100644
    --- a/pkg/services/sqlstore/migrations/annotation_mig.go
    +++ b/pkg/services/sqlstore/migrations/annotation_mig.go
    @@ -106,4 +106,13 @@ func addAnnotationMig(mg *Migrator) {
     	mg.AddMigration("Add index for updated in annotation table", NewAddIndexMigration(table, &Index{
     		Cols: []string{"org_id", "updated"}, Type: IndexType,
     	}))
    +
    +	//
    +	// Convert epoch saved as seconds to miliseconds
    +	//
    +	updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000)"
    +	mg.AddMigration("Convert existing annotations from seconds to miliseconds", new(RawSqlMigration).
    +		Sqlite(updateEpochSql).
    +		Postgres(updateEpochSql).
    +		Mysql(updateEpochSql))
     }
    
    From e722732021bcd243411df2e6a982b7acae9c319d Mon Sep 17 00:00:00 2001
    From: Gerben Meijer 
    Date: Thu, 22 Mar 2018 16:27:24 +0100
    Subject: [PATCH 0138/3000] Return actual user ID in UserProfileDTO
    
    This fixes calls to /api/user where ID is currently always 0
    ---
     pkg/services/sqlstore/user.go | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
    index 73ea07f031f..f42ff5fb2ed 100644
    --- a/pkg/services/sqlstore/user.go
    +++ b/pkg/services/sqlstore/user.go
    @@ -315,6 +315,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
     	}
     
     	query.Result = m.UserProfileDTO{
    +		Id:             user.Id,
     		Name:           user.Name,
     		Email:          user.Email,
     		Login:          user.Login,
    
    From f0f41c2a8ed87d9c07a4b178388cd2893beb0b9c Mon Sep 17 00:00:00 2001
    From: Marcus Efraimsson 
    Date: Thu, 22 Mar 2018 16:44:55 +0100
    Subject: [PATCH 0139/3000] mysql: skip tests by default
    
    ---
     pkg/tsdb/mysql/mysql_test.go | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
    index 668babd5ddf..750704c9965 100644
    --- a/pkg/tsdb/mysql/mysql_test.go
    +++ b/pkg/tsdb/mysql/mysql_test.go
    @@ -21,7 +21,7 @@ import (
     // Thers's also a dashboard.json in same directory that you can import to Grafana
     // once you've created a datasource for the test server/database.
     func TestMySQL(t *testing.T) {
    -	Convey("MySQL", t, func() {
    +	SkipConvey("MySQL", t, func() {
     		x := InitMySQLTestDB(t)
     
     		endpoint := &MysqlQueryEndpoint{
    
    From 823f9030488166f2036873d9567387f9a14777c3 Mon Sep 17 00:00:00 2001
    From: Patrick O'Carroll 
    Date: Thu, 22 Mar 2018 16:59:06 +0100
    Subject: [PATCH 0140/3000] removed trash can icon from save buttons
    
    ---
     public/app/containers/ManageDashboards/FolderSettings.tsx   | 2 +-
     public/app/features/dashboard/partials/folder_settings.html | 1 -
     2 files changed, 1 insertion(+), 2 deletions(-)
    
    diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx
    index 586a8f05b4c..ff4e9cb417f 100644
    --- a/public/app/containers/ManageDashboards/FolderSettings.tsx
    +++ b/public/app/containers/ManageDashboards/FolderSettings.tsx
    @@ -143,7 +143,7 @@ export class FolderSettings extends React.Component {
                       className="btn btn-success"
                       disabled={!folder.folder.canSave || !folder.folder.hasChanged}
                     >
    -                   Save
    +                  Save
                     
                     
    diff --git a/public/app/features/dashboard/save_provisioned_modal.ts b/public/app/features/dashboard/save_provisioned_modal.ts new file mode 100644 index 00000000000..8121248b841 --- /dev/null +++ b/public/app/features/dashboard/save_provisioned_modal.ts @@ -0,0 +1,74 @@ +import coreModule from 'app/core/core_module'; + +const template = ` + +`; + +export class SaveProvisionedDashboardModalCtrl { + dashboardJson: string; + dismiss: () => void; + + /** @ngInject */ + constructor(dashboardSrv) { + var dashboard = dashboardSrv.getCurrent().getSaveModelClone(); + delete dashboard.id; + this.dashboardJson = JSON.stringify(dashboard); + } + + getJsonForClipboard() { + return this.dashboardJson; + } +} + +export function saveProvisionedDashboardModalDirective() { + return { + restrict: 'E', + template: template, + controller: SaveProvisionedDashboardModalCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { dismiss: '&' }, + }; +} + +coreModule.directive('saveProvisionedDashboardModal', saveProvisionedDashboardModalDirective); diff --git a/public/app/features/dashboard/specs/save_provisioned_modal.jest.ts b/public/app/features/dashboard/specs/save_provisioned_modal.jest.ts new file mode 100644 index 00000000000..1a6e1d35562 --- /dev/null +++ b/public/app/features/dashboard/specs/save_provisioned_modal.jest.ts @@ -0,0 +1,28 @@ +import { SaveProvisionedDashboardModalCtrl } from '../save_provisioned_modal'; +import { describe, it, expect } from 'test/lib/common'; + +describe('SaveProvisionedDashboardModalCtrl', () => { + var json = { + title: 'name', + id: 5, + }; + + var mockDashboardSrv = { + getCurrent: function() { + return { + id: 5, + meta: {}, + getSaveModelClone: function() { + return json; + }, + }; + }, + }; + + var ctrl = new SaveProvisionedDashboardModalCtrl(mockDashboardSrv); + + it('verify that the id have been removed', () => { + var copy = ctrl.getJsonForClipboard(); + expect(copy).toBe(`{"title":"name"}`); + }); +}); From b97cede054f22356405a55228bdea6f353e4d8ff Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 26 Mar 2018 16:00:27 +0300 Subject: [PATCH 0186/3000] docs: update heatmap and prometheus docs, #10009 --- .../features/datasources/prometheus.md | 2 +- docs/sources/features/panels/heatmap.md | 33 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index c9bb16441ca..2449119ceda 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -50,7 +50,7 @@ Name | Description *Min step* | Set a lower limit for the Prometheus step option. Step controls how big the jumps are when the Prometheus query engine performs range queries. Sadly there is no official prometheus documentation to link to for this very important option. *Resolution* | Controls the step option. Small steps create high-resolution graphs but can be slow over larger time ranges, lowering the resolution can speed things up. `1/2` will try to set step option to generate 1 data point for every other pixel. A value of `1/10` will try to set step option so there is a data point every 10 pixels. *Metric lookup* | Search for metric names in this input field. -*Format as* | **(New in v4.3)** Switch between Table & Time series. Table format will only work in the Table panel. +*Format as* | **(New in v4.3)** Switch between Table, Time series or Heatmap. Table format will only work in the Table panel. Heatmap format is suitable for displaying metrics having histogram type on Heatmap panel. Under the hood, it converts cumulative histogram to regular and sorts series by the bucket bound. ## Templating diff --git a/docs/sources/features/panels/heatmap.md b/docs/sources/features/panels/heatmap.md index e44527f8695..56ffe29f20f 100644 --- a/docs/sources/features/panels/heatmap.md +++ b/docs/sources/features/panels/heatmap.md @@ -56,26 +56,39 @@ Data and bucket options can be found in the `Axes` tab. Data format | Description ------------ | ------------- *Time series* | Grafana does the bucketing by going through all time series values. The bucket sizes & intervals will be determined using the Buckets options. -*Time series buckets* | Each time series already represents a Y-Axis bucket. The time series name (alias) needs to be a numeric value representing the upper interval for the bucket. Grafana does no bucketing so the bucket size options are hidden. +*Time series buckets* | Each time series already represents a Y-Axis bucket. The time series name (alias) needs to be a numeric value representing the upper or lower interval for the bucket. Grafana does no bucketing so the bucket size options are hidden. + +### Bucket bound + +When Data format is *Time series buckets* datasource returns series with names representing bucket bound. But depending +on datasource, a bound may be *upper* or *lower*. This option allows to adjust a bound type. If *Auto* is set, a bound +option will be chosen based on panels' datasource type. ### Bucket Size The Bucket count & size options are used by Grafana to calculate how big each cell in the heatmap is. You can define the bucket size either by count (the first input box) or by specifying a size interval. For the Y-Axis the size interval is just a value but for the X-bucket you can specify a time range in the *Size* input, for example, -the time range `1h`. This will make the cells 1h wide on the X-axis. +the time range `1h`. This will make the cells 1h wide on the X-axis. ### Pre-bucketed data -If you have a data that is already organized into buckets you can use the `Time series buckets` data format. This format requires that your metric query return regular time series and that each time series has a numeric name -that represent the upper or lower bound of the interval. +If you have a data that is already organized into buckets you can use the `Time series buckets` data format. This format +requires that your metric query return regular time series and that each time series has a numeric name that represent +the upper or lower bound of the interval. -The only data source that supports histograms over time is Elasticsearch. You do this by adding a *Histogram* -bucket aggregation before the *Date Histogram*. +There are a number of datasources supporting histogram over time like Elasticsearch (by using a Histogram bucket +aggregation) or Prometheus (with [histogram](https://prometheus.io/docs/concepts/metric_types/#histogram) metric type +and *Format as* option set to Heatmap). But generally, any datasource could be used if it meets the requirements: +returns series with names representing bucket bound or returns sereis sorted by the bound in ascending order. -![](/img/docs/v43/elastic_histogram.png) +With Elasticsearch you control the size of the buckets using the Histogram interval (Y-Axis) and the Date Histogram interval (X-axis). -You control the size of the buckets using the Histogram interval (Y-Axis) and the Date Histogram interval (X-axis). +![Elastic histogram](/img/docs/v43/elastic_histogram.png) + +With Prometheus you can only control X-axis by adjusting *Min step* and *Resolution* options. + +![Prometheus histogram](/img/docs/v51/prometheus_histogram.png) ## Display Options @@ -100,8 +113,8 @@ but include a group by time interval or maxDataPoints limit coupled with an aggr This all depends on the time range of your query of course. But the important point is to know that the Histogram bucketing that Grafana performs may be done on already aggregated and averaged data. To get more accurate heatmaps it is better -to do the bucketing during metric collection or store the data in Elasticsearch, which currently is the only data source -data supports doing Histogram bucketing on the raw data. +to do the bucketing during metric collection or store the data in Elasticsearch, or in the other data source which +supports doing Histogram bucketing on the raw data. If you remove or lower the group by time (or raise maxDataPoints) in your query to return more data points your heatmap will be more accurate but this can also be very CPU & Memory taxing for your browser and could cause hangs and crashes if the number of From bf4273b5844956e3b3ac6df3264e600ca29e23e1 Mon Sep 17 00:00:00 2001 From: Thomas Rohlik Date: Mon, 26 Mar 2018 16:34:49 +0200 Subject: [PATCH 0187/3000] Add new currency - Czech koruna Currency used in Czech republic. --- public/app/core/utils/kbn.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 4a29f3983e1..dcb04a3e38e 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -447,6 +447,7 @@ 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'); +kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk'); // Data (Binary) kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); @@ -869,6 +870,7 @@ kbn.getUnitFormats = function() { { text: 'Icelandic Króna (kr)', value: 'currencyISK' }, { text: 'Norwegian Krone (kr)', value: 'currencyNOK' }, { text: 'Swedish Krona (kr)', value: 'currencySEK' }, + { text: 'Czech koruna (czk)', value: 'currencyCZK' }, ], }, { From 6c2ef7dca6b34f189ef44c416e98c386117d6010 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 26 Mar 2018 18:34:40 +0200 Subject: [PATCH 0188/3000] handle aggregate functions more generic --- .../plugins/datasource/postgres/query_ctrl.ts | 47 +++++------- .../plugins/datasource/postgres/query_part.ts | 75 ++----------------- 2 files changed, 27 insertions(+), 95 deletions(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index 8a5cb273ef7..d0ec5dc59be 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -81,30 +81,12 @@ export class PostgresQueryCtrl extends QueryCtrl { } buildSelectMenu() { - - if (!queryPart.hasAggregates()) { - this.datasource.metricFindQuery(this.queryBuilder.buildAggregateQuery()) - .then(results => { - queryPart.clearAggregates(); - _.map(results, segment => { queryPart.registerAggregate(segment.text); }); - }) - .catch(this.handleQueryError.bind(this)); - } - var categories = queryPart.getCategories(); - this.selectMenu = _.reduce( - categories, - function(memo, cat, key) { - var menu = { - text: key, - submenu: cat.map(item => { - return { text: item.type, value: item.type }; - }), - }; - memo.push(menu); - return memo; - }, - [] - ); + this.selectMenu = [ + {text: "aggregate", value: "aggregate"}, + {text: "math", value: "math"}, + {text: "alias", value: "alias"}, + {text: "column", value: "column"}, + ]; } toggleEditorMode() { @@ -216,10 +198,19 @@ export class PostgresQueryCtrl extends QueryCtrl { handleSelectPartEvent(selectParts, part, evt) { switch (evt.name) { case 'get-param-options': { - return this.datasource - .metricFindQuery(this.queryBuilder.buildColumnQuery("value")) - .then(this.transformToSegments(true)) - .catch(this.handleQueryError.bind(this)); + switch (part.def.type) { + case "aggregate": + return this.datasource + .metricFindQuery(this.queryBuilder.buildAggregateQuery()) + .then(this.transformToSegments(false)) + .catch(this.handleQueryError.bind(this)); + case "column": + return this.datasource + .metricFindQuery(this.queryBuilder.buildColumnQuery("value")) + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(this)); + } + } case 'part-param-changed': { this.panelCtrl.refresh(); diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index b63ebfae0c1..044f51a2457 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -23,23 +23,16 @@ function register(options: any) { options.category.push(index[options.type]); } -function registerAggregate(name: string) { - register({ - type: name, - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, - }); -} - var groupByTimeFunctions = []; function aliasRenderer(part, innerExpr) { return innerExpr + ' AS ' + '"' + part.params[0] + '"'; } +function aggregateRenderer(part, innerExpr) { + return part.params[0] + '(' + innerExpr + ')'; +} + function columnRenderer(part, innerExpr) { return '"' + part.params[0] + '"'; } @@ -108,59 +101,13 @@ register({ renderer: columnRenderer, }); -// Aggregations register({ - type: 'avg', + type: 'aggregate', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'count', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'sum', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'stddev', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'min', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'max', - addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, - params: [], - defaultParams: [], - renderer: functionRenderer, + params: [{name: 'name', type: 'string', dynamicLookup: true}], + defaultParams: ['avg'], + renderer: aggregateRenderer, }); register({ @@ -203,12 +150,6 @@ register({ export default { create: createPart, - registerAggregate: registerAggregate, - clearAggregates: function() { categories.Aggregations = []; }, - hasAggregates: function() { - // FIXME - return categories.Aggregations.length > 6; - }, getCategories: function() { return categories; }, From d6ac7aee899db14d2306ab9828cb39ea9854d853 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 26 Mar 2018 18:50:03 +0200 Subject: [PATCH 0189/3000] remove unused import --- public/app/plugins/datasource/postgres/query_ctrl.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/postgres/query_ctrl.ts b/public/app/plugins/datasource/postgres/query_ctrl.ts index d0ec5dc59be..dd1da1c75cf 100644 --- a/public/app/plugins/datasource/postgres/query_ctrl.ts +++ b/public/app/plugins/datasource/postgres/query_ctrl.ts @@ -2,7 +2,6 @@ import angular from 'angular'; import _ from 'lodash'; import { PostgresQueryBuilder } from './query_builder'; import { QueryCtrl } from 'app/plugins/sdk'; -import queryPart from './query_part'; import PostgresQuery from './postgres_query'; export interface QueryMeta { From 4042e4b225ad4b989f3f1ecabb52271547ff2af2 Mon Sep 17 00:00:00 2001 From: wph95 Date: Tue, 27 Mar 2018 02:12:43 +0800 Subject: [PATCH 0190/3000] fix a terms bug and add test --- pkg/tsdb/elasticsearch/models.go | 2 +- pkg/tsdb/elasticsearch/query.go | 6 +- pkg/tsdb/elasticsearch/query_test.go | 97 ++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/elasticsearch/models.go b/pkg/tsdb/elasticsearch/models.go index 822df2dd4d1..6ab6fa9f43e 100644 --- a/pkg/tsdb/elasticsearch/models.go +++ b/pkg/tsdb/elasticsearch/models.go @@ -41,7 +41,7 @@ type DateHistogramAgg struct { } type FiltersAgg struct { - Filter map[string]interface{} `json:"filter"` + Filters map[string]interface{} `json:"filters"` } type TermsAggSetting struct { diff --git a/pkg/tsdb/elasticsearch/query.go b/pkg/tsdb/elasticsearch/query.go index 51f1ebb5d7a..c4e30cfcbf4 100644 --- a/pkg/tsdb/elasticsearch/query.go +++ b/pkg/tsdb/elasticsearch/query.go @@ -193,15 +193,17 @@ func (q *Query) getHistogramAgg(model *simplejson.Json) *HistogramAgg { func (q *Query) getFilters(model *simplejson.Json) *FiltersAgg { agg := &FiltersAgg{} + agg.Filters = map[string]interface{}{} settings := simplejson.NewFromAny(model.Get("settings").Interface()) - for filter := range settings.Get("filters").MustArray() { + + for _, filter := range settings.Get("filters").MustArray() { filterJson := simplejson.NewFromAny(filter) query := filterJson.Get("query").MustString("") label := filterJson.Get("label").MustString("") if label == "" { label = query } - agg.Filter[label] = newQueryStringFilter(true, query) + agg.Filters[label] = newQueryStringFilter(true, query) } return agg } diff --git a/pkg/tsdb/elasticsearch/query_test.go b/pkg/tsdb/elasticsearch/query_test.go index 992469175b6..4f7b4d9147e 100644 --- a/pkg/tsdb/elasticsearch/query_test.go +++ b/pkg/tsdb/elasticsearch/query_test.go @@ -325,6 +325,103 @@ func TestElasticSearchQueryBuilder(t *testing.T) { "aggs": {"4":{"aggs":{"2":{"aggs":{"1":{"sum":{"field":"value"}}},"date_histogram":{"extended_bounds":{"max":"","min":""},"field":"timestamp","format":"epoch_millis","interval":"1m","min_doc_count":0}}},"terms":{"field":"name_raw","order":{"_term":"desc"},"size":10}}} }` + testElasticSearchResponse(testElasticsearchModelRequestJSON, expectedElasticsearchQueryJSON) + }) + Convey("Test Filters Aggregates", func() { + testElasticsearchModelRequestJSON := ` + { + "bucketAggs": [ + { + "id": "3", + "settings": { + "filters": [{ + "label": "hello", + "query": "host:\"67.65.185.232\"" + }] + }, + "type": "filters" + }, + { + "field": "time", + "id": "2", + "settings": { + "interval": "auto", + "min_doc_count": 0, + "trimEdges": 0 + }, + "type": "date_histogram" + } + ], + "metrics": [ + { + "pipelineAgg": "select metric", + "field": "bytesSent", + "id": "1", + "meta": {}, + "settings": {}, + "type": "count" + } + ], + "query": "*", + "refId": "A", + "timeField": "time" + }` + + expectedElasticsearchQueryJSON := `{ + "size": 0, + "query": { + "bool": { + "filter": [ + { + "range": { + "time": { + "gte": "", + "lte": "", + "format": "epoch_millis" + } + } + }, + { + "query_string": { + "analyze_wildcard": true, + "query": "*" + } + } + ] + } + }, + "aggs": { + "3": { + "filters": { + "filters": { + "hello": { + "query_string": { + "query": "host:\"67.65.185.232\"", + "analyze_wildcard": true + } + } + } + }, + "aggs": { + "2": { + "date_histogram": { + "interval": "200ms", + "field": "time", + "min_doc_count": 0, + "extended_bounds": { + "min": "", + "max": "" + }, + "format": "epoch_millis" + }, + "aggs": {} + } + } + } + } + } + ` + testElasticSearchResponse(testElasticsearchModelRequestJSON, expectedElasticsearchQueryJSON) }) }) From 8b3c3081689236be24a21645ca852a928d33d9c7 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Mon, 26 Mar 2018 20:15:16 +0200 Subject: [PATCH 0191/3000] remove categories from queryPart --- .../datasource/postgres/postgres_query.ts | 5 +---- .../plugins/datasource/postgres/query_part.ts | 19 +------------------ 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/public/app/plugins/datasource/postgres/postgres_query.ts b/public/app/plugins/datasource/postgres/postgres_query.ts index 4516bf4a4be..a804cb70294 100644 --- a/public/app/plugins/datasource/postgres/postgres_query.ts +++ b/public/app/plugins/datasource/postgres/postgres_query.ts @@ -82,14 +82,11 @@ export default class PostgresQuery { } removeGroupByPart(part, index) { - var categories = queryPart.getCategories(); - if (part.def.type === 'time') { // remove aggregations this.target.select = _.map(this.target.select, (s: any) => { return _.filter(s, (part: any) => { - var partModel = queryPart.create(part); - if (partModel.def.category === categories.Aggregations) { + if (part.type === "aggregate") { return false; } return true; diff --git a/public/app/plugins/datasource/postgres/query_part.ts b/public/app/plugins/datasource/postgres/query_part.ts index 044f51a2457..0d2b365fe66 100644 --- a/public/app/plugins/datasource/postgres/query_part.ts +++ b/public/app/plugins/datasource/postgres/query_part.ts @@ -2,12 +2,6 @@ import _ from 'lodash'; import { QueryPartDef, QueryPart, functionRenderer, suffixRenderer } from 'app/core/components/query_part/query_part'; var index = []; -var categories = { - Aggregations: [], - Math: [], - Aliasing: [], - Columns: [], -}; function createPart(part): any { var def = index[part.type]; @@ -20,11 +14,8 @@ function createPart(part): any { function register(options: any) { index[options.type] = new QueryPartDef(options); - options.category.push(index[options.type]); } -var groupByTimeFunctions = []; - function aliasRenderer(part, innerExpr) { return innerExpr + ' AS ' + '"' + part.params[0] + '"'; } @@ -41,7 +32,7 @@ function replaceAggregationAddStrategy(selectParts, partModel) { // look for existing aggregation for (var i = 0; i < selectParts.length; i++) { var part = selectParts[i]; - if (part.def.category === categories.Aggregations) { + if (part.def.type === "aggregate") { selectParts[i] = partModel; return; } @@ -95,7 +86,6 @@ function addColumnStrategy(selectParts, partModel, query) { register({ type: 'column', addStrategy: addColumnStrategy, - category: categories.Columns, params: [{ type: 'column', dynamicLookup: true }], defaultParams: ['value'], renderer: columnRenderer, @@ -104,7 +94,6 @@ register({ register({ type: 'aggregate', addStrategy: replaceAggregationAddStrategy, - category: categories.Aggregations, params: [{name: 'name', type: 'string', dynamicLookup: true}], defaultParams: ['avg'], renderer: aggregateRenderer, @@ -113,7 +102,6 @@ register({ register({ type: 'math', addStrategy: addMathStrategy, - category: categories.Math, params: [{ name: 'expr', type: 'string' }], defaultParams: [' / 100'], renderer: suffixRenderer, @@ -122,7 +110,6 @@ register({ register({ type: 'alias', addStrategy: addAliasStrategy, - category: categories.Aliasing, params: [{ name: 'name', type: 'string', quote: 'double' }], defaultParams: ['alias'], renderMode: 'suffix', @@ -131,7 +118,6 @@ register({ register({ type: 'time', - category: groupByTimeFunctions, params: [ { name: 'interval', @@ -150,7 +136,4 @@ register({ export default { create: createPart, - getCategories: function() { - return categories; - }, }; From 2c7040c246033cfe1ea13df2d8c92bd3cb5860b0 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 27 Mar 2018 09:48:19 +0300 Subject: [PATCH 0192/3000] docs: prometheus ds, remove "new in v4.3" note --- docs/sources/features/datasources/prometheus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 2449119ceda..af492d435e1 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -50,7 +50,7 @@ Name | Description *Min step* | Set a lower limit for the Prometheus step option. Step controls how big the jumps are when the Prometheus query engine performs range queries. Sadly there is no official prometheus documentation to link to for this very important option. *Resolution* | Controls the step option. Small steps create high-resolution graphs but can be slow over larger time ranges, lowering the resolution can speed things up. `1/2` will try to set step option to generate 1 data point for every other pixel. A value of `1/10` will try to set step option so there is a data point every 10 pixels. *Metric lookup* | Search for metric names in this input field. -*Format as* | **(New in v4.3)** Switch between Table, Time series or Heatmap. Table format will only work in the Table panel. Heatmap format is suitable for displaying metrics having histogram type on Heatmap panel. Under the hood, it converts cumulative histogram to regular and sorts series by the bucket bound. +*Format as* | Switch between Table, Time series or Heatmap. Table format will only work in the Table panel. Heatmap format is suitable for displaying metrics having histogram type on Heatmap panel. Under the hood, it converts cumulative histogram to regular and sorts series by the bucket bound. ## Templating From dbcba4a0094f9c360216182d0d326a0bca127d09 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 27 Mar 2018 10:41:47 +0200 Subject: [PATCH 0193/3000] sidemenu fix for internet explorer 11, changed icon width/height to pixels and added height to logo --- public/sass/base/_icons.scss | 6 ++++-- public/sass/components/_sidemenu.scss | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index c701cc1249e..31e5ee62d6f 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -1,8 +1,10 @@ .gicon { line-height: 1; display: inline-block; - width: 1.1057142857em; - height: 1.1057142857em; + //width: 1.1057142857em; + //height: 1.1057142857em; + height: 22px; + width: 22px; text-align: center; background-repeat: no-repeat; background-position: center; diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 8a5c3779714..e48ab0597a2 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -178,6 +178,7 @@ li.sidemenu-org-switcher { padding: 0.4rem 1rem 0.4rem 0.65rem; min-height: $navbarHeight; position: relative; + height: $navbarHeight - 1px; &:hover { background: $navbarButtonBackgroundHighlight; From e622d5582b29b2aea982bca8893bb07c0771f6af Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 27 Mar 2018 10:56:19 +0200 Subject: [PATCH 0194/3000] changelog: adds note about closing #11102 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e962ba301..a940ff044db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * **Singlestat**: Add color to prefix and postfix in singlestat panel [#11143](https://github.com/grafana/grafana/pull/11143), thx [@ApsOps](https://github.com/ApsOps) * **Dashboards**: Version cleanup fails on old databases with many entries [#11278](https://github.com/grafana/grafana/issues/11278) * **Server**: Adjust permissions of unix socket [#11343](https://github.com/grafana/grafana/pull/11343), thx [@corny](https://github.com/corny) +* **Shortcuts**: Add shortcut for duplicate panel [#11102](https://github.com/grafana/grafana/issues/11102) # 5.0.4 (unreleased) * **Dashboard** Fixed bug where collapsed panels could not be directly linked to/renderer [#11114](https://github.com/grafana/grafana/issues/11114) & [#11086](https://github.com/grafana/grafana/issues/11086) From d4be953d23a4dd15eb2c26a003c11cd0f40dc898 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 27 Mar 2018 12:36:13 +0200 Subject: [PATCH 0195/3000] fixed alignment in search + fixed issue ie popup --- public/app/features/dashboard/unsaved_changes_srv.ts | 4 ++-- public/sass/components/_search.scss | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/unsaved_changes_srv.ts b/public/app/features/dashboard/unsaved_changes_srv.ts index ebf0101cee0..d4c12b8bcd6 100644 --- a/public/app/features/dashboard/unsaved_changes_srv.ts +++ b/public/app/features/dashboard/unsaved_changes_srv.ts @@ -35,12 +35,12 @@ export class Tracker { $window.onbeforeunload = () => { if (this.ignoreChanges()) { - return null; + return undefined; } if (this.hasChanges()) { return 'There are unsaved changes to this dashboard'; } - return null; + return undefined; }; scope.$on('$locationChangeStart', (event, next) => { diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 47d4a926968..e69068b730a 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -31,7 +31,6 @@ //padding: 0.5rem 1.5rem 0.5rem 0; padding: 1rem 1rem 0.75rem 1rem; height: 51px; - line-height: 51px; box-sizing: border-box; outline: none; background: $side-menu-bg; From 52164b0685416bf2f214e1331e6d4c016df68ce1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 27 Mar 2018 14:13:49 +0200 Subject: [PATCH 0196/3000] notes about closing #9210 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a940ff044db..f880be9d110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * **Alerting**: Pausing/un alerts now updates new_state_date [#10942](https://github.com/grafana/grafana/pull/10942) * **Alerting**: Support Pagerduty notification channel using Pagerduty V2 API [#10531](https://github.com/grafana/grafana/issues/10531), thx [@jbaublitz](https://github.com/jbaublitz) * **Templating**: Add comma templating format [#10632](https://github.com/grafana/grafana/issues/10632), thx [@mtanda](https://github.com/mtanda) +* **Prometheus**: Show template variable candidate in query editor [#9210](https://github.com/grafana/grafana/issues/9210), thx [@mtanda](https://github.com/mtanda) * **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda) * **Alerting**: Add support for retries on alert queries [#5855](https://github.com/grafana/grafana/issues/5855), thx [@Thib17](https://github.com/Thib17) From 627df67992e58f64e12bdbae1317ca524cbaa1e7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 27 Mar 2018 15:12:47 +0200 Subject: [PATCH 0197/3000] dashboards: reject updates of provisioned dashboards --- pkg/api/dashboard.go | 3 +- pkg/api/dashboard_test.go | 1 + pkg/models/dashboards.go | 42 ++++++++++--------- pkg/services/dashboards/dashboard_service.go | 22 ++++++++++ .../dashboards/dashboard_service_test.go | 33 ++++++++++++++- .../sqlstore/dashboard_provisioning.go | 7 +++- .../sqlstore/dashboard_provisioning_test.go | 7 ++++ .../dashboard_service_integration_test.go | 5 ++- 8 files changed, 96 insertions(+), 24 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index e5e4fc560e1..24751896f00 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -235,7 +235,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response { err == m.ErrDashboardWithSameUIDExists || err == m.ErrFolderNotFound || err == m.ErrDashboardFolderCannotHaveParent || - err == m.ErrDashboardFolderNameExists { + err == m.ErrDashboardFolderNameExists || + err == m.ErrDashboardCannotSaveProvisionedDashboard { return Error(400, err.Error(), nil) } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 0d87023ce40..3d0cb93f0db 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -720,6 +720,7 @@ func TestDashboardApiEndpoint(t *testing.T) { {SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403}, {SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400}, {SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400}, + {SaveError: m.ErrDashboardCannotSaveProvisionedDashboard, ExpectedStatusCode: 400}, {SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412}, } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index e4f0758fc19..1d52246dcc8 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -13,26 +13,28 @@ import ( // Typed errors var ( - ErrDashboardNotFound = errors.New("Dashboard not found") - ErrDashboardFolderNotFound = errors.New("Folder not found") - ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") - ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists") - ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists") - ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") - ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") - ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder") - ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") - ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data") - ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists") - ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id") - ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder") - ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards") - ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder") - ErrDashboardFolderNameExists = errors.New("A folder with that name already exists") - ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard") - ErrDashboardInvalidUid = errors.New("uid contains illegal characters") - ErrDashboardUidToLong = errors.New("uid to long. max 40 characters") - RootFolderName = "General" + ErrDashboardNotFound = errors.New("Dashboard not found") + ErrDashboardFolderNotFound = errors.New("Folder not found") + ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") + ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists") + ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists") + ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") + ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") + ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder") + ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") + ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data") + ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists") + ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id") + ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder") + ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards") + ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder") + ErrDashboardFolderNameExists = errors.New("A folder with that name already exists") + ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard") + ErrDashboardInvalidUid = errors.New("uid contains illegal characters") + ErrDashboardUidToLong = errors.New("uid to long. max 40 characters") + ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboards") + ErrDashboardProvisioningDoesNotExist = errors.New("Dashboard provisioning does not exist") + RootFolderName = "General" ) type UpdatePluginDashboardError struct { diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 02a6ffc8330..7c51c412d39 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -111,6 +111,11 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, return nil, models.ErrDashboardUpdateAccessDenied } + err := dr.validateDashboardIsNotProvisioned(dash.Id) + if err != nil { + return nil, err + } + cmd := &models.SaveDashboardCommand{ Dashboard: dash.Data, Message: dto.Message, @@ -129,6 +134,23 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, return cmd, nil } +func (dr *dashboardServiceImpl) validateDashboardIsNotProvisioned(dashboardId int64) error { + dpQuery := &models.GetProvisionedDashboardByDashboardId{DashboardId: dashboardId} + err := bus.Dispatch(dpQuery) + + // provisioned dashboards cannot be saved. So we can only save + // this dashboard if ErrDashboardProvisioningDoesNotExist is returned + if err != nil && err != models.ErrDashboardProvisioningDoesNotExist { + return err + } + + if err == nil { + return models.ErrDashboardCannotSaveProvisionedDashboard + } + + return nil +} + func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error { alertCmd := models.UpdateDashboardAlertsCommand{ OrgId: dto.OrgId, diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go index 965b10655b3..65c9cabcdc5 100644 --- a/pkg/services/dashboards/dashboard_service_test.go +++ b/pkg/services/dashboards/dashboard_service_test.go @@ -14,7 +14,9 @@ import ( func TestDashboardService(t *testing.T) { Convey("Dashboard service tests", t, func() { - service := dashboardServiceImpl{} + bus.ClearBusHandlers() + + service := &dashboardServiceImpl{} origNewDashboardGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) @@ -54,6 +56,10 @@ func TestDashboardService(t *testing.T) { return nil }) + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardByDashboardId) error { + return models.ErrDashboardProvisioningDoesNotExist + }) + testCases := []struct { Uid string Error error @@ -77,7 +83,32 @@ func TestDashboardService(t *testing.T) { } }) + Convey("Should return validation error if dashboard is provisioned", func() { + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardByDashboardId) error { + cmd.Result = &models.DashboardProvisioning{} + return nil + }) + + bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { + return nil + }) + + bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { + return nil + }) + + dto.Dashboard = models.NewDashboard("Dash") + dto.Dashboard.SetId(3) + dto.User = &models.SignedInUser{UserId: 1} + _, err := service.buildSaveDashboardCommand(dto, false) + So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard) + }) + Convey("Should return validation error if alert data is invalid", func() { + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardByDashboardId) error { + return models.ErrDashboardProvisioningDoesNotExist + }) + bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { return errors.New("error") }) diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index 99178d38f9c..b28b2fa1a53 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -21,12 +21,17 @@ type DashboardExtras struct { func GetProvisionedDataByDashboardId(cmd *models.GetProvisionedDashboardByDashboardId) error { result := &models.DashboardProvisioning{} - _, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result) + exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result) if err != nil { return err } + if !exist { + return models.ErrDashboardProvisioningDoesNotExist + } + cmd.Result = result + return nil } diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 89b3451a3ac..4bafbbff8ca 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -60,6 +60,13 @@ func TestDashboardProvisioningTest(t *testing.T) { So(query.Result.DashboardId, ShouldEqual, cmd.Result.Id) So(query.Result.Updated, ShouldEqual, now.Unix()) }) + + Convey("Can query for one provisioned dashboard2", func() { + query := &models.GetProvisionedDashboardByDashboardId{DashboardId: 3000} + + err := GetProvisionedDataByDashboardId(query) + So(err, ShouldEqual, models.ErrDashboardProvisioningDoesNotExist) + }) }) }) } diff --git a/pkg/services/sqlstore/dashboard_service_integration_test.go b/pkg/services/sqlstore/dashboard_service_integration_test.go index d005270c33c..75405558412 100644 --- a/pkg/services/sqlstore/dashboard_service_integration_test.go +++ b/pkg/services/sqlstore/dashboard_service_integration_test.go @@ -19,7 +19,6 @@ func TestIntegratedDashboardService(t *testing.T) { var testOrgId int64 = 1 Convey("Given saved folders and dashboards in organization A", func() { - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { return nil }) @@ -28,6 +27,10 @@ func TestIntegratedDashboardService(t *testing.T) { return nil }) + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardByDashboardId) error { + return models.ErrDashboardProvisioningDoesNotExist + }) + savedFolder := saveTestFolder("Saved folder", testOrgId) savedDashInFolder := saveTestDashboard("Saved dash in folder", testOrgId, savedFolder.Id) saveTestDashboard("Other saved dash in folder", testOrgId, savedFolder.Id) From 7f5c2ebdd18e81b3be09bab6411d9a0748b2cb2a Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 27 Mar 2018 15:17:05 +0200 Subject: [PATCH 0198/3000] provisioning: better description for provisioned save modal --- public/app/features/dashboard/save_provisioned_modal.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/save_provisioned_modal.ts b/public/app/features/dashboard/save_provisioned_modal.ts index 8121248b841..80c81b07e3f 100644 --- a/public/app/features/dashboard/save_provisioned_modal.ts +++ b/public/app/features/dashboard/save_provisioned_modal.ts @@ -15,9 +15,10 @@ const template = `
    diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 163970a9ad5..26392c17356 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -48,8 +48,8 @@ Table: Macros: - $__time(column) -> column as "time" - $__timeEpoch -> extract(epoch from column) as "time" -- $__timeFilter(column) -> extract(epoch from column) BETWEEN 1492750877 AND 1492750877 -- $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 +- $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z' +- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877 - $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS time Example of group by and order by with $__timeGroup: @@ -61,8 +61,8 @@ GROUP BY time ORDER BY time Or build your own conditionals using these macros which just return the values: -- $__timeFrom() -> to_timestamp(1492750877) -- $__timeTo() -> to_timestamp(1492750877) +- $__timeFrom() -> '2017-04-21T05:01:17Z' +- $__timeTo() -> '2017-04-21T05:01:17Z' - $__unixEpochFrom() -> 1492750877 - $__unixEpochTo() -> 1492750877
    From ad22e20ecd7f1049b7525e6fd130cc01044cb6c2 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 17 Apr 2018 00:06:25 +0200 Subject: [PATCH 0414/3000] docs: update postgres macro functions documentation In addition to closing #11578 --- docs/sources/features/datasources/postgres.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index e90ba79b686..fa4e5d241ca 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -57,12 +57,12 @@ Macro example | Description ------------ | ------------- *$__time(dateColumn)* | Will be replaced by an expression to rename the column to `time`. For example, *dateColumn as time* *$__timeEpoch(dateColumn)* | Will be replaced by an expression to rename the column to `time` and converting the value to unix timestamp. For example, *extract(epoch from dateColumn) as time* -*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *extract(epoch from dateColumn) BETWEEN 1494410783 AND 1494497183* -*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)* -*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)* +*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-05-10T10:06:23Z' AND '2017-05-10T10:06:23Z'* +*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-05-10T10:06:23Z'* +*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-05-10T10:06:23Z'* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time* *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). -*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* +*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* From b6b152d9ea41384e87af5d98f211a3bbba598b0b Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Tue, 17 Apr 2018 09:00:39 +0200 Subject: [PATCH 0415/3000] Revert "removes codecov from frontend tests" --- Gruntfile.js | 1 + codecov.yml | 13 +++++++++++++ package.json | 1 + scripts/circle-test-frontend.sh | 9 +++++++-- scripts/grunt/options/exec.js | 7 ++++++- 5 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 codecov.yml diff --git a/Gruntfile.js b/Gruntfile.js index 03f70565b57..a0607ef49dc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -22,6 +22,7 @@ module.exports = function (grunt) { } } + config.coverage = grunt.option('coverage'); config.phjs = grunt.option('phjsToRelease'); config.pkg.version = grunt.option('pkgVer') || config.pkg.version; diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..82a86e0232b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,13 @@ +coverage: + precision: 2 + round: down + range: "50...100" + + status: + project: yes + patch: yes + changes: no + +comment: + layout: "diff" + behavior: "once" diff --git a/package.json b/package.json index b74d23f33b2..ce861a25f7b 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", "build": "grunt build", "test": "grunt test", + "test:coverage": "grunt test --coverage=true", "lint": "tslint -c tslint.json --project tsconfig.json --type-check", "karma": "grunt karma:dev", "jest": "jest --notify --watch", diff --git a/scripts/circle-test-frontend.sh b/scripts/circle-test-frontend.sh index 325c24ae7a9..9857e00f70d 100755 --- a/scripts/circle-test-frontend.sh +++ b/scripts/circle-test-frontend.sh @@ -10,5 +10,10 @@ function exit_if_fail { fi } -exit_if_fail npm run test -exit_if_fail npm run build \ No newline at end of file +exit_if_fail npm run test:coverage +exit_if_fail npm run build + +# publish code coverage +echo "Publishing javascript code coverage" +bash <(curl -s https://codecov.io/bash) -cF javascript +rm -rf coverage diff --git a/scripts/grunt/options/exec.js b/scripts/grunt/options/exec.js index be163581bf6..e22d060ea04 100644 --- a/scripts/grunt/options/exec.js +++ b/scripts/grunt/options/exec.js @@ -1,9 +1,14 @@ module.exports = function(config, grunt) { 'use strict'; + var coverage = ''; + if (config.coverage) { + coverage = '--coverage --maxWorkers 2'; + } + return { tslint: 'node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json', - jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2', + jest: 'node ./node_modules/jest-cli/bin/jest.js ' + coverage, webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js', }; }; From 450a3b4a0080149c2fcf6a59c6b48bbdeff00241 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Tue, 17 Apr 2018 09:00:55 +0200 Subject: [PATCH 0416/3000] Revert "build: remove code cov" --- scripts/circle-test-backend.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/circle-test-backend.sh b/scripts/circle-test-backend.sh index 71fc598b609..a63d6354fa6 100755 --- a/scripts/circle-test-backend.sh +++ b/scripts/circle-test-backend.sh @@ -20,4 +20,17 @@ echo "building backend with install to cache pkgs" exit_if_fail time go install ./pkg/cmd/grafana-server echo "running go test" -go test ./pkg/... + +set -e +echo "" > coverage.txt + +time for d in $(go list ./pkg/...); do + exit_if_fail go test -coverprofile=profile.out -covermode=atomic $d + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done + +echo "Publishing go code coverage" +bash <(curl -s https://codecov.io/bash) -cF go From a01e6fc9c7d221385467fe4d4e2b7ccd2715b18b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 17 Apr 2018 09:19:17 +0200 Subject: [PATCH 0417/3000] add some more sort order asserts for permissions store tests --- public/app/stores/PermissionsStore/PermissionsStore.jest.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts index f1713542be5..d6a20e25846 100644 --- a/public/app/stores/PermissionsStore/PermissionsStore.jest.ts +++ b/public/app/stores/PermissionsStore/PermissionsStore.jest.ts @@ -75,6 +75,7 @@ describe('PermissionsStore', () => { it('should be sorted by sort rank and alphabetically', async () => { expect(store.items[0].name).toBe('MyTestTeam'); + expect(store.items[0].dashboardId).toBe(10); expect(store.items[1].name).toBe('Editor'); expect(store.items[2].name).toBe('Viewer'); expect(store.items[3].name).toBe('MyTestTeam2'); @@ -102,9 +103,11 @@ describe('PermissionsStore', () => { it('should be sorted by sort rank and alphabetically', async () => { expect(store.items[0].name).toBe('MyTestTeam'); + expect(store.items[0].dashboardId).toBe(10); expect(store.items[1].name).toBe('Editor'); expect(store.items[2].name).toBe('Viewer'); expect(store.items[3].name).toBe('MyTestTeam'); + expect(store.items[3].dashboardId).toBe(1); expect(store.items[4].name).toBe('MyTestTeam2'); expect(store.items[5].name).toBe('MyTestUser'); }); From 9ec89c1848e65bd079e69bdc26c4fade9591b95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Apr 2018 09:20:43 +0200 Subject: [PATCH 0418/3000] disable codecov comments --- codecov.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/codecov.yml b/codecov.yml index 82a86e0232b..b2a839365ac 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,6 +8,4 @@ coverage: patch: yes changes: no -comment: - layout: "diff" - behavior: "once" +comment: off From 85121e55c9ee2bf3401416b52ff6e4aeb0a70d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Apr 2018 09:41:07 +0200 Subject: [PATCH 0419/3000] fix: Row state is now ignored when looking for dashboard changes (#11608) * fix: Row state is now ignored when looking for dashboawrd changes, had to fix bug in expand logic to make fix work. Also moved the ChangeTracker to it's own file and moved tests to jest, fixes #11208 * removed commented out log calls --- .../app/features/dashboard/change_tracker.ts | 186 +++++++++++++++ .../app/features/dashboard/dashboard_model.ts | 3 +- .../dashboard/specs/change_tracker.jest.ts | 99 ++++++++ .../dashboard/specs/dashboard_model.jest.ts | 14 +- .../specs/unsaved_changes_srv_specs.ts | 95 -------- .../features/dashboard/unsaved_changes_srv.ts | 211 +----------------- .../templating/specs/query_variable.jest.ts | 1 - 7 files changed, 296 insertions(+), 313 deletions(-) create mode 100644 public/app/features/dashboard/change_tracker.ts create mode 100644 public/app/features/dashboard/specs/change_tracker.jest.ts delete mode 100644 public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts diff --git a/public/app/features/dashboard/change_tracker.ts b/public/app/features/dashboard/change_tracker.ts new file mode 100644 index 00000000000..745b76ce347 --- /dev/null +++ b/public/app/features/dashboard/change_tracker.ts @@ -0,0 +1,186 @@ +import angular from 'angular'; +import _ from 'lodash'; +import { DashboardModel } from './dashboard_model'; + +export class ChangeTracker { + current: any; + originalPath: any; + scope: any; + original: any; + next: any; + $window: any; + + /** @ngInject */ + constructor( + dashboard, + scope, + originalCopyDelay, + private $location, + $window, + private $timeout, + private contextSrv, + private $rootScope + ) { + this.$location = $location; + this.$window = $window; + + this.current = dashboard; + this.originalPath = $location.path(); + this.scope = scope; + + // register events + scope.onAppEvent('dashboard-saved', () => { + this.original = this.current.getSaveModelClone(); + this.originalPath = $location.path(); + }); + + $window.onbeforeunload = () => { + if (this.ignoreChanges()) { + return undefined; + } + if (this.hasChanges()) { + return 'There are unsaved changes to this dashboard'; + } + return undefined; + }; + + scope.$on('$locationChangeStart', (event, next) => { + // check if we should look for changes + if (this.originalPath === $location.path()) { + return true; + } + if (this.ignoreChanges()) { + return true; + } + + if (this.hasChanges()) { + event.preventDefault(); + this.next = next; + + this.$timeout(() => { + this.open_modal(); + }); + } + return false; + }); + + if (originalCopyDelay) { + this.$timeout(() => { + // wait for different services to patch the dashboard (missing properties) + this.original = dashboard.getSaveModelClone(); + }, originalCopyDelay); + } else { + this.original = dashboard.getSaveModelClone(); + } + } + + // for some dashboards and users + // changes should be ignored + ignoreChanges() { + if (!this.original) { + return true; + } + if (!this.contextSrv.isEditor) { + return true; + } + if (!this.current || !this.current.meta) { + return true; + } + + var meta = this.current.meta; + return !meta.canSave || meta.fromScript || meta.fromFile; + } + + // remove stuff that should not count in diff + cleanDashboardFromIgnoredChanges(dashData) { + // need to new up the domain model class to get access to expand / collapse row logic + let model = new DashboardModel(dashData); + + // Expand all rows before making comparison. This is required because row expand / collapse + // change order of panel array and panel positions. + model.expandRows(); + + let dash = model.getSaveModelClone(); + + // ignore time and refresh + dash.time = 0; + dash.refresh = 0; + dash.schemaVersion = 0; + + // ignore iteration property + delete dash.iteration; + + dash.panels = _.filter(dash.panels, panel => { + if (panel.repeatPanelId) { + return false; + } + + // remove scopedVars + panel.scopedVars = null; + + // ignore panel legend sort + if (panel.legend) { + delete panel.legend.sort; + delete panel.legend.sortDesc; + } + + return true; + }); + + // ignore template variable values + _.each(dash.templating.list, function(value) { + value.current = null; + value.options = null; + value.filters = null; + }); + + return dash; + } + + hasChanges() { + let current = this.cleanDashboardFromIgnoredChanges(this.current.getSaveModelClone()); + let original = this.cleanDashboardFromIgnoredChanges(this.original); + + var currentTimepicker = _.find(current.nav, { type: 'timepicker' }); + var originalTimepicker = _.find(original.nav, { type: 'timepicker' }); + + if (currentTimepicker && originalTimepicker) { + currentTimepicker.now = originalTimepicker.now; + } + + var currentJson = angular.toJson(current, true); + var originalJson = angular.toJson(original, true); + + return currentJson !== originalJson; + } + + discardChanges() { + this.original = null; + this.gotoNext(); + } + + open_modal() { + this.$rootScope.appEvent('show-modal', { + templateHtml: '', + modalClass: 'modal--narrow confirm-modal', + }); + } + + saveChanges() { + var self = this; + var cancel = this.$rootScope.$on('dashboard-saved', () => { + cancel(); + this.$timeout(() => { + self.gotoNext(); + }); + }); + + this.$rootScope.appEvent('save-dashboard'); + } + + gotoNext() { + var baseLen = this.$location.absUrl().length - this.$location.url().length; + var nextUrl = this.next.substring(baseLen); + this.$location.url(nextUrl); + } +} diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 9130cb7e806..8a300a80341 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -649,6 +649,7 @@ export class DashboardModel { for (let panel of row.panels) { // make sure y is adjusted (in case row moved while collapsed) + // console.log('yDiff', yDiff); panel.gridPos.y -= yDiff; // insert after row this.panels.splice(insertPos, 0, new PanelModel(panel)); @@ -657,7 +658,7 @@ export class DashboardModel { yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h); } - const pushDownAmount = yMax - row.gridPos.y; + const pushDownAmount = yMax - row.gridPos.y - 1; // push panels below down for (let panelIndex = insertPos; panelIndex < this.panels.length; panelIndex++) { diff --git a/public/app/features/dashboard/specs/change_tracker.jest.ts b/public/app/features/dashboard/specs/change_tracker.jest.ts new file mode 100644 index 00000000000..5ec84aadbd0 --- /dev/null +++ b/public/app/features/dashboard/specs/change_tracker.jest.ts @@ -0,0 +1,99 @@ +import { ChangeTracker } from 'app/features/dashboard/change_tracker'; +import { contextSrv } from 'app/core/services/context_srv'; +import { DashboardModel } from '../dashboard_model'; +import { PanelModel } from '../panel_model'; + +jest.mock('app/core/services/context_srv', () => ({ + contextSrv: { + user: { orgId: 1 }, + }, +})); + +describe('ChangeTracker', () => { + let rootScope; + let location; + let timeout; + let tracker: ChangeTracker; + let dash; + let scope; + + beforeEach(() => { + dash = new DashboardModel({ + refresh: false, + panels: [ + { + id: 1, + type: 'graph', + gridPos: { x: 0, y: 0, w: 24, h: 6 }, + legend: { sortDesc: false }, + }, + { + id: 2, + type: 'row', + gridPos: { x: 0, y: 6, w: 24, h: 2 }, + collapsed: true, + panels: [ + { id: 3, type: 'graph', gridPos: { x: 0, y: 6, w: 12, h: 2 } }, + { id: 4, type: 'graph', gridPos: { x: 12, y: 6, w: 12, h: 2 } }, + ], + }, + { id: 5, type: 'row', gridPos: { x: 0, y: 6, w: 1, h: 1 } }, + ], + }); + + scope = { + appEvent: jest.fn(), + onAppEvent: jest.fn(), + $on: jest.fn(), + }; + + rootScope = { + appEvent: jest.fn(), + onAppEvent: jest.fn(), + $on: jest.fn(), + }; + + location = { + path: jest.fn(), + }; + + tracker = new ChangeTracker(dash, scope, undefined, location, window, timeout, contextSrv, rootScope); + }); + + it('No changes should not have changes', () => { + expect(tracker.hasChanges()).toBe(false); + }); + + it('Simple change should be registered', () => { + dash.title = 'google'; + expect(tracker.hasChanges()).toBe(true); + }); + + it('Should ignore a lot of changes', () => { + dash.time = { from: '1h' }; + dash.refresh = true; + dash.schemaVersion = 10; + expect(tracker.hasChanges()).toBe(false); + }); + + it('Should ignore .iteration changes', () => { + dash.iteration = new Date().getTime() + 1; + expect(tracker.hasChanges()).toBe(false); + }); + + it('Should ignore row collapse change', () => { + dash.toggleRow(dash.panels[1]); + expect(tracker.hasChanges()).toBe(false); + }); + + it('Should ignore panel legend changes', () => { + dash.panels[0].legend.sortDesc = true; + dash.panels[0].legend.sort = 'avg'; + expect(tracker.hasChanges()).toBe(false); + }); + + it('Should ignore panel repeats', () => { + dash.panels.push(new PanelModel({ repeatPanelId: 10 })); + expect(tracker.hasChanges()).toBe(false); + }); +}); diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.jest.ts index 99fe727c49d..feede679018 100644 --- a/public/app/features/dashboard/specs/dashboard_model.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_model.jest.ts @@ -374,14 +374,14 @@ describe('DashboardModel', function() { { id: 2, type: 'row', - gridPos: { x: 0, y: 6, w: 24, h: 2 }, + gridPos: { x: 0, y: 6, w: 24, h: 1 }, collapsed: true, panels: [ - { id: 3, type: 'graph', gridPos: { x: 0, y: 2, w: 12, h: 2 } }, - { id: 4, type: 'graph', gridPos: { x: 12, y: 2, w: 12, h: 2 } }, + { id: 3, type: 'graph', gridPos: { x: 0, y: 7, w: 12, h: 2 } }, + { id: 4, type: 'graph', gridPos: { x: 12, y: 7, w: 12, h: 2 } }, ], }, - { id: 5, type: 'row', gridPos: { x: 0, y: 6, w: 1, h: 1 } }, + { id: 5, type: 'row', gridPos: { x: 0, y: 7, w: 1, h: 1 } }, ], }); dashboard.toggleRow(dashboard.panels[1]); @@ -399,16 +399,16 @@ describe('DashboardModel', function() { it('should position them below row', function() { expect(dashboard.panels[2].gridPos).toMatchObject({ x: 0, - y: 8, + y: 7, w: 12, h: 2, }); }); - it('should move panels below down', function() { + it.only('should move panels below down', function() { expect(dashboard.panels[4].gridPos).toMatchObject({ x: 0, - y: 10, + y: 9, w: 1, h: 1, }); diff --git a/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts b/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts deleted file mode 100644 index 8bd639de681..00000000000 --- a/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common'; -import { Tracker } from 'app/features/dashboard/unsaved_changes_srv'; -import 'app/features/dashboard/dashboard_srv'; -import { contextSrv } from 'app/core/core'; - -describe('unsavedChangesSrv', function() { - var _dashboardSrv; - var _contextSrvStub = { isEditor: true }; - var _rootScope; - var _location; - var _timeout; - var _window; - var tracker; - var dash; - var scope; - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.module(function($provide) { - $provide.value('contextSrv', _contextSrvStub); - $provide.value('$window', {}); - }) - ); - - beforeEach( - angularMocks.inject(function($location, $rootScope, dashboardSrv, $timeout, $window) { - _dashboardSrv = dashboardSrv; - _rootScope = $rootScope; - _location = $location; - _timeout = $timeout; - _window = $window; - }) - ); - - beforeEach(function() { - dash = _dashboardSrv.create({ - refresh: false, - panels: [{ test: 'asd', legend: {} }], - rows: [ - { - panels: [{ test: 'asd', legend: {} }], - }, - ], - }); - scope = _rootScope.$new(); - scope.appEvent = sinon.spy(); - scope.onAppEvent = sinon.spy(); - - tracker = new Tracker(dash, scope, undefined, _location, _window, _timeout, contextSrv, _rootScope); - }); - - it('No changes should not have changes', function() { - expect(tracker.hasChanges()).to.be(false); - }); - - it('Simple change should be registered', function() { - dash.property = 'google'; - expect(tracker.hasChanges()).to.be(true); - }); - - it('Should ignore a lot of changes', function() { - dash.time = { from: '1h' }; - dash.refresh = true; - dash.schemaVersion = 10; - expect(tracker.hasChanges()).to.be(false); - }); - - it('Should ignore .iteration changes', () => { - dash.iteration = new Date().getTime() + 1; - expect(tracker.hasChanges()).to.be(false); - }); - - it.skip('Should ignore row collapse change', function() { - dash.rows[0].collapse = true; - expect(tracker.hasChanges()).to.be(false); - }); - - it('Should ignore panel legend changes', function() { - dash.panels[0].legend.sortDesc = true; - dash.panels[0].legend.sort = 'avg'; - expect(tracker.hasChanges()).to.be(false); - }); - - it.skip('Should ignore panel repeats', function() { - dash.rows[0].panels.push({ repeatPanelId: 10 }); - expect(tracker.hasChanges()).to.be(false); - }); - - it.skip('Should ignore row repeats', function() { - dash.addEmptyRow(); - dash.rows[1].repeatRowId = 10; - expect(tracker.hasChanges()).to.be(false); - }); -}); diff --git a/public/app/features/dashboard/unsaved_changes_srv.ts b/public/app/features/dashboard/unsaved_changes_srv.ts index d4c12b8bcd6..0406e6a55d7 100644 --- a/public/app/features/dashboard/unsaved_changes_srv.ts +++ b/public/app/features/dashboard/unsaved_changes_srv.ts @@ -1,217 +1,10 @@ import angular from 'angular'; -import _ from 'lodash'; - -export class Tracker { - current: any; - originalPath: any; - scope: any; - original: any; - next: any; - $window: any; - - /** @ngInject */ - constructor( - dashboard, - scope, - originalCopyDelay, - private $location, - $window, - private $timeout, - private contextSrv, - private $rootScope - ) { - this.$location = $location; - this.$window = $window; - - this.current = dashboard; - this.originalPath = $location.path(); - this.scope = scope; - - // register events - scope.onAppEvent('dashboard-saved', () => { - this.original = this.current.getSaveModelClone(); - this.originalPath = $location.path(); - }); - - $window.onbeforeunload = () => { - if (this.ignoreChanges()) { - return undefined; - } - if (this.hasChanges()) { - return 'There are unsaved changes to this dashboard'; - } - return undefined; - }; - - scope.$on('$locationChangeStart', (event, next) => { - // check if we should look for changes - if (this.originalPath === $location.path()) { - return true; - } - if (this.ignoreChanges()) { - return true; - } - - if (this.hasChanges()) { - event.preventDefault(); - this.next = next; - - this.$timeout(() => { - this.open_modal(); - }); - } - return false; - }); - - if (originalCopyDelay) { - this.$timeout(() => { - // wait for different services to patch the dashboard (missing properties) - this.original = dashboard.getSaveModelClone(); - }, originalCopyDelay); - } else { - this.original = dashboard.getSaveModelClone(); - } - } - - // for some dashboards and users - // changes should be ignored - ignoreChanges() { - if (!this.original) { - return true; - } - if (!this.contextSrv.isEditor) { - return true; - } - if (!this.current || !this.current.meta) { - return true; - } - - var meta = this.current.meta; - return !meta.canSave || meta.fromScript || meta.fromFile; - } - - // remove stuff that should not count in diff - cleanDashboardFromIgnoredChanges(dash) { - // ignore time and refresh - dash.time = 0; - dash.refresh = 0; - dash.schemaVersion = 0; - - // ignore iteration property - delete dash.iteration; - - // filter row and panels properties that should be ignored - dash.rows = _.filter(dash.rows, function(row) { - if (row.repeatRowId) { - return false; - } - - row.panels = _.filter(row.panels, function(panel) { - if (panel.repeatPanelId) { - return false; - } - - // remove scopedVars - panel.scopedVars = null; - - // ignore span changes - panel.span = null; - - // ignore panel legend sort - if (panel.legend) { - delete panel.legend.sort; - delete panel.legend.sortDesc; - } - - return true; - }); - - // ignore collapse state - row.collapse = false; - return true; - }); - - dash.panels = _.filter(dash.panels, panel => { - if (panel.repeatPanelId) { - return false; - } - - // remove scopedVars - panel.scopedVars = null; - - // ignore panel legend sort - if (panel.legend) { - delete panel.legend.sort; - delete panel.legend.sortDesc; - } - - return true; - }); - - // ignore template variable values - _.each(dash.templating.list, function(value) { - value.current = null; - value.options = null; - value.filters = null; - }); - } - - hasChanges() { - var current = this.current.getSaveModelClone(); - var original = this.original; - - this.cleanDashboardFromIgnoredChanges(current); - this.cleanDashboardFromIgnoredChanges(original); - - var currentTimepicker = _.find(current.nav, { type: 'timepicker' }); - var originalTimepicker = _.find(original.nav, { type: 'timepicker' }); - - if (currentTimepicker && originalTimepicker) { - currentTimepicker.now = originalTimepicker.now; - } - - var currentJson = angular.toJson(current); - var originalJson = angular.toJson(original); - - return currentJson !== originalJson; - } - - discardChanges() { - this.original = null; - this.gotoNext(); - } - - open_modal() { - this.$rootScope.appEvent('show-modal', { - templateHtml: '', - modalClass: 'modal--narrow confirm-modal', - }); - } - - saveChanges() { - var self = this; - var cancel = this.$rootScope.$on('dashboard-saved', () => { - cancel(); - this.$timeout(() => { - self.gotoNext(); - }); - }); - - this.$rootScope.appEvent('save-dashboard'); - } - - gotoNext() { - var baseLen = this.$location.absUrl().length - this.$location.url().length; - var nextUrl = this.next.substring(baseLen); - this.$location.url(nextUrl); - } -} +import { ChangeTracker } from './change_tracker'; /** @ngInject */ export function unsavedChangesSrv($rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) { - this.Tracker = Tracker; this.init = function(dashboard, scope) { - this.tracker = new Tracker(dashboard, scope, 1000, $location, $window, $timeout, contextSrv, $rootScope); + this.tracker = new ChangeTracker(dashboard, scope, 1000, $location, $window, $timeout, contextSrv, $rootScope); return this.tracker; }; } diff --git a/public/app/features/templating/specs/query_variable.jest.ts b/public/app/features/templating/specs/query_variable.jest.ts index ce753a4b205..39c51874586 100644 --- a/public/app/features/templating/specs/query_variable.jest.ts +++ b/public/app/features/templating/specs/query_variable.jest.ts @@ -91,7 +91,6 @@ describe('QueryVariable', () => { it('should return in same order', () => { var i = 0; - console.log(result); expect(result.length).toBe(11); expect(result[i++].text).toBe(''); expect(result[i++].text).toBe('0'); From ffe9b426d4a7871078c81c5254a85115f1506e9b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 17 Apr 2018 10:00:09 +0200 Subject: [PATCH 0420/3000] changelog: notes about closing #10747 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f433b102342..81059a4d58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ * **Playlist**: Empty playlists cannot be deleted [#11133](https://github.com/grafana/grafana/issues/11133), thx [@kichristensen](https://github.com/kichristensen) * **Switch Orgs**: Alphabetic order in Switch Organization modal [#11556](https://github.com/grafana/grafana/issues/11556) * **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm) +* **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747) ### Tech * Migrated JavaScript files to TypeScript From e07de80b75266dc64e01d47d34e3137d82d2f684 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Tue, 17 Apr 2018 04:20:01 -0400 Subject: [PATCH 0421/3000] Fix issues with metric reporting (#11518) * report active users in graphite stats * use bus to publish system stats * metrics: avoid using events unless we have to this commit also changes the default interval for updating the stats gauges. Seems like the old values was a product of previous metrics implementation --- pkg/metrics/metrics.go | 42 ++++++++++++++++++++-------------- pkg/services/sqlstore/stats.go | 1 + 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 4cefe12d6e5..e3640378f7e 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -54,6 +54,7 @@ var ( M_Alerting_Active_Alerts prometheus.Gauge M_StatTotal_Dashboards prometheus.Gauge M_StatTotal_Users prometheus.Gauge + M_StatActive_Users prometheus.Gauge M_StatTotal_Orgs prometheus.Gauge M_StatTotal_Playlists prometheus.Gauge M_Grafana_Version *prometheus.GaugeVec @@ -253,6 +254,12 @@ func init() { Namespace: exporterName, }) + M_StatActive_Users = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_active_users", + Help: "number of active users", + Namespace: exporterName, + }) + M_StatTotal_Orgs = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "stat_total_orgs", Help: "total amount of orgs", @@ -270,7 +277,6 @@ func init() { Help: "Information about the Grafana", Namespace: exporterName, }, []string{"version"}) - } func initMetricVars(settings *MetricSettings) { @@ -305,6 +311,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Active_Alerts, M_StatTotal_Dashboards, M_StatTotal_Users, + M_StatActive_Users, M_StatTotal_Orgs, M_StatTotal_Playlists, M_Grafana_Version) @@ -315,35 +322,36 @@ func initMetricVars(settings *MetricSettings) { func instrumentationLoop(settings *MetricSettings) chan struct{} { M_Instance_Start.Inc() + // set the total stats gauges before we publishing metrics + updateTotalStats() + onceEveryDayTick := time.NewTicker(time.Hour * 24) - secondTicker := time.NewTicker(time.Second * time.Duration(settings.IntervalSeconds)) + everyMinuteTicker := time.NewTicker(time.Minute) + defer onceEveryDayTick.Stop() + defer everyMinuteTicker.Stop() for { select { case <-onceEveryDayTick.C: sendUsageStats() - case <-secondTicker.C: + case <-everyMinuteTicker.C: updateTotalStats() } } } -var metricPublishCounter int64 = 0 - func updateTotalStats() { - metricPublishCounter++ - if metricPublishCounter == 1 || metricPublishCounter%10 == 0 { - statsQuery := models.GetSystemStatsQuery{} - if err := bus.Dispatch(&statsQuery); err != nil { - metricsLogger.Error("Failed to get system stats", "error", err) - return - } - - M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards)) - M_StatTotal_Users.Set(float64(statsQuery.Result.Users)) - M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists)) - M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs)) + statsQuery := models.GetSystemStatsQuery{} + if err := bus.Dispatch(&statsQuery); err != nil { + metricsLogger.Error("Failed to get system stats", "error", err) + return } + + M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards)) + M_StatTotal_Users.Set(float64(statsQuery.Result.Users)) + M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers)) + M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists)) + M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs)) } func sendUsageStats() { diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index cfe2d88c82c..0138b7f283d 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -68,6 +68,7 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error { } query.Result = &stats + return err } From a3eff6cf25ae3afe7ff0d85363cc599e6e6adf00 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 17 Apr 2018 10:27:12 +0200 Subject: [PATCH 0422/3000] changelog: notes about closing #11572 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81059a4d58f..951e80c3557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ * **Switch Orgs**: Alphabetic order in Switch Organization modal [#11556](https://github.com/grafana/grafana/issues/11556) * **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm) * **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747) +* **Dashboard**: sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572) ### Tech * Migrated JavaScript files to TypeScript From e21d5748145afb94075e664aba663a35597fd8d5 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 17 Apr 2018 10:28:16 +0200 Subject: [PATCH 0423/3000] changelog: fix typo [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 951e80c3557..45881e602bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ * **Switch Orgs**: Alphabetic order in Switch Organization modal [#11556](https://github.com/grafana/grafana/issues/11556) * **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm) * **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747) -* **Dashboard**: sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572) +* **Dashboard**: Sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572) ### Tech * Migrated JavaScript files to TypeScript From 2fc1558daf86d3c786398b4e4503fb0c677510d0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 17 Apr 2018 11:03:46 +0200 Subject: [PATCH 0424/3000] revert changes of add panel button to require save permission This resolves an issue where the add panel button was shown on the default home dashboard --- public/app/features/dashboard/dashnav/dashnav.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 0c3f949ed7c..269d4b0bada 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -17,7 +17,7 @@ - + (this.scrollbar = element)} className="add-panel__items">
    -
    Preview of values (shows max 20)
    +
    Preview of values
    -
    - {{option.text}} -
    +
    + {{option.text}} +
    +
    + Show more +
    From 3cca45dd885e8045b55a497b23afa59722c8b4ed Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 23 Apr 2018 13:59:52 +0200 Subject: [PATCH 0505/3000] bump version --- latest.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/latest.json b/latest.json index b476f44a00a..5a68ca428b4 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.0.0", - "testing": "5.0.0" + "stable": "5.0.4", + "testing": "5.0.4" } diff --git a/package.json b/package.json index ce861a25f7b..c1f2c3e86a9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.1.0-pre1", + "version": "5.2.0-pre1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From 28ce71253fe8eb2462e18ee949613a93f15c972f Mon Sep 17 00:00:00 2001 From: flopp999 <21694965+flopp999@users.noreply.github.com> Date: Mon, 23 Apr 2018 14:06:25 +0200 Subject: [PATCH 0506/3000] changed rps to reqps --- public/app/core/utils/kbn.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 1928891fd83..0909bd36f69 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -485,7 +485,7 @@ kbn.valueFormats.EHs = kbn.formatBuilders.decimalSIPrefix('H/s', 6); // Throughput kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); -kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps'); +kbn.valueFormats.reqps = kbn.formatBuilders.simpleCountUnit('reqps'); kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps'); kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps'); kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops'); @@ -949,7 +949,7 @@ kbn.getUnitFormats = function() { text: 'throughput', submenu: [ { text: 'ops/sec (ops)', value: 'ops' }, - { text: 'requets/sec (rps)', value: 'rps' }, + { text: 'requets/sec (rps)', value: 'reqps' }, { text: 'reads/sec (rps)', value: 'rps' }, { text: 'writes/sec (wps)', value: 'wps' }, { text: 'I/O ops/sec (iops)', value: 'iops' }, From d14ac54af665c825893cde3edb0c2f9f920d4986 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 23 Apr 2018 16:02:59 +0200 Subject: [PATCH 0507/3000] db: fix failing user auth tests for postgres --- pkg/services/sqlstore/user_auth_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/user_auth_test.go b/pkg/services/sqlstore/user_auth_test.go index 279fd7aa0f5..882e0c7afa5 100644 --- a/pkg/services/sqlstore/user_auth_test.go +++ b/pkg/services/sqlstore/user_auth_test.go @@ -32,7 +32,7 @@ func TestUserAuth(t *testing.T) { So(err, ShouldBeNil) _, err = x.Exec("DELETE FROM org WHERE 1=1") So(err, ShouldBeNil) - _, err = x.Exec("DELETE FROM user WHERE 1=1") + _, err = x.Exec("DELETE FROM " + dialect.Quote("user") + " WHERE 1=1") So(err, ShouldBeNil) _, err = x.Exec("DELETE FROM user_auth WHERE 1=1") So(err, ShouldBeNil) @@ -117,7 +117,7 @@ func TestUserAuth(t *testing.T) { So(query.Result.Login, ShouldEqual, "loginuser1") // remove user - _, err = x.Exec("DELETE FROM user WHERE id=?", query.Result.Id) + _, err = x.Exec("DELETE FROM "+dialect.Quote("user")+" WHERE id=?", query.Result.Id) So(err, ShouldBeNil) // get via user_auth for deleted user From 3a48ea8dde80975e781c6292a4af17f91204a8de Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 23 Apr 2018 16:20:17 +0200 Subject: [PATCH 0508/3000] Fixes signing of packages. Signing was failing as the builds were expected to run as ubuntu but is run as root. Closes #11686 --- scripts/build/rpmmacros | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/rpmmacros b/scripts/build/rpmmacros index a91ba9b8290..c00c8ec2eee 100644 --- a/scripts/build/rpmmacros +++ b/scripts/build/rpmmacros @@ -1,4 +1,4 @@ %_signature gpg -%_gpg_path /home/ubuntu/.gnupg +%_gpg_path /root/.gnupg %_gpg_name Grafana %_gpgbin /usr/bin/gpg From 3eaaa5d32d835f76bba93336710143b24895f7ba Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 23 Apr 2018 17:44:29 +0200 Subject: [PATCH 0509/3000] fixed so user who can edit dashboard can edit row, fixes #11466 --- public/app/features/dashboard/dashgrid/DashboardRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardRow.tsx b/public/app/features/dashboard/dashgrid/DashboardRow.tsx index c2a84cb7da9..a95130de1f2 100644 --- a/public/app/features/dashboard/dashgrid/DashboardRow.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardRow.tsx @@ -95,7 +95,7 @@ export class DashboardRow extends React.Component { {title} ({hiddenPanels} hidden panels)
    - {config.bootData.user.orgRole !== 'Viewer' && ( + {this.dashboard.meta.canEdit === true && (
    From 45e6d9fcc4e4d613b3e18b246d7ef7321207ada3 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 23 Apr 2018 17:45:51 +0200 Subject: [PATCH 0510/3000] removed import config --- public/app/features/dashboard/dashgrid/DashboardRow.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardRow.tsx b/public/app/features/dashboard/dashgrid/DashboardRow.tsx index a95130de1f2..b133d4450bb 100644 --- a/public/app/features/dashboard/dashgrid/DashboardRow.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardRow.tsx @@ -4,7 +4,6 @@ import { PanelModel } from '../panel_model'; import { PanelContainer } from './PanelContainer'; import templateSrv from 'app/features/templating/template_srv'; import appEvents from 'app/core/app_events'; -import config from 'app/core/config'; export interface DashboardRowProps { panel: PanelModel; From 6eb00000fe070435701c38f14023c88abfa25253 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 23 Apr 2018 19:28:54 +0200 Subject: [PATCH 0511/3000] pkg/services: fix ineffassign issues --- pkg/services/alerting/notifiers/telegram.go | 10 ++++++--- pkg/services/alerting/notifiers/victorops.go | 21 ------------------- pkg/services/guardian/guardian_test.go | 3 +++ .../provisioning/dashboards/file_reader.go | 1 - pkg/services/sqlstore/annotation_test.go | 1 + pkg/services/sqlstore/dashboard.go | 2 +- pkg/services/sqlstore/dashboard_acl_test.go | 1 + .../sqlstore/migrations/migrations_test.go | 4 ++-- pkg/services/sqlstore/playlist.go | 3 +++ pkg/services/sqlstore/plugin_setting.go | 3 +++ pkg/services/sqlstore/preferences.go | 3 +++ pkg/services/sqlstore/team_test.go | 3 ++- pkg/services/sqlstore/user.go | 8 ++----- 13 files changed, 28 insertions(+), 35 deletions(-) diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 0b8efe808d5..1e62c68d7eb 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -3,13 +3,14 @@ package notifiers import ( "bytes" "fmt" + "io" + "mime/multipart" + "os" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" - "io" - "mime/multipart" - "os" ) const ( @@ -133,6 +134,9 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval } ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + return nil, err + } metrics := generateMetricsMessage(evalContext) message := generateImageCaption(evalContext, ruleUrl, metrics) diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index 4b4db553cde..a753ca3cbf6 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -83,27 +83,6 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { return nil } - fields := make([]map[string]interface{}, 0) - fieldLimitCount := 4 - for index, evt := range evalContext.EvalMatches { - fields = append(fields, map[string]interface{}{ - "title": evt.Metric, - "value": evt.Value, - "short": true, - }) - if index > fieldLimitCount { - break - } - } - - if evalContext.Error != nil { - fields = append(fields, map[string]interface{}{ - "title": "Error message", - "value": evalContext.Error.Error(), - "short": false, - }) - } - messageType := evalContext.Rule.State if evalContext.Rule.State == models.AlertStateAlerting { // translate 'Alerting' to 'CRITICAL' (Victorops analog) messageType = AlertStateCritical diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index abf92ae0555..5e56b1d88c3 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -649,6 +649,9 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShou } _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList) + if err != nil { + sc.reportFailure(tc, nil, err) + } sc.updatePermissions = permissionList ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index de0a49d34d9..7d4231deeae 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -235,7 +235,6 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardPr func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) { checkFilepath, err := filepath.EvalSymlinks(path) if path != checkFilepath { - path = checkFilepath fi, err := os.Lstat(checkFilepath) if err != nil { return nil, err diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go index 5af5f271993..949ed8135ba 100644 --- a/pkg/services/sqlstore/annotation_test.go +++ b/pkg/services/sqlstore/annotation_test.go @@ -256,6 +256,7 @@ func TestAnnotations(t *testing.T) { annotationId := items[0].Id err = repo.Delete(&annotations.DeleteParams{Id: annotationId}) + So(err, ShouldBeNil) items, err = repo.Find(query) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index c0848f08863..4238967417f 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -77,7 +77,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { } parentVersion := dash.Version - affectedRows := int64(0) + var affectedRows int64 var err error if dash.Id == 0 { diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index c68ffd08cd7..a034a0565a3 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -154,6 +154,7 @@ func TestDashboardAclDataAccess(t *testing.T) { DashboardId: savedFolder.Id, Permission: m.PERMISSION_EDIT, }) + So(err, ShouldBeNil) q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1} err = GetDashboardAclInfoList(q1) diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go index 8e8e4af824f..53b398124af 100644 --- a/pkg/services/sqlstore/migrations/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -27,7 +27,7 @@ func TestMigrations(t *testing.T) { sqlutil.CleanDB(x) - has, err := x.SQL(sql).Get(&r) + _, err = x.SQL(sql).Get(&r) So(err, ShouldNotBeNil) mg := NewMigrator(x) @@ -36,7 +36,7 @@ func TestMigrations(t *testing.T) { err = mg.Start() So(err, ShouldBeNil) - has, err = x.SQL(sql).Get(&r) + has, err := x.SQL(sql).Get(&r) So(err, ShouldBeNil) So(has, ShouldBeTrue) expectedMigrations := mg.MigrationsCount() - 2 //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this diff --git a/pkg/services/sqlstore/playlist.go b/pkg/services/sqlstore/playlist.go index 67720cbadb8..7b726880b9e 100644 --- a/pkg/services/sqlstore/playlist.go +++ b/pkg/services/sqlstore/playlist.go @@ -22,6 +22,9 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error { } _, err := x.Insert(&playlist) + if err != nil { + return err + } playlistItems := make([]m.PlaylistItem, 0) for _, item := range cmd.Items { diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index 312a3bda523..f694fbbd5f0 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -48,6 +48,9 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error { var pluginSetting m.PluginSetting exists, err := sess.Where("org_id=? and plugin_id=?", cmd.OrgId, cmd.PluginId).Get(&pluginSetting) + if err != nil { + return err + } sess.UseBool("enabled") sess.UseBool("pinned") if !exists { diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 399b23f3ffa..a070fa621b5 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -72,6 +72,9 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { var prefs m.Preferences exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs) + if err != nil { + return err + } if !exists { prefs = m.Preferences{ diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index f136411eeba..f4b022906da 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -74,6 +74,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { Convey("Should be able to return all teams a user is member of", func() { groupId := group2.Result.Id err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]}) + So(err, ShouldBeNil) query := &m.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]} err = GetTeamsByUser(query) @@ -103,7 +104,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]}) So(err, ShouldBeNil) err = testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId}) - + So(err, ShouldBeNil) err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId}) So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 1546fb83b21..5e2efbd7fde 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -168,11 +168,9 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error { return m.ErrUserNotFound } - user := new(m.User) - // Try and find the user by login first. // It's not sufficient to assume that a LoginOrEmail with an "@" is an email. - user = &m.User{Login: query.LoginOrEmail} + user := &m.User{Login: query.LoginOrEmail} has, err := x.Get(user) if err != nil { @@ -202,9 +200,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error { return m.ErrUserNotFound } - user := new(m.User) - - user = &m.User{Email: query.Email} + user := &m.User{Email: query.Email} has, err := x.Get(user) if err != nil { From bc570bb140b9c85ca7748b5e11a4d12ddf746e51 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 23 Apr 2018 19:31:23 +0200 Subject: [PATCH 0512/3000] pkg/log: fix ineffassign issues --- pkg/log/file_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/log/file_test.go b/pkg/log/file_test.go index 3e98e0786cc..97a3b8fe82f 100644 --- a/pkg/log/file_test.go +++ b/pkg/log/file_test.go @@ -32,7 +32,9 @@ func TestLogFile(t *testing.T) { Convey("Logging should add lines", func() { err := fileLogWrite.WriteLine("test1\n") + So(err, ShouldBeNil) err = fileLogWrite.WriteLine("test2\n") + So(err, ShouldBeNil) err = fileLogWrite.WriteLine("test3\n") So(err, ShouldBeNil) So(fileLogWrite.maxlines_curlines, ShouldEqual, 3) From 15f11effa0997df18af9305d4ecce20ebd871d96 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 23 Apr 2018 19:34:55 +0200 Subject: [PATCH 0513/3000] pkg/cmd: fix ineffassign issues --- pkg/cmd/grafana-server/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 777176f27da..da99bc9ba40 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -100,9 +100,9 @@ func main() { } func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) { + var code int signalChan := make(chan os.Signal, 1) ignoreChan := make(chan os.Signal, 1) - code := 0 signal.Notify(ignoreChan, syscall.SIGHUP) signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM) From b02a860e66c439c7b717d8be7b4650a8e9eb5891 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 23 Apr 2018 20:03:57 +0200 Subject: [PATCH 0514/3000] pkg/components: fix ineffassign issues --- pkg/components/dashdiffs/compare.go | 4 ++++ pkg/components/dynmap/dynmap_test.go | 4 ++++ .../imguploader/azureblobuploader_test.go | 1 + pkg/components/imguploader/imguploader_test.go | 15 +++++++++------ pkg/components/imguploader/webdavuploader.go | 8 +++++++- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/pkg/components/dashdiffs/compare.go b/pkg/components/dashdiffs/compare.go index d51011fbead..ae940091ed1 100644 --- a/pkg/components/dashdiffs/compare.go +++ b/pkg/components/dashdiffs/compare.go @@ -141,5 +141,9 @@ func getDiff(baseData, newData *simplejson.Json) (interface{}, diff.Diff, error) left := make(map[string]interface{}) err = json.Unmarshal(leftBytes, &left) + if err != nil { + return nil, nil, err + } + return left, jsonDiff, nil } diff --git a/pkg/components/dynmap/dynmap_test.go b/pkg/components/dynmap/dynmap_test.go index fa5f73c3719..62d356bd67d 100644 --- a/pkg/components/dynmap/dynmap_test.go +++ b/pkg/components/dynmap/dynmap_test.go @@ -60,6 +60,7 @@ func TestFirst(t *testing.T) { }` j, err := NewObjectFromBytes([]byte(testJSON)) + assert.True(err == nil, "failed to create new object from bytes") a, err := j.GetObject("address") assert.True(a != nil && err == nil, "failed to create json from string") @@ -108,6 +109,7 @@ func TestFirst(t *testing.T) { //log.Println("address: ", address) s, err = address.GetString("street") + assert.True(s == "Street 42" && err == nil, "street mismatching") addressAsString, err := j.GetString("address") assert.True(addressAsString == "" && err != nil, "address should not be an string") @@ -148,6 +150,7 @@ func TestFirst(t *testing.T) { //assert.True(element.IsObject() == true, "first fail") element, err := elementValue.Object() + assert.True(err == nil, "create element fail") s, err = element.GetString("street") assert.True(s == "Street 42" && err == nil, "second fail") @@ -232,6 +235,7 @@ func TestSecond(t *testing.T) { assert.True(fromName == "Tom Brady" && err == nil, "fromName mismatch") actions, err := dataItem.GetObjectArray("actions") + assert.True(err == nil, "get object from array failed") for index, action := range actions { diff --git a/pkg/components/imguploader/azureblobuploader_test.go b/pkg/components/imguploader/azureblobuploader_test.go index 570e105b321..ca978f70e3d 100644 --- a/pkg/components/imguploader/azureblobuploader_test.go +++ b/pkg/components/imguploader/azureblobuploader_test.go @@ -13,6 +13,7 @@ func TestUploadToAzureBlob(t *testing.T) { err := setting.NewConfigContext(&setting.CommandLineArgs{ HomePath: "../../../", }) + So(err, ShouldBeNil) uploader, _ := NewImageUploader() diff --git a/pkg/components/imguploader/imguploader_test.go b/pkg/components/imguploader/imguploader_test.go index b0311dac975..b272a45e7a5 100644 --- a/pkg/components/imguploader/imguploader_test.go +++ b/pkg/components/imguploader/imguploader_test.go @@ -19,6 +19,7 @@ func TestImageUploaderFactory(t *testing.T) { Convey("with bucket url https://foo.bar.baz.s3-us-east-2.amazonaws.com", func() { s3sec, err := setting.Cfg.GetSection("external_image_storage.s3") + So(err, ShouldBeNil) s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com") s3sec.NewKey("access_key", "access_key") s3sec.NewKey("secret_key", "secret_key") @@ -37,6 +38,7 @@ func TestImageUploaderFactory(t *testing.T) { Convey("with bucket url https://s3.amazonaws.com/mybucket", func() { s3sec, err := setting.Cfg.GetSection("external_image_storage.s3") + So(err, ShouldBeNil) s3sec.NewKey("bucket_url", "https://s3.amazonaws.com/my.bucket.com") s3sec.NewKey("access_key", "access_key") s3sec.NewKey("secret_key", "secret_key") @@ -55,15 +57,15 @@ func TestImageUploaderFactory(t *testing.T) { Convey("with bucket url https://s3-us-west-2.amazonaws.com/mybucket", func() { s3sec, err := setting.Cfg.GetSection("external_image_storage.s3") + So(err, ShouldBeNil) s3sec.NewKey("bucket_url", "https://s3-us-west-2.amazonaws.com/my.bucket.com") s3sec.NewKey("access_key", "access_key") s3sec.NewKey("secret_key", "secret_key") uploader, err := NewImageUploader() - So(err, ShouldBeNil) - original, ok := uploader.(*S3Uploader) + original, ok := uploader.(*S3Uploader) So(ok, ShouldBeTrue) So(original.region, ShouldEqual, "us-west-2") So(original.bucket, ShouldEqual, "my.bucket.com") @@ -82,6 +84,7 @@ func TestImageUploaderFactory(t *testing.T) { setting.ImageUploadProvider = "webdav" webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav") + So(err, ShouldBeNil) webdavSec.NewKey("url", "webdavUrl") webdavSec.NewKey("username", "username") webdavSec.NewKey("password", "password") @@ -107,14 +110,14 @@ func TestImageUploaderFactory(t *testing.T) { setting.ImageUploadProvider = "gcs" gcpSec, err := setting.Cfg.GetSection("external_image_storage.gcs") + So(err, ShouldBeNil) gcpSec.NewKey("key_file", "/etc/secrets/project-79a52befa3f6.json") gcpSec.NewKey("bucket", "project-grafana-east") uploader, err := NewImageUploader() - So(err, ShouldBeNil) - original, ok := uploader.(*GCSUploader) + original, ok := uploader.(*GCSUploader) So(ok, ShouldBeTrue) So(original.keyFile, ShouldEqual, "/etc/secrets/project-79a52befa3f6.json") So(original.bucket, ShouldEqual, "project-grafana-east") @@ -128,15 +131,15 @@ func TestImageUploaderFactory(t *testing.T) { Convey("with container name", func() { azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob") + So(err, ShouldBeNil) azureBlobSec.NewKey("account_name", "account_name") azureBlobSec.NewKey("account_key", "account_key") azureBlobSec.NewKey("container_name", "container_name") uploader, err := NewImageUploader() - So(err, ShouldBeNil) - original, ok := uploader.(*AzureBlobUploader) + original, ok := uploader.(*AzureBlobUploader) So(ok, ShouldBeTrue) So(original.account_name, ShouldEqual, "account_name") So(original.account_key, ShouldEqual, "account_key") diff --git a/pkg/components/imguploader/webdavuploader.go b/pkg/components/imguploader/webdavuploader.go index 53d75247c76..f5478ea8a2f 100644 --- a/pkg/components/imguploader/webdavuploader.go +++ b/pkg/components/imguploader/webdavuploader.go @@ -41,14 +41,20 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) url.Path = path.Join(url.Path, filename) imgData, err := ioutil.ReadFile(pa) + if err != nil { + return "", err + } + req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(imgData)) + if err != nil { + return "", err + } if u.username != "" { req.SetBasicAuth(u.username, u.password) } res, err := netClient.Do(req) - if err != nil { return "", err } From 2e927a1053320a15ba6abbe939f9189b227cc507 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 23 Apr 2018 20:07:31 +0200 Subject: [PATCH 0515/3000] add ineffassign to circleci gometalinter check --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 30146576e63..3e95583ecae 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,12 +27,13 @@ jobs: steps: - checkout - run: 'go get -u gopkg.in/alecthomas/gometalinter.v2' + - run: 'go get -u github.com/gordonklaus/ineffassign' - run: 'go get -u github.com/opennota/check/cmd/structcheck' - run: 'go get -u github.com/mdempsky/unconvert' - run: 'go get -u github.com/opennota/check/cmd/varcheck' - run: name: run linters - command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=structcheck --enable=unconvert --enable=varcheck ./...' + command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' test-frontend: docker: From 1446f5444787a4e2250cc596bd5c59ed232a1eef Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 24 Apr 2018 09:45:53 +0200 Subject: [PATCH 0516/3000] fixed test --- .../app/features/dashboard/specs/DashboardRow.jest.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/specs/DashboardRow.jest.tsx b/public/app/features/dashboard/specs/DashboardRow.jest.tsx index c0ac172aa26..fa1e6acead2 100644 --- a/public/app/features/dashboard/specs/DashboardRow.jest.tsx +++ b/public/app/features/dashboard/specs/DashboardRow.jest.tsx @@ -2,19 +2,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { DashboardRow } from '../dashgrid/DashboardRow'; import { PanelModel } from '../panel_model'; -import config from '../../../core/config'; describe('DashboardRow', () => { let wrapper, panel, getPanelContainer, dashboardMock; beforeEach(() => { dashboardMock = { toggleRow: jest.fn() }; - - config.bootData = { - user: { - orgRole: 'Admin', - }, - }; + dashboardMock.meta = { canEdit: true }; getPanelContainer = jest.fn().mockReturnValue({ getDashboard: jest.fn().mockReturnValue(dashboardMock), @@ -42,7 +36,7 @@ describe('DashboardRow', () => { }); it('should have zero actions as viewer', () => { - config.bootData.user.orgRole = 'Viewer'; + dashboardMock.meta.canEdit = false; panel = new PanelModel({ collapsed: false }); wrapper = shallow(); expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0); From 38a4a2dc60581b0b74e703ef6650a08ae54fc92c Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 24 Apr 2018 11:22:58 +0200 Subject: [PATCH 0517/3000] changed test name and dashboardMock code --- .../app/features/dashboard/specs/DashboardRow.jest.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/specs/DashboardRow.jest.tsx b/public/app/features/dashboard/specs/DashboardRow.jest.tsx index fa1e6acead2..8424346b0c5 100644 --- a/public/app/features/dashboard/specs/DashboardRow.jest.tsx +++ b/public/app/features/dashboard/specs/DashboardRow.jest.tsx @@ -7,8 +7,12 @@ describe('DashboardRow', () => { let wrapper, panel, getPanelContainer, dashboardMock; beforeEach(() => { - dashboardMock = { toggleRow: jest.fn() }; - dashboardMock.meta = { canEdit: true }; + dashboardMock = { + toggleRow: jest.fn(), + meta: { + canEdit: true, + }, + }; getPanelContainer = jest.fn().mockReturnValue({ getDashboard: jest.fn().mockReturnValue(dashboardMock), @@ -35,7 +39,7 @@ describe('DashboardRow', () => { expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(2); }); - it('should have zero actions as viewer', () => { + it('should have zero actions when cannot edit', () => { dashboardMock.meta.canEdit = false; panel = new PanelModel({ collapsed: false }); wrapper = shallow(); From 0695e431ea610d4ab8219273039714a752f0e832 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 24 Apr 2018 12:07:24 +0200 Subject: [PATCH 0518/3000] Move function calls w/ side-effects to componentDidMount * loadStore() modified the url which triggered a new render path, this gets noticed by react. Moved to componentDidMount. --- public/app/containers/ManageDashboards/FolderPermissions.tsx | 3 +++ public/app/containers/ManageDashboards/FolderSettings.tsx | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/containers/ManageDashboards/FolderPermissions.tsx index ac9dd81216b..abbde63a179 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/containers/ManageDashboards/FolderPermissions.tsx @@ -16,6 +16,9 @@ export class FolderPermissions extends Component { constructor(props) { super(props); this.handleAddPermission = this.handleAddPermission.bind(this); + } + + componentDidMount() { this.loadStore(); } diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx index f63b1a74d86..d25a7dc6c06 100644 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ b/public/app/containers/ManageDashboards/FolderSettings.tsx @@ -12,8 +12,7 @@ import appEvents from 'app/core/app_events'; export class FolderSettings extends React.Component { formSnapshot: any; - constructor(props) { - super(props); + componentDidMount() { this.loadStore(); } From 006286ac05b7b74434bafaa0b4ecbd51e33bb0ac Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 24 Apr 2018 12:27:37 +0200 Subject: [PATCH 0519/3000] Renamed helperRequest and removed positional args From review feedback: * s/helper/metadata * combined positional args to _request into options dict * metadataRequest reuses _request() * moved consumption of this.httpMethod into _request, can be overwritten in options due to spread-after --- .../datasource/prometheus/datasource.ts | 30 +++++++------------ .../prometheus/metric_find_query.ts | 8 ++--- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 3eceaf6c622..3a2c78dce2d 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -5,6 +5,7 @@ import kbn from 'app/core/utils/kbn'; import * as dateMath from 'app/core/utils/datemath'; import PrometheusMetricFindQuery from './metric_find_query'; import { ResultTransformer } from './result_transformer'; +import { BackendSrv } from 'app/core/services/backend_srv'; export function prometheusRegularEscape(value) { return value.replace(/'/g, "\\\\'"); @@ -29,7 +30,7 @@ export class PrometheusDatasource { resultTransformer: ResultTransformer; /** @ngInject */ - constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) { + constructor(instanceSettings, private $q, private backendSrv: BackendSrv, private templateSrv, private timeSrv) { this.type = 'prometheus'; this.editorSrc = 'app/features/prometheus/partials/query.editor.html'; this.name = instanceSettings.name; @@ -43,13 +44,13 @@ export class PrometheusDatasource { this.resultTransformer = new ResultTransformer(templateSrv); } - _request(method, url, data?, requestId?) { + _request(url, data?, options?: any) { var options: any = { url: this.url + url, - method: method, - requestId: requestId, + method: this.httpMethod, + ...options, }; - if (method === 'GET') { + if (options.method === 'GET') { if (!_.isEmpty(data)) { options.url = options.url + @@ -82,17 +83,8 @@ export class PrometheusDatasource { } // Use this for tab completion features, wont publish response to other components - helperRequest(url) { - const options: any = { - url: this.url + url, - silent: true, - }; - - if (this.basicAuth || this.withCredentials) { - options.withCredentials = true; - } - - return this.backendSrv.datasourceRequest(options); + metadataRequest(url) { + return this._request(url, null, { silent: true }); } interpolateQueryExpr(value, variable, defaultFormatFn) { @@ -220,7 +212,7 @@ export class PrometheusDatasource { end: end, step: query.step, }; - return this._request(this.httpMethod, url, data, query.requestId); + return this._request(url, data, { requestId: query.requestId }); } performInstantQuery(query, time) { @@ -229,7 +221,7 @@ export class PrometheusDatasource { query: query.expr, time: time, }; - return this._request(this.httpMethod, url, data, query.requestId); + return this._request(url, data, { requestId: query.requestId }); } performSuggestQuery(query, cache = false) { @@ -243,7 +235,7 @@ export class PrometheusDatasource { ); } - return this.helperRequest(url).then(result => { + return this.metadataRequest(url).then(result => { this.metricsNameCache = { data: result.data.data, expire: Date.now() + 60 * 1000, diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index c58f5c097b9..337cd74c14c 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -46,7 +46,7 @@ export default class PrometheusMetricFindQuery { // return label values globally url = '/api/v1/label/' + label + '/values'; - return this.datasource.helperRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(function(result) { return _.map(result.data.data, function(value) { return { text: value }; }); @@ -56,7 +56,7 @@ export default class PrometheusMetricFindQuery { var end = this.datasource.getPrometheusTime(this.range.to, true); url = '/api/v1/series?match[]=' + encodeURIComponent(metric) + '&start=' + start + '&end=' + end; - return this.datasource.helperRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(function(result) { var _labels = _.map(result.data.data, function(metric) { return metric[label] || ''; }).filter(function(label) { @@ -76,7 +76,7 @@ export default class PrometheusMetricFindQuery { metricNameQuery(metricFilterPattern) { var url = '/api/v1/label/__name__/values'; - return this.datasource.helperRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(function(result) { return _.chain(result.data.data) .filter(function(metricName) { var r = new RegExp(metricFilterPattern); @@ -120,7 +120,7 @@ export default class PrometheusMetricFindQuery { var url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; var self = this; - return this.datasource.helperRequest(url).then(function(result) { + return this.datasource.metadataRequest(url).then(function(result) { return _.map(result.data.data, function(metric) { return { text: self.datasource.getOriginalMetricName(metric), From 707700ac7dd1d938f281c110113d274907417692 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 24 Apr 2018 16:26:46 +0200 Subject: [PATCH 0520/3000] force GET for metadataRequests, w/ test --- .../datasource/prometheus/datasource.ts | 2 +- .../prometheus/specs/datasource.jest.ts | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 3a2c78dce2d..cbf701a0abe 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -84,7 +84,7 @@ export class PrometheusDatasource { // Use this for tab completion features, wont publish response to other components metadataRequest(url) { - return this._request(url, null, { silent: true }); + return this._request(url, null, { method: 'GET', silent: true }); } interpolateQueryExpr(value, variable, defaultFormatFn) { diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index d2620b93bbc..a997a2d8233 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -14,6 +14,7 @@ describe('PrometheusDatasource', () => { }; ctx.backendSrvMock = {}; + ctx.templateSrvMock = { replace: a => a, }; @@ -23,6 +24,25 @@ describe('PrometheusDatasource', () => { ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); }); + describe('Datasource metadata requests', () => { + it('should perform a GET request with the default config', () => { + ctx.backendSrvMock.datasourceRequest = jest.fn(); + ctx.ds.metadataRequest('/foo'); + expect(ctx.backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); + expect(ctx.backendSrvMock.datasourceRequest.mock.calls[0][0].method).toBe('GET'); + }); + + it('should still perform a GET request with the DS HTTP method set to POST', () => { + ctx.backendSrvMock.datasourceRequest = jest.fn(); + const postSettings = _.cloneDeep(instanceSettings); + postSettings.jsonData.httpMethod = 'POST'; + const ds = new PrometheusDatasource(postSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); + ds.metadataRequest('/foo'); + expect(ctx.backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); + expect(ctx.backendSrvMock.datasourceRequest.mock.calls[0][0].method).toBe('GET'); + }); + }); + describe('When converting prometheus histogram to heatmap format', () => { beforeEach(() => { ctx.query = { From 79928974186840ebcc89b17f5b545e52f8d6edd8 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 16:30:24 +0200 Subject: [PATCH 0521/3000] docs: add known issues section for mssql documentation Fixes #11707 --- docs/sources/features/datasources/mssql.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index 9a0f53093ec..1676cffa0a8 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -49,6 +49,11 @@ Example: Make sure the user does not get any unwanted privileges from the public role. +### Known Issues + +MSSQL 2008 and 2008 R2 engine cannot handle login records when SSL encryption is not disabled. Due to this you may receive an `Login error: EOF` error when trying to create your datasource. +To fix MSSQL 2008 R2 issue, install MSSQL 2008 R2 Service Pack 2. To fix MSSQL 2008 issue, install Microsoft MSSQL 2008 Service Pack 3 and Cumulative update package 3 for MSSQL 2008 SP3. + ## Query Editor {{< docs-imagebox img="/img/docs/v51/mssql_query_editor.png" class="docs-image--no-shadow" >}} From a40314022b0db1110d6d38d8919f2526dd915ca8 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 24 Apr 2018 17:40:03 +0200 Subject: [PATCH 0522/3000] added pointer to show more, reset values on new query --- public/app/features/templating/editor_ctrl.ts | 1 + public/app/features/templating/partials/editor.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts index 2533ac03739..75a84cca2bf 100644 --- a/public/app/features/templating/editor_ctrl.ts +++ b/public/app/features/templating/editor_ctrl.ts @@ -97,6 +97,7 @@ export class VariableEditorCtrl { }; $scope.runQuery = function() { + $scope.optionsLimit = 20; return variableSrv.updateOptions($scope.current).catch(err => { if (err.data && err.data.message) { err.message = err.data.message; diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 74cb2f23e84..0d8b0ace327 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -286,7 +286,7 @@ {{option.text}}
    From 76bd2aea44da99550ca3713442fc65dfe7a9d135 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 19:50:14 +0200 Subject: [PATCH 0523/3000] sql datasource: extract common logic for converting value column to float --- pkg/tsdb/sql_engine.go | 109 ++++++++++++++++++++++++++++++++++++ pkg/tsdb/sql_engine_test.go | 101 ++++++++++++++++++++++++++++++++- 2 files changed, 207 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 56ed2cd3cb6..274e5b05dc1 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -2,9 +2,12 @@ package tsdb import ( "context" + "fmt" "sync" "time" + "github.com/grafana/grafana/pkg/components/null" + "github.com/go-xorm/core" "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/components/simplejson" @@ -185,3 +188,109 @@ func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) { } } } + +// ConvertSqlValueColumnToFloat converts timeseries value column to float. +func ConvertSqlValueColumnToFloat(columnName string, columnValue interface{}) (null.Float, error) { + var value null.Float + + switch typedValue := columnValue.(type) { + case int: + value = null.FloatFrom(float64(typedValue)) + case *int: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case int64: + value = null.FloatFrom(float64(typedValue)) + case *int64: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case int32: + value = null.FloatFrom(float64(typedValue)) + case *int32: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case int16: + value = null.FloatFrom(float64(typedValue)) + case *int16: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case int8: + value = null.FloatFrom(float64(typedValue)) + case *int8: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint: + value = null.FloatFrom(float64(typedValue)) + case *uint: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint64: + value = null.FloatFrom(float64(typedValue)) + case *uint64: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint32: + value = null.FloatFrom(float64(typedValue)) + case *uint32: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint16: + value = null.FloatFrom(float64(typedValue)) + case *uint16: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case uint8: + value = null.FloatFrom(float64(typedValue)) + case *uint8: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case float64: + value = null.FloatFrom(typedValue) + case *float64: + value = null.FloatFromPtr(typedValue) + case float32: + value = null.FloatFrom(float64(typedValue)) + case *float32: + if typedValue == nil { + value.Valid = false + } else { + value = null.FloatFrom(float64(*typedValue)) + } + case nil: + value.Valid = false + default: + return null.NewFloat(0, false), fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", columnName, typedValue, typedValue) + } + + return value, nil +} diff --git a/pkg/tsdb/sql_engine_test.go b/pkg/tsdb/sql_engine_test.go index 4c6951a0196..ce1fb45de21 100644 --- a/pkg/tsdb/sql_engine_test.go +++ b/pkg/tsdb/sql_engine_test.go @@ -1,10 +1,11 @@ package tsdb import ( - "fmt" "testing" "time" + "github.com/grafana/grafana/pkg/components/null" + . "github.com/smartystreets/goconvey/convey" ) @@ -156,8 +157,6 @@ func TestSqlEngine(t *testing.T) { So(fixtures[1].(float64), ShouldEqual, tMilliseconds) So(fixtures[2].(float64), ShouldEqual, tMilliseconds) So(fixtures[3].(float64), ShouldEqual, tMilliseconds) - fmt.Println(fixtures[4].(float64)) - fmt.Println(tMilliseconds) So(fixtures[4].(float64), ShouldEqual, tMilliseconds) So(fixtures[5].(float64), ShouldEqual, tMilliseconds) So(fixtures[6], ShouldBeNil) @@ -183,5 +182,101 @@ func TestSqlEngine(t *testing.T) { So(fixtures[2], ShouldBeNil) }) }) + + Convey("Given row with value columns", func() { + intValue := 1 + int64Value := int64(1) + int32Value := int32(1) + int16Value := int16(1) + int8Value := int8(1) + float64Value := float64(1) + float32Value := float32(1) + uintValue := uint(1) + uint64Value := uint64(1) + uint32Value := uint32(1) + uint16Value := uint16(1) + uint8Value := uint8(1) + + fixtures := make([]interface{}, 24) + fixtures[0] = intValue + fixtures[1] = &intValue + fixtures[2] = int64Value + fixtures[3] = &int64Value + fixtures[4] = int32Value + fixtures[5] = &int32Value + fixtures[6] = int16Value + fixtures[7] = &int16Value + fixtures[8] = int8Value + fixtures[9] = &int8Value + fixtures[10] = float64Value + fixtures[11] = &float64Value + fixtures[12] = float32Value + fixtures[13] = &float32Value + fixtures[14] = uintValue + fixtures[15] = &uintValue + fixtures[16] = uint64Value + fixtures[17] = &uint64Value + fixtures[18] = uint32Value + fixtures[19] = &uint32Value + fixtures[20] = uint16Value + fixtures[21] = &uint16Value + fixtures[22] = uint8Value + fixtures[23] = &uint8Value + + var intNilPointer *int + var int64NilPointer *int64 + var int32NilPointer *int32 + var int16NilPointer *int16 + var int8NilPointer *int8 + var float64NilPointer *float64 + var float32NilPointer *float32 + var uintNilPointer *uint + var uint64NilPointer *uint64 + var uint32NilPointer *uint32 + var uint16NilPointer *uint16 + var uint8NilPointer *uint8 + + nilPointerFixtures := make([]interface{}, 12) + nilPointerFixtures[0] = intNilPointer + nilPointerFixtures[1] = int64NilPointer + nilPointerFixtures[2] = int32NilPointer + nilPointerFixtures[3] = int16NilPointer + nilPointerFixtures[4] = int8NilPointer + nilPointerFixtures[5] = float64NilPointer + nilPointerFixtures[6] = float32NilPointer + nilPointerFixtures[7] = uintNilPointer + nilPointerFixtures[8] = uint64NilPointer + nilPointerFixtures[9] = uint32NilPointer + nilPointerFixtures[10] = uint16NilPointer + nilPointerFixtures[11] = uint8NilPointer + + Convey("When converting values to float should return expected value", func() { + for _, f := range fixtures { + value, _ := ConvertSqlValueColumnToFloat("col", f) + + if !value.Valid { + t.Fatalf("Failed to convert %T value, expected a valid float value", f) + } + + if value.Float64 != null.FloatFrom(1).Float64 { + t.Fatalf("Failed to convert %T value, expected a float value of 1.000, but got %v", f, value) + } + } + }) + + Convey("When converting nil pointer values to float should return expected value", func() { + for _, f := range nilPointerFixtures { + value, err := ConvertSqlValueColumnToFloat("col", f) + + if err != nil { + t.Fatalf("Failed to convert %T value, expected a non nil error, but got %v", f, err) + } + + if value.Valid { + t.Fatalf("Failed to convert %T value, expected an invalid float value", f) + } + } + }) + }) }) } From 346577b664acdbbc219666437dbd3b3ecf312671 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 19:53:06 +0200 Subject: [PATCH 0524/3000] mysql: fix value columns conversion to float when using timeseries query --- pkg/tsdb/mysql/mysql.go | 12 +++--------- pkg/tsdb/mysql/mysql_test.go | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 4f5cd1b0784..7eceaffdb09 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -265,16 +265,10 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. continue } - switch columnValue := values[i].(type) { - case int64: - value = null.FloatFrom(float64(columnValue)) - case float64: - value = null.FloatFrom(columnValue) - case nil: - value.Valid = false - default: - return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { + return err } + if metricIndex == -1 { metric = col } diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 74cedea803a..29c5b72b408 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -420,12 +420,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeInt64 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeInt64 as time, timeInt64 FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -442,12 +442,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeInt64Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeInt64Nullable as time, timeInt64Nullable FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -464,12 +464,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeFloat64 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeFloat64 as time, timeFloat64 FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -486,12 +486,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeFloat64Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeFloat64Nullable as time, timeFloat64Nullable FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -508,12 +508,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() { + FocusConvey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeInt32 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeInt32 as time, timeInt32 FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -530,12 +530,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeInt32Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeInt32Nullable as time, timeInt32Nullable FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -552,12 +552,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeFloat32 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeFloat32 as time, timeFloat32 FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -574,12 +574,12 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3) }) - Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT timeFloat32Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT timeFloat32Nullable as time, timeFloat32Nullable FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", From cf43007531fb9593574c3ba6a8888b0af6a7a78c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 19:53:36 +0200 Subject: [PATCH 0525/3000] postgres: fix value columns conversion to float when using timeseries query --- pkg/tsdb/postgres/postgres.go | 12 +++-------- pkg/tsdb/postgres/postgres_test.go | 32 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 72d50b32d04..fdf09216e51 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -245,16 +245,10 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co continue } - switch columnValue := values[i].(type) { - case int64: - value = null.FloatFrom(float64(columnValue)) - case float64: - value = null.FloatFrom(columnValue) - case nil: - value.Valid = false - default: - return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { + return err } + if metricIndex == -1 { metric = col } diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index d18251bac7d..7f24d5a2063 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -353,12 +353,12 @@ func TestPostgres(t *testing.T) { _, err = sess.InsertMulti(series) So(err, ShouldBeNil) - Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeInt64" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeInt64" as time, "timeInt64" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -375,12 +375,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeInt64Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeInt64Nullable" as time, "timeInt64Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -397,12 +397,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeFloat64" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeFloat64" as time, "timeFloat64" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -419,12 +419,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeFloat64Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeFloat64Nullable" as time, "timeFloat64Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -441,12 +441,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeInt32" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeInt32" as time, "timeInt32" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -463,12 +463,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeInt32Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeInt32Nullable" as time, "timeInt32Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -485,12 +485,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeFloat32" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeFloat32" as time, "timeFloat32" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", @@ -507,12 +507,12 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3) }) - Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT "timeFloat32Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`, + "rawSql": `SELECT "timeFloat32Nullable" as time, "timeFloat32Nullable" FROM metric_values ORDER BY time LIMIT 1`, "format": "time_series", }), RefId: "A", From 1452634a2a5dc26b25deb958a46145d76c9a21a6 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 24 Apr 2018 19:54:08 +0200 Subject: [PATCH 0526/3000] mssql: fix value columns conversion to float when using timeseries query --- pkg/tsdb/mssql/mssql.go | 12 +++--------- pkg/tsdb/mssql/mssql_test.go | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index a598b7239ed..eb71259b46b 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -256,16 +256,10 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. continue } - switch columnValue := values[i].(type) { - case int64: - value = null.FloatFrom(float64(columnValue)) - case float64: - value = null.FloatFrom(columnValue) - case nil: - value.Valid = false - default: - return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue) + if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { + return err } + if metricIndex == -1 { metric = col } diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 599f4869f6a..167d02a1e07 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -374,12 +374,12 @@ func TestMSSQL(t *testing.T) { _, err = sess.InsertMulti(series) So(err, ShouldBeNil) - Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64) as time column and value column (int64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeInt64 as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeInt64 as time, timeInt64 FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -396,12 +396,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int64 nullable) as time column and value column (int64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeInt64Nullable as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeInt64Nullable as time, timeInt64Nullable FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -418,12 +418,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64) as time column and value column (float64) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeFloat64 as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeFloat64 as time, timeFloat64 FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -440,12 +440,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float64 nullable) as time column and value column (float64 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeFloat64Nullable as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeFloat64Nullable as time, timeFloat64Nullable FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -462,12 +462,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32) as time column and value column (int32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeInt32 as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeInt32 as time, timeInt32 FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -484,12 +484,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (int32 nullable) as time column and value column (int32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeInt32Nullable as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeInt32Nullable as time, timeInt32Nullable FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -506,12 +506,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6)) }) - Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32) as time column and value column (float32) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeFloat32 as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeFloat32 as time, timeFloat32 FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", @@ -528,12 +528,12 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3) }) - Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() { + Convey("When doing a metric query using epoch (float32 nullable) as time column and value column (float32 nullable) should return metric with time in milliseconds", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT TOP 1 timeFloat32Nullable as time, valueOne FROM metric_values ORDER BY time`, + "rawSql": `SELECT TOP 1 timeFloat32Nullable as time, timeFloat32Nullable FROM metric_values ORDER BY time`, "format": "time_series", }), RefId: "A", From 1290087b7869019a07478757dff36ad944fda8a1 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 25 Apr 2018 11:01:33 +0200 Subject: [PATCH 0527/3000] dev: Mac compatible prometheus block. (#11718) --- docker/blocks/prometheus_mac/Dockerfile | 3 ++ docker/blocks/prometheus_mac/alert.rules | 10 +++++ .../blocks/prometheus_mac/docker-compose.yaml | 26 +++++++++++++ docker/blocks/prometheus_mac/prometheus.yml | 39 +++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 docker/blocks/prometheus_mac/Dockerfile create mode 100644 docker/blocks/prometheus_mac/alert.rules create mode 100644 docker/blocks/prometheus_mac/docker-compose.yaml create mode 100644 docker/blocks/prometheus_mac/prometheus.yml diff --git a/docker/blocks/prometheus_mac/Dockerfile b/docker/blocks/prometheus_mac/Dockerfile new file mode 100644 index 00000000000..2098e6527d3 --- /dev/null +++ b/docker/blocks/prometheus_mac/Dockerfile @@ -0,0 +1,3 @@ +FROM prom/prometheus:v1.8.2 +ADD prometheus.yml /etc/prometheus/ +ADD alert.rules /etc/prometheus/ diff --git a/docker/blocks/prometheus_mac/alert.rules b/docker/blocks/prometheus_mac/alert.rules new file mode 100644 index 00000000000..563d1e89994 --- /dev/null +++ b/docker/blocks/prometheus_mac/alert.rules @@ -0,0 +1,10 @@ +# Alert Rules + +ALERT AppCrash + IF process_open_fds > 0 + FOR 15s + LABELS { severity="critical" } + ANNOTATIONS { + summary = "Number of open fds > 0", + description = "Just testing" + } diff --git a/docker/blocks/prometheus_mac/docker-compose.yaml b/docker/blocks/prometheus_mac/docker-compose.yaml new file mode 100644 index 00000000000..ef53b07418a --- /dev/null +++ b/docker/blocks/prometheus_mac/docker-compose.yaml @@ -0,0 +1,26 @@ + prometheus: + build: blocks/prometheus_mac + ports: + - "9090:9090" + + node_exporter: + image: prom/node-exporter + ports: + - "9100:9100" + + fake-prometheus-data: + image: grafana/fake-data-gen + ports: + - "9091:9091" + environment: + FD_DATASOURCE: prom + + alertmanager: + image: quay.io/prometheus/alertmanager + ports: + - "9093:9093" + + prometheus-random-data: + build: blocks/prometheus_random_data + ports: + - "8081:8080" diff --git a/docker/blocks/prometheus_mac/prometheus.yml b/docker/blocks/prometheus_mac/prometheus.yml new file mode 100644 index 00000000000..299447ffb25 --- /dev/null +++ b/docker/blocks/prometheus_mac/prometheus.yml @@ -0,0 +1,39 @@ +# my global config +global: + scrape_interval: 10s # By default, scrape targets every 15 seconds. + evaluation_interval: 10s # By default, scrape targets every 15 seconds. + # scrape_timeout is set to the global default (10s). + +# Load and evaluate rules in this file every 'evaluation_interval' seconds. +rule_files: + - "alert.rules" + # - "first.rules" + # - "second.rules" + +alerting: + alertmanagers: + - scheme: http + static_configs: + - targets: + - "alertmanager:9093" + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'node_exporter' + static_configs: + - targets: ['node_exporter:9100'] + + - job_name: 'fake-data-gen' + static_configs: + - targets: ['fake-prometheus-data:9091'] + + - job_name: 'grafana' + static_configs: + - targets: ['host.docker.internal:3000'] + + - job_name: 'prometheus-random-data' + static_configs: + - targets: ['prometheus-random-data:8080'] From 99aa9a46bcd67de0dc477df003331f7405f04dff Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 25 Apr 2018 12:16:43 +0200 Subject: [PATCH 0528/3000] replaced border hack carot with fontawesome carot fixes #11677 --- public/sass/components/_dropdown.scss | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss index cc94a379e07..37dbdcd89ef 100644 --- a/public/sass/components/_dropdown.scss +++ b/public/sass/components/_dropdown.scss @@ -256,17 +256,15 @@ // Caret to indicate there is a submenu .dropdown-submenu > a::after { - display: block; - content: ' '; - float: right; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - border-width: 5px 0 5px 5px; - border-left-color: $text-color-weak; - margin-top: 5px; - margin-right: -4px; + position: absolute; + top: 35%; + right: $input-padding-x; + background-color: transparent; + color: $text-color-weak; + font: normal normal normal $font-size-sm/1 FontAwesome; + content: '\f0da'; + pointer-events: none; + font-size: 11px; } .dropdown-submenu:hover > a::after { border-left-color: $dropdownLinkColorHover; From 6836268f3ebbb576bda961bfa1198db7092a6c16 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 25 Apr 2018 12:44:39 +0200 Subject: [PATCH 0529/3000] removed height 100% from panel-container to fix ie11 panel edit mode --- public/sass/pages/_dashboard.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 871db4dfc2d..cf32522df7f 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -33,7 +33,6 @@ div.flot-text { border: $panel-border; position: relative; border-radius: 3px; - height: 100%; &.panel-transparent { background-color: transparent; From 6687409efba0efb2e6b71625b9782370b19ff111 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 25 Apr 2018 15:36:00 +0200 Subject: [PATCH 0530/3000] prometheus: fix variable query to fallback correctly to series query Using a query of for example up or up{job=job1} --- public/app/plugins/datasource/prometheus/datasource.ts | 4 ++++ public/app/plugins/datasource/prometheus/metric_find_query.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index cbf701a0abe..2a8b3069a53 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -329,4 +329,8 @@ export class PrometheusDatasource { } return Math.ceil(date.valueOf() / 1000); } + + getOriginalMetricName(labelData) { + return this.resultTransformer.getOriginalMetricName(labelData); + } } diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index 337cd74c14c..13b6d7df8e3 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -121,7 +121,7 @@ export default class PrometheusMetricFindQuery { var self = this; return this.datasource.metadataRequest(url).then(function(result) { - return _.map(result.data.data, function(metric) { + return _.map(result.data.data, metric => { return { text: self.datasource.getOriginalMetricName(metric), expandable: true, From f112e38266a4cbb96c170fc213e2533d0c06c814 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 25 Apr 2018 15:36:47 +0200 Subject: [PATCH 0531/3000] prometheus: convert metric find query tests to jest --- .../prometheus/specs/datasource.jest.ts | 20 ++ .../specs/metric_find_query.jest.ts | 205 ++++++++++++++++++ .../specs/metric_find_query_specs.ts | 181 ---------------- 3 files changed, 225 insertions(+), 181 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts delete mode 100644 public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index a997a2d8233..2ab2895d731 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts @@ -43,6 +43,26 @@ describe('PrometheusDatasource', () => { }); }); + describe('When performing performSuggestQuery', () => { + it('should cache response', async () => { + ctx.backendSrvMock.datasourceRequest.mockReturnValue( + Promise.resolve({ + status: 'success', + data: { data: ['value1', 'value2', 'value3'] }, + }) + ); + + let results = await ctx.ds.performSuggestQuery('value', true); + + expect(results).toHaveLength(3); + + ctx.backendSrvMock.datasourceRequest.mockReset(); + results = await ctx.ds.performSuggestQuery('value', true); + + expect(results).toHaveLength(3); + }); + }); + describe('When converting prometheus histogram to heatmap format', () => { beforeEach(() => { ctx.query = { diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts new file mode 100644 index 00000000000..88f6830cd31 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query.jest.ts @@ -0,0 +1,205 @@ +import moment from 'moment'; +import { PrometheusDatasource } from '../datasource'; +import PrometheusMetricFindQuery from '../metric_find_query'; +import q from 'q'; + +describe('PrometheusMetricFindQuery', function() { + let instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'GET' }, + }; + const raw = { + from: moment.utc('2018-04-25 10:00'), + to: moment.utc('2018-04-25 11:00'), + }; + let ctx: any = { + backendSrvMock: { + datasourceRequest: jest.fn(() => Promise.resolve({})), + }, + templateSrvMock: { + replace: a => a, + }, + timeSrvMock: { + timeRange: () => ({ + from: raw.from, + to: raw.to, + raw: raw, + }), + }, + }; + + ctx.setupMetricFindQuery = (data: any) => { + ctx.backendSrvMock.datasourceRequest.mockReturnValue(Promise.resolve({ status: 'success', data: data.response })); + return new PrometheusMetricFindQuery(ctx.ds, data.query, ctx.timeSrvMock); + }; + + beforeEach(() => { + ctx.backendSrvMock.datasourceRequest.mockReset(); + ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); + }); + + describe('When performing metricFindQuery', () => { + it('label_values(resource) should generate label search query', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'label_values(resource)', + response: { + data: ['value1', 'value2', 'value3'], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: 'proxied/api/v1/label/resource/values', + silent: true, + }); + }); + + it('label_values(metric, resource) should generate series query with correct time', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'label_values(metric, resource)', + response: { + data: [ + { __name__: 'metric', resource: 'value1' }, + { __name__: 'metric', resource: 'value2' }, + { __name__: 'metric', resource: 'value3' }, + ], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/series?match[]=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`, + silent: true, + }); + }); + + it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query with correct time', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'label_values(metric{label1="foo", label2="bar", label3="baz"}, resource)', + response: { + data: [ + { __name__: 'metric', resource: 'value1' }, + { __name__: 'metric', resource: 'value2' }, + { __name__: 'metric', resource: 'value3' }, + ], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/series?match[]=${encodeURIComponent( + 'metric{label1="foo", label2="bar", label3="baz"}' + )}&start=${raw.from.unix()}&end=${raw.to.unix()}`, + silent: true, + }); + }); + + it('label_values(metric, resource) result should not contain empty string', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'label_values(metric, resource)', + response: { + data: [ + { __name__: 'metric', resource: 'value1' }, + { __name__: 'metric', resource: 'value2' }, + { __name__: 'metric', resource: '' }, + ], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(2); + expect(results[0].text).toBe('value1'); + expect(results[1].text).toBe('value2'); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/series?match[]=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`, + silent: true, + }); + }); + + it('metrics(metric.*) should generate metric name query', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'metrics(metric.*)', + response: { + data: ['metric1', 'metric2', 'metric3', 'nomatch'], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: 'proxied/api/v1/label/__name__/values', + silent: true, + }); + }); + + it('query_result(metric) should generate metric name query', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'query_result(metric)', + response: { + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'metric', job: 'testjob' }, + value: [1443454528.0, '3846'], + }, + ], + }, + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(1); + expect(results[0].text).toBe('metric{job="testjob"} 3846 1443454528000'); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/query?query=metric&time=${raw.to.unix()}`, + requestId: undefined, + }); + }); + + it('up{job="job1"} should fallback using generate series query', async () => { + const query = ctx.setupMetricFindQuery({ + query: 'up{job="job1"}', + response: { + data: [ + { __name__: 'up', instance: '127.0.0.1:1234', job: 'job1' }, + { __name__: 'up', instance: '127.0.0.1:5678', job: 'job1' }, + { __name__: 'up', instance: '127.0.0.1:9102', job: 'job1' }, + ], + }, + }); + const results = await query.process(); + + expect(results).toHaveLength(3); + expect(results[0].text).toBe('up{instance="127.0.0.1:1234",job="job1"}'); + expect(results[1].text).toBe('up{instance="127.0.0.1:5678",job="job1"}'); + expect(results[2].text).toBe('up{instance="127.0.0.1:9102",job="job1"}'); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1); + expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({ + method: 'GET', + url: `proxied/api/v1/series?match[]=${encodeURIComponent( + 'up{job="job1"}' + )}&start=${raw.from.unix()}&end=${raw.to.unix()}`, + silent: true, + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts deleted file mode 100644 index e5d7aa81210..00000000000 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; - -import moment from 'moment'; -import helpers from 'test/specs/helpers'; -import { PrometheusDatasource } from '../datasource'; -import PrometheusMetricFindQuery from '../metric_find_query'; - -describe('PrometheusMetricFindQuery', function() { - var ctx = new helpers.ServiceTestContext(); - var instanceSettings = { - url: 'proxied', - directUrl: 'direct', - user: 'test', - password: 'mupp', - jsonData: { httpMethod: 'GET' }, - }; - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) { - ctx.$q = $q; - ctx.$httpBackend = $httpBackend; - ctx.$rootScope = $rootScope; - ctx.ds = $injector.instantiate(PrometheusDatasource, { - instanceSettings: instanceSettings, - }); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); - - describe('When performing metricFindQuery', function() { - var results; - var response; - it('label_values(resource) should generate label search query', function() { - response = { - status: 'success', - data: ['value1', 'value2', 'value3'], - }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - }); - it('label_values(metric, resource) should generate series query', function() { - response = { - status: 'success', - data: [ - { __name__: 'metric', resource: 'value1' }, - { __name__: 'metric', resource: 'value2' }, - { __name__: 'metric', resource: 'value3' }, - ], - }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - }); - it('label_values(metric, resource) should pass correct time', function() { - ctx.timeSrv.setTime({ - from: moment.utc('2011-01-01'), - to: moment.utc('2015-01-01'), - }); - ctx.$httpBackend - .expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=1293840000&end=1420070400/) - .respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - }); - it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() { - response = { - status: 'success', - data: [ - { __name__: 'metric', resource: 'value1' }, - { __name__: 'metric', resource: 'value2' }, - { __name__: 'metric', resource: 'value3' }, - ], - }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - }); - it('label_values(metric, resource) result should not contain empty string', function() { - response = { - status: 'success', - data: [ - { __name__: 'metric', resource: 'value1' }, - { __name__: 'metric', resource: 'value2' }, - { __name__: 'metric', resource: '' }, - ], - }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(2); - expect(results[0].text).to.be('value1'); - expect(results[1].text).to.be('value2'); - }); - it('metrics(metric.*) should generate metric name query', function() { - response = { - status: 'success', - data: ['metric1', 'metric2', 'metric3', 'nomatch'], - }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - }); - it('query_result(metric) should generate metric name query', function() { - response = { - status: 'success', - data: { - resultType: 'vector', - result: [ - { - metric: { __name__: 'metric', job: 'testjob' }, - value: [1443454528.0, '3846'], - }, - ], - }, - }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv); - pm.process().then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(1); - expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000'); - }); - }); - - describe('When performing performSuggestQuery', function() { - var results; - var response; - it('cache response', function() { - response = { - status: 'success', - data: ['value1', 'value2', 'value3'], - }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); - ctx.ds.performSuggestQuery('value', true).then(function(data) { - results = data; - }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - expect(results.length).to.be(3); - ctx.ds.performSuggestQuery('value', true).then(function(data) { - // get from cache, no need to flush - results = data; - expect(results.length).to.be(3); - }); - }); - }); -}); From ddeba41638806bf7174e7a51ce78ecc53dd29309 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Apr 2018 15:49:22 +0200 Subject: [PATCH 0532/3000] fix so that google analytics script are cached --- public/app/core/services/analytics.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index 370773154e5..4f9994302ab 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -7,7 +7,11 @@ export class Analytics { constructor(private $rootScope, private $location) {} gaInit() { - $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut + $.ajax({ + url: 'https://www.google-analytics.com/analytics.js', + dataType: 'script', + cache: true, + }); var ga = ((window).ga = (window).ga || function() { From 44a61a6db33cc3eefe559b1ccb226521c53fb658 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 26 Apr 2018 18:18:54 +0200 Subject: [PATCH 0533/3000] changelog: update for v5.1.0 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ec1965098..0a82a8d0498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 5.1.0 (2018-04-26) + +* **Folders**: Default permissions on folder are not shown as inherited in its dashboards [#11668](https://github.com/grafana/grafana/issues/11668) +* **Templating**: Allow more than 20 previews when creating a variable [#11508](https://github.com/grafana/grafana/issues/11508) +* **Dashboard**: Row edit icon not shown [#11466](https://github.com/grafana/grafana/issues/11466) +* **SQL**: Unsupported data types for value column using time series query [#11703](https://github.com/grafana/grafana/issues/11703) +* **Prometheus**: Prometheus query inspector expands to be very large on autocomplete queries [#11673](https://github.com/grafana/grafana/issues/11673) + # 5.1.0-beta1 (2018-04-20) * **MSSQL**: New Microsoft SQL Server data source [#10093](https://github.com/grafana/grafana/pull/10093), [#11298](https://github.com/grafana/grafana/pull/11298), thx [@linuxchips](https://github.com/linuxchips) From 6fa7ffc23fb5e2ae3be9e80d846efe163a110762 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Apr 2018 18:56:44 +0200 Subject: [PATCH 0534/3000] docs: update installation instructions targeting v5.1.0 stable --- docs/sources/installation/debian.md | 12 +++++++----- docs/sources/installation/rpm.md | 17 +++++++++-------- docs/sources/installation/windows.md | 5 ++++- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 8b2e15ad124..dccb880ec74 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,8 +15,10 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [grafana_5.0.4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb) +Stable for Debian-based Linux | [grafana_5.1.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb) + Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -25,17 +27,17 @@ installation. ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.0.4_amd64.deb +sudo dpkg -i grafana_5.1.0_amd64.deb ``` -## Install Latest Beta + ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 3650560d5cf..e142d6e2c95 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -15,9 +15,10 @@ weight = 2 Description | Download ------------ | ------------- -Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.4 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm) +Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm) + Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -27,29 +28,29 @@ installation. You can install Grafana using Yum directly. ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm ``` -## Install Beta + Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: ```bash -$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm +$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm $ sudo yum install initscripts fontconfig -$ sudo rpm -Uvh grafana-5.0.4-1.x86_64.rpm +$ sudo rpm -Uvh grafana-5.1.0-1.x86_64.rpm ``` #### On OpenSuse: ```bash -$ sudo rpm -i --nodeps grafana-5.0.4-1.x86_64.rpm +$ sudo rpm -i --nodeps grafana-5.1.0-1.x86_64.rpm ``` ## Install via YUM Repository diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 31fe243c01d..8baccac6211 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -12,8 +12,11 @@ weight = 3 Description | Download ------------ | ------------- -Latest stable package for Windows | [grafana-5.0.4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4.windows-x64.zip) +Latest stable package for Windows | [grafana-5.1.0.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0.windows-x64.zip) + + Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. From b53a57610bb2758648c8473084a18ce9f176c7dd Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 26 Apr 2018 18:59:45 +0200 Subject: [PATCH 0535/3000] docs: update current version to 5.1 --- docs/versions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/versions.json b/docs/versions.json index 2dcc7ebe776..61e471938f2 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,6 +1,6 @@ [ - { "version": "v5.1", "path": "/v5.1", "archived": false }, - { "version": "v5.0", "path": "/", "archived": false, "current": true }, + { "version": "v5.1", "path": "/", "archived": false, "current": true }, + { "version": "v5.0", "path": "/v5.0", "archived": true }, { "version": "v4.6", "path": "/v4.6", "archived": true }, { "version": "v4.5", "path": "/v4.5", "archived": true }, { "version": "v4.4", "path": "/v4.4", "archived": true }, From 7aaa1884711f16361e9535540f4b7f7c836c7143 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Tue, 24 Apr 2018 18:42:27 +0200 Subject: [PATCH 0536/3000] build.go: fix deadcode issues --- build.go | 43 ++++++++++--------------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/build.go b/build.go index b86fc838e6b..21b528071e8 100644 --- a/build.go +++ b/build.go @@ -16,7 +16,6 @@ import ( "os/exec" "path" "path/filepath" - "regexp" "runtime" "strconv" "strings" @@ -24,14 +23,14 @@ import ( ) var ( - versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`) - goarch string - goos string - gocc string - gocxx string - cgo string - pkgArch string - version string = "v1" + //versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`) + goarch string + goos string + gocc string + gocxx string + cgo string + pkgArch string + version string = "v1" // deb & rpm does not support semver so have to handle their version a little differently linuxPackageVersion string = "v1" linuxPackageIteration string = "" @@ -44,14 +43,14 @@ var ( isDev bool = false ) -const minGoVersion = 1.8 - func main() { log.SetOutput(os.Stdout) log.SetFlags(0) ensureGoPath() + verifyGitRepoIsClean() + flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH") flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS") flag.StringVar(&gocc, "cc", "", "CC") @@ -352,10 +351,6 @@ func ensureGoPath() { } } -func ChangeWorkingDir(dir string) { - os.Chdir(dir) -} - func grunt(params ...string) { if runtime.GOOS == "windows" { runPrint(`.\node_modules\.bin\grunt`, params...) @@ -492,24 +487,6 @@ func buildStamp() int64 { return s } -func buildArch() string { - os := goos - if os == "darwin" { - os = "macosx" - } - return fmt.Sprintf("%s-%s", os, goarch) -} - -func run(cmd string, args ...string) []byte { - bs, err := runError(cmd, args...) - if err != nil { - log.Println(cmd, strings.Join(args, " ")) - log.Println(string(bs)) - log.Fatal(err) - } - return bytes.TrimSpace(bs) -} - func runError(cmd string, args ...string) ([]byte, error) { ecmd := exec.Command(cmd, args...) bs, err := ecmd.CombinedOutput() From 97fd66db2e52ae830a324c9e82ee681b1851a5c0 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Tue, 24 Apr 2018 18:52:57 +0200 Subject: [PATCH 0537/3000] pkg: fix deadcode issues --- pkg/api/annotations.go | 16 ----- pkg/middleware/render_auth.go | 2 - pkg/services/sqlstore/migrations/stats_mig.go | 63 ++++++++++--------- 3 files changed, 32 insertions(+), 49 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index fdf577a6a6f..52eeb57dbb9 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -294,19 +294,3 @@ func canSave(c *m.ReqContext, repo annotations.Repository, annotationID int64) R return nil } - -func canSaveByRegionID(c *m.ReqContext, repo annotations.Repository, regionID int64) Response { - items, err := repo.Find(&annotations.ItemQuery{RegionId: regionID, OrgId: c.OrgId}) - - if err != nil || len(items) == 0 { - return Error(500, "Could not find annotation to update", err) - } - - dashboardID := items[0].DashboardId - - if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave { - return dashboardGuardianResponse(err) - } - - return nil -} diff --git a/pkg/middleware/render_auth.go b/pkg/middleware/render_auth.go index 6c338becbda..c382eb8e707 100644 --- a/pkg/middleware/render_auth.go +++ b/pkg/middleware/render_auth.go @@ -31,8 +31,6 @@ func initContextWithRenderAuth(ctx *m.ReqContext) bool { return true } -type renderContextFunc func(key string) (string, error) - func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) string { renderKeysLock.Lock() diff --git a/pkg/services/sqlstore/migrations/stats_mig.go b/pkg/services/sqlstore/migrations/stats_mig.go index 7e10eeb9f90..c47b8202c53 100644 --- a/pkg/services/sqlstore/migrations/stats_mig.go +++ b/pkg/services/sqlstore/migrations/stats_mig.go @@ -2,37 +2,38 @@ package migrations import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" -func addStatsMigrations(mg *Migrator) { - statTable := Table{ - Name: "stat", - Columns: []*Column{ - {Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "metric", Type: DB_Varchar, Length: 20, Nullable: false}, - {Name: "type", Type: DB_Int, Nullable: false}, - }, - Indices: []*Index{ - {Cols: []string{"metric"}, Type: UniqueIndex}, - }, - } - - // create table - mg.AddMigration("create stat table", NewAddTableMigration(statTable)) - - // create indices - mg.AddMigration("add index stat.metric", NewAddIndexMigration(statTable, statTable.Indices[0])) - - statValue := Table{ - Name: "stat_value", - Columns: []*Column{ - {Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "value", Type: DB_Double, Nullable: false}, - {Name: "time", Type: DB_DateTime, Nullable: false}, - }, - } - - // create table - mg.AddMigration("create stat_value table", NewAddTableMigration(statValue)) -} +// commented out because of the deadcode CI check +//func addStatsMigrations(mg *Migrator) { +// statTable := Table{ +// Name: "stat", +// Columns: []*Column{ +// {Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true}, +// {Name: "metric", Type: DB_Varchar, Length: 20, Nullable: false}, +// {Name: "type", Type: DB_Int, Nullable: false}, +// }, +// Indices: []*Index{ +// {Cols: []string{"metric"}, Type: UniqueIndex}, +// }, +// } +// +// // create table +// mg.AddMigration("create stat table", NewAddTableMigration(statTable)) +// +// // create indices +// mg.AddMigration("add index stat.metric", NewAddIndexMigration(statTable, statTable.Indices[0])) +// +// statValue := Table{ +// Name: "stat_value", +// Columns: []*Column{ +// {Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true}, +// {Name: "value", Type: DB_Double, Nullable: false}, +// {Name: "time", Type: DB_DateTime, Nullable: false}, +// }, +// } +// +// // create table +// mg.AddMigration("create stat_value table", NewAddTableMigration(statValue)) +//} func addTestDataMigrations(mg *Migrator) { testData := Table{ From 0459261d1910642f7e2ac25d28d0d924abecf994 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Tue, 24 Apr 2018 18:55:43 +0200 Subject: [PATCH 0538/3000] add deadcode linter to circleci --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3e95583ecae..4b717083853 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,13 +27,14 @@ jobs: steps: - checkout - run: 'go get -u gopkg.in/alecthomas/gometalinter.v2' + - run: 'go get -u github.com/tsenart/deadcode' - run: 'go get -u github.com/gordonklaus/ineffassign' - run: 'go get -u github.com/opennota/check/cmd/structcheck' - run: 'go get -u github.com/mdempsky/unconvert' - run: 'go get -u github.com/opennota/check/cmd/varcheck' - run: name: run linters - command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' + command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' test-frontend: docker: From f1220fd2a4d46eacf9b28b3e5b4b1d91b9615856 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 26 Apr 2018 11:58:42 +0200 Subject: [PATCH 0539/3000] Explore WIP --- package.json | 6 + pkg/api/index.go | 11 + public/app/containers/Explore/ElapsedTime.tsx | 46 + public/app/containers/Explore/Explore.tsx | 246 ++ public/app/containers/Explore/Graph.tsx | 123 + public/app/containers/Explore/Legend.tsx | 22 + public/app/containers/Explore/QueryField.tsx | 562 +++ public/app/containers/Explore/Table.tsx | 24 + public/app/containers/Explore/Typeahead.tsx | 66 + .../Explore/slate-plugins/braces.test.ts | 47 + .../Explore/slate-plugins/braces.ts | 51 + .../Explore/slate-plugins/clear.test.ts | 38 + .../containers/Explore/slate-plugins/clear.ts | 22 + .../Explore/slate-plugins/newline.ts | 35 + .../Explore/slate-plugins/prism/index.tsx | 122 + .../Explore/slate-plugins/prism/promql.ts | 123 + .../Explore/slate-plugins/runner.ts | 14 + .../app/containers/Explore/utils/debounce.ts | 14 + public/app/containers/Explore/utils/dom.ts | 40 + .../containers/Explore/utils/prometheus.ts | 20 + public/app/core/components/grafana_app.ts | 16 +- public/app/features/plugins/datasource_srv.ts | 2 +- public/app/routes/ReactContainer.tsx | 10 +- public/app/routes/routes.ts | 8 + public/app/stores/store.ts | 4 +- public/sass/_grafana.scss | 1 + public/sass/layout/_page.scss | 7 + public/sass/pages/_explore.scss | 304 ++ scripts/webpack/webpack.dev.js | 1 + scripts/webpack/webpack.prod.js | 7 +- yarn.lock | 3306 +++++++++-------- 31 files changed, 3685 insertions(+), 1613 deletions(-) create mode 100644 public/app/containers/Explore/ElapsedTime.tsx create mode 100644 public/app/containers/Explore/Explore.tsx create mode 100644 public/app/containers/Explore/Graph.tsx create mode 100644 public/app/containers/Explore/Legend.tsx create mode 100644 public/app/containers/Explore/QueryField.tsx create mode 100644 public/app/containers/Explore/Table.tsx create mode 100644 public/app/containers/Explore/Typeahead.tsx create mode 100644 public/app/containers/Explore/slate-plugins/braces.test.ts create mode 100644 public/app/containers/Explore/slate-plugins/braces.ts create mode 100644 public/app/containers/Explore/slate-plugins/clear.test.ts create mode 100644 public/app/containers/Explore/slate-plugins/clear.ts create mode 100644 public/app/containers/Explore/slate-plugins/newline.ts create mode 100644 public/app/containers/Explore/slate-plugins/prism/index.tsx create mode 100644 public/app/containers/Explore/slate-plugins/prism/promql.ts create mode 100644 public/app/containers/Explore/slate-plugins/runner.ts create mode 100644 public/app/containers/Explore/utils/debounce.ts create mode 100644 public/app/containers/Explore/utils/dom.ts create mode 100644 public/app/containers/Explore/utils/prometheus.ts create mode 100644 public/sass/pages/_explore.scss diff --git a/package.json b/package.json index 495507b2f00..383e0e39ab5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "axios": "^0.17.1", "babel-core": "^6.26.0", "babel-loader": "^7.1.2", + "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-preset-es2015": "^6.24.1", "clean-webpack-plugin": "^0.1.19", "css-loader": "^0.28.7", @@ -150,6 +151,7 @@ "d3-scale-chromatic": "^1.1.1", "eventemitter3": "^2.0.3", "file-saver": "^1.3.3", + "immutable": "^3.8.2", "jquery": "^3.2.1", "lodash": "^4.17.4", "mobx": "^3.4.1", @@ -158,6 +160,7 @@ "moment": "^2.18.1", "mousetrap": "^1.6.0", "mousetrap-global-bind": "^1.1.0", + "prismjs": "^1.6.0", "prop-types": "^15.6.0", "react": "^16.2.0", "react-dom": "^16.2.0", @@ -170,6 +173,9 @@ "remarkable": "^1.7.1", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", + "slate": "^0.33.4", + "slate-plain-serializer": "^0.5.10", + "slate-react": "^0.12.4", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop/tarball/master", "tinycolor2": "^1.4.1" diff --git a/pkg/api/index.go b/pkg/api/index.go index 94094706f68..64eaddcd1a7 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -117,6 +117,17 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { Children: dashboardChildNavs, }) + // data.NavTree = append(data.NavTree, &dtos.NavLink{ + // Text: "Explore", + // Id: "explore", + // SubTitle: "Explore your data", + // Icon: "fa fa-rocket", + // Url: setting.AppSubUrl + "/explore", + // Children: []*dtos.NavLink{ + // {Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore/new"}, + // }, + // }) + if c.IsSignedIn { // Only set login if it's different from the name var login string diff --git a/public/app/containers/Explore/ElapsedTime.tsx b/public/app/containers/Explore/ElapsedTime.tsx new file mode 100644 index 00000000000..123299fd96a --- /dev/null +++ b/public/app/containers/Explore/ElapsedTime.tsx @@ -0,0 +1,46 @@ +import React, { PureComponent } from 'react'; + +const INTERVAL = 150; + +export default class ElapsedTime extends PureComponent { + offset: number; + timer: NodeJS.Timer; + + state = { + elapsed: 0, + }; + + start() { + this.offset = Date.now(); + this.timer = setInterval(this.tick, INTERVAL); + } + + tick = () => { + const jetzt = Date.now(); + const elapsed = jetzt - this.offset; + this.setState({ elapsed }); + }; + + componentWillReceiveProps(nextProps) { + if (nextProps.time) { + clearInterval(this.timer); + } else if (this.props.time) { + this.start(); + } + } + + componentDidMount() { + this.start(); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + render() { + const { elapsed } = this.state; + const { className, time } = this.props; + const value = (time || elapsed) / 1000; + return {value.toFixed(1)}s; + } +} diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx new file mode 100644 index 00000000000..55c1d088ccc --- /dev/null +++ b/public/app/containers/Explore/Explore.tsx @@ -0,0 +1,246 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import colors from 'app/core/utils/colors'; +import TimeSeries from 'app/core/time_series2'; + +import ElapsedTime from './ElapsedTime'; +import Legend from './Legend'; +import QueryField from './QueryField'; +import Graph from './Graph'; +import Table from './Table'; +import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; + +function buildQueryOptions({ format, interval, instant, now, query }) { + const to = now; + const from = to - 1000 * 60 * 60 * 3; + return { + interval, + range: { + from, + to, + }, + targets: [ + { + expr: query, + format, + instant, + }, + ], + }; +} + +function makeTimeSeriesList(dataList, options) { + return dataList.map((seriesData, index) => { + const datapoints = seriesData.datapoints || []; + const alias = seriesData.target; + + const colorIndex = index % colors.length; + const color = colors[colorIndex]; + + const series = new TimeSeries({ + datapoints: datapoints, + alias: alias, + color: color, + unit: seriesData.unit, + }); + + if (datapoints && datapoints.length > 0) { + const last = datapoints[datapoints.length - 1][1]; + const from = options.range.from; + if (last - from < -10000) { + series.isOutsideRange = true; + } + } + + return series; + }); +} + +interface IExploreState { + datasource: any; + datasourceError: any; + datasourceLoading: any; + graphResult: any; + latency: number; + loading: any; + requestOptions: any; + showingGraph: boolean; + showingTable: boolean; + tableResult: any; +} + +// @observer +export class Explore extends React.Component { + datasourceSrv: DatasourceSrv; + query: string; + + constructor(props) { + super(props); + this.state = { + datasource: null, + datasourceError: null, + datasourceLoading: true, + graphResult: null, + latency: 0, + loading: false, + requestOptions: null, + showingGraph: true, + showingTable: true, + tableResult: null, + }; + } + + async componentDidMount() { + const datasource = await this.props.datasourceSrv.get(); + const testResult = await datasource.testDatasource(); + if (testResult.status === 'success') { + this.setState({ datasource, datasourceError: null, datasourceLoading: false }); + } else { + this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false }); + } + } + + handleClickGraphButton = () => { + this.setState(state => ({ showingGraph: !state.showingGraph })); + }; + + handleClickTableButton = () => { + this.setState(state => ({ showingTable: !state.showingTable })); + }; + + handleRequestError({ error }) { + console.error(error); + } + + handleQueryChange = query => { + this.query = query; + }; + + handleSubmit = () => { + const { showingGraph, showingTable } = this.state; + if (showingTable) { + this.runTableQuery(); + } + if (showingGraph) { + this.runGraphQuery(); + } + }; + + async runGraphQuery() { + const { query } = this; + const { datasource } = this.state; + if (!query) { + return; + } + this.setState({ latency: 0, loading: true, graphResult: null }); + const now = Date.now(); + const options = buildQueryOptions({ + format: 'time_series', + interval: datasource.interval, + instant: false, + now, + query, + }); + try { + const res = await datasource.query(options); + const result = makeTimeSeriesList(res.data, options); + const latency = Date.now() - now; + this.setState({ latency, loading: false, graphResult: result, requestOptions: options }); + } catch (error) { + console.error(error); + this.setState({ loading: false, graphResult: error }); + } + } + + async runTableQuery() { + const { query } = this; + const { datasource } = this.state; + if (!query) { + return; + } + this.setState({ latency: 0, loading: true, tableResult: null }); + const now = Date.now(); + const options = buildQueryOptions({ format: 'table', interval: datasource.interval, instant: true, now, query }); + try { + const res = await datasource.query(options); + const tableModel = res.data[0]; + const latency = Date.now() - now; + this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options }); + } catch (error) { + console.error(error); + this.setState({ loading: false, tableResult: null }); + } + } + + request = url => { + const { datasource } = this.state; + return datasource.metadataRequest(url); + }; + + render() { + const { + datasource, + datasourceError, + datasourceLoading, + latency, + loading, + requestOptions, + graphResult, + showingGraph, + showingTable, + tableResult, + } = this.state; + const showingBoth = showingGraph && showingTable; + const graphHeight = showingBoth ? '200px' : null; + const graphButtonClassName = showingBoth || showingGraph ? 'btn m-r-1' : 'btn btn-inverse m-r-1'; + const tableButtonClassName = showingBoth || showingTable ? 'btn m-r-1' : 'btn btn-inverse m-r-1'; + return ( +
    +
    +

    Explore

    + {datasourceLoading ?
    Loading datasource...
    : null} + + {datasourceError ?
    Error connecting to datasource.
    : null} + + {datasource ? ( +
    +
    +
    + +
    +
    + + +
    +
    +
    + +
    + {loading || latency ? : null} +
    + {showingGraph ? ( + + ) : null} + {showingGraph ? : null} + {showingTable ? : null} + + + ) : null} + + + ); + } +} + +export default hot(module)(Explore); diff --git a/public/app/containers/Explore/Graph.tsx b/public/app/containers/Explore/Graph.tsx new file mode 100644 index 00000000000..0a13b39619d --- /dev/null +++ b/public/app/containers/Explore/Graph.tsx @@ -0,0 +1,123 @@ +import $ from 'jquery'; +import React, { Component } from 'react'; + +import TimeSeries from 'app/core/time_series2'; + +import 'vendor/flot/jquery.flot'; +import 'vendor/flot/jquery.flot.time'; + +// Copied from graph.ts +function time_format(ticks, min, max) { + if (min && max && ticks) { + var range = max - min; + var secPerTick = range / ticks / 1000; + var oneDay = 86400000; + var oneYear = 31536000000; + + if (secPerTick <= 45) { + return '%H:%M:%S'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return '%H:%M'; + } + if (secPerTick <= 80000) { + return '%m/%d %H:%M'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return '%m/%d'; + } + return '%Y-%m'; + } + + return '%H:%M'; +} + +const FLOT_OPTIONS = { + legend: { + show: false, + }, + series: { + lines: { + linewidth: 1, + zero: false, + }, + shadowSize: 0, + }, + grid: { + minBorderMargin: 0, + markings: [], + backgroundColor: null, + borderWidth: 0, + // hoverable: true, + clickable: true, + color: '#a1a1a1', + margin: { left: 0, right: 0 }, + labelMarginX: 0, + }, + // selection: { + // mode: 'x', + // color: '#666', + // }, + // crosshair: { + // mode: 'x', + // }, +}; + +class Graph extends Component { + componentDidMount() { + this.draw(); + } + + componentDidUpdate(prevProps) { + if ( + prevProps.data !== this.props.data || + prevProps.options !== this.props.options || + prevProps.height !== this.props.height + ) { + this.draw(); + } + } + + draw() { + const { data, options: userOptions } = this.props; + if (!data) { + return; + } + const series = data.map((ts: TimeSeries) => ({ + label: ts.label, + data: ts.getFlotPairs('null'), + })); + + const $el = $(`#${this.props.id}`); + const ticks = $el.width() / 100; + const min = userOptions.range.from.valueOf(); + const max = userOptions.range.to.valueOf(); + const dynamicOptions = { + xaxis: { + mode: 'time', + min: min, + max: max, + label: 'Datetime', + ticks: ticks, + timeformat: time_format(ticks, min, max), + }, + }; + const options = { + ...FLOT_OPTIONS, + ...dynamicOptions, + ...userOptions, + }; + $.plot($el, series, options); + } + + render() { + const style = { + height: this.props.height || '400px', + width: this.props.width || '100%', + }; + + return
    ; + } +} + +export default Graph; diff --git a/public/app/containers/Explore/Legend.tsx b/public/app/containers/Explore/Legend.tsx new file mode 100644 index 00000000000..e00932fe566 --- /dev/null +++ b/public/app/containers/Explore/Legend.tsx @@ -0,0 +1,22 @@ +import React, { PureComponent } from 'react'; + +const LegendItem = ({ series }) => ( +
    +
    + +
    + {series.alias} +
    +); + +export default class Legend extends PureComponent { + render() { + const { className = '', data } = this.props; + const items = data || []; + return ( +
    + {items.map(series => )} +
    + ); + } +} diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx new file mode 100644 index 00000000000..816473619fd --- /dev/null +++ b/public/app/containers/Explore/QueryField.tsx @@ -0,0 +1,562 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Value } from 'slate'; +import { Editor } from 'slate-react'; +import Plain from 'slate-plain-serializer'; + +// dom also includes Element polyfills +import { getNextCharacter, getPreviousCousin } from './utils/dom'; +import BracesPlugin from './slate-plugins/braces'; +import ClearPlugin from './slate-plugins/clear'; +import NewlinePlugin from './slate-plugins/newline'; +import PluginPrism, { configurePrismMetricsTokens } from './slate-plugins/prism/index'; +import RunnerPlugin from './slate-plugins/runner'; +import debounce from './utils/debounce'; +import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus'; + +import Typeahead from './Typeahead'; + +const EMPTY_METRIC = ''; +const TYPEAHEAD_DEBOUNCE = 300; + +function flattenSuggestions(s) { + return s ? s.reduce((acc, g) => acc.concat(g.items), []) : []; +} + +const getInitialValue = query => + Value.fromJSON({ + document: { + nodes: [ + { + object: 'block', + type: 'paragraph', + nodes: [ + { + object: 'text', + leaves: [ + { + text: query, + }, + ], + }, + ], + }, + ], + }, + }); + +class Portal extends React.Component { + node: any; + constructor(props) { + super(props); + this.node = document.createElement('div'); + this.node.classList.add(`query-field-portal-${props.index}`); + document.body.appendChild(this.node); + } + + componentWillUnmount() { + document.body.removeChild(this.node); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.node); + } +} + +class QueryField extends React.Component { + menuEl: any; + plugins: any; + resetTimer: any; + + constructor(props, context) { + super(props, context); + + this.plugins = [ + BracesPlugin(), + ClearPlugin(), + RunnerPlugin({ handler: props.onPressEnter }), + NewlinePlugin(), + PluginPrism(), + ]; + + this.state = { + labelKeys: {}, + labelValues: {}, + metrics: props.metrics || [], + suggestions: [], + typeaheadIndex: 0, + typeaheadPrefix: '', + value: getInitialValue(props.initialQuery || ''), + }; + } + + componentDidMount() { + this.updateMenu(); + + if (this.props.metrics === undefined) { + this.fetchMetricNames(); + } + } + + componentWillUnmount() { + clearTimeout(this.resetTimer); + } + + componentDidUpdate() { + this.updateMenu(); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.metrics && nextProps.metrics !== this.props.metrics) { + this.setState({ metrics: nextProps.metrics }, this.onMetricsReceived); + } + // initialQuery is null in case the user typed + if (nextProps.initialQuery !== null && nextProps.initialQuery !== this.props.initialQuery) { + this.setState({ value: getInitialValue(nextProps.initialQuery) }); + } + } + + onChange = ({ value }) => { + const changed = value.document !== this.state.value.document; + this.setState({ value }, () => { + if (changed) { + this.handleChangeQuery(); + } + }); + + window.requestAnimationFrame(this.handleTypeahead); + }; + + onMetricsReceived = () => { + if (!this.state.metrics) { + return; + } + configurePrismMetricsTokens(this.state.metrics); + // Trigger re-render + window.requestAnimationFrame(() => { + // Bogus edit to trigger highlighting + const change = this.state.value + .change() + .insertText(' ') + .deleteBackward(1); + this.onChange(change); + }); + }; + + request = url => { + if (this.props.request) { + return this.props.request(url); + } + return fetch(url); + }; + + handleChangeQuery = () => { + // Send text change to parent + const { onQueryChange } = this.props; + if (onQueryChange) { + onQueryChange(Plain.serialize(this.state.value)); + } + }; + + handleTypeahead = debounce(() => { + const selection = window.getSelection(); + if (selection.anchorNode) { + const wrapperNode = selection.anchorNode.parentElement; + const editorNode = wrapperNode.closest('.query-field'); + if (!editorNode || this.state.value.isBlurred) { + // Not inside this editor + return; + } + + const range = selection.getRangeAt(0); + const text = selection.anchorNode.textContent; + const offset = range.startOffset; + const prefix = cleanText(text.substr(0, offset)); + + // Determine candidates by context + const suggestionGroups = []; + const wrapperClasses = wrapperNode.classList; + let typeaheadContext = null; + + // Take first metric as lucky guess + const metricNode = editorNode.querySelector('.metric'); + + if (wrapperClasses.contains('context-range')) { + // Rate ranges + typeaheadContext = 'context-range'; + suggestionGroups.push({ + label: 'Range vector', + items: [...RATE_RANGES], + }); + } else if (wrapperClasses.contains('context-labels') && metricNode) { + const metric = metricNode.textContent; + const labelKeys = this.state.labelKeys[metric]; + if (labelKeys) { + if ((text && text.startsWith('=')) || wrapperClasses.contains('attr-value')) { + // Label values + const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name'); + if (labelKeyNode) { + const labelKey = labelKeyNode.textContent; + const labelValues = this.state.labelValues[metric][labelKey]; + typeaheadContext = 'context-label-values'; + suggestionGroups.push({ + label: 'Label values', + items: labelValues, + }); + } + } else { + // Label keys + typeaheadContext = 'context-labels'; + suggestionGroups.push({ label: 'Labels', items: labelKeys }); + } + } else { + this.fetchMetricLabels(metric); + } + } else if (wrapperClasses.contains('context-labels') && !metricNode) { + // Empty name queries + const defaultKeys = ['job', 'instance']; + // Munge all keys that we have seen together + const labelKeys = Object.keys(this.state.labelKeys).reduce((acc, metric) => { + return acc.concat(this.state.labelKeys[metric].filter(key => acc.indexOf(key) === -1)); + }, defaultKeys); + if ((text && text.startsWith('=')) || wrapperClasses.contains('attr-value')) { + // Label values + const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name'); + if (labelKeyNode) { + const labelKey = labelKeyNode.textContent; + if (this.state.labelValues[EMPTY_METRIC]) { + const labelValues = this.state.labelValues[EMPTY_METRIC][labelKey]; + typeaheadContext = 'context-label-values'; + suggestionGroups.push({ + label: 'Label values', + items: labelValues, + }); + } else { + // Can only query label values for now (API to query keys is under development) + this.fetchLabelValues(labelKey); + } + } + } else { + // Label keys + typeaheadContext = 'context-labels'; + suggestionGroups.push({ label: 'Labels', items: labelKeys }); + } + } else if (metricNode && wrapperClasses.contains('context-aggregation')) { + typeaheadContext = 'context-aggregation'; + const metric = metricNode.textContent; + const labelKeys = this.state.labelKeys[metric]; + if (labelKeys) { + suggestionGroups.push({ label: 'Labels', items: labelKeys }); + } else { + this.fetchMetricLabels(metric); + } + } else if ( + (this.state.metrics && ((prefix && !wrapperClasses.contains('token')) || text.match(/[+\-*/^%]/))) || + wrapperClasses.contains('context-function') + ) { + // Need prefix for metrics + typeaheadContext = 'context-metrics'; + suggestionGroups.push({ + label: 'Metrics', + items: this.state.metrics, + }); + } + + let results = 0; + const filteredSuggestions = suggestionGroups.map(group => { + if (group.items) { + group.items = group.items.filter(c => c.length !== prefix.length && c.indexOf(prefix) > -1); + results += group.items.length; + } + return group; + }); + + console.log('handleTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); + + this.setState({ + typeaheadPrefix: prefix, + typeaheadContext, + typeaheadText: text, + suggestions: results > 0 ? filteredSuggestions : [], + }); + } + }, TYPEAHEAD_DEBOUNCE); + + applyTypeahead(change, suggestion) { + const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state; + + // Modify suggestion based on context + switch (typeaheadContext) { + case 'context-labels': { + const nextChar = getNextCharacter(); + if (!nextChar || nextChar === '}' || nextChar === ',') { + suggestion += '='; + } + break; + } + + case 'context-label-values': { + // Always add quotes and remove existing ones instead + if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) { + suggestion = `"${suggestion}`; + } + if (getNextCharacter() !== '"') { + suggestion = `${suggestion}"`; + } + break; + } + + default: + } + + this.resetTypeahead(); + + // Remove the current, incomplete text and replace it with the selected suggestion + let backward = typeaheadPrefix.length; + const text = cleanText(typeaheadText); + const suffixLength = text.length - typeaheadPrefix.length; + const offset = typeaheadText.indexOf(typeaheadPrefix); + const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestion === typeaheadText); + const forward = midWord ? suffixLength + offset : 0; + + return ( + change + // TODO this line breaks if cursor was moved left and length is longer than whole prefix + .deleteBackward(backward) + .deleteForward(forward) + .insertText(suggestion) + .focus() + ); + } + + onKeyDown = (event, change) => { + if (this.menuEl) { + const { typeaheadIndex, suggestions } = this.state; + + switch (event.key) { + case 'Escape': { + if (this.menuEl) { + event.preventDefault(); + this.resetTypeahead(); + return true; + } + break; + } + + case 'Tab': { + // Dont blur input + event.preventDefault(); + if (!suggestions || suggestions.length === 0) { + return undefined; + } + + // Get the currently selected suggestion + const flattenedSuggestions = flattenSuggestions(suggestions); + const selected = Math.abs(typeaheadIndex); + const selectedIndex = selected % flattenedSuggestions.length || 0; + const suggestion = flattenedSuggestions[selectedIndex]; + + this.applyTypeahead(change, suggestion); + return true; + } + + case 'ArrowDown': { + // Select next suggestion + event.preventDefault(); + this.setState({ typeaheadIndex: typeaheadIndex + 1 }); + break; + } + + case 'ArrowUp': { + // Select previous suggestion + event.preventDefault(); + this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) }); + break; + } + + default: { + // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key); + break; + } + } + } + return undefined; + }; + + resetTypeahead = () => { + this.setState({ + suggestions: [], + typeaheadIndex: 0, + typeaheadPrefix: '', + typeaheadContext: null, + }); + }; + + async fetchLabelValues(key) { + const url = `/api/v1/label/${key}/values`; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + const pairs = this.state.labelValues[EMPTY_METRIC]; + const values = { + ...pairs, + [key]: body.data, + }; + // const labelKeys = { + // ...this.state.labelKeys, + // [EMPTY_METRIC]: keys, + // }; + const labelValues = { + ...this.state.labelValues, + [EMPTY_METRIC]: values, + }; + this.setState({ labelValues }, this.handleTypeahead); + } catch (e) { + if (this.props.onRequestError) { + this.props.onRequestError(e); + } else { + console.error(e); + } + } + } + + async fetchMetricLabels(name) { + const url = `/api/v1/series?match[]=${name}`; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + const { keys, values } = processLabels(body.data); + const labelKeys = { + ...this.state.labelKeys, + [name]: keys, + }; + const labelValues = { + ...this.state.labelValues, + [name]: values, + }; + this.setState({ labelKeys, labelValues }, this.handleTypeahead); + } catch (e) { + if (this.props.onRequestError) { + this.props.onRequestError(e); + } else { + console.error(e); + } + } + } + + async fetchMetricNames() { + const url = '/api/v1/label/__name__/values'; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + this.setState({ metrics: body.data }, this.onMetricsReceived); + } catch (error) { + if (this.props.onRequestError) { + this.props.onRequestError(error); + } else { + console.error(error); + } + } + } + + handleBlur = () => { + const { onBlur } = this.props; + // If we dont wait here, menu clicks wont work because the menu + // will be gone. + this.resetTimer = setTimeout(this.resetTypeahead, 100); + if (onBlur) { + onBlur(); + } + }; + + handleFocus = () => { + const { onFocus } = this.props; + if (onFocus) { + onFocus(); + } + }; + + handleClickMenu = item => { + // Manually triggering change + const change = this.applyTypeahead(this.state.value.change(), item); + this.onChange(change); + }; + + updateMenu = () => { + const { suggestions } = this.state; + const menu = this.menuEl; + const selection = window.getSelection(); + const node = selection.anchorNode; + + // No menu, nothing to do + if (!menu) { + return; + } + + // No suggestions or blur, remove menu + const hasSuggesstions = suggestions && suggestions.length > 0; + if (!hasSuggesstions) { + menu.removeAttribute('style'); + return; + } + + // Align menu overlay to editor node + if (node) { + const rect = node.parentElement.getBoundingClientRect(); + menu.style.opacity = 1; + menu.style.top = `${rect.top + window.scrollY + rect.height + 4}px`; + menu.style.left = `${rect.left + window.scrollX - 2}px`; + } + }; + + menuRef = el => { + this.menuEl = el; + }; + + renderMenu = () => { + const { suggestions } = this.state; + const hasSuggesstions = suggestions && suggestions.length > 0; + if (!hasSuggesstions) { + return null; + } + + // Guard selectedIndex to be within the length of the suggestions + let selectedIndex = Math.max(this.state.typeaheadIndex, 0); + const flattenedSuggestions = flattenSuggestions(suggestions); + selectedIndex = selectedIndex % flattenedSuggestions.length || 0; + const selectedKeys = flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []; + + // Create typeahead in DOM root so we can later position it absolutely + return ( + + + + ); + }; + + render() { + return ( +
    + {this.renderMenu()} + +
    + ); + } +} + +export default QueryField; diff --git a/public/app/containers/Explore/Table.tsx b/public/app/containers/Explore/Table.tsx new file mode 100644 index 00000000000..7179a0fc89a --- /dev/null +++ b/public/app/containers/Explore/Table.tsx @@ -0,0 +1,24 @@ +import React, { PureComponent } from 'react'; +// import TableModel from 'app/core/table_model'; + +const EMPTY_TABLE = { + columns: [], + rows: [], +}; + +export default class Table extends PureComponent { + render() { + const { className = '', data } = this.props; + const tableModel = data || EMPTY_TABLE; + return ( +
    + + {tableModel.columns.map(col => )} + + + {tableModel.rows.map((row, i) => {row.map((content, j) => )})} + +
    {col.text}
    {content}
    + ); + } +} diff --git a/public/app/containers/Explore/Typeahead.tsx b/public/app/containers/Explore/Typeahead.tsx new file mode 100644 index 00000000000..4943622fe4e --- /dev/null +++ b/public/app/containers/Explore/Typeahead.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +function scrollIntoView(el) { + if (!el || !el.offsetParent) { + return; + } + const container = el.offsetParent; + if (el.offsetTop > container.scrollTop + container.offsetHeight || el.offsetTop < container.scrollTop) { + container.scrollTop = el.offsetTop - container.offsetTop; + } +} + +class TypeaheadItem extends React.PureComponent { + el: any; + componentDidUpdate(prevProps) { + if (this.props.isSelected && !prevProps.isSelected) { + scrollIntoView(this.el); + } + } + + getRef = el => { + this.el = el; + }; + + render() { + const { isSelected, label, onClickItem } = this.props; + const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item'; + const onClick = () => onClickItem(label); + return ( +
  • + {label} +
  • + ); + } +} + +class TypeaheadGroup extends React.PureComponent { + render() { + const { items, label, selected, onClickItem } = this.props; + return ( +
  • +
    {label}
    +
      + {items.map(item => ( + -1} label={item} /> + ))} +
    +
  • + ); + } +} + +class Typeahead extends React.PureComponent { + render() { + const { groupedItems, menuRef, selectedItems, onClickItem } = this.props; + return ( +
      + {groupedItems.map(g => ( + + ))} +
    + ); + } +} + +export default Typeahead; diff --git a/public/app/containers/Explore/slate-plugins/braces.test.ts b/public/app/containers/Explore/slate-plugins/braces.test.ts new file mode 100644 index 00000000000..5c9a90ae034 --- /dev/null +++ b/public/app/containers/Explore/slate-plugins/braces.test.ts @@ -0,0 +1,47 @@ +import Plain from 'slate-plain-serializer'; + +import BracesPlugin from './braces'; + +declare global { + interface Window { + KeyboardEvent: any; + } +} + +describe('braces', () => { + const handler = BracesPlugin().onKeyDown; + + it('adds closing braces around empty value', () => { + const change = Plain.deserialize('').change(); + const event = new window.KeyboardEvent('keydown', { key: '(' }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('()'); + }); + + it('adds closing braces around a value', () => { + const change = Plain.deserialize('foo').change(); + const event = new window.KeyboardEvent('keydown', { key: '(' }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('(foo)'); + }); + + it('adds closing braces around the following value only', () => { + const change = Plain.deserialize('foo bar ugh').change(); + let event; + event = new window.KeyboardEvent('keydown', { key: '(' }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('(foo) bar ugh'); + + // Wrap bar + change.move(5); + event = new window.KeyboardEvent('keydown', { key: '(' }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('(foo) (bar) ugh'); + + // Create empty parens after (bar) + change.move(4); + event = new window.KeyboardEvent('keydown', { key: '(' }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('(foo) (bar)() ugh'); + }); +}); diff --git a/public/app/containers/Explore/slate-plugins/braces.ts b/public/app/containers/Explore/slate-plugins/braces.ts new file mode 100644 index 00000000000..b92a224d111 --- /dev/null +++ b/public/app/containers/Explore/slate-plugins/braces.ts @@ -0,0 +1,51 @@ +const BRACES = { + '[': ']', + '{': '}', + '(': ')', +}; + +export default function BracesPlugin() { + return { + onKeyDown(event, change) { + const { value } = change; + if (!value.isCollapsed) { + return undefined; + } + + switch (event.key) { + case '{': + case '[': { + event.preventDefault(); + // Insert matching braces + change + .insertText(`${event.key}${BRACES[event.key]}`) + .move(-1) + .focus(); + return true; + } + + case '(': { + event.preventDefault(); + const text = value.anchorText.text; + const offset = value.anchorOffset; + const space = text.indexOf(' ', offset); + const length = space > 0 ? space : text.length; + const forward = length - offset; + // Insert matching braces + change + .insertText(event.key) + .move(forward) + .insertText(BRACES[event.key]) + .move(-1 - forward) + .focus(); + return true; + } + + default: { + break; + } + } + return undefined; + }, + }; +} diff --git a/public/app/containers/Explore/slate-plugins/clear.test.ts b/public/app/containers/Explore/slate-plugins/clear.test.ts new file mode 100644 index 00000000000..28ba371df14 --- /dev/null +++ b/public/app/containers/Explore/slate-plugins/clear.test.ts @@ -0,0 +1,38 @@ +import Plain from 'slate-plain-serializer'; + +import ClearPlugin from './clear'; + +describe('clear', () => { + const handler = ClearPlugin().onKeyDown; + + it('does not change the empty value', () => { + const change = Plain.deserialize('').change(); + const event = new window.KeyboardEvent('keydown', { + key: 'k', + ctrlKey: true, + }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual(''); + }); + + it('clears to the end of the line', () => { + const change = Plain.deserialize('foo').change(); + const event = new window.KeyboardEvent('keydown', { + key: 'k', + ctrlKey: true, + }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual(''); + }); + + it('clears from the middle to the end of the line', () => { + const change = Plain.deserialize('foo bar').change(); + change.move(4); + const event = new window.KeyboardEvent('keydown', { + key: 'k', + ctrlKey: true, + }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('foo '); + }); +}); diff --git a/public/app/containers/Explore/slate-plugins/clear.ts b/public/app/containers/Explore/slate-plugins/clear.ts new file mode 100644 index 00000000000..5e2789bf544 --- /dev/null +++ b/public/app/containers/Explore/slate-plugins/clear.ts @@ -0,0 +1,22 @@ +// Clears the rest of the line after the caret +export default function ClearPlugin() { + return { + onKeyDown(event, change) { + const { value } = change; + if (!value.isCollapsed) { + return undefined; + } + + if (event.key === 'k' && event.ctrlKey) { + event.preventDefault(); + const text = value.anchorText.text; + const offset = value.anchorOffset; + const length = text.length; + const forward = length - offset; + change.deleteForward(forward); + return true; + } + return undefined; + }, + }; +} diff --git a/public/app/containers/Explore/slate-plugins/newline.ts b/public/app/containers/Explore/slate-plugins/newline.ts new file mode 100644 index 00000000000..cae8af3acb0 --- /dev/null +++ b/public/app/containers/Explore/slate-plugins/newline.ts @@ -0,0 +1,35 @@ +function getIndent(text) { + let offset = text.length - text.trimLeft().length; + if (offset) { + let indent = text[0]; + while (--offset) { + indent += text[0]; + } + return indent; + } + return ''; +} + +export default function NewlinePlugin() { + return { + onKeyDown(event, change) { + const { value } = change; + if (!value.isCollapsed) { + return undefined; + } + + if (event.key === 'Enter' && event.shiftKey) { + event.preventDefault(); + + const { startBlock } = value; + const currentLineText = startBlock.text; + const indent = getIndent(currentLineText); + + return change + .splitBlock() + .insertText(indent) + .focus(); + } + }, + }; +} diff --git a/public/app/containers/Explore/slate-plugins/prism/index.tsx b/public/app/containers/Explore/slate-plugins/prism/index.tsx new file mode 100644 index 00000000000..7c3fa296d8e --- /dev/null +++ b/public/app/containers/Explore/slate-plugins/prism/index.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import Prism from 'prismjs'; + +import Promql from './promql'; + +Prism.languages.promql = Promql; + +const TOKEN_MARK = 'prism-token'; + +export function configurePrismMetricsTokens(metrics) { + Prism.languages.promql.metric = { + alias: 'variable', + pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`), + }; +} + +/** + * Code-highlighting plugin based on Prism and + * https://github.com/ianstormtaylor/slate/blob/master/examples/code-highlighting/index.js + * + * (Adapted to handle nested grammar definitions.) + */ + +export default function PrismPlugin() { + return { + /** + * Render a Slate mark with appropiate CSS class names + * + * @param {Object} props + * @return {Element} + */ + + renderMark(props) { + const { children, mark } = props; + // Only apply spans to marks identified by this plugin + if (mark.type !== TOKEN_MARK) { + return undefined; + } + const className = `token ${mark.data.get('types')}`; + return {children}; + }, + + /** + * Decorate code blocks with Prism.js highlighting. + * + * @param {Node} node + * @return {Array} + */ + + decorateNode(node) { + if (node.type !== 'paragraph') { + return []; + } + + const texts = node.getTexts().toArray(); + const tstring = texts.map(t => t.text).join('\n'); + const grammar = Prism.languages.promql; + const tokens = Prism.tokenize(tstring, grammar); + const decorations = []; + let startText = texts.shift(); + let endText = startText; + let startOffset = 0; + let endOffset = 0; + let start = 0; + + function processToken(token, acc?) { + // Accumulate token types down the tree + const types = `${acc || ''} ${token.type || ''} ${token.alias || ''}`; + + // Add mark for token node + if (typeof token === 'string' || typeof token.content === 'string') { + startText = endText; + startOffset = endOffset; + + const content = typeof token === 'string' ? token : token.content; + const newlines = content.split('\n').length - 1; + const length = content.length - newlines; + const end = start + length; + + let available = startText.text.length - startOffset; + let remaining = length; + + endOffset = startOffset + remaining; + + while (available < remaining) { + endText = texts.shift(); + remaining = length - available; + available = endText.text.length; + endOffset = remaining; + } + + // Inject marks from up the tree (acc) as well + if (typeof token !== 'string' || acc) { + const range = { + anchorKey: startText.key, + anchorOffset: startOffset, + focusKey: endText.key, + focusOffset: endOffset, + marks: [{ type: TOKEN_MARK, data: { types } }], + }; + + decorations.push(range); + } + + start = end; + } else if (token.content && token.content.length) { + // Tokens can be nested + for (const subToken of token.content) { + processToken(subToken, types); + } + } + } + + // Process top-level tokens + for (const token of tokens) { + processToken(token); + } + + return decorations; + }, + }; +} diff --git a/public/app/containers/Explore/slate-plugins/prism/promql.ts b/public/app/containers/Explore/slate-plugins/prism/promql.ts new file mode 100644 index 00000000000..0f0be18cb6f --- /dev/null +++ b/public/app/containers/Explore/slate-plugins/prism/promql.ts @@ -0,0 +1,123 @@ +export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without']; + +const AGGREGATION_OPERATORS = [ + 'sum', + 'min', + 'max', + 'avg', + 'stddev', + 'stdvar', + 'count', + 'count_values', + 'bottomk', + 'topk', + 'quantile', +]; + +export const FUNCTIONS = [ + ...AGGREGATION_OPERATORS, + 'abs', + 'absent', + 'ceil', + 'changes', + 'clamp_max', + 'clamp_min', + 'count_scalar', + 'day_of_month', + 'day_of_week', + 'days_in_month', + 'delta', + 'deriv', + 'drop_common_labels', + 'exp', + 'floor', + 'histogram_quantile', + 'holt_winters', + 'hour', + 'idelta', + 'increase', + 'irate', + 'label_replace', + 'ln', + 'log2', + 'log10', + 'minute', + 'month', + 'predict_linear', + 'rate', + 'resets', + 'round', + 'scalar', + 'sort', + 'sort_desc', + 'sqrt', + 'time', + 'vector', + 'year', + 'avg_over_time', + 'min_over_time', + 'max_over_time', + 'sum_over_time', + 'count_over_time', + 'quantile_over_time', + 'stddev_over_time', + 'stdvar_over_time', +]; + +const tokenizer = { + comment: { + pattern: /(^|[^\n])#.*/, + lookbehind: true, + }, + 'context-aggregation': { + pattern: /((by|without)\s*)\([^)]*\)/, // by () + lookbehind: true, + inside: { + 'label-key': { + pattern: /[^,\s][^,]*[^,\s]*/, + alias: 'attr-name', + }, + }, + }, + 'context-labels': { + pattern: /\{[^}]*(?=})/, + inside: { + 'label-key': { + pattern: /[a-z_]\w*(?=\s*(=|!=|=~|!~))/, + alias: 'attr-name', + }, + 'label-value': { + pattern: /"(?:\\.|[^\\"])*"/, + greedy: true, + alias: 'attr-value', + }, + }, + }, + function: new RegExp(`\\b(?:${FUNCTIONS.join('|')})(?=\\s*\\()`, 'i'), + 'context-range': [ + { + pattern: /\[[^\]]*(?=])/, // [1m] + inside: { + 'range-duration': { + pattern: /\b\d+[smhdwy]\b/i, + alias: 'number', + }, + }, + }, + { + pattern: /(offset\s+)\w+/, // offset 1m + lookbehind: true, + inside: { + 'range-duration': { + pattern: /\b\d+[smhdwy]\b/i, + alias: 'number', + }, + }, + }, + ], + number: /\b-?\d+((\.\d*)?([eE][+-]?\d+)?)?\b/, + operator: new RegExp(`/[-+*/=%^~]|&&?|\\|?\\||!=?|<(?:=>?|<|>)?|>[>=]?|\\b(?:${OPERATORS.join('|')})\\b`, 'i'), + punctuation: /[{};()`,.]/, +}; + +export default tokenizer; diff --git a/public/app/containers/Explore/slate-plugins/runner.ts b/public/app/containers/Explore/slate-plugins/runner.ts new file mode 100644 index 00000000000..44b5943c4a2 --- /dev/null +++ b/public/app/containers/Explore/slate-plugins/runner.ts @@ -0,0 +1,14 @@ +export default function RunnerPlugin({ handler }) { + return { + onKeyDown(event) { + // Handle enter + if (handler && event.key === 'Enter' && !event.shiftKey) { + // Submit on Enter + event.preventDefault(); + handler(event); + return true; + } + return undefined; + }, + }; +} diff --git a/public/app/containers/Explore/utils/debounce.ts b/public/app/containers/Explore/utils/debounce.ts new file mode 100644 index 00000000000..9f2bd35e116 --- /dev/null +++ b/public/app/containers/Explore/utils/debounce.ts @@ -0,0 +1,14 @@ +// Based on underscore.js debounce() +export default function debounce(func, wait) { + let timeout; + return function() { + const context = this; + const args = arguments; + const later = function() { + timeout = null; + func.apply(context, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} diff --git a/public/app/containers/Explore/utils/dom.ts b/public/app/containers/Explore/utils/dom.ts new file mode 100644 index 00000000000..6ba21b54c83 --- /dev/null +++ b/public/app/containers/Explore/utils/dom.ts @@ -0,0 +1,40 @@ +// Node.closest() polyfill +if ('Element' in window && !Element.prototype.closest) { + Element.prototype.closest = function(s) { + const matches = (this.document || this.ownerDocument).querySelectorAll(s); + let el = this; + let i; + // eslint-disable-next-line + do { + i = matches.length; + // eslint-disable-next-line + while (--i >= 0 && matches.item(i) !== el) {} + } while (i < 0 && (el = el.parentElement)); + return el; + }; +} + +export function getPreviousCousin(node, selector) { + let sibling = node.parentElement.previousSibling; + let el; + while (sibling) { + el = sibling.querySelector(selector); + if (el) { + return el; + } + sibling = sibling.previousSibling; + } + return undefined; +} + +export function getNextCharacter(global = window) { + const selection = global.getSelection(); + if (!selection.anchorNode) { + return null; + } + + const range = selection.getRangeAt(0); + const text = selection.anchorNode.textContent; + const offset = range.startOffset; + return text.substr(offset, 1); +} diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts new file mode 100644 index 00000000000..30f9c25b8f7 --- /dev/null +++ b/public/app/containers/Explore/utils/prometheus.ts @@ -0,0 +1,20 @@ +export const RATE_RANGES = ['1m', '5m', '10m', '30m', '1h']; + +export function processLabels(labels) { + const values = {}; + labels.forEach(l => { + const { __name__, ...rest } = l; + Object.keys(rest).forEach(key => { + if (!values[key]) { + values[key] = []; + } + if (values[key].indexOf(rest[key]) === -1) { + values[key].push(rest[key]); + } + }); + }); + return { values, keys: Object.keys(values) }; +} + +// Strip syntax chars +export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 4f4b3a64fa5..89f25776a40 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -8,11 +8,23 @@ import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; +import { BackendSrv } from 'app/core/services/backend_srv'; +import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; export class GrafanaCtrl { /** @ngInject */ - constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, bridgeSrv, backendSrv) { - createStore(backendSrv); + constructor( + $scope, + alertSrv, + utilSrv, + $rootScope, + $controller, + contextSrv, + bridgeSrv, + backendSrv: BackendSrv, + datasourceSrv: DatasourceSrv + ) { + createStore({ backendSrv, datasourceSrv }); $scope.init = function() { $scope.contextSrv = contextSrv; diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index fb7a9ece37a..aef43a4760b 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -15,7 +15,7 @@ export class DatasourceSrv { this.datasources = {}; } - get(name) { + get(name?) { if (!name) { return this.get(config.defaultDatasource); } diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index b7613d9474d..deb16f68bf7 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -1,8 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Provider } from 'mobx-react'; + import coreModule from 'app/core/core_module'; import { store } from 'app/stores/store'; -import { Provider } from 'mobx-react'; +import { BackendSrv } from 'app/core/services/backend_srv'; +import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; function WrapInProvider(store, Component, props) { return ( @@ -13,14 +16,15 @@ function WrapInProvider(store, Component, props) { } /** @ngInject */ -export function reactContainer($route, $location, backendSrv) { +export function reactContainer($route, $location, backendSrv: BackendSrv, datasourceSrv: DatasourceSrv) { return { restrict: 'E', template: '', link(scope, elem) { - let component = $route.current.locals.component; + let component = $route.current.locals.component.default; let props = { backendSrv: backendSrv, + datasourceSrv: datasourceSrv, }; ReactDOM.render(WrapInProvider(store, component, props), elem[0]); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index d9732256c2b..49690561728 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -1,7 +1,9 @@ import './dashboard_loaders'; import './ReactContainer'; + import ServerStats from 'app/containers/ServerStats/ServerStats'; import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList'; +// import Explore from 'app/containers/Explore/Explore'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; @@ -109,6 +111,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'FolderDashboardsCtrl', controllerAs: 'ctrl', }) + .when('/explore', { + template: '', + resolve: { + component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Explore'), + }, + }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller: 'OrgDetailsCtrl', diff --git a/public/app/stores/store.ts b/public/app/stores/store.ts index 8ad53607ac2..dfbd8141198 100644 --- a/public/app/stores/store.ts +++ b/public/app/stores/store.ts @@ -3,11 +3,11 @@ import config from 'app/core/config'; export let store: IRootStore; -export function createStore(backendSrv) { +export function createStore(services) { store = RootStore.create( {}, { - backendSrv: backendSrv, + ...services, navTree: config.bootData.navTree, } ); diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 36072fe8929..afc869f8b15 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -104,5 +104,6 @@ @import 'pages/signup'; @import 'pages/styleguide'; @import 'pages/errorpage'; +@import 'pages/explore'; @import 'old_responsive'; @import 'components/view_states.scss'; diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index c80d461541e..faa5b94d4ad 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -23,6 +23,13 @@ @include clearfix(); } +.page-full { + margin-left: $page-sidebar-margin; + padding-left: $spacer; + padding-right: $spacer; + @include clearfix(); +} + .scroll-canvas { position: absolute; width: 100%; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss new file mode 100644 index 00000000000..4bd0162563b --- /dev/null +++ b/public/sass/pages/_explore.scss @@ -0,0 +1,304 @@ +.explore { + .graph-legend { + flex-wrap: wrap; + } +} + +.query-field { + font-size: 14px; + font-family: Consolas, Menlo, Courier, monospace; + height: auto; +} + +.query-field-wrapper { + position: relative; + display: inline-block; + padding: 6px 7px 4px; + width: calc(100% - 6rem); + cursor: text; + line-height: 1.5; + color: rgba(0, 0, 0, 0.65); + background-color: #fff; + background-image: none; + border: 1px solid lightgray; + border-radius: 4px; + transition: all 0.3s; +} + +.typeahead { + position: absolute; + z-index: auto; + top: -10000px; + left: -10000px; + opacity: 0; + border-radius: 4px; + transition: opacity 0.75s; + border: 1px solid #e4e4e4; + max-height: calc(66vh); + overflow-y: scroll; + max-width: calc(66%); + overflow-x: hidden; + outline: none; + list-style: none; + background: #fff; + color: rgba(0, 0, 0, 0.65); + transition: opacity 0.4s ease-out; +} + +.typeahead-group__title { + color: rgba(0, 0, 0, 0.43); + font-size: 12px; + line-height: 1.5; + padding: 8px 16px; +} + +.typeahead-item { + line-height: 200%; + height: auto; + font-family: Consolas, Menlo, Courier, monospace; + padding: 0 16px 0 28px; + font-size: 12px; + text-overflow: ellipsis; + overflow: hidden; + margin-left: -1px; + left: 1px; + position: relative; + z-index: 1; + display: block; + white-space: nowrap; + cursor: pointer; + transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1); +} + +.typeahead-item__selected { + background-color: #ecf6fd; + color: #108ee9; +} + +/* SYNTAX */ + +/** + * prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML + * Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics); + * @author Tim Shedor + */ + +code[class*='language-'], +pre[class*='language-'] { + color: black; + background: none; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*='language-'] { + position: relative; + margin: 0.5em 0; + overflow: visible; + padding: 0; +} +pre[class*='language-'] > code { + position: relative; + border-left: 10px solid #358ccb; + box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf; + background-color: #fdfdfd; + background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%); + background-size: 3em 3em; + background-origin: content-box; + background-attachment: local; +} + +code[class*='language'] { + max-height: inherit; + height: inherit; + padding: 0 1em; + display: block; + overflow: auto; +} + +/* Margin bottom to accomodate shadow */ +:not(pre) > code[class*='language-'], +pre[class*='language-'] { + background-color: #fdfdfd; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin-bottom: 1em; +} + +/* Inline code */ +:not(pre) > code[class*='language-'] { + position: relative; + padding: 0.2em; + border-radius: 0.3em; + color: #c92c2c; + border: 1px solid rgba(0, 0, 0, 0.1); + display: inline; + white-space: normal; +} + +pre[class*='language-']:before, +pre[class*='language-']:after { + content: ''; + z-index: -2; + display: block; + position: absolute; + bottom: 0.75em; + left: 0.18em; + width: 40%; + height: 20%; + max-height: 13em; + box-shadow: 0px 13px 8px #979797; + -webkit-transform: rotate(-2deg); + -moz-transform: rotate(-2deg); + -ms-transform: rotate(-2deg); + -o-transform: rotate(-2deg); + transform: rotate(-2deg); +} + +:not(pre) > code[class*='language-']:after, +pre[class*='language-']:after { + right: 0.75em; + left: auto; + -webkit-transform: rotate(2deg); + -moz-transform: rotate(2deg); + -ms-transform: rotate(2deg); + -o-transform: rotate(2deg); + transform: rotate(2deg); +} + +.token.comment, +.token.block-comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #7d8b99; +} + +.token.punctuation { + color: #5f6364; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.function-name, +.token.constant, +.token.symbol, +.token.deleted { + color: #c92c2c; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.function, +.token.builtin, +.token.inserted { + color: #2f9c0a; +} + +.token.operator, +.token.entity, +.token.url, +.token.variable { + color: #a67f59; + background: rgba(255, 255, 255, 0.5); +} + +.token.atrule, +.token.attr-value, +.token.keyword, +.token.class-name { + color: #1990b8; +} + +.token.regex, +.token.important { + color: #e90; +} + +.language-css .token.string, +.style .token.string { + color: #a67f59; + background: rgba(255, 255, 255, 0.5); +} + +.token.important { + font-weight: normal; +} + +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.namespace { + opacity: 0.7; +} + +@media screen and (max-width: 767px) { + pre[class*='language-']:before, + pre[class*='language-']:after { + bottom: 14px; + box-shadow: none; + } +} + +/* Plugin styles */ +.token.tab:not(:empty):before, +.token.cr:before, +.token.lf:before { + color: #e0d7d1; +} + +/* Plugin styles: Line Numbers */ +pre[class*='language-'].line-numbers { + padding-left: 0; +} + +pre[class*='language-'].line-numbers code { + padding-left: 3.8em; +} + +pre[class*='language-'].line-numbers .line-numbers-rows { + left: 0; +} + +/* Plugin styles: Line Highlight */ +pre[class*='language-'][data-line] { + padding-top: 0; + padding-bottom: 0; + padding-left: 0; +} +pre[data-line] code { + position: relative; + padding-left: 4em; +} +pre .line-highlight { + margin-top: 0; +} diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index ab06967364a..26af661bf9d 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -71,6 +71,7 @@ module.exports = merge(common, { loader: 'babel-loader', options: { plugins: [ + 'syntax-dynamic-import', 'react-hot-loader/babel', ], }, diff --git a/scripts/webpack/webpack.prod.js b/scripts/webpack/webpack.prod.js index f55de9ec5b3..c01a45adc03 100644 --- a/scripts/webpack/webpack.prod.js +++ b/scripts/webpack/webpack.prod.js @@ -36,7 +36,12 @@ module.exports = merge(common, { test: /\.tsx?$/, exclude: /node_modules/, use: [ - { loader: "awesome-typescript-loader" } + { + loader: 'awesome-typescript-loader', + options: { + errorsAsWarnings: false, + }, + }, ] }, require('./sass.rule.js')({ diff --git a/yarn.lock b/yarn.lock index 35287b2fc39..f23d44867f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,24 +3,30 @@ "@babel/code-frame@^7.0.0-beta.35": - version "7.0.0-beta.36" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.36.tgz#2349d7ec04b3a06945ae173280ef8579b63728e4" + version "7.0.0-beta.46" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.46.tgz#e0d002100805daab1461c0fcb32a07e304f3a4f4" + dependencies: + "@babel/highlight" "7.0.0-beta.46" + +"@babel/highlight@7.0.0-beta.46": + version "7.0.0-beta.46" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.46.tgz#c553c51e65f572bdedd6eff66fc0bb563016645e" dependencies: chalk "^2.0.0" esutils "^2.0.2" js-tokens "^3.0.0" "@types/cheerio@*": - version "0.22.5" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.5.tgz#db749e8470d98f103d51407db9bee5a8b9d20d45" + version "0.22.7" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.7.tgz#4a92eafedfb2b9f4437d3a4410006d81114c66ce" "@types/d3-array@*": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.1.tgz#e489605208d46a1c9d980d2e5772fa9c75d9ec65" "@types/d3-axis@*": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.9.tgz#62ce7bc8d04354298cda57f3f1d1f856ad69b89a" + version "1.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-1.0.10.tgz#41d6b3ea9032f9531ec0d71d83bcf49294511210" dependencies: "@types/d3-selection" "*" @@ -35,12 +41,12 @@ resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.6.tgz#0589eb97a3191f4edaf17b7bde498462890ce1ec" "@types/d3-collection@*": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.5.tgz#bb1f3aa97cdc8d881645541b9d6cf87edfee9bc3" + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-collection/-/d3-collection-1.0.6.tgz#0a5a87fe241fcbd253a637d024b4d8c55f84a369" "@types/d3-color@*": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.0.5.tgz#cad755f0fc6de7b70fa6e5e08afa81ef4c2248de" + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.0.6.tgz#6e955f739c3f92bf94e9e3a8cfa2806734244b60" "@types/d3-dispatch@*": version "1.0.5" @@ -65,12 +71,12 @@ resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-1.1.0.tgz#40925ca3512b63bd424f7c9685e1781b5b0a1d7e" "@types/d3-format@*": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.2.1.tgz#9435fb1771d2fbf6a858c93218f4097c9aa396c1" + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.2.2.tgz#bc60b936bd3cc805225ab4423081eb218e6d1db0" "@types/d3-geo@*": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.9.3.tgz#742ceafa808c6853affccfb11f956cfc8bdccecb" + version "1.10.1" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-1.10.1.tgz#a70732541adde9d2cfcf705ff58622cf4a6819e3" dependencies: "@types/geojson" "*" @@ -110,19 +116,19 @@ dependencies: "@types/d3-dsv" "*" -"@types/d3-scale@*": - version "1.0.10" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-1.0.10.tgz#8c5c1dca54a159eed042b46719dbb3bdb7e8c842" +"@types/d3-scale@^1": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-1.0.12.tgz#f6300e886ce38dc8834172a9ce4a2cbfad74d029" dependencies: "@types/d3-time" "*" "@types/d3-selection@*": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.2.0.tgz#f0a4cca0a0e4187c336c6712a82600cdcd24093f" + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.3.0.tgz#acede3d22c18ec085cc401d4fdab9f040e1a73c7" "@types/d3-shape@*": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.1.tgz#cac2d9f0122f173220c32c8c152dc42ee9349df2" + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.2.2.tgz#f8dcdff7772a7ae37858bf04abd43848a78e590e" dependencies: "@types/d3-path" "*" @@ -156,8 +162,8 @@ "@types/d3-selection" "*" "@types/d3@^4.10.1": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@types/d3/-/d3-4.12.0.tgz#445ede4ab7707db1a011ef43b2bd187d21bdaffc" + version "4.13.0" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-4.13.0.tgz#aae092b368266409cfbf19c611203145ec0d5f65" dependencies: "@types/d3-array" "*" "@types/d3-axis" "*" @@ -180,7 +186,7 @@ "@types/d3-queue" "*" "@types/d3-random" "*" "@types/d3-request" "*" - "@types/d3-scale" "*" + "@types/d3-scale" "^1" "@types/d3-selection" "*" "@types/d3-shape" "*" "@types/d3-time" "*" @@ -198,31 +204,33 @@ "@types/react" "*" "@types/geojson@*": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-1.0.6.tgz#3e02972728c69248c2af08d60a48cbb8680fffdf" + version "7946.0.2" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.2.tgz#0bd0a01ef04e813c2b7580318da9e37c2eadea9c" "@types/jest@^21.1.4": - version "21.1.8" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.8.tgz#d497213725684f1e5a37900b17a47c9c018f1a97" + version "21.1.10" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.10.tgz#dcacb5217ddf997a090cc822bba219b4b2fd7984" "@types/node@*": - version "8.5.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.2.tgz#83b8103fa9a2c2e83d78f701a9aa7c9539739aa5" + version "9.6.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.6.tgz#439b91f9caf3983cad2eef1e11f6bedcbf9431d2" "@types/node@^8.0.31": - version "8.0.53" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.53.tgz#396b35af826fa66aad472c8cb7b8d5e277f4e6d8" + version "8.10.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.10.tgz#fec07bc2ad549d9e6d2f7aa0fb0be3491b83163a" "@types/react-dom@^16.0.3": - version "16.0.3" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64" + version "16.0.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.5.tgz#a757457662e3819409229e8f86795ff37b371f96" dependencies: "@types/node" "*" "@types/react" "*" "@types/react@*", "@types/react@^16.0.25": - version "16.0.25" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.25.tgz#bf696b83fe480c5e0eff4335ee39ebc95884a1ed" + version "16.3.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.12.tgz#68d9146f3e9797e38ffbf22f7ed1dde91a2cfd2e" + dependencies: + csstype "^2.2.0" "@types/tapable@^0": version "0.2.5" @@ -235,26 +243,26 @@ source-map "^0.6.1" "@types/webpack@^3.0.5": - version "3.8.11" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-3.8.11.tgz#df2d7f8db43dbc15b4e8ecbdc91e6f68ed6b83ab" + version "3.8.12" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-3.8.12.tgz#c5db4f273fb8f2a4929db6c486e19e68c350e7ac" dependencies: "@types/node" "*" "@types/tapable" "^0" "@types/uglify-js" "*" source-map "^0.6.0" -JSONStream@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" +JSONStream@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea" dependencies: jsonparse "^1.2.0" through ">=2.2.7 <3" -"JSV@>= 4.0.x": +JSV@^4.0.x: version "4.0.2" resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57" -abab@^1.0.3: +abab@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -269,14 +277,7 @@ accepts@1.3.3: mime-types "~2.1.11" negotiator "0.6.1" -accepts@~1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" - dependencies: - mime-types "~2.1.16" - negotiator "0.6.1" - -accepts@~1.3.5: +accepts@~1.3.4, accepts@~1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" dependencies: @@ -293,7 +294,7 @@ acorn-es7-plugin@^1.0.12: version "1.1.7" resolved "https://registry.yarnpkg.com/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz#f2ee1f3228a90eead1245f9ab1922eb2e71d336b" -acorn-globals@^4.0.0: +acorn-globals@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538" dependencies: @@ -313,13 +314,9 @@ acorn@^4.0.0, acorn@^4.0.3: version "4.0.13" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" -acorn@^5.0.0, acorn@^5.1.1, acorn@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" - -acorn@^5.1.2: - version "5.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822" +acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0: + version "5.5.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" acorn@~2.6.4: version "2.6.4" @@ -330,14 +327,14 @@ after@0.8.2: resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" agent-base@4, agent-base@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.1.2.tgz#80fa6cde440f4dcf9af2617cf246099b5d99f0c8" + version "4.2.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" dependencies: es6-promisify "^5.0.0" agentkeepalive@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.3.0.tgz#6d5de5829afd3be2712201a39275fd11c651857c" + version "3.4.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c" dependencies: humanize-ms "^1.2.1" @@ -345,24 +342,20 @@ ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" -ajv-keywords@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" - ajv-keywords@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" -ajv@^4.7.0, ajv@^4.9.1: +ajv@^4.7.0: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" dependencies: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5: - version "5.5.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.0.tgz#eb2840746e9dc48bd5e063a36e3fd400c5eab5a9" +ajv@^5.0.0, ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" @@ -405,24 +398,24 @@ angular-bindonce@^0.3.1: resolved "https://registry.yarnpkg.com/angular-bindonce/-/angular-bindonce-0.3.1.tgz#af19574abd43f608b9236a302cc5ce49d71dc9c6" angular-mocks@^1.6.6: - version "1.6.7" - resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.6.7.tgz#85bf45a2537eac59fc6f4cf319846102e8000e65" + version "1.6.10" + resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.6.10.tgz#6a139e43c461d0c9a5a1acebc91e63db16031176" angular-native-dragdrop@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/angular-native-dragdrop/-/angular-native-dragdrop-1.2.2.tgz#d646c6b75b131c48073c3f6e36a225b2726d8bae" angular-route@^1.6.6: - version "1.6.7" - resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.7.tgz#020970d93d8b2ce4ca6aff0e0d7922579543cbcf" + version "1.6.10" + resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.10.tgz#4247a32eab19495624623e96c1626dfba17ebf21" angular-sanitize@^1.6.6: - version "1.6.7" - resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.7.tgz#5a3d61ad7b8b699923329635d99248bcfce26408" + version "1.6.10" + resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.10.tgz#635a362afb2dd040179f17d3a5455962b2c1918f" angular@^1.6.6: - version "1.6.7" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.7.tgz#0f89837dae1776b01ccb1fa2096db0d9373d9897" + version "1.6.10" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.10.tgz#eed3080a34d29d0f681ff119b18ce294e3f74826" ansi-align@^2.0.0: version "2.0.0" @@ -435,8 +428,8 @@ ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" ansi-escapes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" ansi-html@0.0.7: version "0.0.7" @@ -454,9 +447,9 @@ 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.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" dependencies: color-convert "^1.9.0" @@ -541,8 +534,8 @@ are-we-there-yet@~1.1.2: readable-stream "^2.0.6" argparse@^1.0.2, argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" dependencies: sprintf-js "~1.0.2" @@ -649,8 +642,8 @@ asap@^2.0.0, asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" asn1.js@^4.0.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" dependencies: bn.js "^4.0.0" inherits "^2.0.1" @@ -706,23 +699,19 @@ async@^1.4.0, async@^1.5.2, async@~1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.1.5, async@^2.4.1: +async@^2.0.0, async@^2.1.2, async@^2.1.4, async@^2.4.1: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" dependencies: lodash "^4.14.0" -async@~0.9.0: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" atob@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" + version "2.1.0" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.0.tgz#ab2b150e51d7b122b9efc8d7340c06b6c41076bc" autolinker@~0.15.0: version "0.15.3" @@ -740,17 +729,16 @@ autoprefixer@^6.3.1, autoprefixer@^6.4.0: postcss-value-parser "^3.2.3" awesome-typescript-loader@^3.2.3: - version "3.4.0" - resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-3.4.0.tgz#aed2c83af614d617d11e3ec368ac3befb55d002f" + version "3.5.0" + resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-3.5.0.tgz#4d4d10cba7a04ed433dfa0334250846fb11a1a5a" dependencies: - colors "^1.1.2" + chalk "^2.3.1" enhanced-resolve "3.3.0" loader-utils "^1.1.0" lodash "^4.17.4" micromatch "^3.0.3" mkdirp "^0.5.1" - object-assign "^4.1.1" - source-map-support "^0.4.15" + source-map-support "^0.5.3" aws-sign2@~0.6.0: version "0.6.0" @@ -761,8 +749,8 @@ aws-sign2@~0.7.0: resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" aws4@^1.2.1, aws4@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + version "1.7.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" axios@^0.17.1: version "0.17.1" @@ -771,7 +759,7 @@ axios@^0.17.1: follow-redirects "^1.2.5" is-buffer "^1.1.5" -babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" dependencies: @@ -779,7 +767,7 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-core@^6.0.0, babel-core@^6.24.1, babel-core@^6.26.0: +babel-core@^6.0.0, babel-core@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" dependencies: @@ -804,8 +792,8 @@ babel-core@^6.0.0, babel-core@^6.24.1, babel-core@^6.26.0: source-map "^0.5.6" babel-generator@^6.18.0, babel-generator@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" dependencies: babel-messages "^6.23.0" babel-runtime "^6.26.0" @@ -813,7 +801,7 @@ babel-generator@^6.18.0, babel-generator@^6.26.0: detect-indent "^4.0.0" jsesc "^1.3.0" lodash "^4.17.4" - source-map "^0.5.6" + source-map "^0.5.7" trim-right "^1.0.1" babel-helper-call-delegate@^6.24.1: @@ -891,16 +879,16 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-jest@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.0.4.tgz#533c46de37d7c9d7612f408c76314be9277e0c26" +babel-jest@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.4.3.tgz#4b7a0b6041691bbd422ab49b3b73654a49a6627a" dependencies: babel-plugin-istanbul "^4.1.5" - babel-preset-jest "^22.0.3" + babel-preset-jest "^22.4.3" babel-loader@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" + version "7.1.4" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015" dependencies: find-cache-dir "^1.0.0" loader-utils "^1.0.2" @@ -919,16 +907,21 @@ babel-plugin-check-es2015-constants@^6.22.0: babel-runtime "^6.22.0" babel-plugin-istanbul@^4.1.4, babel-plugin-istanbul@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e" + version "4.1.6" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" dependencies: + babel-plugin-syntax-object-rest-spread "^6.13.0" find-up "^2.1.0" - istanbul-lib-instrument "^1.7.5" - test-exclude "^4.1.1" + istanbul-lib-instrument "^1.10.1" + test-exclude "^4.2.1" -babel-plugin-jest-hoist@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.0.3.tgz#62cde5fe962fd41ae89c119f481ca5cd7dd48bb4" +babel-plugin-jest-hoist@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.4.3.tgz#7d8bcccadc2667f96a0dcc6afe1891875ee6c14a" + +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" babel-plugin-syntax-object-rest-spread@^6.13.0: version "6.13.0" @@ -1018,7 +1011,7 @@ babel-plugin-transform-es2015-modules-amd@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-plugin-transform-es2015-modules-commonjs@^6.24.1: +babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" dependencies: @@ -1152,11 +1145,11 @@ babel-preset-es2015@^6.24.1: babel-plugin-transform-es2015-unicode-regex "^6.24.1" babel-plugin-transform-regenerator "^6.24.1" -babel-preset-jest@^22.0.1, babel-preset-jest@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.0.3.tgz#e2bb6f6b4a509d3ea0931f013db78c5a84856693" +babel-preset-jest@^22.4.0, babel-preset-jest@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.4.3.tgz#e92eef9813b7026ab4ca675799f37419b5a44156" dependencies: - babel-plugin-jest-hoist "^22.0.3" + babel-plugin-jest-hoist "^22.4.3" babel-plugin-syntax-object-rest-spread "^6.13.0" babel-register@^6.26.0: @@ -1171,7 +1164,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: +babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -1236,8 +1229,8 @@ base64-arraybuffer@0.1.5: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" base64-js@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + version "1.3.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" base64id@1.0.0: version "1.0.0" @@ -1279,23 +1272,38 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" +bfj-node4@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/bfj-node4/-/bfj-node4-5.3.1.tgz#e23d8b27057f1d0214fc561142ad9db998f26830" + dependencies: + bluebird "^3.5.1" + check-types "^7.3.0" + tryer "^1.0.0" + big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" +bin-links@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-1.1.2.tgz#fb74bd54bae6b7befc6c6221f25322ac830d9757" + dependencies: + bluebird "^3.5.0" + cmd-shim "^2.0.2" + gentle-fs "^2.0.0" + graceful-fs "^4.1.11" + write-file-atomic "^2.3.0" + binary-extensions@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" -bindings@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" - bl@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" + version "1.2.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" dependencies: - readable-stream "^2.0.5" + readable-stream "^2.3.5" + safe-buffer "^5.1.1" blob@0.0.4: version "0.0.4" @@ -1307,7 +1315,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@^3.3.0, bluebird@^3.4.7, bluebird@^3.5.0, bluebird@~3.5.0: +bluebird@^3.3.0, bluebird@^3.4.7, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@~3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1363,9 +1371,9 @@ boom@5.x.x: dependencies: hoek "4.x.x" -boxen@^1.0.0, boxen@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.2.2.tgz#3f1d4032c30ffea9d4b02c322eaf2ea741dcbce5" +boxen@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" dependencies: ansi-align "^2.0.0" camelcase "^4.0.0" @@ -1373,11 +1381,11 @@ boxen@^1.0.0, boxen@^1.2.1: cli-boxes "^1.0.0" string-width "^2.0.0" term-size "^1.2.0" - widest-line "^1.0.0" + widest-line "^2.0.0" brace-expansion@^1.0.0, brace-expansion@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -1402,23 +1410,7 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" -braces@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e" - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - define-property "^1.0.0" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@^2.3.1: +braces@^2.3.0, braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" dependencies: @@ -1452,8 +1444,8 @@ browser-stdout@1.3.0: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" dependencies: buffer-xor "^1.0.3" cipher-base "^1.0.0" @@ -1463,16 +1455,16 @@ browserify-aes@^1.0.0, browserify-aes@^1.0.4: safe-buffer "^5.0.1" browserify-cipher@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" dependencies: browserify-aes "^1.0.4" browserify-des "^1.0.0" evp_bytestokey "^1.0.0" browserify-des@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c" dependencies: cipher-base "^1.0.1" des.js "^1.0.0" @@ -1526,6 +1518,10 @@ buffer-crc32@^0.2.1: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-from@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" + buffer-indexof@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" @@ -1554,63 +1550,31 @@ builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" -cacache@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.1.tgz#3e05f6e616117d9b54665b1b20c8aeb93ea5d36f" +cacache@^10.0.0, cacache@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" dependencies: - bluebird "^3.5.0" + bluebird "^3.5.1" chownr "^1.0.1" glob "^7.1.2" graceful-fs "^4.1.11" lru-cache "^4.1.1" - mississippi "^1.3.0" + mississippi "^2.0.0" mkdirp "^0.5.1" move-concurrently "^1.0.1" promise-inflight "^1.0.1" - rimraf "^2.6.1" - ssri "^5.0.0" + rimraf "^2.6.2" + ssri "^5.2.4" unique-filename "^1.1.0" - y18n "^3.2.1" - -cacache@^9.2.9: - version "9.3.0" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.3.0.tgz#9cd58f2dd0b8c8cacf685b7067b416d6d3cf9db1" - dependencies: - bluebird "^3.5.0" - chownr "^1.0.1" - glob "^7.1.2" - graceful-fs "^4.1.11" - lru-cache "^4.1.1" - mississippi "^1.3.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.1" - ssri "^4.1.6" - unique-filename "^1.1.0" - y18n "^3.2.1" - -cacache@~9.2.9: - version "9.2.9" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.2.9.tgz#f9d7ffe039851ec94c28290662afa4dd4bb9e8dd" - dependencies: - bluebird "^3.5.0" - chownr "^1.0.1" - glob "^7.1.2" - graceful-fs "^4.1.11" - lru-cache "^4.1.1" - mississippi "^1.3.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.1" - ssri "^4.1.6" - unique-filename "^1.1.0" - y18n "^3.2.1" + y18n "^4.0.0" cache-base@^1.0.1: version "1.0.1" @@ -1692,8 +1656,8 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000772" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b" + version "1.0.30000830" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000830.tgz#6e45255b345649fd15ff59072da1e12bb3de2f13" capture-stack-trace@^1.0.0: version "1.0.0" @@ -1714,10 +1678,6 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chain-function@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" - chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -1728,13 +1688,13 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" dependencies: - ansi-styles "^3.1.0" + ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" - supports-color "^4.0.0" + supports-color "^5.3.0" chalk@~0.4.0: version "0.4.0" @@ -1745,8 +1705,8 @@ chalk@~0.4.0: strip-ansi "~0.1.0" change-case@3.0.x: - version "3.0.1" - resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.0.1.tgz#ee5f5ad0415ad1ad9e8072cf49cd4cfa7660a554" + version "3.0.2" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.0.2.tgz#fd48746cce02f03f0a672577d1d3a8dc2eceb037" dependencies: camel-case "^3.0.0" constant-case "^2.0.0" @@ -1756,7 +1716,7 @@ change-case@3.0.x: is-upper-case "^1.1.0" lower-case "^1.1.1" lower-case-first "^1.0.0" - no-case "^2.2.0" + no-case "^2.3.2" param-case "^2.1.0" pascal-case "^2.0.0" path-case "^2.1.0" @@ -1767,6 +1727,10 @@ change-case@3.0.x: upper-case "^1.1.1" upper-case-first "^1.1.0" +check-types@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d" + cheerio@^1.0.0-rc.2: version "1.0.0-rc.2" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db" @@ -1778,7 +1742,7 @@ cheerio@^1.0.0-rc.2: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@^1.4.1, chokidar@^1.6.0, chokidar@^1.7.0: +chokidar@^1.4.1, chokidar@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -1793,7 +1757,7 @@ chokidar@^1.4.1, chokidar@^1.6.0, chokidar@^1.7.0: optionalDependencies: fsevents "^1.0.0" -chokidar@^2.0.0: +chokidar@^2.0.0, chokidar@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176" dependencies: @@ -1816,8 +1780,8 @@ chownr@^1.0.1, chownr@~1.0.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" ci-info@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4" + version "1.1.3" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.3.tgz#710193264bb05c77b8c90d02f5aaf22216a667b2" cidr-regex@1.0.6: version "1.0.6" @@ -1841,13 +1805,12 @@ clap@^1.0.9: chalk "^1.1.3" class-utils@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.5.tgz#17e793103750f9627b2176ea34cfd1b565903c80" + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" dependencies: arr-union "^3.1.0" define-property "^0.2.5" isobject "^3.0.0" - lazy-cache "^2.0.2" static-extend "^0.1.1" classnames@2.x, classnames@^2.2.4, classnames@^2.2.5: @@ -1862,8 +1825,8 @@ clean-css@3.4.x, clean-css@~3.4.2: source-map "0.4.x" clean-css@4.1.x: - version "4.1.9" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.9.tgz#35cee8ae7687a49b98034f70de00c4edd3826301" + version "4.1.11" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.11.tgz#2ecdf145aba38f54740f26cefd0ff3e03e125d6a" dependencies: source-map "0.5.x" @@ -1928,6 +1891,14 @@ clipboard@^1.7.1: select "^1.1.2" tiny-emitter "^2.0.0" +clipboard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.0.tgz#4661dc972fb72a4c4770b8db78aa9b1caef52b50" + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -1944,24 +1915,32 @@ cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" -clone-deep@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8" +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +clone-deep@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" dependencies: for-own "^1.0.0" - is-plain-object "^2.0.1" - kind-of "^3.2.2" - shallow-clone "^0.1.2" + is-plain-object "^2.0.4" + kind-of "^6.0.0" + shallow-clone "^1.0.0" clone@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" clone@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" -cmd-shim@~2.0.2: +cmd-shim@^2.0.2, cmd-shim@~2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" dependencies: @@ -2037,7 +2016,11 @@ colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" -colors@^1.1.0, colors@^1.1.2, colors@~1.1.2: +colors@^1.1.0, colors@^1.1.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794" + +colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -2054,15 +2037,15 @@ combine-lists@^1.0.0: dependencies: lodash "^4.5.0" -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" +combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" dependencies: delayed-stream "~1.0.0" -commander@2, commander@2.12.x, commander@^2.11.0, commander@^2.8.1, commander@^2.9.0, commander@~2.12.1: - version "2.12.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.1.tgz#468635c4168d06145b9323356d1da84d14ac4a7a" +commander@2, commander@2.15.x, commander@^2.11.0, commander@^2.12.1, commander@^2.13.0, commander@^2.8.1, commander@^2.9.0, commander@~2.15.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" commander@2.11.0: version "2.11.0" @@ -2090,6 +2073,10 @@ commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" +compare-versions@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.1.0.tgz#43310256a5c555aaed4193c04d8f154cf9c6efd5" + component-bind@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" @@ -2137,7 +2124,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@1.6.0, concat-stream@^1.4.1, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.5.2: +concat-stream@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" dependencies: @@ -2145,6 +2132,15 @@ concat-stream@1.6.0, concat-stream@^1.4.1, concat-stream@^1.4.6, concat-stream@^ readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@^1.4.1, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + config-chain@~1.1.11: version "1.1.11" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" @@ -2153,8 +2149,8 @@ config-chain@~1.1.11: proto-list "~1.2.1" configstore@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90" + version "3.1.2" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" dependencies: dot-prop "^4.1.0" graceful-fs "^4.1.2" @@ -2168,11 +2164,11 @@ connect-history-api-fallback@^1.3.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" connect@^3.6.0: - version "3.6.5" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.5.tgz#fb8dde7ba0763877d0ec9df9dac0b4b40e72c7da" + version "3.6.6" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524" dependencies: debug "2.6.9" - finalhandler "1.0.6" + finalhandler "1.1.0" parseurl "~1.3.2" utils-merge "1.0.1" @@ -2201,10 +2197,6 @@ content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" -content-type-parser@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" - content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -2244,13 +2236,9 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.0.0: - version "2.5.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" - -core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" +core-js@^2.0.0, core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0: + version "2.5.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b" core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -2268,13 +2256,13 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: parse-json "^2.2.0" require-from-string "^1.1.0" -cosmiconfig@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" +cosmiconfig@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc" dependencies: is-directory "^0.3.1" js-yaml "^3.9.0" - parse-json "^3.0.0" + parse-json "^4.0.0" require-from-string "^2.0.1" cpx@^1.5.0: @@ -2305,8 +2293,8 @@ crc@^3.4.4: resolved "https://registry.yarnpkg.com/crc/-/crc-3.5.0.tgz#98b8ba7d489665ba3979f59b21381374101a1964" create-ecdh@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + version "4.0.1" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.1.tgz#44223dfed533193ba5ba54e0df5709b89acf1f82" dependencies: bn.js "^4.1.0" elliptic "^6.0.0" @@ -2318,17 +2306,18 @@ create-error-class@^3.0.0: capture-stack-trace "^1.0.0" create-hash@^1.1.0, create-hash@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" dependencies: cipher-base "^1.0.1" inherits "^2.0.1" - ripemd160 "^2.0.0" + md5.js "^1.3.4" + ripemd160 "^2.0.1" sha.js "^2.4.0" create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: - version "1.1.6" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" dependencies: cipher-base "^1.0.3" create-hash "^1.1.0" @@ -2389,21 +2378,21 @@ css-color-names@0.0.4: resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" css-loader@^0.28.7: - version "0.28.7" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.7.tgz#5f2ee989dd32edd907717f953317656160999c1b" + version "0.28.11" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7" dependencies: - babel-code-frame "^6.11.0" + babel-code-frame "^6.26.0" css-selector-tokenizer "^0.7.0" - cssnano ">=2.6.1 <4" + cssnano "^3.10.0" icss-utils "^2.1.0" loader-utils "^1.0.2" lodash.camelcase "^4.3.0" - object-assign "^4.0.1" + object-assign "^4.1.1" postcss "^5.0.6" - postcss-modules-extract-imports "^1.0.0" - postcss-modules-local-by-default "^1.0.1" - postcss-modules-scope "^1.0.0" - postcss-modules-values "^1.1.0" + postcss-modules-extract-imports "^1.2.0" + postcss-modules-local-by-default "^1.2.0" + postcss-modules-scope "^1.1.0" + postcss-modules-values "^1.3.0" postcss-value-parser "^3.3.0" source-list-map "^2.0.0" @@ -2432,7 +2421,7 @@ cssesc@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" -"cssnano@>=2.6.1 <4": +cssnano@^3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" dependencies: @@ -2486,6 +2475,10 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": dependencies: cssom "0.3.x" +csstype@^2.2.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.4.1.tgz#ba35a94259cffc07ed022954737a1da690dcae2c" + cst@^0.4.3: version "0.4.10" resolved "https://registry.yarnpkg.com/cst/-/cst-0.4.10.tgz#9c05c825290a762f0a85c0aabb8c0fe035ae8516" @@ -2541,7 +2534,11 @@ d3-collection@1, d3-collection@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2" -d3-color@1, d3-color@1.0.3: +d3-color@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.1.0.tgz#73957299b63ca935bf19c6c9d835e90066028329" + +d3-color@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b" @@ -2577,13 +2574,13 @@ d3-force@1.1.0: d3-quadtree "1" d3-timer "1" -d3-format@1, d3-format@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.1.tgz#4e19ecdb081a341dafaf5f555ee956bcfdbf167f" +d3-format@1, d3-format@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a" -d3-geo@1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.0.tgz#15c7d7a8ea9346e59ed150dc7b1f7f95479056e9" +d3-geo@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356" dependencies: d3-array "1" @@ -2627,9 +2624,10 @@ d3-request@1.0.6: xmlhttprequest "1" d3-scale-chromatic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.1.1.tgz#811406e8e09dab78a49dac4a32047d5d3edd0c44" + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.2.0.tgz#25820d059c0eccc33e85f77561f37382a817ab58" dependencies: + d3-color "1" d3-interpolate "1" d3-scale@1.0.7: @@ -2644,9 +2642,9 @@ d3-scale@1.0.7: d3-time "1" d3-time-format "2" -d3-selection@1, d3-selection@1.2.0, d3-selection@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88" +d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" d3-shape@1.2.0: version "1.2.0" @@ -2694,8 +2692,8 @@ d3-zoom@1.7.1: d3-transition "1" d3@^4.11.0: - version "4.12.0" - resolved "https://registry.yarnpkg.com/d3/-/d3-4.12.0.tgz#75eccb39ea40f6018de8cfa2752905bee7daa46f" + version "4.13.0" + resolved "https://registry.yarnpkg.com/d3/-/d3-4.13.0.tgz#ab236ff8cf0cfc27a81e69bf2fb7518bc9b4f33d" dependencies: d3-array "1.2.1" d3-axis "1.0.8" @@ -2708,8 +2706,8 @@ d3@^4.11.0: d3-dsv "1.0.8" d3-ease "1.0.3" d3-force "1.1.0" - d3-format "1.2.1" - d3-geo "1.9.0" + d3-format "1.2.2" + d3-geo "1.9.1" d3-hierarchy "1.1.5" d3-interpolate "1.1.6" d3-path "1.0.5" @@ -2719,7 +2717,7 @@ d3@^4.11.0: d3-random "1.1.0" d3-request "1.0.6" d3-scale "1.0.7" - d3-selection "1.2.0" + d3-selection "1.3.0" d3-shape "1.2.0" d3-time "1.0.8" d3-time-format "2.1.1" @@ -2740,6 +2738,14 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-urls@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.0.0.tgz#24802de4e81c298ea8a9388bb0d8e461c774684f" + dependencies: + abab "^1.0.4" + whatwg-mimetype "^2.0.0" + whatwg-url "^6.4.0" + date-fns@^1.27.2: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" @@ -2755,12 +2761,6 @@ dateformat@~1.0.12: get-stdin "^4.0.1" meow "^3.3.0" -debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.4.1, debug@^2.6.6, debug@^2.6.8: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - dependencies: - ms "2.0.0" - debug@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" @@ -2773,6 +2773,12 @@ debug@2.3.3: dependencies: ms "0.7.2" +debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.2, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + debug@3.1.0, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -2791,6 +2797,12 @@ decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + dependencies: + mimic-response "^1.0.0" + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -2803,11 +2815,11 @@ deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" -deep-for-each@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/deep-for-each/-/deep-for-each-1.0.6.tgz#afa0ce249c58492a9720539478a18d37e1b10bae" +deep-for-each@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/deep-for-each/-/deep-for-each-2.0.3.tgz#640b17b88c69892e33caba853004aa89ce00f5c4" dependencies: - is-plain-object "^2.0.1" + lodash.isplainobject "^4.0.6" deep-is@~0.1.3: version "0.1.3" @@ -2883,18 +2895,18 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" delegate@^3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.3.tgz#9a8251a777d7025faa55737bc3b071742127a9fd" + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" -depd@1.1.1, depd@~1.1.1: +depd@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" -depd@~1.1.2: +depd@~1.1.1, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -2923,7 +2935,7 @@ detect-libc@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-0.2.0.tgz#47fdf567348a17ec25fcbf0b9e446348a76f9fb5" -detect-libc@^1.0.2: +detect-libc@^1.0.2, detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -2959,17 +2971,21 @@ diff@^2.0.2: resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" diff@^3.2.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" diffie-hellman@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" dependencies: bn.js "^4.1.0" miller-rabin "^4.0.0" randombytes "^2.0.0" +direction@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/direction/-/direction-0.1.5.tgz#ce5d797f97e26f8be7beff53f7dc40e1c1a9ec4c" + discontinuous-range@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" @@ -3004,7 +3020,7 @@ dom-converter@~0.1: dependencies: utila "~0.3" -dom-helpers@^3.2.0: +dom-helpers@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" @@ -3029,8 +3045,8 @@ dom-walk@^0.1.0: resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" domain-browser@^1.1.1: - version "1.1.7" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" domelementtype@1, domelementtype@^1.3.0: version "1.3.0" @@ -3041,8 +3057,10 @@ domelementtype@~1.1.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.0.tgz#81fe5df81b3f057052cde3a9fa9bf536a85b9ab0" + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + dependencies: + webidl-conversions "^4.0.2" domhandler@2.1: version "2.1.0" @@ -3076,8 +3094,8 @@ domutils@1.5, domutils@1.5.1: domelementtype "1" domutils@^1.5.1: - version "1.6.2" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" dependencies: dom-serializer "0" domelementtype "1" @@ -3094,9 +3112,9 @@ dot-prop@^4.1.0: dependencies: is-obj "^1.0.0" -dotenv@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" +dotenv@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" duplexer3@^0.1.4: version "0.1.4" @@ -3106,9 +3124,9 @@ duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" -duplexify@^3.1.2, duplexify@^3.4.2: - version "3.5.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.1.tgz#4e1516be68838bc90a49994f0b39a6e5960befcd" +duplexify@^3.4.2, duplexify@^3.5.3: + version "3.5.4" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4" dependencies: end-of-stream "^1.0.0" inherits "^2.0.1" @@ -3140,21 +3158,21 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -ejs@^2.5.6: - version "2.5.7" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" +ejs@^2.5.7: + version "2.5.9" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5" electron-to-chromium@^1.2.7: - version "1.3.27" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d" + version "1.3.42" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz#95c33bf01d0cc405556aec899fe61fd4d76ea0f9" elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" element-resize-detector@^1.1.12: - version "1.1.12" - resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.1.12.tgz#8b3fd6eedda17f9c00b360a0ea2df9927ae80ba2" + version "1.1.14" + resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.1.14.tgz#af064a0a618a820ad570a95c5eec5b77be0128c1" dependencies: batch-processor "^1.0.0" @@ -3188,11 +3206,7 @@ empower@^1.2.3: core-js "^2.0.0" empower-core "^0.6.2" -encodeurl@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" - -encodeurl@~1.0.2: +encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3203,8 +3217,8 @@ encoding@^0.1.11: iconv-lite "~0.4.13" end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" dependencies: once "^1.4.0" @@ -3278,27 +3292,28 @@ entities@^1.1.1, entities@~1.1.1: resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" enzyme-adapter-react-16@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.0.tgz#86c5db7c10f0be6ec25d54ca41b59f2abb397cf4" + version "1.1.1" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4" dependencies: - enzyme-adapter-utils "^1.1.0" + enzyme-adapter-utils "^1.3.0" lodash "^4.17.4" object.assign "^4.0.4" object.values "^1.0.4" - prop-types "^15.5.10" + prop-types "^15.6.0" + react-reconciler "^0.7.0" react-test-renderer "^16.0.0-0" -enzyme-adapter-utils@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.2.0.tgz#7f4471ee0a70b91169ec8860d2bf0a6b551664b2" +enzyme-adapter-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7" dependencies: lodash "^4.17.4" object.assign "^4.0.4" - prop-types "^15.5.10" + prop-types "^15.6.0" enzyme-to-json@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.0.tgz#553e23a09ffb4b0cf09287e2edf9c6539fddaa84" + version "3.3.3" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.3.tgz#ede45938fb309cd87ebd4386f60c754525515a07" dependencies: lodash "^4.17.4" @@ -3327,11 +3342,11 @@ err-code@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" -errno@^0.1.3, errno@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" +errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" dependencies: - prr "~0.0.0" + prr "~1.0.1" error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" @@ -3339,17 +3354,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.5.1, es-abstract@^1.6.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" - -es-abstract@^1.7.0: +es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0: version "1.11.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681" dependencies: @@ -3368,13 +3373,14 @@ es-to-primitive@^1.1.1: is-symbol "^1.0.1" es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.37" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.37.tgz#0ee741d148b80069ba27d020393756af257defc3" + version "0.10.42" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.42.tgz#8c07dd33af04d5dcd1310b5cef13bea63a89ba8d" dependencies: - es6-iterator "~2.0.1" + es6-iterator "~2.0.3" es6-symbol "~3.1.1" + next-tick "1" -es6-iterator@^2.0.1, es6-iterator@~2.0.1: +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" dependencies: @@ -3398,8 +3404,8 @@ es6-promise@^3.0.2: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" es6-promise@^4.0.3: - version "4.1.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.1.tgz#8811e90915d9a0dba36274f0b242dbda78f9c92a" + version "4.2.4" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" es6-promisify@^5.0.0: version "5.0.0" @@ -3428,7 +3434,7 @@ es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: d "1" es5-ext "~0.10.14" -es6-templates@^0.2.2: +es6-templates@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/es6-templates/-/es6-templates-0.2.3.tgz#5cb9ac9fb1ded6eb1239342b81d792bbb4078ee4" dependencies: @@ -3453,15 +3459,15 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" escodegen@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852" + version "1.9.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2" dependencies: esprima "^3.1.3" estraverse "^4.2.0" esutils "^2.0.2" optionator "^0.8.1" optionalDependencies: - source-map "~0.5.6" + source-map "~0.6.1" escope@^3.6.0: version "3.6.0" @@ -3511,10 +3517,10 @@ eslint@^2.7.0: user-home "^2.0.0" espree@^3.1.6: - version "3.5.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" dependencies: - acorn "^5.2.1" + acorn "^5.5.0" acorn-jsx "^3.0.0" esprima@^2.6.0: @@ -3536,11 +3542,14 @@ espurify@^1.6.0: core-js "^2.0.0" esrecurse@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" dependencies: estraverse "^4.1.0" - object-assign "^4.0.1" + +esrever@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/esrever/-/esrever-0.2.0.tgz#96e9d28f4f1b1a76784cd5d490eaae010e7407b8" estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" @@ -3565,14 +3574,14 @@ eventemitter2@~0.4.13: version "0.4.14" resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" -eventemitter3@1.x.x: - version "1.2.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" - eventemitter3@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" +eventemitter3@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" + events@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -3624,7 +3633,7 @@ exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" -exit@0.1.2, exit@0.1.x, exit@~0.1.1, exit@~0.1.2: +exit@0.1.2, exit@0.1.x, exit@^0.1.2, exit@~0.1.1, exit@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -3679,55 +3688,20 @@ expect.js@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/expect.js/-/expect.js-0.2.0.tgz#1028533d2c1c363f74a6796ff57ec0520ded2be1" -expect@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/expect/-/expect-22.0.3.tgz#bb486de7d41bf3eb60d3b16dfd1c158a4d91ddfa" +expect@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/expect/-/expect-22.4.3.tgz#d5a29d0a0e1fb2153557caef2674d4547e914674" dependencies: ansi-styles "^3.2.0" - jest-diff "^22.0.3" - jest-get-type "^22.0.3" - jest-matcher-utils "^22.0.3" - jest-message-util "^22.0.3" - jest-regex-util "^22.0.3" + jest-diff "^22.4.3" + jest-get-type "^22.4.3" + jest-matcher-utils "^22.4.3" + jest-message-util "^22.4.3" + jest-regex-util "^22.4.3" expose-loader@^0.7.3: - version "0.7.4" - resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-0.7.4.tgz#9bcdd3878b5da9107930b55a03f65afe90b3314a" - -express@^4.15.2: - version "4.16.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" - dependencies: - accepts "~1.3.4" - array-flatten "1.1.1" - body-parser "1.18.2" - content-disposition "0.5.2" - content-type "~1.0.4" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.1" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.1.0" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" - path-to-regexp "0.1.7" - proxy-addr "~2.0.2" - qs "6.5.1" - range-parser "~1.2.0" - safe-buffer "5.1.1" - send "0.16.1" - serve-static "1.13.1" - setprototypeof "1.1.0" - statuses "~1.3.1" - type-is "~1.6.15" - utils-merge "1.0.1" - vary "~1.1.2" + version "0.7.5" + resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-0.7.5.tgz#e29ea2d9aeeed3254a3faa1b35f502db9f9c3f6f" express@^4.16.2: version "4.16.3" @@ -3770,13 +3744,7 @@ extend-shallow@^2.0.1: dependencies: is-extendable "^0.1.0" -extend-shallow@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.1.tgz#4b6d8c49b147fee029dc9eb9484adb770f689844" - dependencies: - is-extendable "^1.0.1" - -extend-shallow@^3.0.2: +extend-shallow@^3.0.0, extend-shallow@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" dependencies: @@ -3793,19 +3761,6 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" -extglob@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.2.tgz#3290f46208db1b2e8eb8be0c94ed9e6ad80edbe2" - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -3837,17 +3792,21 @@ extract-zip@^1.6.5: mkdirp "0.5.0" yauzl "2.4.1" -extsprintf@1.3.0, extsprintf@^1.2.0: +extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + eyes@0.1.x: version "0.1.8" resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" fast-json-stable-stringify@^2.0.0: version "2.0.0" @@ -3918,8 +3877,8 @@ file-loader@^0.11.2: loader-utils "^1.0.2" file-saver@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.3.tgz#cdd4c44d3aa264eac2f68ec165bc791c34af1232" + version "1.3.8" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" file-sync-cmp@^0.1.0: version "0.1.1" @@ -3936,9 +3895,9 @@ fileset@^2.0.2: glob "^7.0.3" minimatch "^3.0.3" -filesize@^3.5.9: - version "3.5.11" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.11.tgz#1919326749433bb3cf77368bd158caabcc19e9ee" +filesize@^3.5.11: + version "3.6.1" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" fill-range@^2.1.0: version "2.2.3" @@ -3959,18 +3918,6 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" -finalhandler@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.6.tgz#007aea33d1a4d3e42017f624848ad58d212f814f" - dependencies: - debug "2.6.9" - encodeurl "~1.0.1" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.3.1" - unpipe "~1.0.0" - finalhandler@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" @@ -4007,6 +3954,10 @@ find-index@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" +find-npm-prefix@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz#8d8ce2c78b3b4b9e66c8acc6a37c231eb841cfdf" + find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" @@ -4044,13 +3995,13 @@ flatten@^1.0.2: resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" flush-write-stream@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" + version "1.0.3" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" dependencies: inherits "^2.0.1" readable-stream "^2.0.4" -follow-redirects@^1.2.5: +follow-redirects@^1.0.0, follow-redirects@^1.2.5: version "1.4.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" dependencies: @@ -4093,11 +4044,11 @@ form-data@~2.1.1: mime-types "^2.1.12" form-data@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" dependencies: asynckit "^0.4.0" - combined-stream "^1.0.5" + combined-stream "1.0.6" mime-types "^2.1.12" formatio@1.1.1: @@ -4170,7 +4121,13 @@ fs-extra@^3.0.1: jsonfile "^3.0.0" universalify "^0.1.0" -fs-vacuum@~1.2.10: +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + +fs-vacuum@^1.2.10, fs-vacuum@~1.2.10: version "1.2.10" resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.10.tgz#b7629bec07a4031a2548fdf99f5ecf1cc8b31e36" dependencies: @@ -4192,21 +4149,13 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" fsevents@^1.0.0, fsevents@^1.1.1, fsevents@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" + version "1.2.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.2.tgz#4f598f0f69b273188ef4a62ca4e9e08ace314bbf" dependencies: - nan "^2.3.0" - node-pre-gyp "^0.6.39" + nan "^2.9.2" + node-pre-gyp "^0.9.0" -fstream-ignore@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" - dependencies: - fstream "^1.0.0" - inherits "2" - minimatch "^3.0.0" - -fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: +fstream@^1.0.0, fstream@^1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" dependencies: @@ -4260,10 +4209,27 @@ genfun@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/genfun/-/genfun-4.0.1.tgz#ed10041f2e4a7f1b0a38466d17a5c3e27df1dfc1" +gentle-fs@^2.0.0, gentle-fs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/gentle-fs/-/gentle-fs-2.0.1.tgz#585cfd612bfc5cd52471fdb42537f016a5ce3687" + dependencies: + aproba "^1.1.2" + fs-vacuum "^1.2.10" + graceful-fs "^4.1.11" + iferr "^0.1.5" + mkdirp "^0.5.1" + path-is-inside "^1.0.2" + read-cmd-shim "^1.0.1" + slide "^1.1.6" + 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-document@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-document/-/get-document-1.0.0.tgz#4821bce66f1c24cb0331602be6cb6b12c4f01c4b" + 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" @@ -4280,6 +4246,12 @@ get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" +get-window@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/get-window/-/get-window-1.1.2.tgz#65fbaa999fb87f86ea5d30770f4097707044f47f" + dependencies: + get-document "1" + getobject@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/getobject/-/getobject-0.1.0.tgz#047a449789fa160d018f5486ed91320b6ec7885c" @@ -4547,14 +4519,14 @@ grunt-legacy-log-utils@~1.0.0: lodash "~4.3.0" grunt-legacy-log@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/grunt-legacy-log/-/grunt-legacy-log-1.0.0.tgz#fb86f1809847bc07dc47843f9ecd6cacb62df2d5" + version "1.0.1" + resolved "https://registry.yarnpkg.com/grunt-legacy-log/-/grunt-legacy-log-1.0.1.tgz#c7731b2745f4732aa9950ee4d7ae63c553f68469" dependencies: colors "~1.1.2" grunt-legacy-log-utils "~1.0.0" hooker "~0.2.3" - lodash "~3.10.1" - underscore.string "~3.2.3" + lodash "~4.17.5" + underscore.string "~3.3.4" grunt-legacy-util@~1.0.0: version "1.0.0" @@ -4591,11 +4563,11 @@ grunt-sass-lint@^0.2.2: sass-lint "^1.12.0" grunt-sass@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/grunt-sass/-/grunt-sass-2.0.0.tgz#9074cf9d7b4592e20f7788caa727b8f9aa06b60a" + version "2.1.0" + resolved "https://registry.yarnpkg.com/grunt-sass/-/grunt-sass-2.1.0.tgz#b7ba1d85ef4c2d9b7d8195fe65f664ac7554efa1" dependencies: each-async "^1.0.0" - node-sass "^4.0.0" + node-sass "^4.7.2" object-assign "^4.0.1" grunt-usemin@3.1.1: @@ -4608,10 +4580,10 @@ grunt-usemin@3.1.1: path-exists "^1.0.0" grunt-webpack@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/grunt-webpack/-/grunt-webpack-3.0.2.tgz#bcfdea313d431e79b6fc18c42040b48dfac9d32f" + version "3.1.1" + resolved "https://registry.yarnpkg.com/grunt-webpack/-/grunt-webpack-3.1.1.tgz#78de544e88ff41a221c173fc91cad579c97a9087" dependencies: - deep-for-each "^1.0.5" + deep-for-each "^2.0.2" lodash "^4.7.0" grunt@1.0.1: @@ -4642,11 +4614,12 @@ gzip-size@^1.0.0: browserify-zlib "^0.1.4" concat-stream "^1.4.1" -gzip-size@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520" +gzip-size@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-4.1.0.tgz#8ae096257eabe7d69c45be2b67c448124ffb517c" dependencies: duplexer "^0.1.1" + pify "^3.0.0" handle-thing@^1.2.5: version "1.2.5" @@ -4662,10 +4635,6 @@ handlebars@^4.0.3: optionalDependencies: uglify-js "^2.6" -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -4679,13 +4648,6 @@ har-validator@~2.0.6: is-my-json-valid "^2.12.4" pinkie-promise "^2.0.0" -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" - har-validator@~5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" @@ -4766,12 +4728,6 @@ has@^1.0.1: dependencies: function-bind "^1.0.2" -hash-base@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" - dependencies: - inherits "^2.0.1" - hash-base@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" @@ -4793,7 +4749,7 @@ hasha@^2.2.0: is-stream "^1.0.1" pinkie-promise "^2.0.0" -hawk@3.1.3, hawk@~3.1.3: +hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" dependencies: @@ -4823,8 +4779,8 @@ header-case@^1.0.0: upper-case "^1.1.3" highlight-words-core@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.1.2.tgz#5c2717c4f6c6e7ea2462ab85b43ff8b24f58ec3e" + version "1.2.0" + resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.0.tgz#232bec301cbf2a4943d335dc748ce70e9024f3b1" hmac-drbg@^1.0.0: version "1.0.1" @@ -4839,14 +4795,10 @@ hoek@2.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" hoek@4.x.x: - version "4.2.0" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" -hoist-non-react-statics@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" - -hoist-non-react-statics@^2.5.0: +hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40" @@ -4861,9 +4813,9 @@ hooker@^0.2.3, hooker@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" -hosted-git-info@^2.1.4, hosted-git-info@^2.4.2, hosted-git-info@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" +hosted-git-info@^2.1.4, hosted-git-info@^2.4.2, hosted-git-info@^2.5.0, hosted-git-info@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" hpack.js@^2.1.6: version "2.1.6" @@ -4878,7 +4830,7 @@ html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" -html-encoding-sniffer@^1.0.1: +html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" dependencies: @@ -4889,27 +4841,26 @@ html-entities@^1.2.0: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" html-loader@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.5.1.tgz#4f1e8396a1ea6ab42bedc987dfac058070861ebe" + version "0.5.5" + resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.5.5.tgz#6356dbeb0c49756d8ebd5ca327f16ff06ab5faea" dependencies: - es6-templates "^0.2.2" + es6-templates "^0.2.3" fastparse "^1.1.1" - html-minifier "^3.0.1" - loader-utils "^1.0.2" - object-assign "^4.1.0" + html-minifier "^3.5.8" + loader-utils "^1.1.0" + object-assign "^4.1.1" -html-minifier@^3.0.1, html-minifier@^3.2.3: - version "3.5.7" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.7.tgz#511e69bb5a8e7677d1012ebe03819aa02ca06208" +html-minifier@^3.2.3, html-minifier@^3.5.8: + version "3.5.15" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.15.tgz#f869848d4543cbfd84f26d5514a2a87cbf9a05e0" dependencies: camel-case "3.0.x" clean-css "4.1.x" - commander "2.12.x" + commander "2.15.x" he "1.1.x" - ncname "1.0.x" param-case "2.1.x" relateurl "0.2.x" - uglify-js "3.2.x" + uglify-js "3.3.x" html-minifier@~2.1.2: version "2.1.7" @@ -4972,14 +4923,14 @@ htmlparser2@~3.3.0: readable-stream "1.0" http-cache-semantics@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.0.tgz#1e3ce248730e189ac692a6697b9e3fdea2ff8da3" + version "3.8.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" -http-errors@1.6.2, http-errors@~1.6.2: +http-errors@1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" dependencies: @@ -4988,16 +4939,25 @@ http-errors@1.6.2, http-errors@~1.6.2: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-parser-js@>=0.4.0: - version "0.4.11" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.11.tgz#5b720849c650903c27e521633d94696ee95f3529" + version "0.4.12" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.12.tgz#b9cfbf4a2cf26f0fc34b10ca1489a27771e3474f" http-proxy-agent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.0.0.tgz#46482a2f0523a4d6082551709f469cb3e4a85ff4" + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" dependencies: agent-base "4" - debug "2" + debug "3.1.0" http-proxy-middleware@~0.17.4: version "0.17.4" @@ -5009,11 +4969,12 @@ http-proxy-middleware@~0.17.4: micromatch "^2.3.11" http-proxy@^1.13.0, http-proxy@^1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + version "1.17.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" dependencies: - eventemitter3 "1.x.x" - requires-port "1.x.x" + eventemitter3 "^3.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" http-signature@~1.1.0: version "1.1.1" @@ -5036,11 +4997,11 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" https-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.1.0.tgz#1391bee7fd66aeabc0df2a1fa90f58954f43e443" + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" dependencies: agent-base "^4.1.0" - debug "^2.4.1" + debug "^3.1.0" humanize-ms@^1.2.1: version "1.2.1" @@ -5060,7 +5021,13 @@ i@0.3.x: version "0.3.6" resolved "https://registry.yarnpkg.com/i/-/i-0.3.6.tgz#d96c92732076f072711b6b10fd7d4f65ad8ee23d" -iconv-lite@0.4, iconv-lite@0.4.19, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@^0.4.4, iconv-lite@~0.4.13: + version "0.4.21" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798" + dependencies: + safer-buffer "^2.1.0" + +iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -5075,8 +5042,8 @@ icss-utils@^2.1.0: postcss "^6.0.1" ieee754@^1.1.4: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + version "1.1.11" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" iferr@^0.1.5, iferr@~0.1.5: version "0.1.5" @@ -5089,8 +5056,8 @@ ignore-walk@^3.0.1: minimatch "^3.0.4" ignore@^3.1.2: - version "3.3.7" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + version "3.3.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b" iltorb@^1.0.13: version "1.3.10" @@ -5101,6 +5068,10 @@ iltorb@^1.0.13: node-gyp "^3.6.2" prebuild-install "^2.3.0" +immutable@^3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -5157,16 +5128,16 @@ inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" -ini@^1.3.4, ini@~1.3.0, ini@~1.3.4: +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" -init-package-json@~1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.10.1.tgz#cd873a167796befb99612b28762a0b6393fd8f6a" +init-package-json@^1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe" dependencies: glob "^7.1.1" - npm-package-arg "^4.0.0 || ^5.0.0" + npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0" promzard "^0.3.0" read "~1.0.1" read-package-json "1 || 2" @@ -5199,12 +5170,12 @@ internal-ip@1.2.0: meow "^3.3.0" interpret@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0" + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" invariant@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" dependencies: loose-envify "^1.0.0" @@ -5216,10 +5187,6 @@ ip@^1.1.0, ip@^1.1.4, ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" -ipaddr.js@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" - ipaddr.js@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b" @@ -5258,7 +5225,7 @@ is-boolean-object@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" -is-buffer@^1.0.2, is-buffer@^1.1.5: +is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5273,8 +5240,8 @@ is-callable@^1.1.1, is-callable@^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" + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5" dependencies: ci-info "^1.0.0" @@ -5308,15 +5275,7 @@ is-descriptor@^0.1.0: is-data-descriptor "^0.1.4" kind-of "^5.0.0" -is-descriptor@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.1.tgz#2c6023599bde2de9d5d2c8b9a9d94082036b6ef2" - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.2: +is-descriptor@^1.0.0, is-descriptor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" dependencies: @@ -5332,6 +5291,10 @@ is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" +is-empty@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-empty/-/is-empty-1.2.0.tgz#de9bb5b278738a05a0b09a57e1fb4d4a341a9f6b" + is-equal-shallow@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" @@ -5372,6 +5335,10 @@ is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" +is-generator-fn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a" + is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" @@ -5390,6 +5357,14 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-hotkey@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.2.tgz#aeda5e4f542284700ae18b46980fb0637c021198" + +is-in-browser@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" + is-installed-globally@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" @@ -5403,12 +5378,17 @@ is-lower-case@^1.1.0: dependencies: lower-case "^1.1.0" +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: - version "2.16.1" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11" + version "2.17.2" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c" dependencies: generate-function "^2.0.0" generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" jsonpointer "^4.0.0" xtend "^4.0.0" @@ -5450,12 +5430,6 @@ is-observable@^0.2.0: dependencies: symbol-observable "^0.2.2" -is-odd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088" - dependencies: - is-number "^3.0.0" - is-odd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24" @@ -5467,14 +5441,14 @@ is-path-cwd@^1.0.0: resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" dependencies: is-path-inside "^1.0.0" is-path-inside@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" dependencies: path-is-inside "^1.0.1" @@ -5519,10 +5493,8 @@ is-regexp@^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" - dependencies: - tryit "^1.0.1" + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" is-retry-allowed@^1.0.0: version "1.1.0" @@ -5564,6 +5536,10 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" +is-window@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -5598,6 +5574,10 @@ isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" +isomorphic-base64@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/isomorphic-base64/-/isomorphic-base64-1.0.2.tgz#f426aae82569ba8a4ec5ca73ad21a44ab1ee7803" + isomorphic-fetch@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" @@ -5610,102 +5590,116 @@ isstream@0.1.x, isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" istanbul-api@^1.1.14: - version "1.2.1" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.2.1.tgz#0c60a0515eb11c7d65c6b50bba2c6e999acd8620" + version "1.3.1" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.1.tgz#4c3b05d18c0016d1022e079b98dc82c40f488954" dependencies: async "^2.1.4" + compare-versions "^3.1.0" fileset "^2.0.2" - istanbul-lib-coverage "^1.1.1" - istanbul-lib-hook "^1.1.0" - istanbul-lib-instrument "^1.9.1" - istanbul-lib-report "^1.1.2" - istanbul-lib-source-maps "^1.2.2" - istanbul-reports "^1.1.3" + istanbul-lib-coverage "^1.2.0" + istanbul-lib-hook "^1.2.0" + istanbul-lib-instrument "^1.10.1" + istanbul-lib-report "^1.1.4" + istanbul-lib-source-maps "^1.2.4" + istanbul-reports "^1.3.0" js-yaml "^3.7.0" mkdirp "^0.5.1" once "^1.4.0" -istanbul-lib-coverage@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" +istanbul-lib-coverage@^1.1.1, istanbul-lib-coverage@^1.1.2, istanbul-lib-coverage@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341" -istanbul-lib-hook@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz#8538d970372cb3716d53e55523dd54b557a8d89b" +istanbul-lib-hook@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.0.tgz#ae556fd5a41a6e8efa0b1002b1e416dfeaf9816c" dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0, istanbul-lib-instrument@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e" +istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.8.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b" dependencies: babel-generator "^6.18.0" babel-template "^6.16.0" babel-traverse "^6.18.0" babel-types "^6.18.0" babylon "^6.18.0" - istanbul-lib-coverage "^1.1.1" + istanbul-lib-coverage "^1.2.0" semver "^5.3.0" -istanbul-lib-report@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz#922be27c13b9511b979bd1587359f69798c1d425" +istanbul-lib-report@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.4.tgz#e886cdf505c4ebbd8e099e4396a90d0a28e2acb5" dependencies: - istanbul-lib-coverage "^1.1.1" + istanbul-lib-coverage "^1.2.0" mkdirp "^0.5.1" path-parse "^1.0.5" supports-color "^3.1.2" -istanbul-lib-source-maps@^1.2.1, istanbul-lib-source-maps@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz#750578602435f28a0c04ee6d7d9e0f2960e62c1c" +istanbul-lib-source-maps@^1.2.1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz#20fb54b14e14b3fb6edb6aca3571fd2143db44e6" dependencies: debug "^3.1.0" - istanbul-lib-coverage "^1.1.1" + istanbul-lib-coverage "^1.1.2" mkdirp "^0.5.1" rimraf "^2.6.1" source-map "^0.5.3" -istanbul-reports@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.3.tgz#3b9e1e8defb6d18b1d425da8e8b32c5a163f2d10" +istanbul-lib-source-maps@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.4.tgz#cc7ccad61629f4efff8e2f78adb8c522c9976ec7" + dependencies: + debug "^3.1.0" + istanbul-lib-coverage "^1.2.0" + mkdirp "^0.5.1" + rimraf "^2.6.1" + source-map "^0.5.3" + +istanbul-reports@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.3.0.tgz#2f322e81e1d9520767597dca3c20a0cce89a3554" dependencies: handlebars "^4.0.3" -jest-changed-files@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.0.3.tgz#3771315acfa24a0ed7e6c545de620db6f1b2d164" +jest-changed-files@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.4.3.tgz#8882181e022c38bd46a2e4d18d44d19d90a90fb2" dependencies: throat "^4.0.0" -jest-cli@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.0.4.tgz#0052abaad45c57861c05da8ab5d27bad13ad224d" +jest-cli@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.4.3.tgz#bf16c4a5fb7edc3fa5b9bb7819e34139e88a72c7" dependencies: ansi-escapes "^3.0.0" chalk "^2.0.1" + exit "^0.1.2" glob "^7.1.2" graceful-fs "^4.1.11" + import-local "^1.0.0" is-ci "^1.0.10" istanbul-api "^1.1.14" istanbul-lib-coverage "^1.1.1" istanbul-lib-instrument "^1.8.0" istanbul-lib-source-maps "^1.2.1" - jest-changed-files "^22.0.3" - jest-config "^22.0.4" - jest-environment-jsdom "^22.0.4" - jest-get-type "^22.0.3" - jest-haste-map "^22.0.3" - jest-message-util "^22.0.3" - jest-regex-util "^22.0.3" - jest-resolve-dependencies "^22.0.3" - jest-runner "^22.0.4" - jest-runtime "^22.0.4" - jest-snapshot "^22.0.3" - jest-util "^22.0.4" - jest-worker "^22.0.3" + jest-changed-files "^22.4.3" + jest-config "^22.4.3" + jest-environment-jsdom "^22.4.3" + jest-get-type "^22.4.3" + jest-haste-map "^22.4.3" + jest-message-util "^22.4.3" + jest-regex-util "^22.4.3" + jest-resolve-dependencies "^22.4.3" + jest-runner "^22.4.3" + jest-runtime "^22.4.3" + jest-snapshot "^22.4.3" + jest-util "^22.4.3" + jest-validate "^22.4.3" + jest-worker "^22.4.3" micromatch "^2.3.11" - node-notifier "^5.1.2" + node-notifier "^5.2.1" realpath-native "^1.0.0" rimraf "^2.5.4" slash "^1.0.0" @@ -5714,104 +5708,105 @@ jest-cli@^22.0.4: which "^1.2.12" yargs "^10.0.3" -jest-config@^22.0.1, jest-config@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.0.4.tgz#9c2a46c0907b1a1af54d9cdbf18e99b447034e11" +jest-config@^22.4.2, jest-config@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.4.3.tgz#0e9d57db267839ea31309119b41dc2fa31b76403" dependencies: chalk "^2.0.1" glob "^7.1.1" - jest-environment-jsdom "^22.0.4" - jest-environment-node "^22.0.4" - jest-get-type "^22.0.3" - jest-jasmine2 "^22.0.4" - jest-regex-util "^22.0.3" - jest-resolve "^22.0.4" - jest-util "^22.0.4" - jest-validate "^22.0.3" - pretty-format "^22.0.3" + jest-environment-jsdom "^22.4.3" + jest-environment-node "^22.4.3" + jest-get-type "^22.4.3" + jest-jasmine2 "^22.4.3" + jest-regex-util "^22.4.3" + jest-resolve "^22.4.3" + jest-util "^22.4.3" + jest-validate "^22.4.3" + pretty-format "^22.4.3" -jest-diff@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-22.0.3.tgz#ffed5aba6beaf63bb77819ba44dd520168986321" +jest-diff@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-22.4.3.tgz#e18cc3feff0aeef159d02310f2686d4065378030" dependencies: chalk "^2.0.1" diff "^3.2.0" - jest-get-type "^22.0.3" - pretty-format "^22.0.3" + jest-get-type "^22.4.3" + pretty-format "^22.4.3" -jest-docblock@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.0.3.tgz#c33aa22682b9fc68a5373f5f82994428a2ded601" +jest-docblock@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.4.3.tgz#50886f132b42b280c903c592373bb6e93bb68b19" dependencies: detect-newline "^2.1.0" -jest-environment-jsdom@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.0.4.tgz#5723d4e724775ed38948de792e62f2d6a7f452df" +jest-environment-jsdom@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.4.3.tgz#d67daa4155e33516aecdd35afd82d4abf0fa8a1e" dependencies: - jest-mock "^22.0.3" - jest-util "^22.0.4" + jest-mock "^22.4.3" + jest-util "^22.4.3" jsdom "^11.5.1" -jest-environment-node@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.0.4.tgz#068671f85a545f96a5469be3a3dd228fca79c709" +jest-environment-node@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.4.3.tgz#54c4eaa374c83dd52a9da8759be14ebe1d0b9129" dependencies: - jest-mock "^22.0.3" - jest-util "^22.0.4" + jest-mock "^22.4.3" + jest-util "^22.4.3" 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-get-type@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.0.3.tgz#fa894b677c0fcd55eff3fd8ee28c7be942e32d36" +jest-get-type@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" -jest-haste-map@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.0.3.tgz#c9ecb5c871c5465d4bde4139e527fa0dc784aa2d" +jest-haste-map@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.4.3.tgz#25842fa2ba350200767ac27f658d58b9d5c2e20b" dependencies: fb-watchman "^2.0.0" graceful-fs "^4.1.11" - jest-docblock "^22.0.3" - jest-worker "^22.0.3" + jest-docblock "^22.4.3" + jest-serializer "^22.4.3" + jest-worker "^22.4.3" micromatch "^2.3.11" sane "^2.0.0" -jest-jasmine2@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.0.4.tgz#f7c0965116efe831ec674dc954b0134639b3dcee" +jest-jasmine2@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.4.3.tgz#4daf64cd14c793da9db34a7c7b8dcfe52a745965" dependencies: - callsites "^2.0.0" chalk "^2.0.1" - expect "^22.0.3" + co "^4.6.0" + expect "^22.4.3" graceful-fs "^4.1.11" - jest-diff "^22.0.3" - jest-matcher-utils "^22.0.3" - jest-message-util "^22.0.3" - jest-snapshot "^22.0.3" + is-generator-fn "^1.0.0" + jest-diff "^22.4.3" + jest-matcher-utils "^22.4.3" + jest-message-util "^22.4.3" + jest-snapshot "^22.4.3" + jest-util "^22.4.3" source-map-support "^0.5.0" -jest-leak-detector@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-22.0.3.tgz#b64904f0e8954a11edb79b0809ff4717fa762d99" +jest-leak-detector@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-22.4.3.tgz#2b7b263103afae8c52b6b91241a2de40117e5b35" dependencies: - pretty-format "^22.0.3" - optionalDependencies: - weak "^1.0.1" + pretty-format "^22.4.3" -jest-matcher-utils@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.0.3.tgz#2ec15ca1af7dcabf4daddc894ccce224b948674e" +jest-matcher-utils@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz#4632fe428ebc73ebc194d3c7b65d37b161f710ff" dependencies: chalk "^2.0.1" - jest-get-type "^22.0.3" - pretty-format "^22.0.3" + jest-get-type "^22.4.3" + pretty-format "^22.4.3" -jest-message-util@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.0.3.tgz#bf674b2762ef2dd53facf2136423fcca264976df" +jest-message-util@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.4.3.tgz#cf3d38aafe4befddbfc455e57d65d5239e399eb7" dependencies: "@babel/code-frame" "^7.0.0-beta.35" chalk "^2.0.1" @@ -5819,57 +5814,60 @@ jest-message-util@^22.0.3: slash "^1.0.0" stack-utils "^1.0.1" -jest-mock@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.0.3.tgz#c875e47b5b729c6c020a2fab317b275c0cf88961" +jest-mock@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.4.3.tgz#f63ba2f07a1511772cdc7979733397df770aabc7" -jest-regex-util@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-22.0.3.tgz#c5c10229de5ce2b27bf4347916d95b802ae9aa4d" +jest-regex-util@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-22.4.3.tgz#a826eb191cdf22502198c5401a1fc04de9cef5af" -jest-resolve-dependencies@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-22.0.3.tgz#202ddf370069702cd1865a1952fcc7e52c92720e" +jest-resolve-dependencies@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-22.4.3.tgz#e2256a5a846732dc3969cb72f3c9ad7725a8195e" dependencies: - jest-regex-util "^22.0.3" + jest-regex-util "^22.4.3" -jest-resolve@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.0.4.tgz#a6e47f55e9388c7341b5e9732aedc6fe30906121" +jest-resolve@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.4.3.tgz#0ce9d438c8438229aa9b916968ec6b05c1abb4ea" dependencies: browser-resolve "^1.11.2" chalk "^2.0.1" -jest-runner@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.0.4.tgz#3aa43a31b05ce8271539df580c2eb916023d3367" +jest-runner@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.4.3.tgz#298ddd6a22b992c64401b4667702b325e50610c3" dependencies: - jest-config "^22.0.4" - jest-docblock "^22.0.3" - jest-haste-map "^22.0.3" - jest-jasmine2 "^22.0.4" - jest-leak-detector "^22.0.3" - jest-message-util "^22.0.3" - jest-runtime "^22.0.4" - jest-util "^22.0.4" - jest-worker "^22.0.3" + exit "^0.1.2" + jest-config "^22.4.3" + jest-docblock "^22.4.3" + jest-haste-map "^22.4.3" + jest-jasmine2 "^22.4.3" + jest-leak-detector "^22.4.3" + jest-message-util "^22.4.3" + jest-runtime "^22.4.3" + jest-util "^22.4.3" + jest-worker "^22.4.3" throat "^4.0.0" -jest-runtime@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.0.4.tgz#8f69aa7b5fbb3acd35dc262cbf654e563f69b7b4" +jest-runtime@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.4.3.tgz#b69926c34b851b920f666c93e86ba2912087e3d0" dependencies: babel-core "^6.0.0" - babel-jest "^22.0.4" + babel-jest "^22.4.3" babel-plugin-istanbul "^4.1.5" chalk "^2.0.1" convert-source-map "^1.4.0" + exit "^0.1.2" graceful-fs "^4.1.11" - jest-config "^22.0.4" - jest-haste-map "^22.0.3" - jest-regex-util "^22.0.3" - jest-resolve "^22.0.4" - jest-util "^22.0.4" + jest-config "^22.4.3" + jest-haste-map "^22.4.3" + jest-regex-util "^22.4.3" + jest-resolve "^22.4.3" + jest-util "^22.4.3" + jest-validate "^22.4.3" json-stable-stringify "^1.0.1" micromatch "^2.3.11" realpath-native "^1.0.0" @@ -5878,28 +5876,32 @@ jest-runtime@^22.0.4: write-file-atomic "^2.1.0" yargs "^10.0.3" -jest-snapshot@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.0.3.tgz#a949b393781d2fdb4773f6ea765dd67ad1da291e" +jest-serializer@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-22.4.3.tgz#a679b81a7f111e4766235f4f0c46d230ee0f7436" + +jest-snapshot@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.4.3.tgz#b5c9b42846ffb9faccb76b841315ba67887362d2" dependencies: chalk "^2.0.1" - jest-diff "^22.0.3" - jest-matcher-utils "^22.0.3" + jest-diff "^22.4.3" + jest-matcher-utils "^22.4.3" mkdirp "^0.5.1" natural-compare "^1.4.0" - pretty-format "^22.0.3" + pretty-format "^22.4.3" -jest-util@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.0.4.tgz#d920a513e0645aaab030cee38e4fe7d5bed8bb6d" +jest-util@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.4.3.tgz#c70fec8eec487c37b10b0809dc064a7ecf6aafac" dependencies: callsites "^2.0.0" chalk "^2.0.1" graceful-fs "^4.1.11" is-ci "^1.0.10" - jest-message-util "^22.0.3" - jest-validate "^22.0.3" + jest-message-util "^22.4.3" mkdirp "^0.5.1" + source-map "^0.6.0" jest-validate@^21.1.0: version "21.2.1" @@ -5910,42 +5912,44 @@ jest-validate@^21.1.0: leven "^2.1.0" pretty-format "^21.2.1" -jest-validate@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.0.3.tgz#2850d949a36c48b1a40f7eebae1d8539126f7829" +jest-validate@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.4.3.tgz#0780954a5a7daaeec8d3c10834b9280865976b30" dependencies: chalk "^2.0.1" - jest-get-type "^22.0.3" + jest-config "^22.4.3" + jest-get-type "^22.4.3" leven "^2.1.0" - pretty-format "^22.0.3" + pretty-format "^22.4.3" -jest-worker@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.0.3.tgz#30433faca67814a8f80559f75ab2ceaa61332fd2" +jest-worker@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.4.3.tgz#5c421417cba1c0abf64bf56bd5fb7968d79dd40b" dependencies: merge-stream "^1.0.1" jest@^22.0.4: - version "22.0.4" - resolved "https://registry.yarnpkg.com/jest/-/jest-22.0.4.tgz#d3cf560ece6b825b115dce80b9826ceb40f87961" + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-22.4.3.tgz#2261f4b117dc46d9a4a1a673d2150958dee92f16" dependencies: - jest-cli "^22.0.4" + import-local "^1.0.0" + jest-cli "^22.4.3" jquery@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + version "3.3.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" js-base64@^2.1.8, js-base64@^2.1.9: - version "2.3.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf" + version "2.4.3" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + version "3.11.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -5984,8 +5988,8 @@ jscs-jsdoc@^2.0.0: jsdoctypeparser "~1.2.0" jscs-preset-wikimedia@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/jscs-preset-wikimedia/-/jscs-preset-wikimedia-1.0.0.tgz#fff563342038fc2e8826b7bb7309c3ae3406fc7e" + version "1.0.1" + resolved "https://registry.yarnpkg.com/jscs-preset-wikimedia/-/jscs-preset-wikimedia-1.0.1.tgz#a6a5fa5967fd67a5d609038e1c794eaf41d4233d" jscs@~3.0.5: version "3.0.7" @@ -6025,33 +6029,35 @@ jsdoctypeparser@~1.2.0: lodash "^3.7.0" jsdom@^11.5.1: - version "11.5.1" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.5.1.tgz#5df753b8d0bca20142ce21f4f6c039f99a992929" + version "11.9.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.9.0.tgz#58ac6dfd248d560d736b0202d74eedad55590cd9" dependencies: - abab "^1.0.3" - acorn "^5.1.2" - acorn-globals "^4.0.0" + abab "^1.0.4" + acorn "^5.3.0" + acorn-globals "^4.1.0" array-equal "^1.0.0" - browser-process-hrtime "^0.1.2" - content-type-parser "^1.0.1" cssom ">= 0.3.2 < 0.4.0" cssstyle ">= 0.2.37 < 0.3.0" + data-urls "^1.0.0" domexception "^1.0.0" escodegen "^1.9.0" - html-encoding-sniffer "^1.0.1" + html-encoding-sniffer "^1.0.2" left-pad "^1.2.0" nwmatcher "^1.4.3" - parse5 "^3.0.2" - pn "^1.0.0" + parse5 "4.0.0" + pn "^1.1.0" request "^2.83.0" - request-promise-native "^1.0.3" - sax "^1.2.1" - symbol-tree "^3.2.1" + request-promise-native "^1.0.5" + sax "^1.2.4" + symbol-tree "^3.2.2" tough-cookie "^2.3.3" + w3c-hr-time "^1.0.1" webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.1" - whatwg-url "^6.3.0" - xml-name-validator "^2.0.1" + whatwg-encoding "^1.0.3" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.0" + ws "^4.0.0" + xml-name-validator "^3.0.0" jsesc@^0.5.0, jsesc@~0.5.0: version "0.5.0" @@ -6089,9 +6095,9 @@ json-loader@^0.5.4, json-loader@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" -json-parse-better-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a" +json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" json-schema-traverse@^0.3.0: version "0.3.1" @@ -6142,11 +6148,11 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" jsonlint@~1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.2.tgz#5737045085f55eb455c68b1ff4ebc01bd50e8830" + version "1.6.3" + resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.3.tgz#cb5e31efc0b78291d0d862fbef05900adf212988" dependencies: - JSV ">= 4.0.x" - nomnom ">= 1.5.x" + JSV "^4.0.x" + nomnom "^1.5.x" jsonparse@^1.2.0: version "1.3.1" @@ -6202,12 +6208,13 @@ karma-sourcemap-loader@^0.3.7: graceful-fs "^4.1.2" karma-webpack@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.6.tgz#967918e59750ebe0f40829263435fde7ac81bdb4" + version "2.0.13" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.13.tgz#cf56e3056c15b7747a0bb2140fc9a6be41dd9f02" dependencies: - async "~0.9.0" - loader-utils "^0.2.5" - lodash "^3.8.0" + async "^2.0.0" + babel-runtime "^6.0.0" + loader-utils "^1.0.0" + lodash "^4.0.0" source-map "^0.5.6" webpack-dev-middleware "^1.12.0" @@ -6247,17 +6254,15 @@ kew@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" +keycode@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04" + killable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" -kind-of@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" - dependencies: - is-buffer "^1.0.2" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^3.2.2: +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" dependencies: @@ -6269,15 +6274,11 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" -kind-of@^5.0.0, kind-of@^5.0.2: +kind-of@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" -kind-of@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.1.tgz#4948e6263553ac3712fc44d305b77851d9e40ea4" - -kind-of@^6.0.2: +kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" @@ -6297,20 +6298,10 @@ latest-version@^3.0.0: dependencies: package-json "^4.0.0" -lazy-cache@^0.2.3: - version "0.2.7" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" - lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" -lazy-cache@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" - dependencies: - set-getter "^0.1.0" - lazy-property@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazy-property/-/lazy-property-1.0.0.tgz#84ddc4b370679ba8bd4cdcfa4c06b43d57111147" @@ -6328,8 +6319,8 @@ lcid@^1.0.0: invert-kv "^1.0.0" left-pad@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.2.0.tgz#d30a73c6b8201d8f7d8e7956ba9616087a68e0ee" + version "1.3.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" leven@^2.1.0: version "2.1.0" @@ -6342,27 +6333,45 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libnpx@~9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/libnpx/-/libnpx-9.6.0.tgz#c441ddd698b043bd8e8dc78384fa8eb7d77991e5" +libcipm@^1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/libcipm/-/libcipm-1.6.2.tgz#5a9d83b8606b9733cfff016ad9b37d3b8198ae09" dependencies: - dotenv "^4.0.0" - npm-package-arg "^5.1.2" - rimraf "^2.6.1" + bin-links "^1.1.0" + bluebird "^3.5.1" + find-npm-prefix "^1.0.2" + graceful-fs "^4.1.11" + lock-verify "^2.0.0" + npm-lifecycle "^2.0.0" + npm-logical-tree "^1.2.1" + npm-package-arg "^6.0.0" + pacote "^7.5.1" + protoduck "^5.0.0" + read-package-json "^2.0.12" + rimraf "^2.6.2" + worker-farm "^1.5.4" + +libnpx@^10.0.1: + version "10.2.0" + resolved "https://registry.yarnpkg.com/libnpx/-/libnpx-10.2.0.tgz#1bf4a1c9f36081f64935eb014041da10855e3102" + dependencies: + dotenv "^5.0.1" + npm-package-arg "^6.0.0" + rimraf "^2.6.2" safe-buffer "^5.1.0" - update-notifier "^2.2.0" - which "^1.2.14" - y18n "^3.2.1" - yargs "^8.0.2" + update-notifier "^2.3.0" + which "^1.3.0" + y18n "^4.0.0" + yargs "^11.0.0" lint-staged@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.0.0.tgz#7ab7d345f2fe302ff196f1de6a005594ace03210" + version "6.1.1" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.1.1.tgz#cd08c4d9b8ccc2d37198d1c47ce77d22be6cf324" dependencies: app-root-path "^2.0.0" chalk "^2.1.0" commander "^2.11.0" - cosmiconfig "^3.1.0" + cosmiconfig "^4.0.0" debug "^3.1.0" dedent "^0.7.0" execa "^0.8.0" @@ -6377,7 +6386,7 @@ lint-staged@^6.0.0: p-map "^1.1.1" path-is-inside "^1.0.2" pify "^3.0.0" - staged-git-files "0.0.4" + staged-git-files "1.0.0" stringify-object "^3.2.0" listr-silent-renderer@^1.1.1: @@ -6460,7 +6469,7 @@ loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" -loader-utils@1.1.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@1.1.0, loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" dependencies: @@ -6468,7 +6477,7 @@ loader-utils@1.1.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1. emojis-list "^2.0.0" json5 "^0.5.0" -loader-utils@^0.2.16, loader-utils@^0.2.5: +loader-utils@^0.2.16: version "0.2.17" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" dependencies: @@ -6484,9 +6493,18 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +lock-verify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.0.1.tgz#6d671eea60b459c6048b3b26b62959208be67682" + dependencies: + npm-package-arg "^5.1.2" + semver "^5.4.1" + lockfile@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.3.tgz#2638fc39a0331e9cac1a04b71799931c9c50df79" + version "1.0.4" + resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609" + dependencies: + signal-exit "^3.0.2" lodash._baseuniq@~4.6.0: version "4.6.0" @@ -6519,6 +6537,10 @@ 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.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -6527,6 +6549,10 @@ lodash.isequal@^4.0.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + lodash.kebabcase@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" @@ -6536,8 +6562,8 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" lodash.mergewith@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" lodash.sortby@^4.7.0: version "4.7.0" @@ -6547,6 +6573,10 @@ lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + lodash.union@4.6.0, lodash.union@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" @@ -6563,17 +6593,13 @@ lodash@3.7.x: version "3.7.0" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.7.0.tgz#3678bd8ab995057c07ade836ed2ef087da811d45" -lodash@^3.10.1, lodash@^3.5.0, lodash@^3.6.0, lodash@^3.7.0, lodash@^3.8.0, lodash@~3.10.0, lodash@~3.10.1: +lodash@^3.10.1, lodash@^3.5.0, lodash@^3.6.0, lodash@^3.7.0, lodash@^3.8.0, lodash@~3.10.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.4: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - -lodash@^4.17.2: - version "4.17.5" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" +lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.4, lodash@~4.17.5: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" lodash@~4.3.0: version "4.3.0" @@ -6590,8 +6616,8 @@ log-symbols@^1.0.0, log-symbols@^1.0.2: chalk "^1.0.0" log-symbols@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6" + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" dependencies: chalk "^2.0.1" @@ -6645,16 +6671,12 @@ lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" lowercase-keys@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" -lru-cache@2.2.x: - version "2.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" - -lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@~4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" +lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@~4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" dependencies: pseudomap "^1.0.2" yallist "^2.1.2" @@ -6664,12 +6686,12 @@ macaddress@^0.2.8: resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" make-dir@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" + version "1.2.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" dependencies: pify "^3.0.0" -make-fetch-happen@^2.4.13, make-fetch-happen@^2.5.0: +make-fetch-happen@^2.5.0, make-fetch-happen@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38" dependencies: @@ -6797,25 +6819,7 @@ micromatch@^2.1.5, micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.0.3: - version "3.1.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.4.tgz#bb812e741a41f982c854e42b421a7eac458796f4" - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.0" - define-property "^1.0.0" - extend-shallow "^2.0.1" - extglob "^2.0.2" - fragment-cache "^0.2.1" - kind-of "^6.0.0" - nanomatch "^1.2.5" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -micromatch@^3.1.4: +micromatch@^3.0.3, micromatch@^3.1.4, micromatch@^3.1.8: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: @@ -6844,17 +6848,7 @@ miller-rabin@^4.0.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" - -mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" - dependencies: - mime-db "~1.30.0" - -mime-types@~2.1.18: +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: version "2.1.18" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: @@ -6864,13 +6858,17 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" -mime@^1.3.4, mime@^1.4.1, mime@^1.5.0: +mime@^1.3.4, mime@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" mimic-fn@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + +mimic-response@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" min-document@^2.19.0: version "2.19.0" @@ -6879,8 +6877,8 @@ min-document@^2.19.0: dom-walk "^0.1.0" minimalistic-assert@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" @@ -6914,21 +6912,22 @@ minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" -minipass@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.1.tgz#5ada97538b1027b4cf7213432428578cb564011f" +minipass@^2.2.1, minipass@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.4.tgz#03c824d84551ec38a8d1bb5bc350a5a30a354a40" dependencies: + safe-buffer "^5.1.1" yallist "^3.0.0" -minizlib@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.0.4.tgz#8ebb51dd8bbe40b0126b5633dbb36b284a2f523c" +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" dependencies: minipass "^2.2.1" -mississippi@^1.2.0, mississippi@^1.3.0, mississippi@~1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.0.tgz#d201583eb12327e3c5c1642a404a9cacf94e34f5" +mississippi@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.1.tgz#2a8bb465e86550ac8b36a7b6f45599171d78671e" dependencies: concat-stream "^1.5.0" duplexify "^3.4.2" @@ -6941,12 +6940,42 @@ mississippi@^1.2.0, mississippi@^1.3.0, mississippi@~1.3.0: stream-each "^1.1.0" through2 "^2.0.0" +mississippi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^2.0.1" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + mixin-deep@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.2.0.tgz#d02b8c6f8b6d4b8f5982d3fd009c4919851c3fe2" + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" dependencies: for-in "^1.0.2" - is-extendable "^0.1.1" + is-extendable "^1.0.1" mixin-object@^2.0.1: version "2.0.1" @@ -6972,22 +7001,22 @@ mobx-react-devtools@^4.2.15: resolved "https://registry.yarnpkg.com/mobx-react-devtools/-/mobx-react-devtools-4.2.15.tgz#881c038fb83db4dffd1e72bbaf5374d26b2fdebb" mobx-react@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-4.3.5.tgz#76853f2f2ef4a6f960c374bcd9f01e875929c04c" + version "4.4.3" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-4.4.3.tgz#baa9ec41165ee35ae7b9df19bca10190f36f117e" dependencies: hoist-non-react-statics "^2.3.1" mobx-state-tree@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-1.3.1.tgz#9e1ba9b8b6ea183f1a4a2ae1f67bfa8f2bcae4fe" + version "1.4.0" + resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-1.4.0.tgz#c914c855d5ec5c1c16e4ba6d6925679df42c8110" mobx@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-3.4.1.tgz#37abe5ee882d401828d9f26c6c1a2f47614bbbef" + version "3.6.2" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-3.6.2.tgz#fb9f5ff5090539a1ad54e75dc4c098b602693320" mocha@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" + version "4.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794" dependencies: browser-stdout "1.3.0" commander "2.11.0" @@ -7001,8 +7030,8 @@ mocha@^4.0.1: supports-color "4.4.0" moment@^2.18.1: - version "2.19.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" + version "2.22.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad" mousetrap-global-bind@^1.1.0: version "1.1.0" @@ -7012,7 +7041,7 @@ mousetrap@^1.6.0: version "1.6.1" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.1.tgz#2a085f5c751294c75e7e81f6ec2545b29cbf42d9" -move-concurrently@^1.0.1, move-concurrently@~1.0.1: +move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" dependencies: @@ -7031,10 +7060,14 @@ ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" -ms@2.0.0, ms@^2.0.0: +ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" +ms@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -7063,25 +7096,9 @@ mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.0.5, nan@^2.3.0, nan@^2.3.2, nan@^2.6.2: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" - -nanomatch@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.5.tgz#5c9ab02475c76676275731b0bf0a7395c624a9c4" - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^1.0.0" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - is-odd "^1.0.0" - kind-of "^5.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" +nan@^2.10.0, nan@^2.6.2, nan@^2.9.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" nanomatch@^1.2.9: version "1.2.9" @@ -7119,17 +7136,34 @@ ncp@0.4.x: resolved "https://registry.yarnpkg.com/ncp/-/ncp-0.4.2.tgz#abcc6cbd3ec2ed2a729ff6e7c1fa8f01784a8574" nearley@^2.7.10: - version "2.11.0" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.11.0.tgz#5e626c79a6cd2f6ab9e7e5d5805e7668967757ae" + version "2.13.0" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.13.0.tgz#6e7b0f4e68bfc3e74c99eaef2eda39e513143439" dependencies: nomnom "~1.6.2" railroad-diagrams "^1.0.0" - randexp "^0.4.2" + randexp "0.4.6" + semver "^5.4.1" + +needle@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.0.tgz#f14efc69cee1024b72c8b21c7bdf94a731dc12fa" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +neo-async@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee" + +next-tick@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + ng-annotate-loader@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/ng-annotate-loader/-/ng-annotate-loader-0.6.1.tgz#e9b7b7a1562b9c79737d50886d558de7f0df4257" @@ -7188,15 +7222,15 @@ ngtemplate-loader@^2.0.1: jsesc "^0.5.0" loader-utils "^1.0.2" -no-case@^2.2.0: +no-case@^2.2.0, no-case@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" dependencies: lower-case "^1.1.1" -node-abi@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.1.2.tgz#4da6caceb6685fcd31e7dd1994ef6bb7d0a9c0b2" +node-abi@^2.2.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.0.tgz#3c27515cb842f5bbc132a31254f9f1e1c55c7b83" dependencies: semver "^5.4.1" @@ -7219,7 +7253,7 @@ node-forge@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" -node-gyp@^3.3.1, node-gyp@^3.6.2, node-gyp@~3.6.2: +node-gyp@^3.3.1, node-gyp@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" dependencies: @@ -7269,34 +7303,33 @@ node-libs-browser@^2.0.0: util "^0.10.3" vm-browserify "0.0.4" -node-notifier@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff" +node-notifier@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea" dependencies: growly "^1.3.0" - semver "^5.3.0" - shellwords "^0.1.0" - which "^1.2.12" + semver "^5.4.1" + shellwords "^0.1.1" + which "^1.3.0" -node-pre-gyp@^0.6.39: - version "0.6.39" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" +node-pre-gyp@^0.9.0: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.9.1.tgz#f11c07516dd92f87199dbc7e1838eab7cd56c9e0" dependencies: detect-libc "^1.0.2" - hawk "3.1.3" mkdirp "^0.5.1" + needle "^2.2.0" nopt "^4.0.1" + npm-packlist "^1.1.6" npmlog "^4.0.2" rc "^1.1.7" - request "2.81.0" rimraf "^2.6.1" semver "^5.3.0" - tar "^2.2.1" - tar-pack "^3.4.0" + tar "^4" -node-sass@^4.0.0: - version "4.7.2" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e" +node-sass@^4.7.2: + version "4.9.0" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.0.tgz#d1b8aa855d98ed684d6848db929a20771cc2ae52" dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -7310,7 +7343,7 @@ node-sass@^4.0.0: lodash.mergewith "^4.6.0" meow "^3.7.0" mkdirp "^0.5.1" - nan "^2.3.2" + nan "^2.10.0" node-gyp "^3.3.1" npmlog "^4.0.0" request "~2.79.0" @@ -7318,7 +7351,7 @@ node-sass@^4.0.0: stdout-stream "^1.4.0" "true-case-path" "^1.0.2" -"nomnom@>= 1.5.x": +nomnom@^1.5.x: version "1.8.1" resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" dependencies: @@ -7399,17 +7432,33 @@ npm-install-checks@~3.0.0: dependencies: semver "^2.3.0 || 3.x || 4 || 5" -npm-lifecycle@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-1.0.3.tgz#4cd60543247dbba631281e48ce665ffd52380cce" +npm-lifecycle@^2.0.0, npm-lifecycle@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.0.1.tgz#897313f05ed24db8e28d99fa8b42c31b625e6237" dependencies: + byline "^5.0.0" graceful-fs "^4.1.11" + node-gyp "^3.6.2" + resolve-from "^4.0.0" slide "^1.1.6" uid-number "0.0.6" umask "^1.1.0" which "^1.3.0" -"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0", "npm-package-arg@^4.0.0 || ^5.0.0", npm-package-arg@^5.1.2, npm-package-arg@~5.1.2: +npm-logical-tree@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/npm-logical-tree/-/npm-logical-tree-1.2.1.tgz#44610141ca24664cad35d1e607176193fd8f5b88" + +"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1" + dependencies: + hosted-git-info "^2.6.0" + osenv "^0.1.5" + semver "^5.5.0" + validate-npm-package-name "^3.0.0" + +npm-package-arg@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-5.1.2.tgz#fb18d17bb61e60900d6312619919bd753755ab37" dependencies: @@ -7418,7 +7467,16 @@ npm-lifecycle@~1.0.3: semver "^5.1.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.6, npm-packlist@~1.1.9: +npm-package-arg@~6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.0.0.tgz#8cce04b49d3f9faec3f56b0fe5f4391aeb9d2fac" + dependencies: + hosted-git-info "^2.5.0" + osenv "^0.1.4" + semver "^5.4.1" + validate-npm-package-name "^3.0.0" + +npm-packlist@^1.1.10, npm-packlist@^1.1.6, npm-packlist@~1.1.10: version "1.1.10" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a" dependencies: @@ -7426,39 +7484,40 @@ npm-packlist@^1.1.6, npm-packlist@~1.1.9: 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" + version "2.0.4" + resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64" 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" +npm-pick-manifest@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.1.0.tgz#dc381bdd670c35d81655e1d5a94aa3dd4d87fce5" dependencies: - npm-package-arg "^5.1.2" - semver "^5.3.0" + npm-package-arg "^6.0.0" + semver "^5.4.1" -npm-profile@~2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-2.0.5.tgz#0e61b8f1611bd19d1eeff5e3d5c82e557da3b9d7" +npm-profile@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-3.0.1.tgz#65a1018340f14399a086b5d0a9bd0d13145d8e57" dependencies: aproba "^1.1.2" make-fetch-happen "^2.5.0" -npm-registry-client@~8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.5.0.tgz#4878fb6fa1f18a5dc08ae83acf94d0d0112d7ed0" +npm-registry-client@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.5.1.tgz#8115809c0a4b40938b8a109b8ea74d26c6f5d7f1" dependencies: concat-stream "^1.5.2" graceful-fs "^4.1.6" normalize-package-data "~1.0.1 || ^2.0.0" - npm-package-arg "^3.0.0 || ^4.0.0 || ^5.0.0" + npm-package-arg "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" once "^1.3.3" request "^2.74.0" retry "^0.10.0" + safe-buffer "^5.1.1" semver "2 >=2.2.1 || 3.x || 4 || 5" slide "^1.1.3" - ssri "^4.1.2" + ssri "^5.2.4" optionalDependencies: npmlog "2 || ^3.1.0 || ^4.0.0" @@ -7481,18 +7540,19 @@ npm-which@^3.0.1: which "^1.2.10" npm@^5.4.2: - version "5.5.1" - resolved "https://registry.yarnpkg.com/npm/-/npm-5.5.1.tgz#5bef2b01c51c8144412d5873caf83e22f1ec6b84" + version "5.8.0" + resolved "https://registry.yarnpkg.com/npm/-/npm-5.8.0.tgz#5e4bfb8c2e7ada01dd41ec0555d13dd0f446ddb2" dependencies: - JSONStream "~1.3.1" + JSONStream "^1.3.2" abbrev "~1.1.1" ansi-regex "~3.0.0" ansicolors "~0.3.2" ansistyles "~0.1.3" aproba "~1.2.0" archy "~1.0.0" - bluebird "~3.5.0" - cacache "~9.2.9" + bin-links "^1.1.0" + bluebird "~3.5.1" + cacache "^10.0.4" call-limit "~1.1.0" chownr "~1.0.1" cli-table2 "~0.2.0" @@ -7500,22 +7560,27 @@ npm@^5.4.2: columnify "~1.5.4" config-chain "~1.1.11" detect-indent "~5.0.0" + detect-newline "^2.1.0" dezalgo "~1.0.3" editor "~1.0.0" + find-npm-prefix "^1.0.2" fs-vacuum "~1.2.10" fs-write-stream-atomic "~1.0.10" + gentle-fs "^2.0.1" glob "~7.1.2" graceful-fs "~4.1.11" has-unicode "~2.0.1" - hosted-git-info "~2.5.0" + hosted-git-info "^2.6.0" iferr "~0.1.5" inflight "~1.0.6" inherits "~2.0.3" - ini "~1.3.4" - init-package-json "~1.10.1" + ini "^1.3.5" + init-package-json "^1.10.3" is-cidr "~1.0.0" + json-parse-better-errors "^1.0.1" lazy-property "~1.0.0" - libnpx "~9.6.0" + libcipm "^1.6.0" + libnpx "^10.0.1" lockfile "~1.0.3" lodash._baseuniq "~4.6.0" lodash.clonedeep "~4.5.0" @@ -7524,60 +7589,59 @@ npm@^5.4.2: lodash.without "~4.4.0" lru-cache "~4.1.1" meant "~1.0.1" - mississippi "~1.3.0" + mississippi "^3.0.0" mkdirp "~0.5.1" - move-concurrently "~1.0.1" - node-gyp "~3.6.2" + move-concurrently "^1.0.1" nopt "~4.0.1" normalize-package-data "~2.4.0" npm-cache-filename "~1.0.2" npm-install-checks "~3.0.0" - npm-lifecycle "~1.0.3" - npm-package-arg "~5.1.2" - npm-packlist "~1.1.9" - npm-profile "~2.0.4" - npm-registry-client "~8.5.0" + npm-lifecycle "^2.0.1" + npm-package-arg "~6.0.0" + npm-packlist "~1.1.10" + npm-profile "^3.0.1" + npm-registry-client "^8.5.1" npm-user-validate "~1.0.0" npmlog "~4.1.2" once "~1.4.0" opener "~1.4.3" - osenv "~0.1.4" - pacote "~6.0.2" + osenv "^0.1.5" + pacote "^7.6.1" path-is-inside "~1.0.2" promise-inflight "~1.0.1" qrcode-terminal "~0.11.0" - query-string "~5.0.0" + query-string "^5.1.0" qw "~1.0.1" read "~1.0.7" read-cmd-shim "~1.0.1" read-installed "~4.0.3" - read-package-json "~2.0.12" + read-package-json "^2.0.13" read-package-tree "~5.1.6" - readable-stream "~2.3.3" + readable-stream "^2.3.5" request "~2.83.0" retry "~0.10.1" rimraf "~2.6.2" safe-buffer "~5.1.1" - semver "~5.4.1" + semver "^5.5.0" sha "~2.0.1" slide "~1.1.6" sorted-object "~2.0.1" sorted-union-stream "~2.1.3" - ssri "~4.1.6" + ssri "^5.2.4" strip-ansi "~4.0.0" - tar "~4.0.1" + tar "^4.4.0" text-table "~0.2.0" uid-number "0.0.6" umask "~1.1.0" unique-filename "~1.1.0" unpipe "~1.0.0" - update-notifier "~2.2.0" - uuid "~3.1.0" + update-notifier "~2.3.0" + uuid "^3.2.1" validate-npm-package-name "~3.0.0" which "~1.3.0" - worker-farm "~1.5.0" + worker-farm "^1.5.4" wrappy "~1.0.2" - write-file-atomic "~2.1.0" + write-file-atomic "^2.3.0" "npmlog@0 || 1 || 2 || 3 || 4", "npmlog@2 || ^3.1.0 || ^4.0.0", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.0.2, npmlog@~4.1.2: version "4.1.2" @@ -7607,8 +7671,8 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" nwmatcher@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c" + version "1.4.4" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e" oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" @@ -7721,7 +7785,7 @@ once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0, once@~1.4.0: onetime@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" opener@^1.4.3, opener@~1.4.3: version "1.4.3" @@ -7806,9 +7870,9 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" -osenv@0, osenv@^0.1.4, osenv@~0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" +osenv@0, osenv@^0.1.4, osenv@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" dependencies: os-homedir "^1.0.0" os-tmpdir "^1.0.0" @@ -7818,8 +7882,10 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" p-limit@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" p-locate@^2.0.0: version "2.0.0" @@ -7831,6 +7897,10 @@ p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + package-json@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" @@ -7840,29 +7910,32 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" -pacote@~6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-6.0.4.tgz#9384c4ca9a9dbbaa625bfbe653e0330eeaa1427b" +pacote@^7.5.1, pacote@^7.6.1: + version "7.6.1" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-7.6.1.tgz#d44621c89a5a61f173989b60236757728387c094" dependencies: - bluebird "^3.5.0" - cacache "^9.2.9" + bluebird "^3.5.1" + cacache "^10.0.4" + get-stream "^3.0.0" glob "^7.1.2" lru-cache "^4.1.1" - make-fetch-happen "^2.4.13" + make-fetch-happen "^2.6.0" minimatch "^3.0.4" - mississippi "^1.2.0" + mississippi "^3.0.0" + mkdirp "^0.5.1" normalize-package-data "^2.4.0" - npm-package-arg "^5.1.2" - npm-packlist "^1.1.6" - npm-pick-manifest "^1.0.4" - osenv "^0.1.4" + npm-package-arg "^6.0.0" + npm-packlist "^1.1.10" + npm-pick-manifest "^2.1.0" + osenv "^0.1.5" promise-inflight "^1.0.1" promise-retry "^1.1.1" - protoduck "^4.0.0" + protoduck "^5.0.0" + rimraf "^2.6.2" safe-buffer "^5.1.1" - semver "^5.4.1" - ssri "^4.1.6" - tar "^4.0.0" + semver "^5.5.0" + ssri "^5.2.4" + tar "^4.4.0" unique-filename "^1.1.0" which "^1.3.0" @@ -7889,8 +7962,8 @@ param-case@2.1.x, param-case@^2.1.0: no-case "^2.2.0" parse-asn1@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" dependencies: asn1.js "^4.0.0" browserify-aes "^1.0.0" @@ -7913,13 +7986,18 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse-json@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13" +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" dependencies: error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" -parse5@^3.0.1, parse5@^3.0.2: +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + +parse5@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" dependencies: @@ -8025,8 +8103,8 @@ pathval@~0.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-0.1.1.tgz#08f911cdca9cce5942880da7817bc0b723b66d82" pbkdf2@^3.0.3: - version "3.0.14" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + version "3.0.16" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c" dependencies: create-hash "^1.1.2" create-hmac "^1.1.4" @@ -8038,10 +8116,6 @@ pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -8108,13 +8182,13 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" -pn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.0.0.tgz#1cf5a30b0d806cd18f88fc41a6b5d4ad615b3ba9" +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" popper.js@^1.12.5: - version "1.12.9" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.12.9.tgz#0dfbc2dff96c451bb332edcfcfaaf566d331d5b3" + version "1.14.3" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095" portfinder@^1.0.9: version "1.0.13" @@ -8219,13 +8293,13 @@ postcss-load-plugins@^2.3.0: object-assign "^4.1.0" postcss-loader@^2.0.6: - version "2.0.9" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.9.tgz#001fdf7bfeeb159405ee61d1bb8e59b528dbd309" + version "2.1.4" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.1.4.tgz#f44a6390e03c84108b2b2063182d1a1011b2ce76" dependencies: loader-utils "^1.1.0" postcss "^6.0.0" postcss-load-config "^1.2.0" - schema-utils "^0.3.0" + schema-utils "^0.4.0" postcss-merge-idents@^2.1.5: version "2.1.7" @@ -8288,27 +8362,27 @@ postcss-minify-selectors@^2.0.4: postcss "^5.0.14" postcss-selector-parser "^2.0.0" -postcss-modules-extract-imports@^1.0.0: +postcss-modules-extract-imports@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" dependencies: postcss "^6.0.1" -postcss-modules-local-by-default@^1.0.1: +postcss-modules-local-by-default@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" dependencies: css-selector-tokenizer "^0.7.0" postcss "^6.0.1" -postcss-modules-scope@^1.0.0: +postcss-modules-scope@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" dependencies: css-selector-tokenizer "^0.7.0" postcss "^6.0.1" -postcss-modules-values@^1.1.0: +postcss-modules-values@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" dependencies: @@ -8414,12 +8488,12 @@ postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0. supports-color "^3.2.3" postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.8: - version "6.0.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885" + version "6.0.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d" dependencies: - chalk "^2.3.0" + chalk "^2.3.2" source-map "^0.6.1" - supports-color "^4.4.0" + supports-color "^5.3.0" power-assert-context-formatter@^1.0.7: version "1.1.1" @@ -8500,8 +8574,8 @@ power-assert-util-string-width@^1.1.1: eastasianwidth "^0.1.1" power-assert@^1.2.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/power-assert/-/power-assert-1.4.4.tgz#9295ea7437196f5a601fde420f042631186d7517" + version "1.5.0" + resolved "https://registry.yarnpkg.com/power-assert/-/power-assert-1.5.0.tgz#624caa76a5dc228c00f36704bb1762657c174fee" dependencies: define-properties "^1.1.2" empower "^1.2.3" @@ -8510,23 +8584,24 @@ power-assert@^1.2.0: xtend "^4.0.0" prebuild-install@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.3.0.tgz#19481247df728b854ab57b187ce234211311b485" + version "2.5.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.5.3.tgz#9f65f242782d370296353710e9bc843490c19f69" dependencies: + detect-libc "^1.0.3" expand-template "^1.0.2" github-from-package "0.0.0" minimist "^1.2.0" mkdirp "^0.5.1" - node-abi "^2.1.1" + node-abi "^2.2.0" noop-logger "^0.1.1" npmlog "^4.0.1" os-homedir "^1.0.1" - pump "^1.0.1" + pump "^2.0.1" rc "^1.1.6" - simple-get "^1.4.2" + simple-get "^2.7.0" tar-fs "^1.13.0" tunnel-agent "^0.6.0" - xtend "4.0.1" + which-pm-runs "^1.0.0" prelude-ls@~1.1.2: version "1.1.2" @@ -8569,21 +8644,23 @@ pretty-format@^21.2.1: ansi-regex "^3.0.0" ansi-styles "^3.2.0" -pretty-format@^22.0.3: - version "22.0.3" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.0.3.tgz#a2bfa59fc33ad24aa4429981bb52524b41ba5dd7" +pretty-format@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f" dependencies: ansi-regex "^3.0.0" ansi-styles "^3.2.0" +prismjs@^1.6.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.14.0.tgz#bbccfdb8be5d850d26453933cb50122ca0362ae0" + optionalDependencies: + clipboard "^2.0.0" + private@^0.1.6, private@^0.1.7, private@~0.1.5: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" @@ -8633,15 +8710,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0: - version "15.6.0" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" - dependencies: - fbjs "^0.8.16" - loose-envify "^1.3.1" - object-assign "^4.1.1" - -prop-types@^15.6.1: +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1: version "15.6.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" dependencies: @@ -8653,19 +8722,12 @@ proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" -protoduck@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-4.0.0.tgz#fe4874d8c7913366cfd9ead12453a22cd3657f8e" +protoduck@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.0.tgz#752145e6be0ad834cb25716f670a713c860dce70" dependencies: genfun "^4.0.1" -proxy-addr@~2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.5.2" - proxy-addr@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" @@ -8673,17 +8735,17 @@ proxy-addr@~2.0.3: forwarded "~0.1.2" ipaddr.js "1.6.0" -prr@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" public-encrypt@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + version "4.0.2" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994" dependencies: bn.js "^4.1.0" browserify-rsa "^4.0.0" @@ -8691,20 +8753,34 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" -pump@^1.0.0, pump@^1.0.1: +pump@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" dependencies: end-of-stream "^1.1.0" once "^1.3.1" -pumpify@^1.3.3: - version "1.3.5" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.3.5.tgz#1b671c619940abcaeac0ad0e3a3c164be760993b" +pump@^2.0.0, pump@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" dependencies: - duplexify "^3.1.2" - inherits "^2.0.1" - pump "^1.0.0" + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" + dependencies: + duplexify "^3.5.3" + inherits "^2.0.3" + pump "^2.0.0" punycode@1.3.2: version "1.3.2" @@ -8723,8 +8799,8 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" qjobs@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" qrcode-terminal@~0.11.0: version "0.11.0" @@ -8738,10 +8814,6 @@ qs@~6.3.0: version "6.3.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -8749,9 +8821,9 @@ query-string@^4.1.0: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -query-string@~5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88" +query-string@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" dependencies: decode-uri-component "^0.2.0" object-assign "^4.1.0" @@ -8769,9 +8841,9 @@ querystringify@0.0.x: version "0.0.4" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" -querystringify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" +querystringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755" qw@~1.0.1: version "1.0.1" @@ -8787,7 +8859,7 @@ railroad-diagrams@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" -randexp@^0.4.2: +randexp@0.4.6: version "0.4.6" resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" dependencies: @@ -8802,14 +8874,14 @@ randomatic@^1.1.3: kind-of "^4.0.0" randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" + version "2.0.6" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" dependencies: safe-buffer "^5.1.0" randomfill@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" dependencies: randombytes "^2.0.5" safe-buffer "^5.1.0" @@ -8828,8 +8900,8 @@ raw-body@2.3.2: unpipe "1.0.0" rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: - version "1.2.2" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077" + version "1.2.6" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092" dependencies: deep-extend "~0.4.0" ini "~1.3.0" @@ -8837,22 +8909,15 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: strip-json-comments "~2.0.1" react-dom@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044" + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.2.tgz#cb90f107e09536d683d84ed5d4888e9640e0e4df" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.0" -"react-draggable@^2.2.6 || ^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4" - dependencies: - classnames "^2.2.5" - prop-types "^15.5.10" - -react-draggable@^3.0.3: +"react-draggable@^2.2.6 || ^3.0.3", react-draggable@^3.0.3: version "3.0.5" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.5.tgz#c031e0ed4313531f9409d6cd84c8ebcec0ddfe2d" dependencies: @@ -8877,21 +8942,34 @@ react-highlight-words@^0.10.0: prop-types "^15.5.8" react-hot-loader@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.0.1.tgz#48284350ae5d7ba07dac872bd5bbc6e477352593" + version "4.1.2" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.1.2.tgz#5e8025f5bc5605506586b46eb2c6cc4006fd54d7" dependencies: fast-levenshtein "^2.0.6" global "^4.3.0" hoist-non-react-statics "^2.5.0" prop-types "^15.6.1" + react-lifecycles-compat "^3.0.2" shallowequal "^1.0.2" +react-immutable-proptypes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.1.0.tgz#023d6f39bb15c97c071e9e60d00d136eac5fa0b4" + react-input-autosize@^2.1.2: version "2.2.1" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8" dependencies: prop-types "^15.5.8" +react-is@^16.3.2: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" + +react-lifecycles-compat@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz#7279047275bd727a912e25f734c0559527e84eff" + react-popper@^0.7.5: version "0.7.5" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.7.5.tgz#71c25946f291db381231281f6b95729e8b801596" @@ -8899,6 +8977,21 @@ react-popper@^0.7.5: popper.js "^1.12.5" prop-types "^15.5.10" +react-portal@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.2.0.tgz#4224e19b2b05d5cbe730a7ba0e34ec7585de0043" + dependencies: + prop-types "^15.5.8" + +react-reconciler@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" + react-resizable@^1.7.5: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" @@ -8907,50 +9000,49 @@ react-resizable@^1.7.5: react-draggable "^2.2.6 || ^3.0.3" react-select@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.2.0.tgz#4f91df941c4ecdb94701faca2533b60e31d7508e" + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.2.1.tgz#a2fe58a569eb14dcaa6543816260b97e538120d1" dependencies: classnames "^2.2.4" prop-types "^15.5.8" react-input-autosize "^2.1.2" react-sizeme@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.3.6.tgz#d60ea2634acc3fd827a3c7738d41eea0992fa678" + version "2.4.2" + resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.4.2.tgz#9e1683f926f92b3db7881d09f9efa3879e8dfde2" dependencies: element-resize-detector "^1.1.12" invariant "^2.2.2" - lodash "^4.17.4" + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" react-test-renderer@^16.0.0, react-test-renderer@^16.0.0-0: - version "16.1.1" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.1.1.tgz#a05184688d564be799f212449262525d1e350537" + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.3.2.tgz#3d1ed74fda8db42521fdf03328e933312214749a" dependencies: fbjs "^0.8.16" object-assign "^4.1.1" prop-types "^15.6.0" + react-is "^16.3.2" react-transition-group@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10" + version "2.3.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.3.1.tgz#31d611b33e143a5e0f2d94c348e026a0f3b474b6" dependencies: - chain-function "^1.0.0" - classnames "^2.2.5" - dom-helpers "^3.2.0" + dom-helpers "^3.3.1" loose-envify "^1.3.1" - prop-types "^15.5.8" - warning "^3.0.0" + prop-types "^15.6.1" react@^16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba" + version "16.3.2" + resolved "https://registry.yarnpkg.com/react/-/react-16.3.2.tgz#fdc8420398533a1e58872f59091b272ce2f91ea9" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.0" -read-cmd-shim@~1.0.1: +read-cmd-shim@^1.0.1, read-cmd-shim@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz#2d5d157786a37c055d22077c32c53f8329e91c7b" dependencies: @@ -8969,12 +9061,12 @@ read-installed@~4.0.3: optionalDependencies: graceful-fs "^4.1.2" -"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@~2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.12.tgz#68ea45f98b3741cb6e10ae3bbd42a605026a6951" +"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.12, read-package-json@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.13.tgz#2e82ebd9f613baa6d2ebe3aa72cefe3f68e41f4a" dependencies: glob "^7.1.1" - json-parse-better-errors "^1.0.0" + json-parse-better-errors "^1.0.1" normalize-package-data "^2.0.0" slash "^1.0.0" optionalDependencies: @@ -9026,16 +9118,16 @@ read@1, read@1.0.x, read@~1.0.1, read@~1.0.7: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.3.3, readable-stream@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" dependencies: core-util-is "~1.0.0" inherits "~2.0.3" isarray "~1.0.0" - process-nextick-args "~1.0.6" + process-nextick-args "~2.0.0" safe-buffer "~5.1.1" - string_decoder "~1.0.3" + string_decoder "~1.1.1" util-deprecate "~1.0.1" readable-stream@1.0, readable-stream@~1.0.2: @@ -9056,18 +9148,6 @@ readable-stream@1.1: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.2.9: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readable-stream@~1.1.10: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -9152,8 +9232,8 @@ regenerator-runtime@^0.10.5: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" regenerator-runtime@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" regenerator-transform@^0.10.0: version "0.10.1" @@ -9169,13 +9249,7 @@ regex-cache@^0.4.2: dependencies: is-equal-shallow "^0.1.3" -regex-not@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9" - dependencies: - extend-shallow "^2.0.1" - -regex-not@^1.0.2: +regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" dependencies: @@ -9199,8 +9273,8 @@ regexpu-core@^2.0.0: regjsparser "^0.1.4" registry-auth-token@^3.0.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.1.tgz#fb0d3289ee0d9ada2cbb52af5dfe66cb070d3006" + version "3.3.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.3.2.tgz#851fd49038eecb586911115af845260eec983f20" dependencies: rc "^1.1.6" safe-buffer "^5.0.1" @@ -9276,7 +9350,7 @@ request-promise-core@1.1.1: dependencies: lodash "^4.13.1" -request-promise-native@^1.0.3: +request-promise-native@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" dependencies: @@ -9284,9 +9358,9 @@ request-promise-native@^1.0.3: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" -request@2, request@^2.74.0, request@^2.81.0, request@^2.83.0, request@~2.83.0: - version "2.83.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" +request@2, request@^2.74.0, request@^2.81.0, request@^2.83.0: + version "2.85.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" dependencies: aws-sign2 "~0.7.0" aws4 "^1.6.0" @@ -9311,33 +9385,6 @@ request@2, request@^2.74.0, request@^2.81.0, request@^2.83.0, request@~2.83.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@2.81.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - request@~2.79.0: version "2.79.0" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" @@ -9363,6 +9410,33 @@ request@~2.79.0: tunnel-agent "~0.4.1" uuid "^3.0.0" +request@~2.83.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -9372,8 +9446,8 @@ require-from-string@^1.1.0: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" require-from-string@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" require-main-filename@^1.0.1: version "1.0.1" @@ -9386,7 +9460,7 @@ require-uncached@^1.0.2: caller-path "^0.1.0" resolve-from "^1.0.0" -requires-port@1.0.x, requires-port@1.x.x, requires-port@~1.0.0: +requires-port@1.0.x, requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -9412,6 +9486,10 @@ resolve-from@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + resolve-pkg@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-0.1.0.tgz#02cc993410e2936962bd97166a1b077da9725531" @@ -9427,8 +9505,8 @@ resolve@1.1.7, resolve@~1.1.0: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + version "1.7.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" dependencies: path-parse "^1.0.5" @@ -9466,7 +9544,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@~2.6.2: +rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -9477,10 +9555,10 @@ rimraf@~2.2.8: resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" dependencies: - hash-base "^2.0.0" + hash-base "^3.0.0" inherits "^2.0.1" rst-selector-parser@^2.2.3: @@ -9516,28 +9594,30 @@ 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.2: - version "5.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.5.tgz#e164f11d38eaf29f56f08c3447f74ff02dd84e97" +rxjs@^5.4.2, rxjs@^5.4.3: + version "5.5.10" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045" dependencies: symbol-observable "1.0.1" -rxjs@^5.4.3: - version "5.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3" - dependencies: - symbol-observable "^1.0.1" - -safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" dependencies: ret "~0.1.10" +safer-buffer@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + samsam@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -9547,13 +9627,13 @@ samsam@~1.1: resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621" sane@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56" + version "2.5.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.0.tgz#6359cd676f5efd9988b264d8ce3b827dd6b27bec" dependencies: - anymatch "^1.3.0" + anymatch "^2.0.0" exec-sh "^0.2.0" fb-watchman "^2.0.0" - minimatch "^3.0.2" + micromatch "^3.1.4" minimist "^1.1.1" walker "~1.0.5" watch "~0.18.0" @@ -9589,16 +9669,16 @@ sass-lint@^1.10.2, sass-lint@^1.12.0: util "^0.10.3" sass-loader@^6.0.6: - version "6.0.6" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9" + version "6.0.7" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.7.tgz#dd2fdb3e7eeff4a53f35ba6ac408715488353d00" dependencies: - async "^2.1.5" - clone-deep "^0.3.0" + clone-deep "^2.0.1" loader-utils "^1.0.1" lodash.tail "^4.1.1" + neo-async "^2.5.0" pify "^3.0.0" -sax@^1.2.1, sax@~1.2.1: +sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -9608,7 +9688,7 @@ schema-utils@^0.3.0: dependencies: ajv "^5.0.0" -schema-utils@^0.4.5: +schema-utils@^0.4.0, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" dependencies: @@ -9630,6 +9710,10 @@ select@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" +selection-is-backward@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/selection-is-backward/-/selection-is-backward-1.0.0.tgz#97a54633188a511aba6419fc5c1fa91b467e6be1" + selfsigned@^1.9.1: version "1.10.2" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.2.tgz#b4449580d99929b65b10a48389301a6592088758" @@ -9642,9 +9726,9 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@~5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" +"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" semver@~4.3.3: version "4.3.6" @@ -9654,24 +9738,6 @@ semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" -send@0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" - dependencies: - debug "2.6.9" - depd "~1.1.1" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.1" - send@0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" @@ -9709,15 +9775,6 @@ serve-index@^1.7.2: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.1" - serve-static@1.13.2: version "1.13.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" @@ -9731,12 +9788,6 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" -set-getter@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" - dependencies: - to-object-path "^0.3.0" - set-immediate-shim@^1.0.0, set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" @@ -9772,8 +9823,8 @@ setprototypeof@1.1.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.9" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d" + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" dependencies: inherits "^2.0.1" safe-buffer "^5.0.1" @@ -9785,13 +9836,12 @@ sha@~2.0.1: graceful-fs "^4.1.2" readable-stream "^2.0.2" -shallow-clone@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" +shallow-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" dependencies: is-extendable "^0.1.1" - kind-of "^2.0.1" - lazy-cache "^0.2.3" + kind-of "^5.0.0" mixin-object "^2.0.1" shallowequal@^1.0.2: @@ -9825,7 +9875,7 @@ shelljs@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" -shellwords@^0.1.0: +shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -9833,17 +9883,21 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +simple-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" + simple-fmt@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/simple-fmt/-/simple-fmt-0.1.0.tgz#191bf566a59e6530482cb25ab53b4a8dc85c3a6b" -simple-get@^1.4.2: - version "1.4.3" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-1.4.3.tgz#e9755eda407e96da40c5e5158c9ea37b33becbeb" +simple-get@^2.7.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d" dependencies: + decompress-response "^3.3.0" once "^1.3.1" - unzip-response "^1.0.0" - xtend "^4.0.0" + simple-concat "^1.0.0" simple-is@~0.2.0: version "0.2.0" @@ -9862,11 +9916,71 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +slate-base64-serializer@^0.2.29: + version "0.2.29" + resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.29.tgz#eaf4c92296a52510023ac36dd39a34a6b7a5df00" + dependencies: + isomorphic-base64 "^1.0.2" + +slate-dev-logger@^0.1.39: + version "0.1.39" + resolved "https://registry.yarnpkg.com/slate-dev-logger/-/slate-dev-logger-0.1.39.tgz#744a69b85034244713e6de51483af5713c345af4" + +slate-plain-serializer@^0.5.10: + version "0.5.10" + resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.5.10.tgz#0a430824485f3dd4c7bf5bcae1bae37f13741c5c" + dependencies: + slate-dev-logger "^0.1.39" + +slate-prop-types@^0.4.27: + version "0.4.27" + resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.27.tgz#9b3c13f0a1a1b034f8e66095a28bd537af03ba95" + dependencies: + slate-dev-logger "^0.1.39" + +slate-react@^0.12.4: + version "0.12.4" + resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.12.4.tgz#36407e38e7230e6cd0c93fa75e49d29bd447f983" + dependencies: + debug "^2.3.2" + get-window "^1.1.1" + is-hotkey "^0.1.1" + is-in-browser "^1.1.3" + is-window "^1.0.2" + keycode "^2.1.2" + lodash "^4.1.1" + prop-types "^15.5.8" + react-immutable-proptypes "^2.1.0" + react-portal "^3.1.0" + selection-is-backward "^1.0.0" + slate-base64-serializer "^0.2.29" + slate-dev-logger "^0.1.39" + slate-plain-serializer "^0.5.10" + slate-prop-types "^0.4.27" + +slate-schema-violations@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/slate-schema-violations/-/slate-schema-violations-0.1.8.tgz#b6bfbcc4defaa4971a96bf2d324c5b67fb4a4e20" + +slate@^0.33.4: + version "0.33.4" + resolved "https://registry.yarnpkg.com/slate/-/slate-0.33.4.tgz#8f39000a7a0fb1ec04b211c3e264e45677dfd9d8" + dependencies: + debug "^2.3.2" + direction "^0.1.5" + esrever "^0.2.0" + is-empty "^1.0.0" + is-plain-object "^2.0.4" + lodash "^4.17.4" + slate-dev-logger "^0.1.39" + slate-schema-violations "^0.1.8" + type-of "^2.0.1" + slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" -slide@^1.1.3, slide@^1.1.5, slide@^1.1.6, slide@~1.1.3, slide@~1.1.6: +slide@^1.1.3, slide@^1.1.6, slide@~1.1.3, slide@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" @@ -9895,8 +10009,8 @@ snapdragon-util@^3.0.1: kind-of "^3.2.0" snapdragon@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370" + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" dependencies: base "^0.11.1" debug "^2.2.0" @@ -9905,7 +10019,7 @@ snapdragon@^0.8.1: map-cache "^0.2.2" source-map "^0.5.6" source-map-resolve "^0.5.0" - use "^2.0.0" + use "^3.1.0" sntp@1.x.x: version "1.0.9" @@ -10036,10 +10150,11 @@ source-map-support@^0.4.0, source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map-support@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.0.tgz#2018a7ad2bdf8faf2691e5fddab26bed5a2bacab" +source-map-support@^0.5.0, source-map-support@^0.5.3: + version "0.5.5" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.5.tgz#0d4af9e00493e855402e8ec36ebed2d266fceb90" dependencies: + buffer-from "^1.0.0" source-map "^0.6.0" source-map-url@^0.4.0: @@ -10056,7 +10171,7 @@ source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" -source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3, source-map@~0.5.6: +source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -10064,19 +10179,27 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" dependencies: - spdx-license-ids "^1.0.2" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" spdy-transport@^2.0.18: version "2.1.0" @@ -10107,13 +10230,17 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +sprintf-js@^1.0.3: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.1.tgz#36be78320afe5801f6cea3ee78b6e5aab940ea0c" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" sshpk@^1.7.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + version "1.14.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -10125,21 +10252,15 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" -ssri@^4.1.2, ssri@^4.1.6, ssri@~4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-4.1.6.tgz#0cb49b6ac84457e7bdd466cb730c3cb623e9a25b" +ssri@^5.0.0, ssri@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" dependencies: - safe-buffer "^5.1.0" - -ssri@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.0.0.tgz#13c19390b606c821f2a10d02b351c1729b94d8cf" - dependencies: - safe-buffer "^5.1.0" + safe-buffer "^5.1.1" stable@~0.1.3, stable@~0.1.5: - version "0.1.6" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.6.tgz#910f5d2aed7b520c6e777499c1f32e139fdecb10" + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" stack-parser@^0.0.1: version "0.0.1" @@ -10153,9 +10274,9 @@ stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" -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" +staged-git-files@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.0.0.tgz#cdb847837c1fcc52c08a872d4883cc0877668a80" static-extend@^0.1.1: version "0.1.2" @@ -10164,14 +10285,18 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.3.1 < 2", statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" +"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + stdout-stream@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" @@ -10201,12 +10326,12 @@ stream-each@^1.1.0: stream-shift "^1.0.0" stream-http@^2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" + version "2.8.1" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.1.tgz#d0441be1a457a73a733a8a7b53570bebd9ef66a4" dependencies: builtin-status-codes "^3.0.0" inherits "^2.0.1" - readable-stream "^2.2.6" + readable-stream "^2.3.3" to-arraybuffer "^1.0.0" xtend "^4.0.0" @@ -10252,16 +10377,16 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0: +string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string_decoder@^1.0.0, string_decoder@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" +string_decoder@^1.0.0, string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" dependencies: safe-buffer "~5.1.0" @@ -10269,12 +10394,6 @@ string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - dependencies: - safe-buffer "~5.1.0" - stringifier@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/stringifier/-/stringifier-1.3.0.tgz#def18342f6933db0f2dbfc9aa02175b448c17959" @@ -10284,8 +10403,8 @@ stringifier@^1.3.0: type-name "^2.0.1" stringify-object@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d" + version "3.2.2" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.2.tgz#9853052e5a88fb605a44cd27445aa257ad7ffbcd" dependencies: get-own-enumerable-property-symbols "^2.0.1" is-obj "^1.0.1" @@ -10380,13 +10499,13 @@ supports-color@^3.1.2, 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: +supports-color@^4.2.1: version "4.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" dependencies: has-flag "^2.0.0" -supports-color@^5.1.0: +supports-color@^5.1.0, supports-color@^5.3.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" dependencies: @@ -10419,17 +10538,13 @@ symbol-observable@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" -symbol-observable@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" - -symbol-tree@^3.2.1: +symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" systemjs-plugin-css@^0.1.36: - version "0.1.36" - resolved "https://registry.yarnpkg.com/systemjs-plugin-css/-/systemjs-plugin-css-0.1.36.tgz#1ab38811ae6dd71190cefe33f1fe41976a09387d" + version "0.1.37" + resolved "https://registry.yarnpkg.com/systemjs-plugin-css/-/systemjs-plugin-css-0.1.37.tgz#684847252ca69b7da24a1201094c86274324e82f" systemjs@0.20.19: version "0.20.19" @@ -10459,19 +10574,6 @@ tar-fs@^1.13.0: pump "^1.0.0" tar-stream "^1.1.2" -tar-pack@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" - dependencies: - debug "^2.2.0" - fstream "^1.0.10" - fstream-ignore "^1.0.5" - once "^1.3.3" - readable-stream "^2.1.4" - rimraf "^2.5.1" - tar "^2.2.1" - uid-number "^0.0.6" - tar-stream@^1.1.2, tar-stream@^1.5.0: version "1.5.5" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55" @@ -10481,7 +10583,7 @@ tar-stream@^1.1.2, tar-stream@^1.5.0: readable-stream "^2.0.0" xtend "^4.0.0" -tar@^2.0.0, tar@^2.2.1: +tar@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" dependencies: @@ -10489,14 +10591,16 @@ tar@^2.0.0, tar@^2.2.1: fstream "^1.0.2" inherits "2" -tar@^4.0.0, tar@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.0.2.tgz#e8e22bf3eec330e5c616d415a698395e294e8fad" +tar@^4, tar@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.1.tgz#b25d5a8470c976fd7a9a8a350f42c59e9fa81749" dependencies: chownr "^1.0.1" - minipass "^2.2.1" - minizlib "^1.0.4" + fs-minipass "^1.2.5" + minipass "^2.2.4" + minizlib "^1.1.0" mkdirp "^0.5.0" + safe-buffer "^5.1.1" yallist "^3.0.2" term-size@^1.2.0: @@ -10505,12 +10609,12 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" -test-exclude@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26" +test-exclude@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa" dependencies: arrify "^1.0.1" - micromatch "^2.3.11" + micromatch "^3.1.8" object-assign "^4.1.0" read-pkg-up "^1.0.1" require-main-filename "^1.0.1" @@ -10522,8 +10626,8 @@ test-exclude@^4.1.1: tether "^1.1.0" tether@^1.1.0, tether@^1.4.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.2.tgz#ab9605b5ecf38f088b3da3d54d2b439207e48d04" + version "1.4.4" + resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.4.tgz#9dc6eb2b3e601da2098fd264e7f7a8b264de1125" text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" @@ -10561,8 +10665,8 @@ timed-out@^4.0.0: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" timers-browserify@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" + version "2.0.10" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" dependencies: setimmediate "^1.0.4" @@ -10626,15 +10730,7 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" -to-regex@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae" - dependencies: - define-property "^0.2.5" - extend-shallow "^2.0.1" - regex-not "^1.0.0" - -to-regex@^3.0.2: +to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" dependencies: @@ -10652,12 +10748,12 @@ toposort@^1.0.0: resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" dependencies: punycode "^1.4.1" -tr46@^1.0.0: +tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" dependencies: @@ -10681,45 +10777,45 @@ trim-right@^1.0.1: dependencies: glob "^6.0.4" -tryit@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" +tryer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7" tryor@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" ts-jest@^22.0.0: - version "22.0.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-22.0.0.tgz#cce1a5f1106150ca002d09f7e85913355ceb5e8d" + version "22.4.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-22.4.4.tgz#7b5c0abb2188fe7170840df9f80e78659aaf8a24" dependencies: - babel-core "^6.24.1" + babel-core "^6.26.0" babel-plugin-istanbul "^4.1.4" - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-preset-jest "^22.0.1" + babel-plugin-transform-es2015-modules-commonjs "^6.26.0" + babel-preset-jest "^22.4.0" cpx "^1.5.0" fs-extra "4.0.3" - jest-config "^22.0.1" + jest-config "^22.4.2" pkg-dir "^2.0.0" - source-map-support "^0.5.0" - yargs "^10.0.3" + yargs "^11.0.0" ts-loader@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-3.2.0.tgz#23211922179b81f7448754b7fdfca45b8374a15a" + version "3.5.0" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-3.5.0.tgz#151d004dcddb4cf8e381a3bf9d6b74c2d957a9c0" dependencies: chalk "^2.3.0" enhanced-resolve "^3.0.0" loader-utils "^1.0.2" + micromatch "^3.1.4" semver "^5.0.1" -tslib@^1.7.1: - version "1.8.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" +tslib@^1.8.0, tslib@^1.8.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" tslint-loader@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.5.3.tgz#343f74122d94f356b689457d3f59f64a69ab606f" + version "3.6.0" + resolved "https://registry.yarnpkg.com/tslint-loader/-/tslint-loader-3.6.0.tgz#12ed4d5ef57d68be25cd12692fb2108b66469d76" dependencies: loader-utils "^1.0.2" mkdirp "^0.5.1" @@ -10728,26 +10824,27 @@ tslint-loader@^3.5.3: semver "^5.3.0" tslint@^5.8.0: - version "5.8.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.8.0.tgz#1f49ad5b2e77c76c3af4ddcae552ae4e3612eb13" + version "5.9.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.9.1.tgz#1255f87a3ff57eb0b0e1f0e610a8b4748046c9ae" dependencies: babel-code-frame "^6.22.0" builtin-modules "^1.1.1" - chalk "^2.1.0" - commander "^2.9.0" + chalk "^2.3.0" + commander "^2.12.1" diff "^3.2.0" glob "^7.1.1" + js-yaml "^3.7.0" minimatch "^3.0.4" resolve "^1.3.2" semver "^5.3.0" - tslib "^1.7.1" + tslib "^1.8.0" tsutils "^2.12.1" tsutils@^2.12.1: - version "2.12.2" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.12.2.tgz#ad58a4865d17ec3ddb6631b6ca53be14a5656ff3" + version "2.26.2" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.26.2.tgz#a9f9f63434a456a5e0c95a45d9a59181cb32d3bf" dependencies: - tslib "^1.7.1" + tslib "^1.8.1" tty-browserify@0.0.0: version "0.0.0" @@ -10773,14 +10870,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@~1.6.15: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" - dependencies: - media-typer "0.3.0" - mime-types "~2.1.15" - -type-is@~1.6.16: +type-is@~1.6.15, type-is@~1.6.16: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" dependencies: @@ -10791,13 +10881,17 @@ type-name@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/type-name/-/type-name-2.0.2.tgz#efe7d4123d8ac52afff7f40c7e4dec5266008fb4" +type-of@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-of/-/type-of-2.0.1.tgz#e72a1741896568e9f628378d816d6912f7f23972" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" typescript@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" + version "2.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.8.3.tgz#5d817f9b6f31bb871835f4edf0089f21abe6c170" ua-parser-js@^0.7.9: version "0.7.17" @@ -10812,11 +10906,11 @@ uglify-js@2.6.x: uglify-to-browserify "~1.0.0" yargs "~3.10.0" -uglify-js@3.2.x: - version "3.2.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.2.0.tgz#cb411ee4ca0e0cadbfe3a4e1a1da97e6fa0d19c1" +uglify-js@3.3.x: + version "3.3.22" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.22.tgz#e5f0e50ddd386b7e35b728b51600bf7a7ad0b0dc" dependencies: - commander "~2.12.1" + commander "~2.15.0" source-map "~0.6.1" uglify-js@^2.6, uglify-js@^2.8.29: @@ -10840,7 +10934,7 @@ uglifyjs-webpack-plugin@^0.4.6: uglify-js "^2.8.29" webpack-sources "^1.0.1" -uid-number@0.0.6, uid-number@^0.0.6: +uid-number@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -10848,10 +10942,6 @@ ultron@1.0.x: version "1.0.2" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" -ultron@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" - umask@^1.1.0, umask@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" @@ -10864,6 +10954,13 @@ underscore.string@~3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.2.3.tgz#806992633665d5e5fcb4db1fb3a862eb68e9e6da" +underscore.string@~3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.4.tgz#2c2a3f9f83e64762fdc45e6ceac65142864213db" + dependencies: + sprintf-js "^1.0.3" + util-deprecate "^1.0.2" + underscore@~1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" @@ -10946,10 +11043,6 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -unzip-response@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" - unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" @@ -10958,7 +11051,22 @@ upath@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d" -update-notifier@^2.2.0: +update-notifier@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-ci "^1.0.10" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +update-notifier@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451" dependencies: @@ -10972,19 +11080,6 @@ update-notifier@^2.2.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" -update-notifier@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.2.0.tgz#1b5837cf90c0736d88627732b661c138f86de72f" - dependencies: - boxen "^1.0.0" - chalk "^1.0.0" - configstore "^3.0.0" - import-lazy "^2.1.0" - is-npm "^1.0.0" - latest-version "^3.0.0" - semver-diff "^2.0.0" - xdg-basedir "^3.0.0" - upper-case-first@^1.1.0, upper-case-first@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" @@ -11019,11 +11114,11 @@ url-parse@1.0.x: requires-port "1.0.x" url-parse@^1.1.8: - version "1.3.0" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.3.0.tgz#04a06c420d22beb9804f7ada2d57ad13160a4258" + version "1.4.0" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.0.tgz#6bfdaad60098c7fe06f623e42b22de62de0d3d75" dependencies: - querystringify "~1.0.0" - requires-port "~1.0.0" + querystringify "^2.0.0" + requires-port "^1.0.0" url@^0.11.0: version "0.11.0" @@ -11032,13 +11127,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -use@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" +use@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544" dependencies: - define-property "^0.2.5" - isobject "^3.0.0" - lazy-cache "^2.0.2" + kind-of "^6.0.2" user-home@^2.0.0: version "2.0.0" @@ -11047,13 +11140,13 @@ user-home@^2.0.0: os-homedir "^1.0.0" useragent@^2.1.12: - version "2.2.1" - resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.2.1.tgz#cf593ef4f2d175875e8bb658ea92e18a4fd06d8e" + version "2.3.0" + resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" dependencies: - lru-cache "2.2.x" + lru-cache "4.1.x" tmp "0.0.x" -util-deprecate@~1.0.1: +util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -11101,20 +11194,16 @@ uuid@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -uuid@^3.0.0, uuid@^3.1.0, uuid@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - -uuid@^3.0.1: +uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0: version "3.0.0" @@ -11127,8 +11216,8 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" vendors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + version "1.0.2" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.2.tgz#7fcb5eef9f5623b156bcea89ec37d63676f21801" verror@1.10.0: version "1.10.0" @@ -11171,6 +11260,12 @@ w3c-blob@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/w3c-blob/-/w3c-blob-0.0.1.tgz#b0cd352a1a50f515563420ffd5861f950f1d85b8" +w3c-hr-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" + dependencies: + browser-process-hrtime "^0.1.2" + walkdir@^0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532" @@ -11181,12 +11276,6 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" -warning@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" - dependencies: - loose-envify "^1.0.0" - watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" @@ -11195,12 +11284,12 @@ watch@~0.18.0: minimist "^1.2.0" watchpack@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + version "1.5.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed" dependencies: - async "^2.1.2" - chokidar "^1.7.0" + chokidar "^2.0.2" graceful-fs "^4.1.2" + neo-async "^2.5.0" wbuf@^1.1.0, wbuf@^1.7.2: version "1.7.3" @@ -11214,32 +11303,26 @@ wcwidth@^1.0.0: dependencies: defaults "^1.0.3" -weak@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/weak/-/weak-1.0.1.tgz#ab99aab30706959aa0200cb8cf545bb9cb33b99e" - dependencies: - bindings "^1.2.1" - nan "^2.0.5" - -webidl-conversions@^4.0.1, webidl-conversions@^4.0.2: +webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" webpack-bundle-analyzer@^2.9.0: - version "2.9.1" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.9.1.tgz#c2c8e03e8e5768ed288b39ae9e27a8b8d7b9d476" + version "2.11.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.1.tgz#b9fbfb6a32c0a8c1c3237223e90890796b950ab9" dependencies: - acorn "^5.1.1" - chalk "^1.1.3" - commander "^2.9.0" - ejs "^2.5.6" - express "^4.15.2" - filesize "^3.5.9" - gzip-size "^3.0.0" + acorn "^5.3.0" + bfj-node4 "^5.2.0" + chalk "^2.3.0" + commander "^2.13.0" + ejs "^2.5.7" + express "^4.16.2" + filesize "^3.5.11" + gzip-size "^4.1.0" lodash "^4.17.4" mkdirp "^0.5.1" opener "^1.4.3" - ws "^3.3.1" + ws "^4.0.0" webpack-cleanup-plugin@^0.5.1: version "0.5.1" @@ -11256,7 +11339,7 @@ webpack-core@^0.6.5: source-list-map "~0.1.7" source-map "~0.4.1" -webpack-dev-middleware@1.12.2: +webpack-dev-middleware@1.12.2, webpack-dev-middleware@^1.12.0: version "1.12.2" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e" dependencies: @@ -11266,16 +11349,6 @@ webpack-dev-middleware@1.12.2: range-parser "^1.0.3" time-stamp "^2.0.0" -webpack-dev-middleware@^1.12.0: - version "1.12.1" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.1.tgz#338be3ca930973be1c2ce07d84d275e997e1a25a" - dependencies: - memory-fs "~0.4.1" - mime "^1.4.1" - path-is-absolute "^1.0.0" - range-parser "^1.0.3" - time-stamp "^2.0.0" - webpack-dev-server@2.11.1: version "2.11.1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.1.tgz#6f9358a002db8403f016e336816f4485384e5ec0" @@ -11309,10 +11382,10 @@ webpack-dev-server@2.11.1: yargs "6.6.0" webpack-merge@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.1.tgz#f1197a0a973e69c6fbeeb6d658219aa8c0c13555" + version "4.1.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.2.tgz#5d372dddd3e1e5f8874f5bf5a8e929db09feb216" dependencies: - lodash "^4.17.4" + lodash "^4.17.5" webpack-sources@^1.0.1: version "1.1.0" @@ -11322,13 +11395,13 @@ webpack-sources@^1.0.1: source-map "~0.6.1" webpack@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.10.0.tgz#5291b875078cf2abf42bdd23afe3f8f96c17d725" + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" - ajv "^5.1.5" - ajv-keywords "^2.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" async "^2.1.2" enhanced-resolve "^3.4.0" escope "^3.6.0" @@ -11359,23 +11432,27 @@ websocket-extensions@>=0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" -whatwg-encoding@^1.0.1: +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz#57c235bc8657e914d24e1a397d3c82daee0a6ba3" dependencies: iconv-lite "0.4.19" whatwg-fetch@>=0.10.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + version "2.0.4" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" -whatwg-url@^6.3.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.0.tgz#08fdf2b9e872783a7a1f6216260a1d66cc722e08" +whatwg-mimetype@^2.0.0, whatwg-mimetype@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz#f0f21d76cbba72362eb609dbed2a30cd17fcc7d4" + +whatwg-url@^6.4.0: + version "6.4.1" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.1.tgz#fdb94b440fd4ad836202c16e9737d511f012fd67" dependencies: lodash.sortby "^4.7.0" - tr46 "^1.0.0" - webidl-conversions "^4.0.1" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" whet.extend@~0.9.9: version "0.9.9" @@ -11389,7 +11466,11 @@ 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.2.1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1.2.9, which@^1.3.0, which@~1.3.0: +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + +which@1, which@^1.2.1, which@^1.2.10, which@^1.2.12, which@^1.2.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: @@ -11407,11 +11488,11 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2" -widest-line@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-1.0.0.tgz#0c09c85c2a94683d0d7eaf8ee097d564bf0e105c" +widest-line@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.0.tgz#0142a4e8a243f8882c0233aa0e0281aa76152273" dependencies: - string-width "^1.0.1" + string-width "^2.1.1" window-size@0.1.0: version "0.1.0" @@ -11441,12 +11522,11 @@ wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" -worker-farm@~1.5.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" +worker-farm@^1.5.4: + version "1.6.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0" dependencies: - errno "^0.1.4" - xtend "^4.0.1" + errno "~0.1.7" wrap-ansi@^2.0.0: version "2.1.0" @@ -11459,7 +11539,7 @@ wrappy@1, wrappy@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -write-file-atomic@^2.0.0, write-file-atomic@^2.1.0: +write-file-atomic@^2.0.0, write-file-atomic@^2.1.0, write-file-atomic@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" dependencies: @@ -11467,14 +11547,6 @@ write-file-atomic@^2.0.0, write-file-atomic@^2.1.0: imurmurhash "^0.1.4" signal-exit "^3.0.2" -write-file-atomic@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.1.0.tgz#1769f4b551eedce419f0505deae2e26763542d37" - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - slide "^1.1.5" - write@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" @@ -11488,13 +11560,12 @@ ws@1.1.2: options ">=0.0.5" ultron "1.0.x" -ws@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.2.tgz#96c1d08b3fefda1d5c1e33700d3bfaa9be2d5608" +ws@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289" dependencies: async-limiter "~1.0.0" safe-buffer "~5.1.0" - ultron "~1.1.0" wtf-8@1.0.0: version "1.0.0" @@ -11508,9 +11579,9 @@ xml-char-classes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/xml-char-classes/-/xml-char-classes-1.0.0.tgz#64657848a20ffc5df583a42ad8a277b4512bbc4d" -xml-name-validator@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" xmlbuilder@^3.1.0: version "3.1.0" @@ -11526,7 +11597,7 @@ xmlhttprequest@1: version "1.8.0" resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" -xtend@4.0.1, xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -11534,6 +11605,10 @@ y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -11560,9 +11635,15 @@ yargs-parser@^7.0.0: dependencies: camelcase "^4.1.0" -yargs-parser@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.0.0.tgz#21d476330e5a82279a4b881345bf066102e219c6" +yargs-parser@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" + dependencies: + camelcase "^4.1.0" + +yargs-parser@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" dependencies: camelcase "^4.1.0" @@ -11585,10 +11666,10 @@ yargs@6.6.0: yargs-parser "^4.2.0" yargs@^10.0.3: - version "10.0.3" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.0.3.tgz#6542debd9080ad517ec5048fb454efe9e4d4aaae" + version "10.1.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5" dependencies: - cliui "^3.2.0" + cliui "^4.0.0" decamelize "^1.1.1" find-up "^2.1.0" get-caller-file "^1.0.1" @@ -11599,7 +11680,24 @@ yargs@^10.0.3: string-width "^2.0.0" which-module "^2.0.0" y18n "^3.2.1" - yargs-parser "^8.0.0" + yargs-parser "^8.1.0" + +yargs@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b" + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^9.0.2" yargs@^7.0.0: version "7.1.0" From 8e9b3507c5df813aae4d47d62d4fe88550df2676 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 27 Apr 2018 10:39:06 +0200 Subject: [PATCH 0540/3000] tech: removes unused code --- build.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/build.go b/build.go index 21b528071e8..1bdbf4aac5c 100644 --- a/build.go +++ b/build.go @@ -49,8 +49,6 @@ func main() { ensureGoPath() - verifyGitRepoIsClean() - flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH") flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS") flag.StringVar(&gocc, "cc", "", "CC") @@ -325,20 +323,6 @@ func createPackage(options linuxPackageOptions) { runPrint("fpm", append([]string{"-t", options.packageType}, args...)...) } -func verifyGitRepoIsClean() { - rs, err := runError("git", "ls-files", "--modified") - if err != nil { - log.Fatalf("Failed to check if git tree was clean, %v, %v\n", string(rs), err) - return - } - count := len(string(rs)) - if count > 0 { - log.Fatalf("Git repository has modified files, aborting") - } - - log.Println("Git repository is clean") -} - func ensureGoPath() { if os.Getenv("GOPATH") == "" { cwd, err := os.Getwd() From 1e6e89121ca8649cf27eb82d221c926fff2659dd Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 27 Apr 2018 11:39:14 +0200 Subject: [PATCH 0541/3000] Settings to enable Explore UI --- conf/defaults.ini | 5 +++++ conf/sample.ini | 5 +++++ pkg/api/index.go | 22 ++++++++++++---------- pkg/setting/setting.go | 6 ++++++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 11d173d955d..d45e270d65d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -442,6 +442,11 @@ enabled = true # Makes it possible to turn off alert rule execution but alerting UI is visible execute_alerts = true +#################################### Explore ############################# +[explore] +# Enable the Explore section +enabled = false + #################################### Internal Grafana Metrics ############ # Metrics available at HTTP API Url /metrics [metrics] diff --git a/conf/sample.ini b/conf/sample.ini index 9f0c2a73c25..f12d917039d 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -377,6 +377,11 @@ log_queries = # Makes it possible to turn off alert rule execution but alerting UI is visible ;execute_alerts = true +#################################### Explore ############################# +[explore] +# Enable the Explore section +;enabled = false + #################################### Internal Grafana Metrics ########################## # Metrics available at HTTP API Url /metrics [metrics] diff --git a/pkg/api/index.go b/pkg/api/index.go index 64eaddcd1a7..75e3594d854 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -117,16 +117,18 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { Children: dashboardChildNavs, }) - // data.NavTree = append(data.NavTree, &dtos.NavLink{ - // Text: "Explore", - // Id: "explore", - // SubTitle: "Explore your data", - // Icon: "fa fa-rocket", - // Url: setting.AppSubUrl + "/explore", - // Children: []*dtos.NavLink{ - // {Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore/new"}, - // }, - // }) + if setting.ExploreEnabled { + data.NavTree = append(data.NavTree, &dtos.NavLink{ + Text: "Explore", + Id: "explore", + SubTitle: "Explore your data", + Icon: "fa fa-rocket", + Url: setting.AppSubUrl + "/explore", + Children: []*dtos.NavLink{ + {Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore/new"}, + }, + }) + } if c.IsSignedIn { // Only set login if it's different from the name diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 30a40602b1c..37646979095 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -168,6 +168,9 @@ var ( AlertingEnabled bool ExecuteAlerts bool + // Explore UI + ExploreEnabled bool + // logger logger log.Logger @@ -609,6 +612,9 @@ func NewConfigContext(args *CommandLineArgs) error { AlertingEnabled = alerting.Key("enabled").MustBool(true) ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true) + explore := Cfg.Section("explore") + ExploreEnabled = explore.Key("enabled").MustBool(true) + readSessionConfig() readSmtpSettings() readQuotaSettings() From d338b7ea7bf5b4561b77d779233030fc40c5dec8 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 27 Apr 2018 11:49:11 +0200 Subject: [PATCH 0542/3000] Import and typescript fixups --- public/app/containers/Explore/ElapsedTime.tsx | 4 ++-- public/app/routes/ReactContainer.tsx | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/public/app/containers/Explore/ElapsedTime.tsx b/public/app/containers/Explore/ElapsedTime.tsx index 123299fd96a..9cd8f674186 100644 --- a/public/app/containers/Explore/ElapsedTime.tsx +++ b/public/app/containers/Explore/ElapsedTime.tsx @@ -4,7 +4,7 @@ const INTERVAL = 150; export default class ElapsedTime extends PureComponent { offset: number; - timer: NodeJS.Timer; + timer: number; state = { elapsed: 0, @@ -12,7 +12,7 @@ export default class ElapsedTime extends PureComponent { start() { this.offset = Date.now(); - this.timer = setInterval(this.tick, INTERVAL); + this.timer = window.setInterval(this.tick, INTERVAL); } tick = () => { diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index deb16f68bf7..d6d34372090 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -21,8 +21,12 @@ export function reactContainer($route, $location, backendSrv: BackendSrv, dataso restrict: 'E', template: '', link(scope, elem) { - let component = $route.current.locals.component.default; - let props = { + let component = $route.current.locals.component; + // Dynamic imports return whole module, need to extract default export + if (component.default) { + component = component.default; + } + const props = { backendSrv: backendSrv, datasourceSrv: datasourceSrv, }; From c2b720835b8f7cf1f58b14ab120a723b6835aed4 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 27 Apr 2018 19:34:10 +0900 Subject: [PATCH 0543/3000] fix to match table column name and order --- public/app/plugins/datasource/prometheus/datasource.ts | 1 + .../app/plugins/datasource/prometheus/result_transformer.ts | 6 +++--- public/app/plugins/panel/table/module.ts | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 2a8b3069a53..6d654438271 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -153,6 +153,7 @@ export class PrometheusDatasource { end: end, responseListLength: responseList.length, responseIndex: index, + refId: activeTargets[index].refId, }; this.resultTransformer.transform(result, response, transformerOptions); diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 6d97b783983..d5feda7d28c 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -8,7 +8,7 @@ export class ResultTransformer { let prometheusResult = response.data.data.result; if (options.format === 'table') { - result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.responseIndex)); + result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)); } else if (options.format === 'heatmap') { let seriesList = []; prometheusResult.sort(sortSeriesByLabel); @@ -58,7 +58,7 @@ export class ResultTransformer { return { target: metricLabel, datapoints: dps }; } - transformMetricDataToTable(md, resultCount: number, resultIndex: number) { + transformMetricDataToTable(md, resultCount: number, refId: string) { var table = new TableModel(); var i, j; var metricLabels = {}; @@ -83,7 +83,7 @@ export class ResultTransformer { metricLabels[label] = labelIndex + 1; table.columns.push({ text: label }); }); - let valueText = resultCount > 1 ? `Value #${String.fromCharCode(65 + resultIndex)}` : 'Value'; + let valueText = resultCount > 1 ? `Value #${refId}` : 'Value'; table.columns.push({ text: valueText }); // Populate rows, set value to empty string when label not present. diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 27eab205f09..f4728982ad5 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -154,6 +154,11 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.render(); } + moveQuery(target, direction) { + super.moveQuery(target, direction); + super.refresh(); + } + exportCsv() { var scope = this.$scope.$new(true); scope.tableData = this.renderer.render_values(); From 138c8c348eaf209c59c72d478528006d6082e6d1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 27 Apr 2018 13:41:20 +0200 Subject: [PATCH 0544/3000] revert renaming of unit key ppm #11211 removed the unit key ppm in favor of conppm. A change which is not forward compatible. This commit revert the unit key back to ppm. Also adds some better error description if trying to use a unit which don't exists. Fixes #11743 --- public/app/core/utils/kbn.ts | 4 ++-- public/app/plugins/panel/graph/graph.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 0909bd36f69..f4ee7af3383 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -596,7 +596,7 @@ kbn.valueFormats.radr = kbn.formatBuilders.decimalSIPrefix('R'); kbn.valueFormats.radsvh = kbn.formatBuilders.decimalSIPrefix('Sv/h'); // Concentration -kbn.valueFormats.conppm = kbn.formatBuilders.fixedUnit('ppm'); +kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm'); kbn.valueFormats.conppb = kbn.formatBuilders.fixedUnit('ppb'); kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m3'); kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm3'); @@ -1101,7 +1101,7 @@ kbn.getUnitFormats = function() { { text: 'concentration', submenu: [ - { text: 'parts-per-million (ppm)', value: 'conppm' }, + { text: 'parts-per-million (ppm)', value: 'ppm' }, { text: 'parts-per-billion (ppb)', value: 'conppb' }, { text: 'nanogram per cubic metre (ng/m3)', value: 'conngm3' }, { text: 'nanogram per normal cubic metre (ng/Nm3)', value: 'conngNm3' }, diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 8a2aea8c4c2..07ce0fed49f 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -634,6 +634,9 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { function configureAxisMode(axis, format) { axis.tickFormatter = function(val, axis) { + if (!kbn.valueFormats[format]) { + throw new Error(`Unit '${format}' is not supported`); + } return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals); }; } From 28f7b6dad1c0f80ae49085e8883ac7a37dbb8b51 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Fri, 27 Apr 2018 13:41:58 +0200 Subject: [PATCH 0545/3000] Enable Grafana extensions at build time. (#11752) * extensions: import and build * bus: use predefined error * enterprise: build script for enterprise packages * poc: auto registering services and dependency injection (cherry picked from commit b5b1ef875f905473af41e49f8071cb9028edc845) * poc: backend services registry progress (cherry picked from commit 97be69725881241bfbf1e7adf0e66801d6b0af3d) * poc: minor update (cherry picked from commit 03d7a6888b81403f458b94305792e075568f0794) * ioc: introduce manuel ioc * enterprise: adds setting for enterprise * build: test and build specific ee commit * cleanup: test testing code * removes example hello service --- .circleci/config.yml | 20 + .gitignore | 1 + Gopkg.lock | 211 ++++++- build.go | 23 +- pkg/api/api.go | 2 +- pkg/api/http_server.go | 9 +- pkg/api/index.go | 2 +- pkg/api/route_register.go | 5 +- pkg/api/route_register_test.go | 6 +- pkg/bus/bus.go | 13 +- pkg/cmd/grafana-server/main.go | 3 + pkg/cmd/grafana-server/server.go | 96 ++- pkg/extensions/main.go | 3 + pkg/plugins/plugins.go | 2 +- pkg/registry/registry.go | 33 + pkg/services/alerting/engine.go | 38 +- pkg/services/cleanup/cleanup.go | 25 +- pkg/services/search/handlers.go | 16 +- pkg/services/search/handlers_test.go | 3 +- pkg/services/sqlstore/sqlstore.go | 4 +- pkg/setting/setting.go | 13 +- scripts/build/build_enterprise.sh | 58 ++ vendor/github.com/facebookgo/inject/inject.go | 576 ++++++++++++++++++ vendor/github.com/facebookgo/inject/license | 30 + vendor/github.com/facebookgo/inject/patents | 33 + .../github.com/facebookgo/structtag/license | 27 + .../facebookgo/structtag/structtag.go | 61 ++ vendor/github.com/pkg/errors/LICENSE | 23 + vendor/github.com/pkg/errors/errors.go | 269 ++++++++ vendor/github.com/pkg/errors/stack.go | 178 ++++++ 30 files changed, 1678 insertions(+), 105 deletions(-) create mode 100644 pkg/extensions/main.go create mode 100644 pkg/registry/registry.go create mode 100755 scripts/build/build_enterprise.sh create mode 100644 vendor/github.com/facebookgo/inject/inject.go create mode 100644 vendor/github.com/facebookgo/inject/license create mode 100644 vendor/github.com/facebookgo/inject/patents create mode 100644 vendor/github.com/facebookgo/structtag/license create mode 100644 vendor/github.com/facebookgo/structtag/structtag.go create mode 100644 vendor/github.com/pkg/errors/LICENSE create mode 100644 vendor/github.com/pkg/errors/errors.go create mode 100644 vendor/github.com/pkg/errors/stack.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b717083853..d3e6c71b520 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -93,6 +93,22 @@ jobs: - scripts/*.sh - scripts/publish + build-enterprise: + docker: + - image: grafana/build-container:v0.1 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: build and package grafana + command: './scripts/build/build_enterprise.sh' + - run: + name: sign packages + command: './scripts/build/sign_packages.sh' + - run: + name: sha-sum packages + command: 'go run build.go sha-dist' + deploy-master: docker: - image: circleci/python:2.7-stretch @@ -176,3 +192,7 @@ workflows: ignore: /.*/ tags: only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ + - build-enterprise: + filters: + tags: + only: /.*/ diff --git a/.gitignore b/.gitignore index 974fb618af9..cf13dac6d9b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ profile.cov /pkg/cmd/grafana-cli/grafana-cli /pkg/cmd/grafana-server/grafana-server /pkg/cmd/grafana-server/debug +/pkg/extensions debug.test /examples/*/dist /packaging/**/*.rpm diff --git a/Gopkg.lock b/Gopkg.lock index a35f5b23cda..3a7466c312a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -27,7 +27,37 @@ [[projects]] name = "github.com/aws/aws-sdk-go" - packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/endpoints","aws/request","aws/session","aws/signer/v4","internal/shareddefaults","private/protocol","private/protocol/ec2query","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/restxml","private/protocol/xml/xmlutil","service/cloudwatch","service/ec2","service/ec2/ec2iface","service/s3","service/sts"] + packages = [ + "aws", + "aws/awserr", + "aws/awsutil", + "aws/client", + "aws/client/metadata", + "aws/corehandlers", + "aws/credentials", + "aws/credentials/ec2rolecreds", + "aws/credentials/endpointcreds", + "aws/credentials/stscreds", + "aws/defaults", + "aws/ec2metadata", + "aws/endpoints", + "aws/request", + "aws/session", + "aws/signer/v4", + "internal/shareddefaults", + "private/protocol", + "private/protocol/ec2query", + "private/protocol/query", + "private/protocol/query/queryutil", + "private/protocol/rest", + "private/protocol/restxml", + "private/protocol/xml/xmlutil", + "service/cloudwatch", + "service/ec2", + "service/ec2/ec2iface", + "service/s3", + "service/sts" + ] revision = "decd990ddc5dcdf2f73309cbcab90d06b996ca28" version = "v1.12.67" @@ -75,7 +105,10 @@ [[projects]] name = "github.com/denisenkom/go-mssqldb" - packages = [".","internal/cp"] + packages = [ + ".", + "internal/cp" + ] revision = "270bc3860bb94dd3a3ffd047377d746c5e276726" [[projects]] @@ -117,7 +150,12 @@ [[projects]] branch = "master" name = "github.com/go-macaron/session" - packages = [".","memcache","postgres","redis"] + packages = [ + ".", + "memcache", + "postgres", + "redis" + ] revision = "b8e286a0dba8f4999042d6b258daf51b31d08938" [[projects]] @@ -152,7 +190,13 @@ [[projects]] branch = "master" name = "github.com/golang/protobuf" - packages = ["proto","ptypes","ptypes/any","ptypes/duration","ptypes/timestamp"] + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp" + ] revision = "c65a0412e71e8b9b3bfd22925720d23c0f054237" [[projects]] @@ -221,7 +265,10 @@ [[projects]] name = "github.com/klauspost/compress" - packages = ["flate","gzip"] + packages = [ + "flate", + "gzip" + ] revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf" version = "v1.2.1" @@ -252,7 +299,10 @@ [[projects]] branch = "master" name = "github.com/lib/pq" - packages = [".","oid"] + packages = [ + ".", + "oid" + ] revision = "61fe37aa2ee24fabcdbe5c4ac1d4ac566f88f345" [[projects]] @@ -287,7 +337,11 @@ [[projects]] name = "github.com/opentracing/opentracing-go" - packages = [".","ext","log"] + packages = [ + ".", + "ext", + "log" + ] revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38" version = "v1.0.2" @@ -297,9 +351,20 @@ revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0" version = "v2.1.0" +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + [[projects]] name = "github.com/prometheus/client_golang" - packages = ["api","api/prometheus/v1","prometheus","prometheus/promhttp"] + packages = [ + "api", + "api/prometheus/v1", + "prometheus", + "prometheus/promhttp" + ] revision = "967789050ba94deca04a5e84cce8ad472ce313c1" version = "v0.9.0-pre1" @@ -312,13 +377,22 @@ [[projects]] branch = "master" name = "github.com/prometheus/common" - packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","model"] + packages = [ + "expfmt", + "internal/bitbucket.org/ww/goautoneg", + "model" + ] revision = "89604d197083d4781071d3c65855d24ecfb0a563" [[projects]] branch = "master" name = "github.com/prometheus/procfs" - packages = [".","internal/util","nfsd","xfs"] + packages = [ + ".", + "internal/util", + "nfsd", + "xfs" + ] revision = "85fadb6e89903ef7cca6f6a804474cd5ea85b6e1" [[projects]] @@ -335,13 +409,21 @@ [[projects]] name = "github.com/smartystreets/assertions" - packages = [".","internal/go-render/render","internal/oglematchers"] + packages = [ + ".", + "internal/go-render/render", + "internal/oglematchers" + ] revision = "0b37b35ec7434b77e77a4bb29b79677cced992ea" version = "1.8.1" [[projects]] name = "github.com/smartystreets/goconvey" - packages = ["convey","convey/gotest","convey/reporting"] + packages = [ + "convey", + "convey/gotest", + "convey/reporting" + ] revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857" version = "1.6.3" @@ -353,7 +435,21 @@ [[projects]] name = "github.com/uber/jaeger-client-go" - packages = [".","config","internal/baggage","internal/baggage/remote","internal/spanlog","log","rpcmetrics","thrift-gen/agent","thrift-gen/baggage","thrift-gen/jaeger","thrift-gen/sampling","thrift-gen/zipkincore","utils"] + packages = [ + ".", + "config", + "internal/baggage", + "internal/baggage/remote", + "internal/spanlog", + "log", + "rpcmetrics", + "thrift-gen/agent", + "thrift-gen/baggage", + "thrift-gen/jaeger", + "thrift-gen/sampling", + "thrift-gen/zipkincore", + "utils" + ] revision = "3ac96c6e679cb60a74589b0d0aa7c70a906183f7" version = "v2.11.2" @@ -365,7 +461,10 @@ [[projects]] name = "github.com/yudai/gojsondiff" - packages = [".","formatter"] + packages = [ + ".", + "formatter" + ] revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6" version = "1.0.0" @@ -378,19 +477,37 @@ [[projects]] branch = "master" name = "golang.org/x/crypto" - packages = ["md4","pbkdf2"] + packages = [ + "md4", + "pbkdf2" + ] revision = "3d37316aaa6bd9929127ac9a527abf408178ea7b" [[projects]] branch = "master" name = "golang.org/x/net" - packages = ["context","context/ctxhttp","http2","http2/hpack","idna","internal/timeseries","lex/httplex","trace"] + packages = [ + "context", + "context/ctxhttp", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "lex/httplex", + "trace" + ] revision = "5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec" [[projects]] branch = "master" name = "golang.org/x/oauth2" - packages = [".","google","internal","jws","jwt"] + packages = [ + ".", + "google", + "internal", + "jws", + "jwt" + ] revision = "b28fcf2b08a19742b43084fb40ab78ac6c3d8067" [[projects]] @@ -408,12 +525,39 @@ [[projects]] branch = "master" name = "golang.org/x/text" - packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable" + ] revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" [[projects]] name = "google.golang.org/appengine" - packages = [".","cloudsql","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"] + packages = [ + ".", + "cloudsql", + "internal", + "internal/app_identity", + "internal/base", + "internal/datastore", + "internal/log", + "internal/modules", + "internal/remote_api", + "internal/urlfetch", + "urlfetch" + ] revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" version = "v1.0.0" @@ -425,7 +569,32 @@ [[projects]] name = "google.golang.org/grpc" - packages = [".","balancer","balancer/base","balancer/roundrobin","codes","connectivity","credentials","encoding","grpclb/grpc_lb_v1/messages","grpclog","health","health/grpc_health_v1","internal","keepalive","metadata","naming","peer","resolver","resolver/dns","resolver/passthrough","stats","status","tap","transport"] + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "codes", + "connectivity", + "credentials", + "encoding", + "grpclb/grpc_lb_v1/messages", + "grpclog", + "health", + "health/grpc_health_v1", + "internal", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap", + "transport" + ] revision = "6b51017f791ae1cfbec89c52efdf444b13b550ef" version = "v1.9.2" @@ -480,6 +649,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "ad3c71fd3244369c313978e9e7464c7116faee764386439a17de0707a08103aa" + inputs-digest = "2bd5b309496d57e2189a1cc28f5c1c41398c19729ba0cf53c8cbb17ea3f706b5" solver-name = "gps-cdcl" solver-version = 1 diff --git a/build.go b/build.go index 1bdbf4aac5c..7e7183b8b83 100644 --- a/build.go +++ b/build.go @@ -41,6 +41,7 @@ var ( buildNumber int = 0 binaries []string = []string{"grafana-server", "grafana-cli"} isDev bool = false + enterprise bool = false ) func main() { @@ -58,6 +59,7 @@ func main() { flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary") flag.BoolVar(&race, "race", race, "Use race detector") flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name") + flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana") flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system") flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps") flag.Parse() @@ -283,19 +285,33 @@ func createPackage(options linuxPackageOptions) { "-s", "dir", "--description", "Grafana", "-C", packageRoot, - "--vendor", "Grafana", "--url", "https://grafana.com", - "--license", "\"Apache 2.0\"", "--maintainer", "contact@grafana.com", "--config-files", options.initdScriptFilePath, "--config-files", options.etcDefaultFilePath, "--config-files", options.systemdServiceFilePath, "--after-install", options.postinstSrc, - "--name", "grafana", + "--version", linuxPackageVersion, "-p", "./dist", } + name := "grafana" + if enterprise { + name += "-enterprise" + } + args = append(args, "--name", name) + + description := "Grafana" + if enterprise { + description += " Enterprise" + } + args = append(args, "--vendor", description) + + if !enterprise { + args = append(args, "--license", "\"Apache 2.0\"") + } + if options.packageType == "rpm" { args = append(args, "--rpm-posttrans", "packaging/rpm/control/posttrans") } @@ -412,6 +428,7 @@ func ldflags() string { b.WriteString(fmt.Sprintf(" -X main.version=%s", version)) b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha())) b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp())) + b.WriteString(fmt.Sprintf(" -X main.enterprise=%t", enterprise)) return b.String() } diff --git a/pkg/api/api.go b/pkg/api/api.go index 96b764b95b9..493f9eb9d01 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -23,7 +23,7 @@ func (hs *HTTPServer) registerRoutes() { // automatically set HEAD for every GET macaronR.SetAutoHead(true) - r := newRouteRegister(middleware.RequestMetrics, middleware.RequestTracing) + r := hs.RouteRegister // not logged in views r.Get("/", reqSignedIn, Index) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 8e01e869329..8d1d0dc0a60 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -35,15 +35,14 @@ type HTTPServer struct { context context.Context streamManager *live.StreamManager cache *gocache.Cache + RouteRegister RouteRegister `inject:""` httpSrv *http.Server } -func NewHTTPServer() *HTTPServer { - return &HTTPServer{ - log: log.New("http.server"), - cache: gocache.New(5*time.Minute, 10*time.Minute), - } +func (hs *HTTPServer) Init() { + hs.log = log.New("http.server") + hs.cache = gocache.New(5*time.Minute, 10*time.Minute) } func (hs *HTTPServer) Start(ctx context.Context) error { diff --git a/pkg/api/index.go b/pkg/api/index.go index 94094706f68..7c954f89ada 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -289,7 +289,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Help", - SubTitle: fmt.Sprintf(`Grafana v%s (%s)`, setting.BuildVersion, setting.BuildCommit), + SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit), Id: "help", Url: "#", Icon: "gicon gicon-question", diff --git a/pkg/api/route_register.go b/pkg/api/route_register.go index 76ebb633ca1..926de13c546 100644 --- a/pkg/api/route_register.go +++ b/pkg/api/route_register.go @@ -11,6 +11,8 @@ type Router interface { Get(pattern string, handlers ...macaron.Handler) *macaron.Route } +// RouteRegister allows you to add routes and macaron.Handlers +// that the web server should serve. type RouteRegister interface { Get(string, ...macaron.Handler) Post(string, ...macaron.Handler) @@ -26,7 +28,8 @@ type RouteRegister interface { type RegisterNamedMiddleware func(name string) macaron.Handler -func newRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister { +// NewRouteRegister creates a new RouteRegister with all middlewares sent as params +func NewRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister { return &routeRegister{ prefix: "", routes: []route{}, diff --git a/pkg/api/route_register_test.go b/pkg/api/route_register_test.go index f8a043c48df..3b5d79599a8 100644 --- a/pkg/api/route_register_test.go +++ b/pkg/api/route_register_test.go @@ -51,7 +51,7 @@ func TestRouteSimpleRegister(t *testing.T) { } // Setup - rr := newRouteRegister(func(name string) macaron.Handler { + rr := NewRouteRegister(func(name string) macaron.Handler { return emptyHandler(name) }) @@ -96,7 +96,7 @@ func TestRouteGroupedRegister(t *testing.T) { } // Setup - rr := newRouteRegister() + rr := NewRouteRegister() rr.Delete("/admin", emptyHandler("1")) rr.Get("/down", emptyHandler("1"), emptyHandler("2")) @@ -150,7 +150,7 @@ func TestNamedMiddlewareRouteRegister(t *testing.T) { } // Setup - rr := newRouteRegister(func(name string) macaron.Handler { + rr := NewRouteRegister(func(name string) macaron.Handler { return emptyHandler(name) }) diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index 59d4592766e..437796991a5 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -2,7 +2,7 @@ package bus import ( "context" - "fmt" + "errors" "reflect" ) @@ -10,6 +10,8 @@ type HandlerFunc interface{} type CtxHandlerFunc func() type Msg interface{} +var ErrHandlerNotFound = errors.New("handler not found") + type Bus interface { Dispatch(msg Msg) error DispatchCtx(ctx context.Context, msg Msg) error @@ -38,12 +40,17 @@ func New() Bus { return bus } +// Want to get rid of global bus +func GetBus() Bus { + return globalBus +} + func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error { var msgName = reflect.TypeOf(msg).Elem().Name() var handler = b.handlers[msgName] if handler == nil { - return fmt.Errorf("handler not found for %s", msgName) + return ErrHandlerNotFound } var params = make([]reflect.Value, 2) @@ -64,7 +71,7 @@ func (b *InProcBus) Dispatch(msg Msg) error { var handler = b.handlers[msgName] if handler == nil { - return fmt.Errorf("handler not found for %s", msgName) + return ErrHandlerNotFound } var params = make([]reflect.Value, 1) diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index da99bc9ba40..466e97ff2d6 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/setting" + _ "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/services/alerting/conditions" _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" _ "github.com/grafana/grafana/pkg/tsdb/cloudwatch" @@ -33,6 +34,7 @@ import ( var version = "5.0.0" var commit = "NA" var buildstamp string +var enterprise string var configFile = flag.String("config", "", "path to config file") var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory") @@ -76,6 +78,7 @@ func main() { setting.BuildVersion = version setting.BuildCommit = commit setting.BuildStamp = buildstampInt64 + setting.Enterprise, _ = strconv.ParseBool(enterprise) metrics.M_Grafana_Version.WithLabelValues(version).Set(1) shutdownCompleted := make(chan int) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index b8387403161..1bf0e90915f 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -8,9 +8,15 @@ import ( "net" "os" "path/filepath" + "reflect" "strconv" "time" + "github.com/facebookgo/inject" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning" "golang.org/x/sync/errgroup" @@ -20,15 +26,17 @@ import ( "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/alerting" - "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/notifications" - "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" "github.com/grafana/grafana/pkg/tracing" + + _ "github.com/grafana/grafana/pkg/extensions" + _ "github.com/grafana/grafana/pkg/services/alerting" + _ "github.com/grafana/grafana/pkg/services/cleanup" + _ "github.com/grafana/grafana/pkg/services/search" ) func NewGrafanaServer() *GrafanaServerImpl { @@ -48,18 +56,20 @@ type GrafanaServerImpl struct { shutdownFn context.CancelFunc childRoutines *errgroup.Group log log.Logger + RouteRegister api.RouteRegister `inject:""` - httpServer *api.HTTPServer + HttpServer *api.HTTPServer `inject:""` } func (g *GrafanaServerImpl) Start() error { g.initLogging() g.writePIDFile() - initSql() + // initSql + sqlstore.NewEngine() // TODO: this should return an error + sqlstore.EnsureAdminUser() metrics.Init(setting.Cfg) - search.Init() login.Init() social.NewOAuthService() @@ -79,30 +89,64 @@ func (g *GrafanaServerImpl) Start() error { } defer tracingCloser.Close() - // init alerting - if setting.AlertingEnabled && setting.ExecuteAlerts { - engine := alerting.NewEngine() - g.childRoutines.Go(func() error { return engine.Run(g.context) }) - } - - // cleanup service - cleanUpService := cleanup.NewCleanUpService() - g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) }) - if err = notifications.Init(); err != nil { return fmt.Errorf("Notification service failed to initialize. error: %v", err) } + serviceGraph := inject.Graph{} + serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) + serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()}) + serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) + serviceGraph.Provide(&inject.Object{Value: api.HTTPServer{}}) + services := registry.GetServices() + + // Add all services to dependency graph + for _, service := range services { + serviceGraph.Provide(&inject.Object{Value: service}) + } + + serviceGraph.Provide(&inject.Object{Value: g}) + + // Inject dependencies to services + if err := serviceGraph.Populate(); err != nil { + return fmt.Errorf("Failed to populate service dependency: %v", err) + } + + // Init & start services + for _, service := range services { + if registry.IsDisabled(service) { + continue + } + + g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name()) + + if err := service.Init(); err != nil { + return fmt.Errorf("Service init failed %v", err) + } + } + + // Start background services + for index := range services { + service, ok := services[index].(registry.BackgroundService) + if !ok { + continue + } + + if registry.IsDisabled(services[index]) { + continue + } + + g.childRoutines.Go(func() error { + err := service.Run(g.context) + g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) + return err + }) + } + sendSystemdNotification("READY=1") - return g.startHttpServer() } -func initSql() { - sqlstore.NewEngine() - sqlstore.EnsureAdminUser() -} - func (g *GrafanaServerImpl) initLogging() { err := setting.NewConfigContext(&setting.CommandLineArgs{ Config: *configFile, @@ -115,14 +159,14 @@ func (g *GrafanaServerImpl) initLogging() { os.Exit(1) } - g.log.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0)) + g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0)) setting.LogConfigurationInfo() } func (g *GrafanaServerImpl) startHttpServer() error { - g.httpServer = api.NewHTTPServer() + g.HttpServer.Init() - err := g.httpServer.Start(g.context) + err := g.HttpServer.Start(g.context) if err != nil { return fmt.Errorf("Fail to start server. error: %v", err) @@ -134,7 +178,7 @@ func (g *GrafanaServerImpl) startHttpServer() error { func (g *GrafanaServerImpl) Shutdown(code int, reason string) { g.log.Info("Shutdown started", "code", code, "reason", reason) - err := g.httpServer.Shutdown(g.context) + err := g.HttpServer.Shutdown(g.context) if err != nil { g.log.Error("Failed to shutdown server", "error", err) } diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go new file mode 100644 index 00000000000..34ac9da7e86 --- /dev/null +++ b/pkg/extensions/main.go @@ -0,0 +1,3 @@ +package extensions + +import _ "github.com/pkg/errors" diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 417f565dd0c..45e7c934bea 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -58,7 +58,7 @@ func (p *PluginManager) Run(ctx context.Context) error { p.Kill() } - p.log.Info("Stopped Plugins", "error", ctx.Err()) + p.log.Info("Stopped Plugins", "reason", ctx.Err()) return ctx.Err() } diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go new file mode 100644 index 00000000000..ba3229d6df6 --- /dev/null +++ b/pkg/registry/registry.go @@ -0,0 +1,33 @@ +package registry + +import ( + "context" +) + +var services = []Service{} + +func RegisterService(srv Service) { + services = append(services, srv) +} + +func GetServices() []Service { + return services +} + +type Service interface { + Init() error +} + +// Useful for alerting service +type CanBeDisabled interface { + IsDisabled() bool +} + +type BackgroundService interface { + Run(ctx context.Context) error +} + +func IsDisabled(srv Service) bool { + canBeDisabled, ok := srv.(CanBeDisabled) + return ok && canBeDisabled.IsDisabled() +} diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 0945a2a5330..bdd8ff2cfe2 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -11,6 +11,8 @@ import ( "github.com/benbjohnson/clock" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/setting" "golang.org/x/sync/errgroup" ) @@ -25,31 +27,37 @@ type Engine struct { resultHandler ResultHandler } -func NewEngine() *Engine { - e := &Engine{ - ticker: NewTicker(time.Now(), time.Second*0, clock.New()), - execQueue: make(chan *Job, 1000), - scheduler: NewScheduler(), - evalHandler: NewEvalHandler(), - ruleReader: NewRuleReader(), - log: log.New("alerting.engine"), - resultHandler: NewResultHandler(), - } +func init() { + registry.RegisterService(&Engine{}) +} +func NewEngine() *Engine { + e := &Engine{} + e.Init() return e } +func (e *Engine) IsDisabled() bool { + return !setting.AlertingEnabled || !setting.ExecuteAlerts +} + +func (e *Engine) Init() error { + e.ticker = NewTicker(time.Now(), time.Second*0, clock.New()) + e.execQueue = make(chan *Job, 1000) + e.scheduler = NewScheduler() + e.evalHandler = NewEvalHandler() + e.ruleReader = NewRuleReader() + e.log = log.New("alerting.engine") + e.resultHandler = NewResultHandler() + return nil +} + func (e *Engine) Run(ctx context.Context) error { - e.log.Info("Initializing Alerting") - alertGroup, ctx := errgroup.WithContext(ctx) - alertGroup.Go(func() error { return e.alertingTicker(ctx) }) alertGroup.Go(func() error { return e.runJobDispatcher(ctx) }) err := alertGroup.Wait() - - e.log.Info("Stopped Alerting", "reason", err) return err } diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index 5e9efeea3b0..ef474fd2eb2 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -7,11 +7,10 @@ import ( "path" "time" - "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" ) @@ -19,24 +18,16 @@ type CleanUpService struct { log log.Logger } -func NewCleanUpService() *CleanUpService { - return &CleanUpService{ - log: log.New("cleanup"), - } +func init() { + registry.RegisterService(&CleanUpService{}) +} + +func (service *CleanUpService) Init() error { + service.log = log.New("cleanup") + return nil } func (service *CleanUpService) Run(ctx context.Context) error { - service.log.Info("Initializing CleanUpService") - - g, _ := errgroup.WithContext(ctx) - g.Go(func() error { return service.start(ctx) }) - - err := g.Wait() - service.log.Info("Stopped CleanUpService", "reason", err) - return err -} - -func (service *CleanUpService) start(ctx context.Context) error { service.cleanUpTmpFiles() ticker := time.NewTicker(time.Minute * 10) diff --git a/pkg/services/search/handlers.go b/pkg/services/search/handlers.go index cf194c320bb..9d40697f489 100644 --- a/pkg/services/search/handlers.go +++ b/pkg/services/search/handlers.go @@ -5,13 +5,23 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" ) -func Init() { - bus.AddHandler("search", searchHandler) +func init() { + registry.RegisterService(&SearchService{}) } -func searchHandler(query *Query) error { +type SearchService struct { + Bus bus.Bus `inject:""` +} + +func (s *SearchService) Init() error { + s.Bus.AddHandler(s.searchHandler) + return nil +} + +func (s *SearchService) searchHandler(query *Query) error { dashQuery := FindPersistedDashboardsQuery{ Title: query.Title, SignedInUser: query.SignedInUser, diff --git a/pkg/services/search/handlers_test.go b/pkg/services/search/handlers_test.go index fc223b2ef4b..5cf934cbc92 100644 --- a/pkg/services/search/handlers_test.go +++ b/pkg/services/search/handlers_test.go @@ -12,6 +12,7 @@ func TestSearch(t *testing.T) { Convey("Given search query", t, func() { query := Query{Limit: 2000, SignedInUser: &m.SignedInUser{IsGrafanaAdmin: true}} + ss := &SearchService{} bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error { query.Result = HitList{ @@ -35,7 +36,7 @@ func TestSearch(t *testing.T) { }) Convey("That is empty", func() { - err := searchHandler(&query) + err := ss.searchHandler(&query) So(err, ShouldBeNil) Convey("should return sorted results", func() { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 782318fa188..e4be3208c86 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -77,7 +77,7 @@ func EnsureAdminUser() { log.Info("Created default admin user: %v", setting.AdminUser) } -func NewEngine() { +func NewEngine() *xorm.Engine { x, err := getEngine() if err != nil { @@ -91,6 +91,8 @@ func NewEngine() { sqlog.Error("Fail to initialize orm engine", "error", err) os.Exit(1) } + + return x } func SetEngine(engine *xorm.Engine) (err error) { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 30a40602b1c..58a33b2202f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -45,9 +45,11 @@ var ( InstanceName string // build - BuildVersion string - BuildCommit string - BuildStamp int64 + BuildVersion string + BuildCommit string + BuildStamp int64 + Enterprise bool + ApplicationName string // Paths LogsPath string @@ -486,6 +488,11 @@ func NewConfigContext(args *CommandLineArgs) error { return err } + ApplicationName = "Grafana" + if Enterprise { + ApplicationName += " Enterprise" + } + Env = Cfg.Section("").Key("app_mode").MustString("development") InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name") PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath) diff --git a/scripts/build/build_enterprise.sh b/scripts/build/build_enterprise.sh new file mode 100755 index 00000000000..02d8c78c885 --- /dev/null +++ b/scripts/build/build_enterprise.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# +# This script is executed from within the container. +# + +echo "building enterprise version" + +GOPATH=/go +REPO_PATH=$GOPATH/src/github.com/grafana/grafana + + +cd /go/src/github.com/grafana/grafana +echo "current dir: $(pwd)" + +cd .. +git clone -b ee_build --single-branch git@github.com:grafana/grafana-enterprise.git --depth 10 +cd grafana-enterprise +git checkout 7fbae9c1be3467c4a39cf6ad85278a6896ceb49f +./build.sh + +cd ../grafana + +function exit_if_fail { + command=$@ + echo "Executing '$command'" + eval $command + rc=$? + if [ $rc -ne 0 ]; then + echo "'$command' returned $rc." + exit $rc + fi +} + +exit_if_fail go test ./pkg/extensions/... + + +if [ "$CIRCLE_TAG" != "" ]; then + echo "Building a release from tag $ls" + go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true -includeBuildNumber=false build +else + echo "Building incremental build for $CIRCLE_BRANCH" + go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true build +fi + +yarn install --pure-lockfile --no-progress + +source /etc/profile.d/rvm.sh + +echo "current dir: $(pwd)" + +if [ "$CIRCLE_TAG" != "" ]; then + echo "Packaging a release from tag $CIRCLE_TAG" + go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true -includeBuildNumber=false package latest +else + echo "Packaging incremental build for $CIRCLE_BRANCH" + go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true package latest +fi diff --git a/vendor/github.com/facebookgo/inject/inject.go b/vendor/github.com/facebookgo/inject/inject.go new file mode 100644 index 00000000000..300b9a37622 --- /dev/null +++ b/vendor/github.com/facebookgo/inject/inject.go @@ -0,0 +1,576 @@ +// Package inject provides a reflect based injector. A large application built +// with dependency injection in mind will typically involve the boring work of +// setting up the object graph. This library attempts to take care of this +// boring work by creating and connecting the various objects. Its use involves +// you seeding the object graph with some (possibly incomplete) objects, where +// the underlying types have been tagged for injection. Given this, the +// library will populate the objects creating new ones as necessary. It uses +// singletons by default, supports optional private instances as well as named +// instances. +// +// It works using Go's reflection package and is inherently limited in what it +// can do as opposed to a code-gen system with respect to private fields. +// +// The usage pattern for the library involves struct tags. It requires the tag +// format used by the various standard libraries, like json, xml etc. It +// involves tags in one of the three forms below: +// +// `inject:""` +// `inject:"private"` +// `inject:"dev logger"` +// +// The first no value syntax is for the common case of a singleton dependency +// of the associated type. The second triggers creation of a private instance +// for the associated type. Finally the last form is asking for a named +// dependency called "dev logger". +package inject + +import ( + "bytes" + "fmt" + "math/rand" + "reflect" + + "github.com/facebookgo/structtag" +) + +// Logger allows for simple logging as inject traverses and populates the +// object graph. +type Logger interface { + Debugf(format string, v ...interface{}) +} + +// Populate is a short-hand for populating a graph with the given incomplete +// object values. +func Populate(values ...interface{}) error { + var g Graph + for _, v := range values { + if err := g.Provide(&Object{Value: v}); err != nil { + return err + } + } + return g.Populate() +} + +// An Object in the Graph. +type Object struct { + Value interface{} + Name string // Optional + Complete bool // If true, the Value will be considered complete + Fields map[string]*Object // Populated with the field names that were injected and their corresponding *Object. + reflectType reflect.Type + reflectValue reflect.Value + private bool // If true, the Value will not be used and will only be populated + created bool // If true, the Object was created by us + embedded bool // If true, the Object is an embedded struct provided internally +} + +// String representation suitable for human consumption. +func (o *Object) String() string { + var buf bytes.Buffer + fmt.Fprint(&buf, o.reflectType) + if o.Name != "" { + fmt.Fprintf(&buf, " named %s", o.Name) + } + return buf.String() +} + +func (o *Object) addDep(field string, dep *Object) { + if o.Fields == nil { + o.Fields = make(map[string]*Object) + } + o.Fields[field] = dep +} + +// The Graph of Objects. +type Graph struct { + Logger Logger // Optional, will trigger debug logging. + unnamed []*Object + unnamedType map[reflect.Type]bool + named map[string]*Object +} + +// Provide objects to the Graph. The Object documentation describes +// the impact of various fields. +func (g *Graph) Provide(objects ...*Object) error { + for _, o := range objects { + o.reflectType = reflect.TypeOf(o.Value) + o.reflectValue = reflect.ValueOf(o.Value) + + if o.Fields != nil { + return fmt.Errorf( + "fields were specified on object %s when it was provided", + o, + ) + } + + if o.Name == "" { + if !isStructPtr(o.reflectType) { + return fmt.Errorf( + "expected unnamed object value to be a pointer to a struct but got type %s "+ + "with value %v", + o.reflectType, + o.Value, + ) + } + + if !o.private { + if g.unnamedType == nil { + g.unnamedType = make(map[reflect.Type]bool) + } + + if g.unnamedType[o.reflectType] { + return fmt.Errorf( + "provided two unnamed instances of type *%s.%s", + o.reflectType.Elem().PkgPath(), o.reflectType.Elem().Name(), + ) + } + g.unnamedType[o.reflectType] = true + } + g.unnamed = append(g.unnamed, o) + } else { + if g.named == nil { + g.named = make(map[string]*Object) + } + + if g.named[o.Name] != nil { + return fmt.Errorf("provided two instances named %s", o.Name) + } + g.named[o.Name] = o + } + + if g.Logger != nil { + if o.created { + g.Logger.Debugf("created %s", o) + } else if o.embedded { + g.Logger.Debugf("provided embedded %s", o) + } else { + g.Logger.Debugf("provided %s", o) + } + } + } + return nil +} + +// Populate the incomplete Objects. +func (g *Graph) Populate() error { + for _, o := range g.named { + if o.Complete { + continue + } + + if err := g.populateExplicit(o); err != nil { + return err + } + } + + // We append and modify our slice as we go along, so we don't use a standard + // range loop, and do a single pass thru each object in our graph. + i := 0 + for { + if i == len(g.unnamed) { + break + } + + o := g.unnamed[i] + i++ + + if o.Complete { + continue + } + + if err := g.populateExplicit(o); err != nil { + return err + } + } + + // A Second pass handles injecting Interface values to ensure we have created + // all concrete types first. + for _, o := range g.unnamed { + if o.Complete { + continue + } + + if err := g.populateUnnamedInterface(o); err != nil { + return err + } + } + + for _, o := range g.named { + if o.Complete { + continue + } + + if err := g.populateUnnamedInterface(o); err != nil { + return err + } + } + + return nil +} + +func (g *Graph) populateExplicit(o *Object) error { + // Ignore named value types. + if o.Name != "" && !isStructPtr(o.reflectType) { + return nil + } + +StructLoop: + for i := 0; i < o.reflectValue.Elem().NumField(); i++ { + field := o.reflectValue.Elem().Field(i) + fieldType := field.Type() + fieldTag := o.reflectType.Elem().Field(i).Tag + fieldName := o.reflectType.Elem().Field(i).Name + tag, err := parseTag(string(fieldTag)) + if err != nil { + return fmt.Errorf( + "unexpected tag format `%s` for field %s in type %s", + string(fieldTag), + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + // Skip fields without a tag. + if tag == nil { + continue + } + + // Cannot be used with unexported fields. + if !field.CanSet() { + return fmt.Errorf( + "inject requested on unexported field %s in type %s", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + // Inline tag on anything besides a struct is considered invalid. + if tag.Inline && fieldType.Kind() != reflect.Struct { + return fmt.Errorf( + "inline requested on non inlined field %s in type %s", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + // Don't overwrite existing values. + if !isNilOrZero(field, fieldType) { + continue + } + + // Named injects must have been explicitly provided. + if tag.Name != "" { + existing := g.named[tag.Name] + if existing == nil { + return fmt.Errorf( + "did not find object named %s required by field %s in type %s", + tag.Name, + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + if !existing.reflectType.AssignableTo(fieldType) { + return fmt.Errorf( + "object named %s of type %s is not assignable to field %s (%s) in type %s", + tag.Name, + fieldType, + o.reflectType.Elem().Field(i).Name, + existing.reflectType, + o.reflectType, + ) + } + + field.Set(reflect.ValueOf(existing.Value)) + if g.Logger != nil { + g.Logger.Debugf( + "assigned %s to field %s in %s", + existing, + o.reflectType.Elem().Field(i).Name, + o, + ) + } + o.addDep(fieldName, existing) + continue StructLoop + } + + // Inline struct values indicate we want to traverse into it, but not + // inject itself. We require an explicit "inline" tag for this to work. + if fieldType.Kind() == reflect.Struct { + if tag.Private { + return fmt.Errorf( + "cannot use private inject on inline struct on field %s in type %s", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + if !tag.Inline { + return fmt.Errorf( + "inline struct on field %s in type %s requires an explicit \"inline\" tag", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + err := g.Provide(&Object{ + Value: field.Addr().Interface(), + private: true, + embedded: o.reflectType.Elem().Field(i).Anonymous, + }) + if err != nil { + return err + } + continue + } + + // Interface injection is handled in a second pass. + if fieldType.Kind() == reflect.Interface { + continue + } + + // Maps are created and required to be private. + if fieldType.Kind() == reflect.Map { + if !tag.Private { + return fmt.Errorf( + "inject on map field %s in type %s must be named or private", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + field.Set(reflect.MakeMap(fieldType)) + if g.Logger != nil { + g.Logger.Debugf( + "made map for field %s in %s", + o.reflectType.Elem().Field(i).Name, + o, + ) + } + continue + } + + // Can only inject Pointers from here on. + if !isStructPtr(fieldType) { + return fmt.Errorf( + "found inject tag on unsupported field %s in type %s", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + // Unless it's a private inject, we'll look for an existing instance of the + // same type. + if !tag.Private { + for _, existing := range g.unnamed { + if existing.private { + continue + } + if existing.reflectType.AssignableTo(fieldType) { + field.Set(reflect.ValueOf(existing.Value)) + if g.Logger != nil { + g.Logger.Debugf( + "assigned existing %s to field %s in %s", + existing, + o.reflectType.Elem().Field(i).Name, + o, + ) + } + o.addDep(fieldName, existing) + continue StructLoop + } + } + } + + newValue := reflect.New(fieldType.Elem()) + newObject := &Object{ + Value: newValue.Interface(), + private: tag.Private, + created: true, + } + + // Add the newly ceated object to the known set of objects. + err = g.Provide(newObject) + if err != nil { + return err + } + + // Finally assign the newly created object to our field. + field.Set(newValue) + if g.Logger != nil { + g.Logger.Debugf( + "assigned newly created %s to field %s in %s", + newObject, + o.reflectType.Elem().Field(i).Name, + o, + ) + } + o.addDep(fieldName, newObject) + } + return nil +} + +func (g *Graph) populateUnnamedInterface(o *Object) error { + // Ignore named value types. + if o.Name != "" && !isStructPtr(o.reflectType) { + return nil + } + + for i := 0; i < o.reflectValue.Elem().NumField(); i++ { + field := o.reflectValue.Elem().Field(i) + fieldType := field.Type() + fieldTag := o.reflectType.Elem().Field(i).Tag + fieldName := o.reflectType.Elem().Field(i).Name + tag, err := parseTag(string(fieldTag)) + if err != nil { + return fmt.Errorf( + "unexpected tag format `%s` for field %s in type %s", + string(fieldTag), + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + // Skip fields without a tag. + if tag == nil { + continue + } + + // We only handle interface injection here. Other cases including errors + // are handled in the first pass when we inject pointers. + if fieldType.Kind() != reflect.Interface { + continue + } + + // Interface injection can't be private because we can't instantiate new + // instances of an interface. + if tag.Private { + return fmt.Errorf( + "found private inject tag on interface field %s in type %s", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + + // Don't overwrite existing values. + if !isNilOrZero(field, fieldType) { + continue + } + + // Named injects must have already been handled in populateExplicit. + if tag.Name != "" { + panic(fmt.Sprintf("unhandled named instance with name %s", tag.Name)) + } + + // Find one, and only one assignable value for the field. + var found *Object + for _, existing := range g.unnamed { + if existing.private { + continue + } + if existing.reflectType.AssignableTo(fieldType) { + if found != nil { + return fmt.Errorf( + "found two assignable values for field %s in type %s. one type "+ + "%s with value %v and another type %s with value %v", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + found.reflectType, + found.Value, + existing.reflectType, + existing.reflectValue, + ) + } + found = existing + field.Set(reflect.ValueOf(existing.Value)) + if g.Logger != nil { + g.Logger.Debugf( + "assigned existing %s to interface field %s in %s", + existing, + o.reflectType.Elem().Field(i).Name, + o, + ) + } + o.addDep(fieldName, existing) + } + } + + // If we didn't find an assignable value, we're missing something. + if found == nil { + return fmt.Errorf( + "found no assignable value for field %s in type %s", + o.reflectType.Elem().Field(i).Name, + o.reflectType, + ) + } + } + return nil +} + +// Objects returns all known objects, named as well as unnamed. The returned +// elements are not in a stable order. +func (g *Graph) Objects() []*Object { + objects := make([]*Object, 0, len(g.unnamed)+len(g.named)) + for _, o := range g.unnamed { + if !o.embedded { + objects = append(objects, o) + } + } + for _, o := range g.named { + if !o.embedded { + objects = append(objects, o) + } + } + // randomize to prevent callers from relying on ordering + for i := 0; i < len(objects); i++ { + j := rand.Intn(i + 1) + objects[i], objects[j] = objects[j], objects[i] + } + return objects +} + +var ( + injectOnly = &tag{} + injectPrivate = &tag{Private: true} + injectInline = &tag{Inline: true} +) + +type tag struct { + Name string + Inline bool + Private bool +} + +func parseTag(t string) (*tag, error) { + found, value, err := structtag.Extract("inject", t) + if err != nil { + return nil, err + } + if !found { + return nil, nil + } + if value == "" { + return injectOnly, nil + } + if value == "inline" { + return injectInline, nil + } + if value == "private" { + return injectPrivate, nil + } + return &tag{Name: value}, nil +} + +func isStructPtr(t reflect.Type) bool { + return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct +} + +func isNilOrZero(v reflect.Value, t reflect.Type) bool { + switch v.Kind() { + default: + return reflect.DeepEqual(v.Interface(), reflect.Zero(t).Interface()) + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } +} diff --git a/vendor/github.com/facebookgo/inject/license b/vendor/github.com/facebookgo/inject/license new file mode 100644 index 00000000000..953e8f7f10d --- /dev/null +++ b/vendor/github.com/facebookgo/inject/license @@ -0,0 +1,30 @@ +BSD License + +For inject software + +Copyright (c) 2015, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/facebookgo/inject/patents b/vendor/github.com/facebookgo/inject/patents new file mode 100644 index 00000000000..f33dc8011a8 --- /dev/null +++ b/vendor/github.com/facebookgo/inject/patents @@ -0,0 +1,33 @@ +Additional Grant of Patent Rights Version 2 + +"Software" means the inject software distributed by Facebook, Inc. + +Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software +("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable +(subject to the termination provision below) license under any Necessary +Claims, to make, have made, use, sell, offer to sell, import, and otherwise +transfer the Software. For avoidance of doubt, no license is granted under +Facebook’s rights in any patent claims that are infringed by (i) modifications +to the Software made by you or any third party or (ii) the Software in +combination with any software or other technology. + +The license granted hereunder will terminate, automatically and without notice, +if you (or any of your subsidiaries, corporate affiliates or agents) initiate +directly or indirectly, or take a direct financial interest in, any Patent +Assertion: (i) against Facebook or any of its subsidiaries or corporate +affiliates, (ii) against any party if such Patent Assertion arises in whole or +in part from any software, technology, product or service of Facebook or any of +its subsidiaries or corporate affiliates, or (iii) against any party relating +to the Software. Notwithstanding the foregoing, if Facebook or any of its +subsidiaries or corporate affiliates files a lawsuit alleging patent +infringement against you in the first instance, and you respond by filing a +patent infringement counterclaim in that lawsuit against that party that is +unrelated to the Software, the license granted hereunder will not terminate +under section (i) of this paragraph due to such counterclaim. + +A "Necessary Claim" is a claim of a patent owned by Facebook that is +necessarily infringed by the Software standing alone. + +A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, +or contributory infringement or inducement to infringe any patent, including a +cross-claim or counterclaim. diff --git a/vendor/github.com/facebookgo/structtag/license b/vendor/github.com/facebookgo/structtag/license new file mode 100644 index 00000000000..74487567632 --- /dev/null +++ b/vendor/github.com/facebookgo/structtag/license @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/facebookgo/structtag/structtag.go b/vendor/github.com/facebookgo/structtag/structtag.go new file mode 100644 index 00000000000..be9bc2293be --- /dev/null +++ b/vendor/github.com/facebookgo/structtag/structtag.go @@ -0,0 +1,61 @@ +// Package structtag provides parsing of the defacto struct tag style. +package structtag + +import ( + "errors" + "strconv" +) + +var errInvalidTag = errors.New("invalid tag") + +// Extract the quoted value for the given name returning it if it is found. The +// found boolean helps differentiate between the "empty and found" vs "empty +// and not found" nature of default empty strings. +func Extract(name, tag string) (found bool, value string, err error) { + for tag != "" { + // skip leading space + i := 0 + for i < len(tag) && tag[i] == ' ' { + i++ + } + tag = tag[i:] + if tag == "" { + break + } + + // scan to colon. + // a space or a quote is a syntax error + i = 0 + for i < len(tag) && tag[i] != ' ' && tag[i] != ':' && tag[i] != '"' { + i++ + } + if i+1 >= len(tag) || tag[i] != ':' || tag[i+1] != '"' { + return false, "", errInvalidTag + } + foundName := string(tag[:i]) + tag = tag[i+1:] + + // scan quoted string to find value + i = 1 + for i < len(tag) && tag[i] != '"' { + if tag[i] == '\\' { + i++ + } + i++ + } + if i >= len(tag) { + return false, "", errInvalidTag + } + qvalue := string(tag[:i+1]) + tag = tag[i+1:] + + if foundName == name { + value, err := strconv.Unquote(qvalue) + if err != nil { + return false, "", err + } + return true, value, nil + } + } + return false, "", nil +} diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 00000000000..835ba3e755c --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 00000000000..842ee80456d --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,269 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// and the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required the errors.WithStack and errors.WithMessage +// functions destructure errors.Wrap into its component operations of annotating +// an error with a stack trace and an a message, respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error which does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// causer interface is not exported by this package, but is considered a part +// of stable public API. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported +// +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface. +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// Where errors.StackTrace is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// stackTracer interface is not exported by this package, but is considered a part +// of stable public API. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is call, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 00000000000..6b1f2891a5a --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,178 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s path of source file relative to the compile time GOPATH +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} + +func trimGOPATH(name, file string) string { + // Here we want to get the source file path relative to the compile time + // GOPATH. As of Go 1.6.x there is no direct way to know the compiled + // GOPATH at runtime, but we can infer the number of path segments in the + // GOPATH. We note that fn.Name() returns the function name qualified by + // the import path, which does not include the GOPATH. Thus we can trim + // segments from the beginning of the file path until the number of path + // separators remaining is one more than the number of path separators in + // the function name. For example, given: + // + // GOPATH /home/user + // file /home/user/src/pkg/sub/file.go + // fn.Name() pkg/sub.Type.Method + // + // We want to produce: + // + // pkg/sub/file.go + // + // From this we can easily see that fn.Name() has one less path separator + // than our desired output. We count separators from the end of the file + // path until it finds two more than in the function name and then move + // one character forward to preserve the initial path segment without a + // leading separator. + const sep = "/" + goal := strings.Count(name, sep) + 2 + i := len(file) + for n := 0; n < goal; n++ { + i = strings.LastIndex(file[:i], sep) + if i == -1 { + // not enough separators found, set i so that the slice expression + // below leaves file unmodified + i = -len(sep) + break + } + } + // get back to 0 or trim the leading separator + file = file[i+len(sep):] + return file +} From df71fe33fdaa048c51e709327d6d971cadf4fbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Apr 2018 13:01:32 +0200 Subject: [PATCH 0546/3000] refactor: refactoring notification service to use new service registry hooks --- pkg/api/org_invite.go | 2 + pkg/cmd/grafana-server/server.go | 11 +-- pkg/services/notifications/mailer.go | 32 ------- pkg/services/notifications/notifications.go | 91 ++++++++++++++----- .../notifications/notifications_test.go | 17 ++-- .../send_email_integration_test.go | 20 ++-- pkg/services/notifications/webhook.go | 31 +------ 7 files changed, 93 insertions(+), 111 deletions(-) diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index d6ab1c9d372..9f4f714af45 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -60,7 +60,9 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response { } // send invite email + c.Logger.Error("sending?") if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) { + c.Logger.Error("yes sending?") emailCmd := m.SendEmailCommand{ To: []string{inviteDto.LoginOrEmail}, Template: "new_user_invite.html", diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 1bf0e90915f..3d4f75978bd 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -26,16 +26,17 @@ import ( "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" "github.com/grafana/grafana/pkg/tracing" + // self registering services _ "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" + _ "github.com/grafana/grafana/pkg/services/notifications" _ "github.com/grafana/grafana/pkg/services/search" ) @@ -56,9 +57,9 @@ type GrafanaServerImpl struct { shutdownFn context.CancelFunc childRoutines *errgroup.Group log log.Logger - RouteRegister api.RouteRegister `inject:""` - HttpServer *api.HTTPServer `inject:""` + RouteRegister api.RouteRegister `inject:""` + HttpServer *api.HTTPServer `inject:""` } func (g *GrafanaServerImpl) Start() error { @@ -89,10 +90,6 @@ func (g *GrafanaServerImpl) Start() error { } defer tracingCloser.Close() - if err = notifications.Init(); err != nil { - return fmt.Errorf("Notification service failed to initialize. error: %v", err) - } - serviceGraph := inject.Graph{} serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()}) diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 1bac5025244..37169661d73 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -11,44 +11,12 @@ import ( "html/template" "net" "strconv" - "strings" - "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" gomail "gopkg.in/mail.v2" ) -var mailQueue chan *Message - -func initMailQueue() { - mailQueue = make(chan *Message, 10) - go processMailQueue() -} - -func processMailQueue() { - for { - select { - case msg := <-mailQueue: - num, err := send(msg) - tos := strings.Join(msg.To, "; ") - info := "" - if err != nil { - if len(msg.Info) > 0 { - info = ", info: " + msg.Info - } - log.Error(4, fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err)) - } else { - log.Trace(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info)) - } - } - } -} - -var addToMailQueue = func(msg *Message) { - mailQueue <- msg -} - func send(msg *Message) (int, error) { dialer, err := createDialer() if err != nil { diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index 25eb2b5936a..ad776057ad7 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -7,11 +7,13 @@ import ( "html/template" "net/url" "path/filepath" + "strings" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -21,20 +23,31 @@ var tmplResetPassword = "reset_password.html" var tmplSignUpStarted = "signup_started.html" var tmplWelcomeOnSignUp = "welcome_on_signup.html" -func Init() error { - initMailQueue() - initWebhookQueue() +func init() { + registry.RegisterService(&NotificationService{}) +} - bus.AddHandler("email", sendResetPasswordEmail) - bus.AddHandler("email", validateResetPasswordCode) - bus.AddHandler("email", sendEmailCommandHandler) +type NotificationService struct { + Bus bus.Bus `inject:""` + mailQueue chan *Message + webhookQueue chan *Webhook + log log.Logger +} - bus.AddCtxHandler("email", sendEmailCommandHandlerSync) +func (ns *NotificationService) Init() error { + ns.log = log.New("notifications") + ns.mailQueue = make(chan *Message, 10) + ns.webhookQueue = make(chan *Webhook, 10) - bus.AddCtxHandler("webhook", SendWebhookSync) + ns.Bus.AddHandler(ns.sendResetPasswordEmail) + ns.Bus.AddHandler(ns.validateResetPasswordCode) + ns.Bus.AddHandler(ns.sendEmailCommandHandler) - bus.AddEventListener(signUpStartedHandler) - bus.AddEventListener(signUpCompletedHandler) + ns.Bus.AddCtxHandler(ns.sendEmailCommandHandlerSync) + ns.Bus.AddCtxHandler(ns.SendWebhookSync) + + ns.Bus.AddEventListener(ns.signUpStartedHandler) + ns.Bus.AddEventListener(ns.signUpCompletedHandler) mailTemplates = template.New("name") mailTemplates.Funcs(template.FuncMap{ @@ -58,8 +71,37 @@ func Init() error { return nil } -func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { - return sendWebRequestSync(ctx, &Webhook{ +func (ns *NotificationService) Run(ctx context.Context) error { + for { + select { + case webhook := <-ns.webhookQueue: + err := ns.sendWebRequestSync(context.Background(), webhook) + + if err != nil { + ns.log.Error("Failed to send webrequest ", "error", err) + } + case msg := <-ns.mailQueue: + num, err := send(msg) + tos := strings.Join(msg.To, "; ") + info := "" + if err != nil { + if len(msg.Info) > 0 { + info = ", info: " + msg.Info + } + ns.log.Error(fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err)) + } else { + ns.log.Debug(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info)) + } + case <-ctx.Done(): + return ctx.Err() + } + } + + return nil +} + +func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { + return ns.sendWebRequestSync(ctx, &Webhook{ Url: cmd.Url, User: cmd.User, Password: cmd.Password, @@ -74,7 +116,7 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string { return "" } -func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error { +func (ns *NotificationService) sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error { message, err := buildEmailMessage(&m.SendEmailCommand{ Data: cmd.Data, Info: cmd.Info, @@ -89,24 +131,22 @@ func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSyn } _, err = send(message) - return err } -func sendEmailCommandHandler(cmd *m.SendEmailCommand) error { +func (ns *NotificationService) sendEmailCommandHandler(cmd *m.SendEmailCommand) error { message, err := buildEmailMessage(cmd) if err != nil { return err } - addToMailQueue(message) - + ns.mailQueue <- message return nil } -func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error { - return sendEmailCommandHandler(&m.SendEmailCommand{ +func (ns *NotificationService) sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error { + return ns.sendEmailCommandHandler(&m.SendEmailCommand{ To: []string{cmd.User.Email}, Template: tmplResetPassword, Data: map[string]interface{}{ @@ -116,7 +156,7 @@ func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error { }) } -func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error { +func (ns *NotificationService) validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error { login := getLoginForEmailCode(query.Code) if login == "" { return m.ErrInvalidEmailCode @@ -135,18 +175,18 @@ func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error { return nil } -func signUpStartedHandler(evt *events.SignUpStarted) error { +func (ns *NotificationService) signUpStartedHandler(evt *events.SignUpStarted) error { if !setting.VerifyEmailEnabled { return nil } - log.Info("User signup started: %s", evt.Email) + ns.log.Info("User signup started", "email", evt.Email) if evt.Email == "" { return nil } - err := sendEmailCommandHandler(&m.SendEmailCommand{ + err := ns.sendEmailCommandHandler(&m.SendEmailCommand{ To: []string{evt.Email}, Template: tmplSignUpStarted, Data: map[string]interface{}{ @@ -155,6 +195,7 @@ func signUpStartedHandler(evt *events.SignUpStarted) error { "SignUpUrl": setting.ToAbsUrl(fmt.Sprintf("signup/?email=%s&code=%s", url.QueryEscape(evt.Email), url.QueryEscape(evt.Code))), }, }) + if err != nil { return err } @@ -163,12 +204,12 @@ func signUpStartedHandler(evt *events.SignUpStarted) error { return bus.Dispatch(&emailSentCmd) } -func signUpCompletedHandler(evt *events.SignUpCompleted) error { +func (ns *NotificationService) signUpCompletedHandler(evt *events.SignUpCompleted) error { if evt.Email == "" || !setting.Smtp.SendWelcomeEmailOnSignUp { return nil } - return sendEmailCommandHandler(&m.SendEmailCommand{ + return ns.sendEmailCommandHandler(&m.SendEmailCommand{ To: []string{evt.Email}, Template: tmplWelcomeOnSignUp, Data: map[string]interface{}{ diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index 3a5ff5fedb7..a86bd3b19ed 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -3,6 +3,7 @@ package notifications import ( "testing" + "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" @@ -17,25 +18,23 @@ type testTriggeredAlert struct { func TestNotifications(t *testing.T) { Convey("Given the notifications service", t, func() { - //bus.ClearBusHandlers() - setting.StaticRootPath = "../../../public/" setting.Smtp.Enabled = true setting.Smtp.TemplatesPattern = "emails/*.html" setting.Smtp.FromAddress = "from@address.com" setting.Smtp.FromName = "Grafana Admin" - err := Init() + ns := &NotificationService{} + ns.Bus = bus.New() + + err := ns.Init() So(err, ShouldBeNil) - var sentMsg *Message - addToMailQueue = func(msg *Message) { - sentMsg = msg - } - Convey("When sending reset email password", func() { - err := sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}}) + err := ns.sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}}) So(err, ShouldBeNil) + + sentMsg := <-ns.mailQueue So(sentMsg.Body, ShouldContainSubstring, "body") So(sentMsg.Subject, ShouldEqual, "Reset your Grafana password - asd@asd.com") So(sentMsg.Body, ShouldNotContainSubstring, "Subject") diff --git a/pkg/services/notifications/send_email_integration_test.go b/pkg/services/notifications/send_email_integration_test.go index a9a5215d3ca..a9f37018a3a 100644 --- a/pkg/services/notifications/send_email_integration_test.go +++ b/pkg/services/notifications/send_email_integration_test.go @@ -12,8 +12,6 @@ import ( func TestEmailIntegrationTest(t *testing.T) { SkipConvey("Given the notifications service", t, func() { - bus.ClearBusHandlers() - setting.StaticRootPath = "../../../public/" setting.Smtp.Enabled = true setting.Smtp.TemplatesPattern = "emails/*.html" @@ -21,14 +19,11 @@ func TestEmailIntegrationTest(t *testing.T) { setting.Smtp.FromName = "Grafana Admin" setting.BuildVersion = "4.0.0" - err := Init() - So(err, ShouldBeNil) + ns := &NotificationService{} + ns.Bus = bus.New() - addToMailQueue = func(msg *Message) { - So(msg.From, ShouldEqual, "Grafana Admin ") - So(msg.To[0], ShouldEqual, "asdf@asdf.com") - ioutil.WriteFile("../../../tmp/test_email.html", []byte(msg.Body), 0777) - } + err := ns.Init() + So(err, ShouldBeNil) Convey("When sending reset email password", func() { cmd := &m.SendEmailCommand{ @@ -59,8 +54,13 @@ func TestEmailIntegrationTest(t *testing.T) { Template: "alert_notification.html", } - err := sendEmailCommandHandler(cmd) + err := ns.sendEmailCommandHandler(cmd) So(err, ShouldBeNil) + + sentMsg := <-ns.mailQueue + So(sentMsg.From, ShouldEqual, "Grafana Admin ") + So(sentMsg.To[0], ShouldEqual, "asdf@asdf.com") + ioutil.WriteFile("../../../tmp/test_email.html", []byte(sentMsg.Body), 0777) }) }) } diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index 0636a6adadc..01db2d56471 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -11,7 +11,6 @@ import ( "golang.org/x/net/context/ctxhttp" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/util" ) @@ -37,32 +36,8 @@ var netClient = &http.Client{ Transport: netTransport, } -var ( - webhookQueue chan *Webhook - webhookLog log.Logger -) - -func initWebhookQueue() { - webhookLog = log.New("notifications.webhook") - webhookQueue = make(chan *Webhook, 10) - go processWebhookQueue() -} - -func processWebhookQueue() { - for { - select { - case webhook := <-webhookQueue: - err := sendWebRequestSync(context.Background(), webhook) - - if err != nil { - webhookLog.Error("Failed to send webrequest ", "error", err) - } - } - } -} - -func sendWebRequestSync(ctx context.Context, webhook *Webhook) error { - webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod) +func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *Webhook) error { + ns.log.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod) if webhook.HttpMethod == "" { webhook.HttpMethod = http.MethodPost @@ -98,6 +73,6 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error { return err } - webhookLog.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body)) + ns.log.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body)) return fmt.Errorf("Webhook response status %v", resp.Status) } From 44b0f15a61778d9a0c85cf29c5cfc577ea4594e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Apr 2018 14:28:42 +0200 Subject: [PATCH 0547/3000] fix: removed log calls used while troubleshooting --- pkg/api/org_invite.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 9f4f714af45..d6ab1c9d372 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -60,9 +60,7 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response { } // send invite email - c.Logger.Error("sending?") if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) { - c.Logger.Error("yes sending?") emailCmd := m.SendEmailCommand{ To: []string{inviteDto.LoginOrEmail}, Template: "new_user_invite.html", From a8eed9d3440513338b656ff87bd2a3a141c0a33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Apr 2018 15:11:55 +0200 Subject: [PATCH 0548/3000] Refactoring PluginManager to be a self registering service (#11755) * refator: refactored PluginManager to be a self registering service, a lot more work needed to fully make plugin manager use instance variables and not so many globals --- pkg/cmd/grafana-server/server.go | 10 +--- pkg/plugins/dashboard_importer_test.go | 6 +-- pkg/plugins/dashboards_test.go | 5 +- pkg/plugins/dashboards_updater.go | 8 +-- pkg/plugins/datasource_plugin.go | 2 +- pkg/plugins/plugins.go | 73 +++++++++++++++----------- pkg/plugins/plugins_test.go | 9 ++-- pkg/plugins/update_checker.go | 25 +++------ 8 files changed, 64 insertions(+), 74 deletions(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 1bf0e90915f..da070cc0f40 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/provisioning" "golang.org/x/sync/errgroup" @@ -25,8 +26,6 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" @@ -34,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/tracing" _ "github.com/grafana/grafana/pkg/extensions" + _ "github.com/grafana/grafana/pkg/plugins" _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" _ "github.com/grafana/grafana/pkg/services/search" @@ -73,12 +73,6 @@ func (g *GrafanaServerImpl) Start() error { login.Init() social.NewOAuthService() - pluginManager, err := plugins.NewPluginManager(g.context) - if err != nil { - return fmt.Errorf("Failed to start plugins. error: %v", err) - } - g.childRoutines.Go(func() error { return pluginManager.Run(g.context) }) - if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil { return fmt.Errorf("Failed to provision Grafana from config. error: %v", err) } diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index 549b3bb4cf9..d8460a1875c 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -1,7 +1,6 @@ package plugins import ( - "context" "io/ioutil" "testing" @@ -91,10 +90,11 @@ func pluginScenario(desc string, t *testing.T, fn func()) { setting.Cfg = ini.Empty() sec, _ := setting.Cfg.NewSection("plugin.test-app") sec.NewKey("path", "../../tests/test-app") - err := initPlugins(context.Background()) + + pm := &PluginManager{} + err := pm.Init() So(err, ShouldBeNil) - Convey(desc, fn) }) } diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go index 8573d452409..241e41d7bb2 100644 --- a/pkg/plugins/dashboards_test.go +++ b/pkg/plugins/dashboards_test.go @@ -1,7 +1,6 @@ package plugins import ( - "context" "testing" "github.com/grafana/grafana/pkg/bus" @@ -18,7 +17,9 @@ func TestPluginDashboards(t *testing.T) { setting.Cfg = ini.Empty() sec, _ := setting.Cfg.NewSection("plugin.test-app") sec.NewKey("path", "../../tests/test-app") - err := initPlugins(context.Background()) + + pm := &PluginManager{} + err := pm.Init() So(err, ShouldBeNil) diff --git a/pkg/plugins/dashboards_updater.go b/pkg/plugins/dashboards_updater.go index 835e8873810..04d3dc035cc 100644 --- a/pkg/plugins/dashboards_updater.go +++ b/pkg/plugins/dashboards_updater.go @@ -1,8 +1,6 @@ package plugins import ( - "time" - "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -11,10 +9,8 @@ func init() { bus.AddEventListener(handlePluginStateChanged) } -func updateAppDashboards() { - time.Sleep(time.Second * 5) - - plog.Debug("Looking for App Dashboard Updates") +func (pm *PluginManager) updateAppDashboards() { + pm.log.Debug("Looking for App Dashboard Updates") query := m.GetPluginSettingsQuery{OrgId: 0} diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go index 37ce175efe4..114b71deefc 100644 --- a/pkg/plugins/datasource_plugin.go +++ b/pkg/plugins/datasource_plugin.go @@ -76,7 +76,7 @@ func composeBinaryName(executable, os, arch string) string { return fmt.Sprintf("%s_%s_%s%s", executable, os, strings.ToLower(arch), extension) } -func (p *DataSourcePlugin) initBackendPlugin(ctx context.Context, log log.Logger) error { +func (p *DataSourcePlugin) startBackendPlugin(ctx context.Context, log log.Logger) error { p.log = log.New("plugin-id", p.Id) err := p.spawnSubProcess() diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 45e7c934bea..7ce0ac38919 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -11,8 +11,10 @@ import ( "path/filepath" "reflect" "strings" + "time" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -39,30 +41,12 @@ type PluginManager struct { log log.Logger } -func NewPluginManager(ctx context.Context) (*PluginManager, error) { - err := initPlugins(ctx) - - if err != nil { - return nil, err - } - - return &PluginManager{ - log: log.New("plugins"), - }, nil +func init() { + registry.RegisterService(&PluginManager{}) } -func (p *PluginManager) Run(ctx context.Context) error { - <-ctx.Done() - - for _, p := range DataSources { - p.Kill() - } - - p.log.Info("Stopped Plugins", "reason", ctx.Err()) - return ctx.Err() -} - -func initPlugins(ctx context.Context) error { +func (pm *PluginManager) Init() error { + pm.log = log.New("plugins") plog = log.New("plugins") DataSources = map[string]*DataSourcePlugin{} @@ -76,7 +60,7 @@ func initPlugins(ctx context.Context) error { "app": AppPlugin{}, } - plog.Info("Starting plugin search") + pm.log.Info("Starting plugin search") scan(path.Join(setting.StaticRootPath, "app/plugins")) // check if plugins dir exists @@ -99,13 +83,6 @@ func initPlugins(ctx context.Context) error { } for _, ds := range DataSources { - if ds.Backend { - err := ds.initBackendPlugin(ctx, plog) - if err != nil { - plog.Error("Failed to init plugin.", "error", err, "plugin", ds.Id) - } - } - ds.initFrontendPlugin() } @@ -113,8 +90,40 @@ func initPlugins(ctx context.Context) error { app.initApp() } - go StartPluginUpdateChecker() - go updateAppDashboards() + return nil +} + +func (pm *PluginManager) startBackendPlugins(ctx context.Context) error { + for _, ds := range DataSources { + if ds.Backend { + if err := ds.startBackendPlugin(ctx, plog); err != nil { + pm.log.Error("Failed to init plugin.", "error", err, "plugin", ds.Id) + } + } + } + + return nil +} + +func (pm *PluginManager) Run(ctx context.Context) error { + pm.startBackendPlugins(ctx) + pm.updateAppDashboards() + pm.checkForUpdates() + + ticker := time.NewTicker(time.Minute * 10) + for { + select { + case <-ticker.C: + pm.checkForUpdates() + case <-ctx.Done(): + break + } + } + + // kil backend plugins + for _, p := range DataSources { + p.Kill() + } return nil } diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 00329b4a8a1..7566d054b7f 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -1,7 +1,6 @@ package plugins import ( - "context" "path/filepath" "testing" @@ -15,7 +14,9 @@ func TestPluginScans(t *testing.T) { Convey("When scanning for plugins", t, func() { setting.StaticRootPath, _ = filepath.Abs("../../public/") setting.Cfg = ini.Empty() - err := initPlugins(context.Background()) + + pm := &PluginManager{} + err := pm.Init() So(err, ShouldBeNil) So(len(DataSources), ShouldBeGreaterThan, 1) @@ -30,7 +31,9 @@ func TestPluginScans(t *testing.T) { setting.Cfg = ini.Empty() sec, _ := setting.Cfg.NewSection("plugin.nginx-app") sec.NewKey("path", "../../tests/test-app") - err := initPlugins(context.Background()) + + pm := &PluginManager{} + err := pm.Init() So(err, ShouldBeNil) So(len(Apps), ShouldBeGreaterThan, 0) diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go index 946d215b1c2..57f6d2ca651 100644 --- a/pkg/plugins/update_checker.go +++ b/pkg/plugins/update_checker.go @@ -26,23 +26,6 @@ type GithubLatest struct { Testing string `json:"testing"` } -func StartPluginUpdateChecker() { - if !setting.CheckForUpdates { - return - } - - // do one check directly - go checkForUpdates() - - ticker := time.NewTicker(time.Minute * 10) - for { - select { - case <-ticker.C: - checkForUpdates() - } - } -} - func getAllExternalPluginSlugs() string { var result []string for _, plug := range Plugins { @@ -56,8 +39,12 @@ func getAllExternalPluginSlugs() string { return strings.Join(result, ",") } -func checkForUpdates() { - log.Trace("Checking for updates") +func (pm *PluginManager) checkForUpdates() { + if !setting.CheckForUpdates { + return + } + + pm.log.Debug("Checking for updates") pluginSlugs := getAllExternalPluginSlugs() resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion) From 25d3ec5bbf09f8d424b36c53288725a6a29cf9bb Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 27 Apr 2018 15:35:46 +0200 Subject: [PATCH 0549/3000] Fixed settings default and explore path --- pkg/api/index.go | 2 +- pkg/setting/setting.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index 75e3594d854..568b04f95ae 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -125,7 +125,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { Icon: "fa fa-rocket", Url: setting.AppSubUrl + "/explore", Children: []*dtos.NavLink{ - {Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore/new"}, + {Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore"}, }, }) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 37646979095..756417c082d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -613,7 +613,7 @@ func NewConfigContext(args *CommandLineArgs) error { ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true) explore := Cfg.Section("explore") - ExploreEnabled = explore.Key("enabled").MustBool(true) + ExploreEnabled = explore.Key("enabled").MustBool(false) readSessionConfig() readSmtpSettings() From 949e3d29e80d62456bedd153e5e777b2aad9111f Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 27 Apr 2018 15:42:35 +0200 Subject: [PATCH 0550/3000] Explore: add support for multiple queries * adds +/- buttons to query rows in the Explore section * on Run Query all query expressions are submitted * `generateQueryKey` and `ensureQueries` are helpers to ensure each query field has a unique key for react. --- public/app/containers/Explore/Explore.tsx | 102 ++++++++++--------- public/app/containers/Explore/QueryRows.tsx | 69 +++++++++++++ public/app/containers/Explore/utils/query.ts | 31 ++++++ public/sass/pages/_explore.scss | 21 +++- 4 files changed, 175 insertions(+), 48 deletions(-) create mode 100644 public/app/containers/Explore/QueryRows.tsx create mode 100644 public/app/containers/Explore/utils/query.ts diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 55c1d088ccc..eae3f4d0a1f 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -5,29 +5,11 @@ import TimeSeries from 'app/core/time_series2'; import ElapsedTime from './ElapsedTime'; import Legend from './Legend'; -import QueryField from './QueryField'; +import QueryRows from './QueryRows'; import Graph from './Graph'; import Table from './Table'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; - -function buildQueryOptions({ format, interval, instant, now, query }) { - const to = now; - const from = to - 1000 * 60 * 60 * 3; - return { - interval, - range: { - from, - to, - }, - targets: [ - { - expr: query, - format, - instant, - }, - ], - }; -} +import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query'; function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { @@ -63,6 +45,7 @@ interface IExploreState { graphResult: any; latency: number; loading: any; + queries: any; requestOptions: any; showingGraph: boolean; showingTable: boolean; @@ -72,7 +55,6 @@ interface IExploreState { // @observer export class Explore extends React.Component { datasourceSrv: DatasourceSrv; - query: string; constructor(props) { super(props); @@ -83,6 +65,7 @@ export class Explore extends React.Component { graphResult: null, latency: 0, loading: false, + queries: ensureQueries(), requestOptions: null, showingGraph: true, showingTable: true, @@ -100,6 +83,27 @@ export class Explore extends React.Component { } } + handleAddQueryRow = index => { + const { queries } = this.state; + const nextQueries = [ + ...queries.slice(0, index + 1), + { query: '', key: generateQueryKey() }, + ...queries.slice(index + 1), + ]; + this.setState({ queries: nextQueries }); + }; + + handleChangeQuery = (query, index) => { + const { queries } = this.state; + const nextQuery = { + ...queries[index], + query, + }; + const nextQueries = [...queries]; + nextQueries[index] = nextQuery; + this.setState({ queries: nextQueries }); + }; + handleClickGraphButton = () => { this.setState(state => ({ showingGraph: !state.showingGraph })); }; @@ -108,12 +112,13 @@ export class Explore extends React.Component { this.setState(state => ({ showingTable: !state.showingTable })); }; - handleRequestError({ error }) { - console.error(error); - } - - handleQueryChange = query => { - this.query = query; + handleRemoveQueryRow = index => { + const { queries } = this.state; + if (queries.length <= 1) { + return; + } + const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; + this.setState({ queries: nextQueries }, () => this.handleSubmit()); }; handleSubmit = () => { @@ -127,9 +132,8 @@ export class Explore extends React.Component { }; async runGraphQuery() { - const { query } = this; - const { datasource } = this.state; - if (!query) { + const { datasource, queries } = this.state; + if (!hasQuery(queries)) { return; } this.setState({ latency: 0, loading: true, graphResult: null }); @@ -139,7 +143,7 @@ export class Explore extends React.Component { interval: datasource.interval, instant: false, now, - query, + queries: queries.map(q => q.query), }); try { const res = await datasource.query(options); @@ -153,14 +157,19 @@ export class Explore extends React.Component { } async runTableQuery() { - const { query } = this; - const { datasource } = this.state; - if (!query) { + const { datasource, queries } = this.state; + if (!hasQuery(queries)) { return; } this.setState({ latency: 0, loading: true, tableResult: null }); const now = Date.now(); - const options = buildQueryOptions({ format: 'table', interval: datasource.interval, instant: true, now, query }); + const options = buildQueryOptions({ + format: 'table', + interval: datasource.interval, + instant: true, + now, + queries: queries.map(q => q.query), + }); try { const res = await datasource.query(options); const tableModel = res.data[0]; @@ -182,10 +191,11 @@ export class Explore extends React.Component { datasource, datasourceError, datasourceLoading, + graphResult, latency, loading, + queries, requestOptions, - graphResult, showingGraph, showingTable, tableResult, @@ -205,7 +215,8 @@ export class Explore extends React.Component { {datasource ? (
    -
    +
    + {loading || latency ? : null} @@ -219,15 +230,14 @@ export class Explore extends React.Component {
    -
    - -
    - {loading || latency ? : null} +
    {showingGraph ? ( diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx new file mode 100644 index 00000000000..a0a9981368d --- /dev/null +++ b/public/app/containers/Explore/QueryRows.tsx @@ -0,0 +1,69 @@ +import React, { PureComponent } from 'react'; + +import QueryField from './QueryField'; + +class QueryRow extends PureComponent { + constructor(props) { + super(props); + this.state = { + query: '', + }; + } + + handleChangeQuery = value => { + const { index, onChangeQuery } = this.props; + this.setState({ query: value }); + if (onChangeQuery) { + onChangeQuery(value, index); + } + }; + + handleClickAddButton = () => { + const { index, onAddQueryRow } = this.props; + if (onAddQueryRow) { + onAddQueryRow(index); + } + }; + + handleClickRemoveButton = () => { + const { index, onRemoveQueryRow } = this.props; + if (onRemoveQueryRow) { + onRemoveQueryRow(index); + } + }; + + handlePressEnter = () => { + const { onExecuteQuery } = this.props; + if (onExecuteQuery) { + onExecuteQuery(); + } + }; + + render() { + const { request } = this.props; + return ( +
    +
    + + +
    +
    + +
    +
    + ); + } +} + +export default class QueryRows extends PureComponent { + render() { + const { className = '', queries, ...handlers } = this.props; + return ( +
    {queries.map((q, index) => )}
    + ); + } +} diff --git a/public/app/containers/Explore/utils/query.ts b/public/app/containers/Explore/utils/query.ts new file mode 100644 index 00000000000..d51c7339944 --- /dev/null +++ b/public/app/containers/Explore/utils/query.ts @@ -0,0 +1,31 @@ +export function buildQueryOptions({ format, interval, instant, now, queries }) { + const to = now; + const from = to - 1000 * 60 * 60 * 3; + return { + interval, + range: { + from, + to, + }, + targets: queries.map(expr => ({ + expr, + format, + instant, + })), + }; +} + +export function generateQueryKey(index = 0) { + return `Q-${Date.now()}-${Math.random()}-${index}`; +} + +export function ensureQueries(queries?) { + if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0] === 'string') { + return queries.map((query, i) => ({ key: generateQueryKey(i), query })); + } + return [{ key: generateQueryKey(), query: '' }]; +} + +export function hasQuery(queries) { + return queries.some(q => q.query); +} diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 4bd0162563b..74a19c1d2c2 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -4,6 +4,23 @@ } } +.query-row { + position: relative; + + & + & { + margin-top: 0.5rem; + } +} + +.query-row-tools { + position: absolute; + left: -4rem; + top: 0.33rem; + > * { + margin-right: 0.25rem; + } +} + .query-field { font-size: 14px; font-family: Consolas, Menlo, Courier, monospace; @@ -14,14 +31,14 @@ position: relative; display: inline-block; padding: 6px 7px 4px; - width: calc(100% - 6rem); + width: 100%; cursor: text; line-height: 1.5; color: rgba(0, 0, 0, 0.65); background-color: #fff; background-image: none; border: 1px solid lightgray; - border-radius: 4px; + border-radius: 3px; transition: all 0.3s; } From 0cbeb56af16b3a3bcec088a30d34a283ae98775f Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 27 Apr 2018 16:41:07 +0200 Subject: [PATCH 0551/3000] disable ent build to avoid slowing down build speed --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d3e6c71b520..bad5a7c1cd0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -192,7 +192,7 @@ workflows: ignore: /.*/ tags: only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ - - build-enterprise: - filters: - tags: - only: /.*/ + # - build-enterprise: + # filters: + # tags: + # only: /.*/ From ec23816df65a327cd72bf6125369b9b98d93a661 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 27 Apr 2018 17:06:08 +0200 Subject: [PATCH 0552/3000] docs: further documents changes to the docker image. (#11763) * docs: further documents changes to the docker image. * docs: explains the changes to user id better. --- docs/sources/installation/docker.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index d6f3ae16466..e78796845c4 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -132,7 +132,28 @@ docker run -d --user $ID --volume "$PWD/data:/var/lib/grafana" -p 3000:3000 graf ## Migration from a previous version of the docker container to 5.1 or later -In 5.1 we switched the id of the grafana user. Unfortunately this means that files created prior to 5.1 won't have the correct permissions for later versions. We made this change so that it would be easier for you to control what user Grafana is executed as (see examples below). +The docker container for Grafana has seen a major rewrite for 5.1. + +**Important changes** + +* file ownership is no longer modified during startup with `chown` +* default user id `472` instead of `104` +* no more implicit volumes + - `/var/lib/grafana` + - `/etc/grafana` + - `/var/log/grafana` + +### Removal of implicit volumes + +Previously `/var/lib/grafana`, `/etc/grafana` and `/var/log/grafana` were defined as volumes in the `Dockerfile`. This led to the creation of three volumes each time a new instance of the Grafana container started, whether you wanted it or not. + +You should always be careful to define your own named volume for storage, but if you depended on these volumes you should be aware that an upgraded container will no longer have them. + +**Warning**: when migrating from an earlier version to 5.1 or later using docker compose and implicit volumes you need to use `docker inspect` to find out which volumes your container is mapped to so that you can map them to the upgraded container as well. You will also have to change file ownership (or user) as documented below. + +### User ID changes + +In 5.1 we switched the id of the grafana user. Unfortunately this means that files created prior to 5.1 won't have the correct permissions for later versions. We made this change so that it would be more likely that the grafana users id would be unique to Grafana. For example, on Ubuntu 16.04 `104` is already in use by the syslog user. Version | User | User ID --------|---------|--------- @@ -141,13 +162,13 @@ Version | User | User ID There are two possible solutions to this problem. Either you start the new container as the root user and change ownership from `104` to `472` or you start the upgraded container as user `104`. -### Running docker as a different user +#### Running docker as a different user ```bash docker run --user 104 --volume "" grafana/grafana:5.1.0 ``` -#### docker-compose.yml with custom user +##### Specifying a user in docker-compose.yml ```yaml version: "2" @@ -159,7 +180,7 @@ services: user: "104" ``` -### Modifying permissions +#### Modifying permissions The commands below will run bash inside the Grafana container with your volume mapped in. This makes it possible to modify the file ownership to match the new container. Always be careful when modifying permissions. From 7e2fb5e92e4b5dc66846904f6600c9c40e392031 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Fri, 27 Apr 2018 16:53:42 +0200 Subject: [PATCH 0553/3000] appveyor: uppercase the C drive in go path Fixes #11758 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 2b0bddde162..a71eb9f81b4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana environment: nodejs_version: "6" - GOPATH: c:\gopath + GOPATH: C:\gopath GOVERSION: 1.10 install: From b3531362cafedd3c93c0e5311b8a950db7b17820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 27 Apr 2018 21:22:29 +0200 Subject: [PATCH 0554/3000] fix: minor fix to plugin service shut down flow --- pkg/plugins/plugins.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 7ce0ac38919..8ccdb9cf18b 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -111,11 +111,14 @@ func (pm *PluginManager) Run(ctx context.Context) error { pm.checkForUpdates() ticker := time.NewTicker(time.Minute * 10) - for { + run := true + + for run { select { case <-ticker.C: pm.checkForUpdates() case <-ctx.Done(): + run = false break } } @@ -125,7 +128,7 @@ func (pm *PluginManager) Run(ctx context.Context) error { p.Kill() } - return nil + return ctx.Err() } func checkPluginPaths() error { From b7adf28501464c9fe5c933d70f0be0ad785846c5 Mon Sep 17 00:00:00 2001 From: Karsten Weiss Date: Fri, 27 Apr 2018 22:14:36 +0200 Subject: [PATCH 0555/3000] Remove redundancy in variable declarations (golint) This commit fixes the following golint warnings: pkg/api/avatar/avatar.go:229:12: should omit type *http.Client from declaration of var client; it will be inferred from the right-hand side pkg/login/brute_force_login_protection.go:13:26: should omit type time.Duration from declaration of var loginAttemptsWindow; it will be inferred from the right-hand side pkg/metrics/graphitebridge/graphite.go:58:26: should omit type []string from declaration of var metricCategoryPrefix; it will be inferred from the right-hand side pkg/metrics/graphitebridge/graphite.go:69:22: should omit type []string from declaration of var trimMetricPrefix; it will be inferred from the right-hand side pkg/models/alert.go:37:36: should omit type error from declaration of var ErrCannotChangeStateOnPausedAlert; it will be inferred from the right-hand side pkg/models/alert.go:38:36: should omit type error from declaration of var ErrRequiresNewState; it will be inferred from the right-hand side pkg/models/datasource.go:61:28: should omit type map[string]bool from declaration of var knownDatasourcePlugins; it will be inferred from the right-hand side pkg/plugins/update_checker.go:16:13: should omit type http.Client from declaration of var httpClient; it will be inferred from the right-hand side pkg/services/alerting/engine.go:103:24: should omit type time.Duration from declaration of var unfinishedWorkTimeout; it will be inferred from the right-hand side pkg/services/alerting/engine.go:105:19: should omit type time.Duration from declaration of var alertTimeout; it will be inferred from the right-hand side pkg/services/alerting/engine.go:106:19: should omit type int from declaration of var alertMaxAttempts; it will be inferred from the right-hand side pkg/services/alerting/notifier.go:143:23: should omit type map[string]*NotifierPlugin from declaration of var notifierFactories; it will be inferred from the right-hand side pkg/services/alerting/rule.go:136:24: should omit type map[string]ConditionFactory from declaration of var conditionFactories; it will be inferred from the right-hand side pkg/services/alerting/conditions/evaluator.go:12:15: should omit type []string from declaration of var defaultTypes; it will be inferred from the right-hand side pkg/services/alerting/conditions/evaluator.go:13:15: should omit type []string from declaration of var rangedTypes; it will be inferred from the right-hand side pkg/services/alerting/notifiers/opsgenie.go:44:19: should omit type string from declaration of var opsgenieAlertURL; it will be inferred from the right-hand side pkg/services/alerting/notifiers/pagerduty.go:43:23: should omit type string from declaration of var pagerdutyEventApiUrl; it will be inferred from the right-hand side pkg/services/alerting/notifiers/telegram.go:21:17: should omit type string from declaration of var telegramApiUrl; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/config_reader_test.go:11:24: should omit type string from declaration of var simpleDashboardConfig; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/config_reader_test.go:12:24: should omit type string from declaration of var oldVersion; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/config_reader_test.go:13:24: should omit type string from declaration of var brokenConfigs; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/file_reader.go:22:30: should omit type time.Duration from declaration of var checkDiskForChangesInterval; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/file_reader.go:24:23: should omit type error from declaration of var ErrFolderNameMissing; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:15:34: should omit type string from declaration of var twoDatasourcesConfig; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:16:34: should omit type string from declaration of var twoDatasourcesConfigPurgeOthers; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:17:34: should omit type string from declaration of var doubleDatasourcesConfig; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:18:34: should omit type string from declaration of var allProperties; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:19:34: should omit type string from declaration of var versionZero; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:20:34: should omit type string from declaration of var brokenYaml; it will be inferred from the right-hand side pkg/services/sqlstore/stats.go:16:25: should omit type time.Duration from declaration of var activeUserTimeLimit; it will be inferred from the right-hand side pkg/services/sqlstore/migrator/mysql_dialect.go:69:14: should omit type bool from declaration of var hasLen1; it will be inferred from the right-hand side pkg/services/sqlstore/migrator/mysql_dialect.go:70:14: should omit type bool from declaration of var hasLen2; it will be inferred from the right-hand side pkg/services/sqlstore/migrator/postgres_dialect.go:95:14: should omit type bool from declaration of var hasLen1; it will be inferred from the right-hand side pkg/services/sqlstore/migrator/postgres_dialect.go:96:14: should omit type bool from declaration of var hasLen2; it will be inferred from the right-hand side pkg/setting/setting.go:42:15: should omit type string from declaration of var Env; it will be inferred from the right-hand side pkg/setting/setting.go:161:18: should omit type bool from declaration of var LdapAllowSignup; it will be inferred from the right-hand side pkg/setting/setting.go:473:30: should omit type bool from declaration of var skipStaticRootValidation; it will be inferred from the right-hand side pkg/tsdb/interval.go:14:21: should omit type time.Duration from declaration of var defaultMinInterval; it will be inferred from the right-hand side pkg/tsdb/interval.go:15:21: should omit type time.Duration from declaration of var year; it will be inferred from the right-hand side pkg/tsdb/interval.go:16:21: should omit type time.Duration from declaration of var day; it will be inferred from the right-hand side pkg/tsdb/cloudwatch/credentials.go:26:24: should omit type map[string]cache from declaration of var awsCredentialCache; it will be inferred from the right-hand side pkg/tsdb/influxdb/query.go:15:27: should omit type *regexp.Regexp from declaration of var regexpOperatorPattern; it will be inferred from the right-hand side pkg/tsdb/influxdb/query.go:16:27: should omit type *regexp.Regexp from declaration of var regexpMeasurementPattern; it will be inferred from the right-hand side pkg/tsdb/mssql/mssql_test.go:25:14: should omit type string from declaration of var serverIP; it will be inferred from the right-hand side --- pkg/api/avatar/avatar.go | 2 +- pkg/login/brute_force_login_protection.go | 4 ++-- pkg/metrics/graphitebridge/graphite.go | 4 ++-- pkg/models/alert.go | 4 ++-- pkg/models/datasource.go | 2 +- pkg/plugins/update_checker.go | 2 +- pkg/services/alerting/conditions/evaluator.go | 4 ++-- pkg/services/alerting/engine.go | 6 +++--- pkg/services/alerting/notifier.go | 2 +- pkg/services/alerting/notifiers/opsgenie.go | 2 +- pkg/services/alerting/notifiers/pagerduty.go | 2 +- pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/alerting/rule.go | 2 +- .../provisioning/dashboards/config_reader_test.go | 6 +++--- .../provisioning/dashboards/file_reader.go | 4 ++-- .../datasources/config_reader_test.go | 15 ++++++++------- pkg/services/sqlstore/migrator/mysql_dialect.go | 4 ++-- .../sqlstore/migrator/postgres_dialect.go | 4 ++-- pkg/services/sqlstore/stats.go | 2 +- pkg/setting/setting.go | 6 +++--- pkg/tsdb/cloudwatch/credentials.go | 2 +- pkg/tsdb/influxdb/query.go | 4 ++-- pkg/tsdb/interval.go | 8 ++++---- pkg/tsdb/mssql/mssql_test.go | 2 +- 24 files changed, 48 insertions(+), 47 deletions(-) diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 9f282794076..5becf90ca35 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -226,7 +226,7 @@ func (this *thunderTask) Fetch() { this.Done() } -var client *http.Client = &http.Client{ +var client = &http.Client{ Timeout: time.Second * 2, Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, } diff --git a/pkg/login/brute_force_login_protection.go b/pkg/login/brute_force_login_protection.go index ca5e0a667ff..d524c420540 100644 --- a/pkg/login/brute_force_login_protection.go +++ b/pkg/login/brute_force_login_protection.go @@ -9,8 +9,8 @@ import ( ) var ( - maxInvalidLoginAttempts int64 = 5 - loginAttemptsWindow time.Duration = time.Minute * 5 + maxInvalidLoginAttempts int64 = 5 + loginAttemptsWindow = time.Minute * 5 ) var validateLoginAttempts = func(username string) error { diff --git a/pkg/metrics/graphitebridge/graphite.go b/pkg/metrics/graphitebridge/graphite.go index 670636cedce..5b61f078e6c 100644 --- a/pkg/metrics/graphitebridge/graphite.go +++ b/pkg/metrics/graphitebridge/graphite.go @@ -55,7 +55,7 @@ const ( AbortOnError ) -var metricCategoryPrefix []string = []string{ +var metricCategoryPrefix = []string{ "proxy_", "api_", "page_", @@ -66,7 +66,7 @@ var metricCategoryPrefix []string = []string{ "go_", "process_"} -var trimMetricPrefix []string = []string{"grafana_"} +var trimMetricPrefix = []string{"grafana_"} // Config defines the Graphite bridge config. type Config struct { diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 88b49350b97..b72d87e94b2 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -34,8 +34,8 @@ const ( ) var ( - ErrCannotChangeStateOnPausedAlert error = fmt.Errorf("Cannot change state on pause alert") - ErrRequiresNewState error = fmt.Errorf("update alert state requires a new state.") + ErrCannotChangeStateOnPausedAlert = fmt.Errorf("Cannot change state on pause alert") + ErrRequiresNewState = fmt.Errorf("update alert state requires a new state.") ) func (s AlertStateType) IsValid() bool { diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index f2236ad8477..b7e3e3eaa17 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -58,7 +58,7 @@ type DataSource struct { Updated time.Time } -var knownDatasourcePlugins map[string]bool = map[string]bool{ +var knownDatasourcePlugins = map[string]bool{ DS_ES: true, DS_GRAPHITE: true, DS_INFLUXDB: true, diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go index 57f6d2ca651..e61f4cf1df7 100644 --- a/pkg/plugins/update_checker.go +++ b/pkg/plugins/update_checker.go @@ -13,7 +13,7 @@ import ( ) var ( - httpClient http.Client = http.Client{Timeout: 10 * time.Second} + httpClient = http.Client{Timeout: 10 * time.Second} ) type GrafanaNetPlugin struct { diff --git a/pkg/services/alerting/conditions/evaluator.go b/pkg/services/alerting/conditions/evaluator.go index dfc058940cf..8d7ca57f010 100644 --- a/pkg/services/alerting/conditions/evaluator.go +++ b/pkg/services/alerting/conditions/evaluator.go @@ -9,8 +9,8 @@ import ( ) var ( - defaultTypes []string = []string{"gt", "lt"} - rangedTypes []string = []string{"within_range", "outside_range"} + defaultTypes = []string{"gt", "lt"} + rangedTypes = []string{"within_range", "outside_range"} ) type AlertEvaluator interface { diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index bdd8ff2cfe2..ddc91c0eb10 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -100,10 +100,10 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error { } var ( - unfinishedWorkTimeout time.Duration = time.Second * 5 + unfinishedWorkTimeout = time.Second * 5 // TODO: Make alertTimeout and alertMaxAttempts configurable in the config file. - alertTimeout time.Duration = time.Second * 30 - alertMaxAttempts int = 3 + alertTimeout = time.Second * 30 + alertMaxAttempts = 3 ) func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index af9ba52a52a..a30ca18b41d 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -140,7 +140,7 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not type NotifierFactory func(notification *m.AlertNotification) (Notifier, error) -var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin) +var notifierFactories = make(map[string]*NotifierPlugin) func RegisterNotifier(plugin *NotifierPlugin) { notifierFactories[plugin.Type] = plugin diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 5d8b15160c4..f0f5142cf05 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -41,7 +41,7 @@ func init() { } var ( - opsgenieAlertURL string = "https://api.opsgenie.com/v2/alerts" + opsgenieAlertURL = "https://api.opsgenie.com/v2/alerts" ) func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) { diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 58484051432..02219b2203d 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -40,7 +40,7 @@ func init() { } var ( - pagerdutyEventApiUrl string = "https://events.pagerduty.com/v2/enqueue" + pagerdutyEventApiUrl = "https://events.pagerduty.com/v2/enqueue" ) func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) { diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 1e62c68d7eb..1b259298eae 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -18,7 +18,7 @@ const ( ) var ( - telegramApiUrl string = "https://api.telegram.org/bot%s/%s" + telegramApiUrl = "https://api.telegram.org/bot%s/%s" ) func init() { diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 027ff96d6c0..0326b25de32 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -133,7 +133,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { type ConditionFactory func(model *simplejson.Json, index int) (Condition, error) -var conditionFactories map[string]ConditionFactory = make(map[string]ConditionFactory) +var conditionFactories = make(map[string]ConditionFactory) func RegisterCondition(typeName string, factory ConditionFactory) { conditionFactories[typeName] = factory diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index ecbf6435c36..72664c37990 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -8,9 +8,9 @@ import ( ) var ( - simpleDashboardConfig string = "./test-configs/dashboards-from-disk" - oldVersion string = "./test-configs/version-0" - brokenConfigs string = "./test-configs/broken-configs" + simpleDashboardConfig = "./test-configs/dashboards-from-disk" + oldVersion = "./test-configs/version-0" + brokenConfigs = "./test-configs/broken-configs" ) func TestDashboardsAsConfig(t *testing.T) { diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 7d4231deeae..e5186e12f06 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -19,9 +19,9 @@ import ( ) var ( - checkDiskForChangesInterval time.Duration = time.Second * 3 + checkDiskForChangesInterval = time.Second * 3 - ErrFolderNameMissing error = errors.New("Folder name missing") + ErrFolderNameMissing = errors.New("Folder name missing") ) type fileReader struct { diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go index 7d621ffe70f..89ecc5a0b68 100644 --- a/pkg/services/provisioning/datasources/config_reader_test.go +++ b/pkg/services/provisioning/datasources/config_reader_test.go @@ -11,13 +11,14 @@ import ( ) var ( - logger log.Logger = log.New("fake.log") - twoDatasourcesConfig string = "./test-configs/two-datasources" - twoDatasourcesConfigPurgeOthers string = "./test-configs/insert-two-delete-two" - doubleDatasourcesConfig string = "./test-configs/double-default" - allProperties string = "./test-configs/all-properties" - versionZero string = "./test-configs/version-0" - brokenYaml string = "./test-configs/broken-yaml" + logger log.Logger = log.New("fake.log") + + twoDatasourcesConfig = "./test-configs/two-datasources" + twoDatasourcesConfigPurgeOthers = "./test-configs/insert-two-delete-two" + doubleDatasourcesConfig = "./test-configs/double-default" + allProperties = "./test-configs/all-properties" + versionZero = "./test-configs/version-0" + brokenYaml = "./test-configs/broken-yaml" fakeRepo *fakeRepository ) diff --git a/pkg/services/sqlstore/migrator/mysql_dialect.go b/pkg/services/sqlstore/migrator/mysql_dialect.go index 1968558dbb8..300224135f0 100644 --- a/pkg/services/sqlstore/migrator/mysql_dialect.go +++ b/pkg/services/sqlstore/migrator/mysql_dialect.go @@ -66,8 +66,8 @@ func (db *Mysql) SqlType(c *Column) string { res = c.Type } - var hasLen1 bool = (c.Length > 0) - var hasLen2 bool = (c.Length2 > 0) + var hasLen1 = (c.Length > 0) + var hasLen2 = (c.Length2 > 0) if res == DB_BigInt && !hasLen1 && !hasLen2 { c.Length = 20 diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index 8de26194411..e2da562c14e 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -92,8 +92,8 @@ func (db *Postgres) SqlType(c *Column) string { res = t } - var hasLen1 bool = (c.Length > 0) - var hasLen2 bool = (c.Length2 > 0) + var hasLen1 = (c.Length > 0) + var hasLen2 = (c.Length2 > 0) if hasLen2 { res += "(" + strconv.Itoa(c.Length) + "," + strconv.Itoa(c.Length2) + ")" } else if hasLen1 { diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 47020d1a6f7..173a1e56634 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -13,7 +13,7 @@ func init() { bus.AddHandler("sql", GetAdminStats) } -var activeUserTimeLimit time.Duration = time.Hour * 24 * 30 +var activeUserTimeLimit = time.Hour * 24 * 30 func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error { var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type` diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 3aeb9eddaf0..922eea607d1 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -39,7 +39,7 @@ const ( var ( // App settings. - Env string = DEV + Env = DEV AppUrl string AppSubUrl string InstanceName string @@ -158,7 +158,7 @@ var ( // LDAP LdapEnabled bool LdapConfigFile string - LdapAllowSignup bool = true + LdapAllowSignup = true // SMTP email settings Smtp SmtpSettings @@ -470,7 +470,7 @@ func setHomePath(args *CommandLineArgs) { } } -var skipStaticRootValidation bool = false +var skipStaticRootValidation = false func validateStaticRootPath() error { if skipStaticRootValidation { diff --git a/pkg/tsdb/cloudwatch/credentials.go b/pkg/tsdb/cloudwatch/credentials.go index 06848323fbb..8b32c76daa3 100644 --- a/pkg/tsdb/cloudwatch/credentials.go +++ b/pkg/tsdb/cloudwatch/credentials.go @@ -23,7 +23,7 @@ type cache struct { expiration *time.Time } -var awsCredentialCache map[string]cache = make(map[string]cache) +var awsCredentialCache = make(map[string]cache) var credentialCacheLock sync.RWMutex func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 9fbe133c055..0637a5bbb44 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -12,8 +12,8 @@ import ( ) var ( - regexpOperatorPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) - regexpMeasurementPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) + regexpOperatorPattern = regexp.MustCompile(`^\/.*\/$`) + regexpMeasurementPattern = regexp.MustCompile(`^\/.*\/$`) ) func (query *Query) Build(queryContext *tsdb.TsdbQuery) (string, error) { diff --git a/pkg/tsdb/interval.go b/pkg/tsdb/interval.go index e26d39f3986..49904f27a37 100644 --- a/pkg/tsdb/interval.go +++ b/pkg/tsdb/interval.go @@ -10,10 +10,10 @@ import ( ) var ( - defaultRes int64 = 1500 - defaultMinInterval time.Duration = 1 * time.Millisecond - year time.Duration = time.Hour * 24 * 365 - day time.Duration = time.Hour * 24 + defaultRes int64 = 1500 + defaultMinInterval = time.Millisecond * 1 + year = time.Hour * 24 * 365 + day = time.Hour * 24 ) type Interval struct { diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 167d02a1e07..e62d30a6325 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -22,7 +22,7 @@ import ( // There is also a dashboard.json in same directory that you can import to Grafana // once you've created a datasource for the test server/database. // If needed, change the variable below to the IP address of the database. -var serverIP string = "localhost" +var serverIP = "localhost" func TestMSSQL(t *testing.T) { SkipConvey("MSSQL", t, func() { From de8696d5d3f1864a495b0246972bd4adc9ba8ea6 Mon Sep 17 00:00:00 2001 From: Karsten Weiss Date: Fri, 27 Apr 2018 22:42:49 +0200 Subject: [PATCH 0556/3000] Outdent code after if block that ends with return (golint) This commit fixes the following golint warnings: pkg/bus/bus.go:64:9: if block ends with a return statement, so drop this else and outdent its block pkg/bus/bus.go:84:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:137:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:177:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:183:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:199:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:208:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/components/dynmap/dynmap.go:236:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:242:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:257:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:263:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:278:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:284:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:299:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:331:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:350:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:356:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:366:12: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:390:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:396:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:405:12: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:427:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:433:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:442:12: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:459:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:465:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:474:12: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:491:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:497:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:506:12: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:523:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:529:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:538:12: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:555:9: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:561:10: if block ends with a return statement, so drop this else and outdent its block pkg/components/dynmap/dynmap.go:570:12: if block ends with a return statement, so drop this else and outdent its block pkg/login/ldap.go:55:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/login/ldap_test.go:372:10: if block ends with a return statement, so drop this else and outdent its block pkg/middleware/middleware_test.go:213:12: if block ends with a return statement, so drop this else and outdent its block pkg/plugins/dashboard_importer.go:153:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/plugins/dashboards_updater.go:39:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/plugins/dashboards_updater.go:121:10: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/plugins/plugins.go:210:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/plugins/plugins.go:235:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/alerting/eval_context.go:111:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/alerting/notifier.go:92:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/alerting/notifier.go:98:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/alerting/notifier.go:122:10: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/alerting/rule.go:108:10: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/alerting/rule.go:118:10: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/alerting/rule.go:121:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/alerting/notifiers/telegram.go:94:10: if block ends with a return statement, so drop this else and outdent its block pkg/services/sqlstore/annotation.go:34:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/sqlstore/annotation.go:99:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/sqlstore/dashboard_test.go:107:13: if block ends with a return statement, so drop this else and outdent its block pkg/services/sqlstore/plugin_setting.go:78:10: if block ends with a return statement, so drop this else and outdent its block pkg/services/sqlstore/preferences.go:91:10: if block ends with a return statement, so drop this else and outdent its block pkg/services/sqlstore/user.go:50:10: if block ends with a return statement, so drop this else and outdent its block pkg/services/sqlstore/migrator/migrator.go:106:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/services/sqlstore/migrator/postgres_dialect.go:48:10: if block ends with a return statement, so drop this else and outdent its block pkg/tsdb/time_range.go:59:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/tsdb/time_range.go:67:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) pkg/tsdb/cloudwatch/metric_find_query.go:225:9: if block ends with a return statement, so drop this else and outdent its block pkg/util/filepath.go:68:11: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary) --- pkg/bus/bus.go | 6 +- pkg/components/dynmap/dynmap.go | 286 +++++++----------- pkg/login/ldap.go | 10 +- pkg/login/ldap_test.go | 5 +- pkg/middleware/middleware_test.go | 3 +- pkg/plugins/dashboard_importer.go | 6 +- pkg/plugins/dashboards_updater.go | 44 +-- pkg/plugins/plugins.go | 12 +- pkg/services/alerting/eval_context.go | 6 +- pkg/services/alerting/notifier.go | 22 +- pkg/services/alerting/notifiers/telegram.go | 3 +- pkg/services/alerting/rule.go | 20 +- pkg/services/sqlstore/annotation.go | 28 +- pkg/services/sqlstore/dashboard_test.go | 3 +- pkg/services/sqlstore/migrator/migrator.go | 10 +- .../sqlstore/migrator/postgres_dialect.go | 3 +- pkg/services/sqlstore/plugin_setting.go | 53 ++-- pkg/services/sqlstore/preferences.go | 15 +- pkg/services/sqlstore/user.go | 5 +- pkg/tsdb/cloudwatch/metric_find_query.go | 3 +- pkg/tsdb/time_range.go | 12 +- pkg/util/filepath.go | 3 +- 22 files changed, 238 insertions(+), 320 deletions(-) diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index 437796991a5..32a591b6672 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -61,9 +61,8 @@ func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error { err := ret[0].Interface() if err == nil { return nil - } else { - return err.(error) } + return err.(error) } func (b *InProcBus) Dispatch(msg Msg) error { @@ -81,9 +80,8 @@ func (b *InProcBus) Dispatch(msg Msg) error { err := ret[0].Interface() if err == nil { return nil - } else { - return err.(error) } + return err.(error) } func (b *InProcBus) Publish(msg Msg) error { diff --git a/pkg/components/dynmap/dynmap.go b/pkg/components/dynmap/dynmap.go index 2b86ce384eb..96effb24332 100644 --- a/pkg/components/dynmap/dynmap.go +++ b/pkg/components/dynmap/dynmap.go @@ -134,9 +134,8 @@ func (v *Value) get(key string) (*Value, error) { child, ok := obj.Map()[key] if ok { return child, nil - } else { - return nil, KeyNotFoundError{key} } + return nil, KeyNotFoundError{key} } return nil, err @@ -174,17 +173,13 @@ func (v *Object) GetObject(keys ...string) (*Object, error) { if err != nil { return nil, err - } else { - - obj, err := child.Object() - - if err != nil { - return nil, err - } else { - return obj, nil - } - } + obj, err := child.Object() + + if err != nil { + return nil, err + } + return obj, nil } // Gets the value at key path and attempts to typecast the value into a string. @@ -196,18 +191,17 @@ func (v *Object) GetString(keys ...string) (string, error) { if err != nil { return "", err - } else { - return child.String() } + return child.String() } func (v *Object) MustGetString(path string, def string) string { keys := strings.Split(path, ".") - if str, err := v.GetString(keys...); err != nil { + str, err := v.GetString(keys...) + if err != nil { return def - } else { - return str } + return str } // Gets the value at key path and attempts to typecast the value into null. @@ -233,16 +227,13 @@ func (v *Object) GetNumber(keys ...string) (json.Number, error) { if err != nil { return "", err - } else { - - n, err := child.Number() - - if err != nil { - return "", err - } else { - return n, nil - } } + n, err := child.Number() + + if err != nil { + return "", err + } + return n, nil } // Gets the value at key path and attempts to typecast the value into a float64. @@ -254,16 +245,13 @@ func (v *Object) GetFloat64(keys ...string) (float64, error) { if err != nil { return 0, err - } else { - - n, err := child.Float64() - - if err != nil { - return 0, err - } else { - return n, nil - } } + n, err := child.Float64() + + if err != nil { + return 0, err + } + return n, nil } // Gets the value at key path and attempts to typecast the value into a float64. @@ -275,16 +263,13 @@ func (v *Object) GetInt64(keys ...string) (int64, error) { if err != nil { return 0, err - } else { - - n, err := child.Int64() - - if err != nil { - return 0, err - } else { - return n, nil - } } + n, err := child.Int64() + + if err != nil { + return 0, err + } + return n, nil } // Gets the value at key path and attempts to typecast the value into a float64. @@ -296,9 +281,8 @@ func (v *Object) GetInterface(keys ...string) (interface{}, error) { if err != nil { return nil, err - } else { - return child.Interface(), nil } + return child.Interface(), nil } // Gets the value at key path and attempts to typecast the value into a bool. @@ -311,7 +295,6 @@ func (v *Object) GetBoolean(keys ...string) (bool, error) { if err != nil { return false, err } - return child.Boolean() } @@ -328,11 +311,8 @@ func (v *Object) GetValueArray(keys ...string) ([]*Value, error) { if err != nil { return nil, err - } else { - - return child.Array() - } + return child.Array() } // Gets the value at key path and attempts to typecast the value into an array of objects. @@ -347,30 +327,24 @@ func (v *Object) GetObjectArray(keys ...string) ([]*Object, error) { if err != nil { return nil, err - } else { + } + array, err := child.Array() - array, err := child.Array() + if err != nil { + return nil, err + } + typedArray := make([]*Object, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem. + Object() if err != nil { return nil, err - } else { - - typedArray := make([]*Object, len(array)) - - for index, arrayItem := range array { - typedArrayItem, err := arrayItem. - Object() - - if err != nil { - return nil, err - } else { - typedArray[index] = typedArrayItem - } - - } - return typedArray, nil } + typedArray[index] = typedArrayItem } + return typedArray, nil } // Gets the value at key path and attempts to typecast the value into an array of string. @@ -387,29 +361,23 @@ func (v *Object) GetStringArray(keys ...string) ([]string, error) { if err != nil { return nil, err - } else { + } + array, err := child.Array() - array, err := child.Array() + if err != nil { + return nil, err + } + typedArray := make([]string, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.String() if err != nil { return nil, err - } else { - - typedArray := make([]string, len(array)) - - for index, arrayItem := range array { - typedArrayItem, err := arrayItem.String() - - if err != nil { - return nil, err - } else { - typedArray[index] = typedArrayItem - } - - } - return typedArray, nil } + typedArray[index] = typedArrayItem } + return typedArray, nil } // Gets the value at key path and attempts to typecast the value into an array of numbers. @@ -424,29 +392,23 @@ func (v *Object) GetNumberArray(keys ...string) ([]json.Number, error) { if err != nil { return nil, err - } else { + } + array, err := child.Array() - array, err := child.Array() + if err != nil { + return nil, err + } + typedArray := make([]json.Number, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Number() if err != nil { return nil, err - } else { - - typedArray := make([]json.Number, len(array)) - - for index, arrayItem := range array { - typedArrayItem, err := arrayItem.Number() - - if err != nil { - return nil, err - } else { - typedArray[index] = typedArrayItem - } - - } - return typedArray, nil } + typedArray[index] = typedArrayItem } + return typedArray, nil } // Gets the value at key path and attempts to typecast the value into an array of floats. @@ -456,29 +418,23 @@ func (v *Object) GetFloat64Array(keys ...string) ([]float64, error) { if err != nil { return nil, err - } else { + } + array, err := child.Array() - array, err := child.Array() + if err != nil { + return nil, err + } + typedArray := make([]float64, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Float64() if err != nil { return nil, err - } else { - - typedArray := make([]float64, len(array)) - - for index, arrayItem := range array { - typedArrayItem, err := arrayItem.Float64() - - if err != nil { - return nil, err - } else { - typedArray[index] = typedArrayItem - } - - } - return typedArray, nil } + typedArray[index] = typedArrayItem } + return typedArray, nil } // Gets the value at key path and attempts to typecast the value into an array of ints. @@ -488,29 +444,23 @@ func (v *Object) GetInt64Array(keys ...string) ([]int64, error) { if err != nil { return nil, err - } else { + } + array, err := child.Array() - array, err := child.Array() + if err != nil { + return nil, err + } + typedArray := make([]int64, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Int64() if err != nil { return nil, err - } else { - - typedArray := make([]int64, len(array)) - - for index, arrayItem := range array { - typedArrayItem, err := arrayItem.Int64() - - if err != nil { - return nil, err - } else { - typedArray[index] = typedArrayItem - } - - } - return typedArray, nil } + typedArray[index] = typedArrayItem } + return typedArray, nil } // Gets the value at key path and attempts to typecast the value into an array of bools. @@ -520,29 +470,23 @@ func (v *Object) GetBooleanArray(keys ...string) ([]bool, error) { if err != nil { return nil, err - } else { + } + array, err := child.Array() - array, err := child.Array() + if err != nil { + return nil, err + } + typedArray := make([]bool, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Boolean() if err != nil { return nil, err - } else { - - typedArray := make([]bool, len(array)) - - for index, arrayItem := range array { - typedArrayItem, err := arrayItem.Boolean() - - if err != nil { - return nil, err - } else { - typedArray[index] = typedArrayItem - } - - } - return typedArray, nil } + typedArray[index] = typedArrayItem } + return typedArray, nil } // Gets the value at key path and attempts to typecast the value into an array of nulls. @@ -552,29 +496,23 @@ func (v *Object) GetNullArray(keys ...string) (int64, error) { if err != nil { return 0, err - } else { + } + array, err := child.Array() - array, err := child.Array() + if err != nil { + return 0, err + } + var length int64 = 0 + + for _, arrayItem := range array { + err := arrayItem.Null() if err != nil { return 0, err - } else { - - var length int64 = 0 - - for _, arrayItem := range array { - err := arrayItem.Null() - - if err != nil { - return 0, err - } else { - length++ - } - - } - return length, nil } + length++ } + return length, nil } // Returns an error if the value is not actually null @@ -590,9 +528,7 @@ func (v *Value) Null() error { if valid { return nil } - return ErrNotNull - } // Attempts to typecast the current value into an array. @@ -612,17 +548,13 @@ func (v *Value) Array() ([]*Value, error) { var slice []*Value if valid { - for _, element := range v.data.([]interface{}) { child := Value{element, true} slice = append(slice, &child) } - return slice, nil } - return slice, ErrNotArray - } // Attempts to typecast the current value into a number. diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index de530c0cf63..49b92648561 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -50,12 +50,12 @@ func (a *ldapAuther) Dial() error { if a.server.RootCACert != "" { certPool = x509.NewCertPool() for _, caCertFile := range strings.Split(a.server.RootCACert, " ") { - if pem, err := ioutil.ReadFile(caCertFile); err != nil { + pem, err := ioutil.ReadFile(caCertFile) + if err != nil { return err - } else { - if !certPool.AppendCertsFromPEM(pem) { - return errors.New("Failed to append CA certificate " + caCertFile) - } + } + if !certPool.AppendCertsFromPEM(pem) { + return errors.New("Failed to append CA certificate " + caCertFile) } } } diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index 6085fffb638..b8ef261c815 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -369,10 +369,9 @@ func (sc *scenarioContext) userQueryReturns(user *m.User) { bus.AddHandler("test", func(query *m.GetUserByAuthInfoQuery) error { if user == nil { return m.ErrUserNotFound - } else { - query.Result = user - return nil } + query.Result = user + return nil }) bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error { return nil diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 072cb793d3c..b827751b1a5 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -210,9 +210,8 @@ func TestMiddlewareContext(t *testing.T) { if query.UserId > 0 { query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} return nil - } else { - return m.ErrUserNotFound } + return m.ErrUserNotFound }) bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index fb4d63a1fe4..1364fded987 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -148,11 +148,11 @@ func (this *DashTemplateEvaluator) evalValue(source *simplejson.Json) interface{ switch v := sourceValue.(type) { case string: interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string { - if replacement, exists := this.variables[match]; exists { + replacement, exists := this.variables[match] + if exists { return replacement - } else { - return match } + return match }) return interpolated case bool: diff --git a/pkg/plugins/dashboards_updater.go b/pkg/plugins/dashboards_updater.go index 04d3dc035cc..ebe11ed32d4 100644 --- a/pkg/plugins/dashboards_updater.go +++ b/pkg/plugins/dashboards_updater.go @@ -34,23 +34,24 @@ func (pm *PluginManager) updateAppDashboards() { } func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64) error { - if dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path); err != nil { + dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path) + if err != nil { return err - } else { - plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision) - updateCmd := ImportDashboardCommand{ - OrgId: orgId, - PluginId: pluginDashInfo.PluginId, - Overwrite: true, - Dashboard: dash.Data, - User: &m.SignedInUser{UserId: 0, OrgRole: m.ROLE_ADMIN}, - Path: pluginDashInfo.Path, - } - - if err := bus.Dispatch(&updateCmd); err != nil { - return err - } } + plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision) + updateCmd := ImportDashboardCommand{ + OrgId: orgId, + PluginId: pluginDashInfo.PluginId, + Overwrite: true, + Dashboard: dash.Data, + User: &m.SignedInUser{UserId: 0, OrgRole: m.ROLE_ADMIN}, + Path: pluginDashInfo.Path, + } + + if err := bus.Dispatch(&updateCmd); err != nil { + return err + } + return nil } @@ -118,15 +119,14 @@ func handlePluginStateChanged(event *m.PluginStateChangedEvent) error { if err := bus.Dispatch(&query); err != nil { return err - } else { - for _, dash := range query.Result { - deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id} + } + for _, dash := range query.Result { + deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id} - plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug) + plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug) - if err := bus.Dispatch(&deleteCmd); err != nil { - return err - } + if err := bus.Dispatch(&deleteCmd); err != nil { + return err } } } diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 8ccdb9cf18b..aa4131ae06d 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -205,11 +205,11 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { } var loader PluginLoader - if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists { + pluginGoType, exists := PluginTypes[pluginCommon.Type] + if !exists { return errors.New("Unknown plugin type " + pluginCommon.Type) - } else { - loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader) } + loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader) reader.Seek(0, 0) return loader.Load(jsonParser, currentDir) @@ -230,9 +230,9 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) { return make([]byte, 0), nil } - if data, err := ioutil.ReadFile(path); err != nil { + data, err := ioutil.ReadFile(path) + if err != nil { return nil, err - } else { - return data, nil } + return data, nil } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 91d0e179a14..d0441d379b7 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -106,11 +106,11 @@ func (c *EvalContext) GetRuleUrl() (string, error) { return setting.AppUrl, nil } - if ref, err := c.GetDashboardUID(); err != nil { + ref, err := c.GetDashboardUID() + if err != nil { return "", err - } else { - return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil } + return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil } func (c *EvalContext) GetNewState() m.AlertStateType { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index a30ca18b41d..1d5affbd3ec 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -87,17 +87,17 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { IsAlertContext: true, } - if ref, err := context.GetDashboardUID(); err != nil { + ref, err := context.GetDashboardUID() + if err != nil { return err - } else { - renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId) } + renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId) - if imagePath, err := renderer.RenderToPng(renderOpts); err != nil { + imagePath, err := renderer.RenderToPng(renderOpts) + if err != nil { return err - } else { - context.ImageOnDiskPath = imagePath } + context.ImageOnDiskPath = imagePath context.ImagePublicUrl, err = uploader.Upload(context.Ctx, context.ImageOnDiskPath) if err != nil { @@ -117,12 +117,12 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] var result []Notifier for _, notification := range query.Result { - if not, err := n.createNotifierFor(notification); err != nil { + not, err := n.createNotifierFor(notification) + if err != nil { return nil, err - } else { - if not.ShouldNotify(context) { - result = append(result, not) - } + } + if not.ShouldNotify(context) { + result = append(result, not) } } diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 1b259298eae..ca24c996914 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -91,9 +91,8 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se cmd, err := this.buildMessageInlineImage(evalContext) if err == nil { return cmd - } else { - this.log.Error("Could not generate Telegram message with inline image.", "err", err) } + this.log.Error("Could not generate Telegram message with inline image.", "err", err) } return this.buildMessageLinkedImage(evalContext) diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 0326b25de32..018d138dbe4 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -103,25 +103,25 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) - if id, err := jsonModel.Get("id").Int64(); err != nil { + id, err := jsonModel.Get("id").Int64() + if err != nil { return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} - } else { - model.Notifications = append(model.Notifications, id) } + model.Notifications = append(model.Notifications, id) } for index, condition := range ruleDef.Settings.Get("conditions").MustArray() { conditionModel := simplejson.NewFromAny(condition) conditionType := conditionModel.Get("type").MustString() - if factory, exist := conditionFactories[conditionType]; !exist { + factory, exist := conditionFactories[conditionType] + if !exist { return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} - } else { - if queryCondition, err := factory(conditionModel, index); err != nil { - return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} - } else { - model.Conditions = append(model.Conditions, queryCondition) - } } + queryCondition, err := factory(conditionModel, index) + if err != nil { + return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} + } + model.Conditions = append(model.Conditions, queryCondition) } if len(model.Conditions) == 0 { diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 1066be0ef74..1710679cea1 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -29,13 +29,13 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error { } if item.Tags != nil { - if tags, err := r.ensureTagsExist(sess, tags); err != nil { + tags, err := r.ensureTagsExist(sess, tags) + if 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 - } + } + 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 } } } @@ -94,17 +94,17 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error { } if item.Tags != nil { - if tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)); err != nil { + tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)) + if err != nil { return err - } else { - if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil { + } + 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 } - 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 - } - } } } diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 9124a686236..6d7c7a93e47 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -104,9 +104,8 @@ func TestDashboardDataAccess(t *testing.T) { timesCalled += 1 if timesCalled <= 2 { return savedDash.Uid - } else { - return util.GenerateShortUid() } + return util.GenerateShortUid() } cmd := m.SaveDashboardCommand{ OrgId: 1, diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 0fde3f27c01..cd00cb16712 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -97,17 +97,15 @@ func (mg *Migrator) Start() error { mg.Logger.Debug("Executing", "sql", sql) err := mg.inTransaction(func(sess *xorm.Session) error { - - if err := mg.exec(m, sess); err != nil { + err := mg.exec(m, sess) + if err != nil { mg.Logger.Error("Exec failed", "error", err, "sql", sql) record.Error = err.Error() sess.Insert(&record) return err - } else { - record.Success = true - sess.Insert(&record) } - + record.Success = true + sess.Insert(&record) return nil }) diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index e2da562c14e..e167aa33122 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -45,9 +45,8 @@ func (b *Postgres) Default(col *Column) string { if col.Type == DB_Bool { if col.Default == "0" { return "FALSE" - } else { - return "TRUE" } + return "TRUE" } return col.Default } diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index f694fbbd5f0..676d26fad56 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -75,34 +75,33 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error { _, err = sess.Insert(&pluginSetting) return err - } else { - for key, data := range cmd.SecureJsonData { - encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey) - if err != nil { - return err - } - - pluginSetting.SecureJsonData[key] = encryptedData - } - - // add state change event on commit success - if pluginSetting.Enabled != cmd.Enabled { - sess.events = append(sess.events, &m.PluginStateChangedEvent{ - PluginId: cmd.PluginId, - OrgId: cmd.OrgId, - Enabled: cmd.Enabled, - }) - } - - pluginSetting.Updated = time.Now() - pluginSetting.Enabled = cmd.Enabled - pluginSetting.JsonData = cmd.JsonData - pluginSetting.Pinned = cmd.Pinned - pluginSetting.PluginVersion = cmd.PluginVersion - - _, err = sess.Id(pluginSetting.Id).Update(&pluginSetting) - return err } + for key, data := range cmd.SecureJsonData { + encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey) + if err != nil { + return err + } + + pluginSetting.SecureJsonData[key] = encryptedData + } + + // add state change event on commit success + if pluginSetting.Enabled != cmd.Enabled { + sess.events = append(sess.events, &m.PluginStateChangedEvent{ + PluginId: cmd.PluginId, + OrgId: cmd.OrgId, + Enabled: cmd.Enabled, + }) + } + + pluginSetting.Updated = time.Now() + pluginSetting.Enabled = cmd.Enabled + pluginSetting.JsonData = cmd.JsonData + pluginSetting.Pinned = cmd.Pinned + pluginSetting.PluginVersion = cmd.PluginVersion + + _, err = sess.Id(pluginSetting.Id).Update(&pluginSetting) + return err }) } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index a070fa621b5..885837764fc 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -88,14 +88,13 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { } _, err = sess.Insert(&prefs) return err - } else { - prefs.HomeDashboardId = cmd.HomeDashboardId - prefs.Timezone = cmd.Timezone - prefs.Theme = cmd.Theme - prefs.Updated = time.Now() - prefs.Version += 1 - _, err := sess.Id(prefs.Id).AllCols().Update(&prefs) - return err } + prefs.HomeDashboardId = cmd.HomeDashboardId + prefs.Timezone = cmd.Timezone + prefs.Theme = cmd.Theme + prefs.Updated = time.Now() + prefs.Version += 1 + _, err = sess.Id(prefs.Id).AllCols().Update(&prefs) + return err }) } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 5e2efbd7fde..f19019d28a4 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -47,10 +47,9 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error } if has { return org.Id, nil - } else { - org.Name = "Main Org." - org.Id = 1 } + org.Name = "Main Org." + org.Id = 1 } else { org.Name = cmd.OrgName if len(org.Name) == 0 { diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index d73516ca88f..a7d33645b9b 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -222,9 +222,8 @@ func parseMultiSelectValue(input string) []string { trimValues[i] = strings.TrimSpace(v) } return trimValues - } else { - return []string{trimmedInput} } + return []string{trimmedInput} } // Whenever this list is updated, frontend list should also be updated. diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go index 777fd15907e..18e389e5993 100644 --- a/pkg/tsdb/time_range.go +++ b/pkg/tsdb/time_range.go @@ -54,19 +54,19 @@ func (tr *TimeRange) GetToAsTimeUTC() time.Time { } func (tr *TimeRange) MustGetFrom() time.Time { - if res, err := tr.ParseFrom(); err != nil { + res, err := tr.ParseFrom() + if err != nil { return time.Unix(0, 0) - } else { - return res } + return res } func (tr *TimeRange) MustGetTo() time.Time { - if res, err := tr.ParseTo(); err != nil { + res, err := tr.ParseTo() + if err != nil { return time.Unix(0, 0) - } else { - return res } + return res } func tryParseUnixMsEpoch(val string) (time.Time, bool) { diff --git a/pkg/util/filepath.go b/pkg/util/filepath.go index 3ad8cac3147..d304236fcb1 100644 --- a/pkg/util/filepath.go +++ b/pkg/util/filepath.go @@ -65,9 +65,8 @@ func walk(path string, info os.FileInfo, resolvedPath string, symlinkPathsFollow if _, ok := symlinkPathsFollowed[path2]; ok { errMsg := "Potential SymLink Infinite Loop. Path: %v, Link To: %v" return fmt.Errorf(errMsg, resolvedPath, path2) - } else { - symlinkPathsFollowed[path2] = true } + symlinkPathsFollowed[path2] = true } info2, err := os.Lstat(path2) if err != nil { From 893a91af3aab546e911f4c5aa57bb81df0df5405 Mon Sep 17 00:00:00 2001 From: Karsten Weiss Date: Sat, 28 Apr 2018 10:45:45 +0200 Subject: [PATCH 0557/3000] Use opportunities to unindent code (unindent) This commit fixes the following unindent findings: pkg/api/common.go:102:2: "if x { if y" should be "if x && y" pkg/components/dynmap/dynmap.go:642:2: invert condition and early return pkg/components/dynmap/dynmap.go:681:2: invert condition and early return pkg/components/simplejson/simplejson.go:171:2: "if x { if y" should be "if x && y" pkg/middleware/dashboard_redirect.go:42:3: invert condition and early return pkg/tsdb/mssql/mssql.go:301:3: invert condition and early break pkg/tsdb/mysql/mysql.go:312:3: invert condition and early break pkg/tsdb/postgres/postgres.go:292:3: invert condition and early break pkg/tsdb/sql_engine.go:144:2: invert condition and early return --- pkg/api/common.go | 6 +- pkg/components/dynmap/dynmap.go | 56 +++++++--------- pkg/components/simplejson/simplejson.go | 6 +- pkg/middleware/dashboard_redirect.go | 31 +++++---- pkg/tsdb/mssql/mssql.go | 23 ++++--- pkg/tsdb/mysql/mysql.go | 23 ++++--- pkg/tsdb/postgres/postgres.go | 23 ++++--- pkg/tsdb/sql_engine.go | 89 +++++++++++++------------ 8 files changed, 125 insertions(+), 132 deletions(-) diff --git a/pkg/api/common.go b/pkg/api/common.go index 97f41ff7c72..cd64c57dc92 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -99,10 +99,8 @@ func Error(status int, message string, err error) *NormalResponse { data["message"] = message } - if err != nil { - if setting.Env != setting.PROD { - data["error"] = err.Error() - } + if err != nil && setting.Env != setting.PROD { + data["error"] = err.Error() } resp := JSON(status, data) diff --git a/pkg/components/dynmap/dynmap.go b/pkg/components/dynmap/dynmap.go index 96effb24332..6d3546f3bc5 100644 --- a/pkg/components/dynmap/dynmap.go +++ b/pkg/components/dynmap/dynmap.go @@ -639,26 +639,24 @@ func (v *Value) Object() (*Object, error) { valid = true } + if !valid { + return nil, ErrNotObject + } + obj := new(Object) + obj.valid = valid + + m := make(map[string]*Value) + if valid { - obj := new(Object) - obj.valid = valid - - m := make(map[string]*Value) - - if valid { - for key, element := range v.data.(map[string]interface{}) { - m[key] = &Value{element, true} - - } + for key, element := range v.data.(map[string]interface{}) { + m[key] = &Value{element, true} } - - obj.data = v.data - obj.m = m - - return obj, nil } - return nil, ErrNotObject + obj.data = v.data + obj.m = m + + return obj, nil } // Attempts to typecast the current value into an object arrau. @@ -678,23 +676,19 @@ func (v *Value) ObjectArray() ([]*Object, error) { // Unsure if this is a good way to use slices, it's probably not var slice []*Object - if valid { - - for _, element := range v.data.([]interface{}) { - childValue := Value{element, true} - childObject, err := childValue.Object() - - if err != nil { - return nil, ErrNotObjectArray - } - slice = append(slice, childObject) - } - - return slice, nil + if !valid { + return nil, ErrNotObjectArray } + for _, element := range v.data.([]interface{}) { + childValue := Value{element, true} + childObject, err := childValue.Object() - return nil, ErrNotObjectArray - + if err != nil { + return nil, ErrNotObjectArray + } + slice = append(slice, childObject) + } + return slice, nil } // Attempts to typecast the current value into a string. diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go index 85e2f955943..15293b0cd93 100644 --- a/pkg/components/simplejson/simplejson.go +++ b/pkg/components/simplejson/simplejson.go @@ -168,10 +168,8 @@ func (j *Json) GetPath(branch ...string) *Json { // js.Get("top_level").Get("array").GetIndex(1).Get("key").Int() func (j *Json) GetIndex(index int) *Json { a, err := j.Array() - if err == nil { - if len(a) > index { - return &Json{a[index]} - } + if err == nil && len(a) > index { + return &Json{a[index]} } return &Json{nil} } diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 2edf04d543e..1111929c2f6 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -24,12 +24,12 @@ func RedirectFromLegacyDashboardURL() macaron.Handler { return func(c *m.ReqContext) { slug := c.Params("slug") - if slug != "" { - if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { - url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) - c.Redirect(url, 301) - return - } + if slug == "" { + return + } + if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { + url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) + c.Redirect(url, 301) } } } @@ -39,17 +39,16 @@ func RedirectFromLegacyDashboardSoloURL() macaron.Handler { slug := c.Params("slug") renderRequest := c.QueryBool("render") - if slug != "" { - if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { - if renderRequest && strings.Contains(url, setting.AppSubUrl) { - url = strings.Replace(url, setting.AppSubUrl, "", 1) - } - - url = strings.Replace(url, "/d/", "/d-solo/", 1) - url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) - c.Redirect(url, 301) - return + if slug == "" { + return + } + if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { + if renderRequest && strings.Contains(url, setting.AppSubUrl) { + url = strings.Replace(url, setting.AppSubUrl, "", 1) } + url = strings.Replace(url, "/d/", "/d-solo/", 1) + url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) + c.Redirect(url, 301) } } } diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index eb71259b46b..221670f1bdb 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -298,18 +298,19 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. key := elem.Value.(string) result.Series = append(result.Series, pointsBySeries[key]) - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + if !fillMissing { + break + } + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ } } diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 7eceaffdb09..57986eb7c04 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -309,18 +309,19 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. key := elem.Value.(string) result.Series = append(result.Series, pointsBySeries[key]) - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + if !fillMissing { + break + } + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ } } diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index fdf09216e51..f66c09b5724 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -289,18 +289,19 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co key := elem.Value.(string) result.Series = append(result.Series, pointsBySeries[key]) - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + if !fillMissing { + break + } + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ } } diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 274e5b05dc1..ecf46ac689d 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -141,50 +141,51 @@ func (e *DefaultSqlEngine) Query( // ConvertSqlTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds // to make native datetime types and epoch dates work in annotation and table queries. func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) { - if timeIndex >= 0 { - switch value := values[timeIndex].(type) { - case time.Time: - values[timeIndex] = EpochPrecisionToMs(float64(value.UnixNano())) - case *time.Time: - if value != nil { - values[timeIndex] = EpochPrecisionToMs(float64((*value).UnixNano())) - } - case int64: - values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) - case *int64: - if value != nil { - values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) - } - case uint64: - values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) - case *uint64: - if value != nil { - values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) - } - case int32: - values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) - case *int32: - if value != nil { - values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) - } - case uint32: - values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) - case *uint32: - if value != nil { - values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) - } - case float64: - values[timeIndex] = EpochPrecisionToMs(value) - case *float64: - if value != nil { - values[timeIndex] = EpochPrecisionToMs(*value) - } - case float32: - values[timeIndex] = EpochPrecisionToMs(float64(value)) - case *float32: - if value != nil { - values[timeIndex] = EpochPrecisionToMs(float64(*value)) - } + if timeIndex < 0 { + return + } + switch value := values[timeIndex].(type) { + case time.Time: + values[timeIndex] = EpochPrecisionToMs(float64(value.UnixNano())) + case *time.Time: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(float64((*value).UnixNano())) + } + case int64: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *int64: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case uint64: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *uint64: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case int32: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *int32: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case uint32: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *uint32: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case float64: + values[timeIndex] = EpochPrecisionToMs(value) + case *float64: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(*value) + } + case float32: + values[timeIndex] = EpochPrecisionToMs(float64(value)) + case *float32: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(float64(*value)) } } } From 4f7791b9fa15c2e736b244c6c12b49e472158872 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Apr 2018 11:50:50 +0200 Subject: [PATCH 0558/3000] fix dropdown typeahead issue New explore feature overriding css for dropdown typeahead component. --- public/sass/pages/_explore.scss | 94 +++++++++++++++++---------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 74a19c1d2c2..855d11cb859 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -42,55 +42,57 @@ transition: all 0.3s; } -.typeahead { - position: absolute; - z-index: auto; - top: -10000px; - left: -10000px; - opacity: 0; - border-radius: 4px; - transition: opacity 0.75s; - border: 1px solid #e4e4e4; - max-height: calc(66vh); - overflow-y: scroll; - max-width: calc(66%); - overflow-x: hidden; - outline: none; - list-style: none; - background: #fff; - color: rgba(0, 0, 0, 0.65); - transition: opacity 0.4s ease-out; -} +.explore { + .typeahead { + position: absolute; + z-index: auto; + top: -10000px; + left: -10000px; + opacity: 0; + border-radius: 4px; + transition: opacity 0.75s; + border: 1px solid #e4e4e4; + max-height: calc(66vh); + overflow-y: scroll; + max-width: calc(66%); + overflow-x: hidden; + outline: none; + list-style: none; + background: #fff; + color: rgba(0, 0, 0, 0.65); + transition: opacity 0.4s ease-out; + } -.typeahead-group__title { - color: rgba(0, 0, 0, 0.43); - font-size: 12px; - line-height: 1.5; - padding: 8px 16px; -} + .typeahead-group__title { + color: rgba(0, 0, 0, 0.43); + font-size: 12px; + line-height: 1.5; + padding: 8px 16px; + } -.typeahead-item { - line-height: 200%; - height: auto; - font-family: Consolas, Menlo, Courier, monospace; - padding: 0 16px 0 28px; - font-size: 12px; - text-overflow: ellipsis; - overflow: hidden; - margin-left: -1px; - left: 1px; - position: relative; - z-index: 1; - display: block; - white-space: nowrap; - cursor: pointer; - transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), - background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1); -} + .typeahead-item { + line-height: 200%; + height: auto; + font-family: Consolas, Menlo, Courier, monospace; + padding: 0 16px 0 28px; + font-size: 12px; + text-overflow: ellipsis; + overflow: hidden; + margin-left: -1px; + left: 1px; + position: relative; + z-index: 1; + display: block; + white-space: nowrap; + cursor: pointer; + transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), + background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1); + } -.typeahead-item__selected { - background-color: #ecf6fd; - color: #108ee9; + .typeahead-item__selected { + background-color: #ecf6fd; + color: #108ee9; + } } /* SYNTAX */ From 3d9b7a5892f11920c3be744287f0a4b46cfd5464 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Apr 2018 14:41:52 +0200 Subject: [PATCH 0559/3000] increase length of auth_id column in user_auth table --- pkg/services/sqlstore/migrations/user_auth_mig.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/sqlstore/migrations/user_auth_mig.go b/pkg/services/sqlstore/migrations/user_auth_mig.go index 4d8a18ce33e..be4d112f6f6 100644 --- a/pkg/services/sqlstore/migrations/user_auth_mig.go +++ b/pkg/services/sqlstore/migrations/user_auth_mig.go @@ -21,4 +21,9 @@ func addUserAuthMigrations(mg *Migrator) { mg.AddMigration("create user auth table", NewAddTableMigration(userAuthV1)) // add indices addTableIndicesMigrations(mg, "v1", userAuthV1) + + mg.AddMigration("alter user_auth.auth_id to length 255", new(RawSqlMigration). + Sqlite("SELECT 0 WHERE 0;"). + Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(255);"). + Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(255);")) } From 770acee56a68d3aa48c17fec30bc6c220e693c18 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Apr 2018 15:34:31 +0200 Subject: [PATCH 0560/3000] new property for current user indicating if edit permissions in folders --- pkg/api/dtos/models.go | 31 +++++------ pkg/api/index.go | 36 +++++++------ pkg/models/folders.go | 9 ++++ pkg/services/sqlstore/dashboard.go | 25 +++++++++ .../sqlstore/dashboard_folder_test.go | 53 ++++++++++++++++++- 5 files changed, 123 insertions(+), 31 deletions(-) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 2348e217a41..aead67cd04c 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -22,21 +22,22 @@ type LoginCommand struct { } type CurrentUser struct { - IsSignedIn bool `json:"isSignedIn"` - Id int64 `json:"id"` - Login string `json:"login"` - Email string `json:"email"` - Name string `json:"name"` - LightTheme bool `json:"lightTheme"` - OrgCount int `json:"orgCount"` - OrgId int64 `json:"orgId"` - OrgName string `json:"orgName"` - OrgRole m.RoleType `json:"orgRole"` - IsGrafanaAdmin bool `json:"isGrafanaAdmin"` - GravatarUrl string `json:"gravatarUrl"` - Timezone string `json:"timezone"` - Locale string `json:"locale"` - HelpFlags1 m.HelpFlags1 `json:"helpFlags1"` + IsSignedIn bool `json:"isSignedIn"` + Id int64 `json:"id"` + Login string `json:"login"` + Email string `json:"email"` + Name string `json:"name"` + LightTheme bool `json:"lightTheme"` + OrgCount int `json:"orgCount"` + OrgId int64 `json:"orgId"` + OrgName string `json:"orgName"` + OrgRole m.RoleType `json:"orgRole"` + IsGrafanaAdmin bool `json:"isGrafanaAdmin"` + GravatarUrl string `json:"gravatarUrl"` + Timezone string `json:"timezone"` + Locale string `json:"locale"` + HelpFlags1 m.HelpFlags1 `json:"helpFlags1"` + HasEditPermissionInFolders bool `json:"hasEditPermissionInFolders"` } type MetricRequest struct { diff --git a/pkg/api/index.go b/pkg/api/index.go index ac68dba65b6..2a905b474ce 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -42,23 +42,29 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { settings["appSubUrl"] = "" } + hasEditPermissionInFoldersQuery := m.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} + if err := bus.Dispatch(&hasEditPermissionInFoldersQuery); err != nil { + return nil, err + } + var data = dtos.IndexViewData{ User: &dtos.CurrentUser{ - Id: c.UserId, - IsSignedIn: c.IsSignedIn, - Login: c.Login, - Email: c.Email, - Name: c.Name, - OrgCount: c.OrgCount, - OrgId: c.OrgId, - OrgName: c.OrgName, - OrgRole: c.OrgRole, - GravatarUrl: dtos.GetGravatarUrl(c.Email), - IsGrafanaAdmin: c.IsGrafanaAdmin, - LightTheme: prefs.Theme == "light", - Timezone: prefs.Timezone, - Locale: locale, - HelpFlags1: c.HelpFlags1, + Id: c.UserId, + IsSignedIn: c.IsSignedIn, + Login: c.Login, + Email: c.Email, + Name: c.Name, + OrgCount: c.OrgCount, + OrgId: c.OrgId, + OrgName: c.OrgName, + OrgRole: c.OrgRole, + GravatarUrl: dtos.GetGravatarUrl(c.Email), + IsGrafanaAdmin: c.IsGrafanaAdmin, + LightTheme: prefs.Theme == "light", + Timezone: prefs.Timezone, + Locale: locale, + HelpFlags1: c.HelpFlags1, + HasEditPermissionInFolders: hasEditPermissionInFoldersQuery.Result, }, Settings: settings, Theme: prefs.Theme, diff --git a/pkg/models/folders.go b/pkg/models/folders.go index 0c876edcfd7..f4dd7e5b776 100644 --- a/pkg/models/folders.go +++ b/pkg/models/folders.go @@ -89,3 +89,12 @@ type UpdateFolderCommand struct { Result *Folder } + +// +// QUERIES +// + +type HasEditPermissionInFoldersQuery struct { + SignedInUser *SignedInUser + Result bool +} diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 4238967417f..aff532bb3b5 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -24,6 +24,7 @@ func init() { bus.AddHandler("sql", GetDashboardPermissionsForUser) bus.AddHandler("sql", GetDashboardsBySlug) bus.AddHandler("sql", ValidateDashboardBeforeSave) + bus.AddHandler("sql", HasEditPermissionInFolders) } var generateNewUid func() string = util.GenerateShortUid @@ -614,3 +615,27 @@ func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err return nil }) } + +func HasEditPermissionInFolders(query *m.HasEditPermissionInFoldersQuery) error { + if query.SignedInUser.HasRole(m.ROLE_EDITOR) { + query.Result = true + return nil + } + + builder := &SqlBuilder{} + builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgId, dialect.BooleanStr(true)) + builder.writeDashboardPermissionFilter(query.SignedInUser, m.PERMISSION_EDIT) + + type folderCount struct { + Count int64 + } + + resp := make([]*folderCount, 0) + if err := x.Sql(builder.GetSqlString(), builder.params...).Find(&resp); err != nil { + return err + } + + query.Result = len(resp) > 0 && resp[0].Count > 0 + + return nil +} diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index 4c92c097931..cdd107c3e90 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -221,7 +221,6 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Given two dashboard folders", func() { - folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod") folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod") insertTestDashboard("folder in another org", 2, 0, true, "prod") @@ -264,6 +263,15 @@ func TestDashboardFolderDataAccess(t *testing.T) { So(query.Result[1].DashboardId, ShouldEqual, folder2.Id) So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_ADMIN) }) + + Convey("should have edit permission in folders", func() { + query := &m.HasEditPermissionInFoldersQuery{ + SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: m.ROLE_ADMIN}, + } + err := HasEditPermissionInFolders(query) + So(err, ShouldBeNil) + So(query.Result, ShouldBeTrue) + }) }) Convey("Editor users", func() { @@ -310,6 +318,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { So(query.Result[0].Id, ShouldEqual, folder2.Id) }) + Convey("should have edit permission in folders", func() { + query := &m.HasEditPermissionInFoldersQuery{ + SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgId: 1, OrgRole: m.ROLE_EDITOR}, + } + err := HasEditPermissionInFolders(query) + So(err, ShouldBeNil) + So(query.Result, ShouldBeTrue) + }) }) Convey("Viewer users", func() { @@ -353,6 +369,41 @@ func TestDashboardFolderDataAccess(t *testing.T) { So(len(query.Result), ShouldEqual, 1) So(query.Result[0].Id, ShouldEqual, folder1.Id) }) + + Convey("should not have edit permission in folders", func() { + query := &m.HasEditPermissionInFoldersQuery{ + SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, + } + err := HasEditPermissionInFolders(query) + So(err, ShouldBeNil) + So(query.Result, ShouldBeFalse) + }) + + Convey("and admin permission is given for user with org role viewer in one dashboard folder", func() { + testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_ADMIN}) + + Convey("should have edit permission in folders", func() { + query := &m.HasEditPermissionInFoldersQuery{ + SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, + } + err := HasEditPermissionInFolders(query) + So(err, ShouldBeNil) + So(query.Result, ShouldBeTrue) + }) + }) + + Convey("and edit permission is given for user with org role viewer in one dashboard folder", func() { + testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_EDIT}) + + Convey("should have edit permission in folders", func() { + query := &m.HasEditPermissionInFoldersQuery{ + SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER}, + } + err := HasEditPermissionInFolders(query) + So(err, ShouldBeNil) + So(query.Result, ShouldBeTrue) + }) + }) }) }) }) From 5c57c7cff56eb5f10c64312b73b9d47b54df571b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Apr 2018 15:38:46 +0200 Subject: [PATCH 0561/3000] dashboard: show save as button if can edit and has edit permission to folders --- public/app/core/services/context_srv.ts | 3 +++ public/app/features/dashboard/settings/settings.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 5a879895267..be8a0af7b7b 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -11,6 +11,7 @@ export class User { timezone: string; helpFlags1: number; lightTheme: boolean; + hasEditPermissionInFolders: boolean; constructor() { if (config.bootData.user) { @@ -28,6 +29,7 @@ export class ContextSrv { isEditor: any; sidemenu: any; sidemenuSmallBreakpoint = false; + hasEditPermissionInFolders: boolean; constructor() { this.sidemenu = store.getBool('grafana.sidemenu', true); @@ -44,6 +46,7 @@ export class ContextSrv { this.isSignedIn = this.user.isSignedIn; this.isGrafanaAdmin = this.user.isGrafanaAdmin; this.isEditor = this.hasRole('Editor') || this.hasRole('Admin'); + this.hasEditPermissionInFolders = this.user.hasEditPermissionInFolders; } hasRole(role) { diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index e9d5c6180be..68fd20a3b91 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -30,7 +30,7 @@ export class SettingsCtrl { }); }); - this.canSaveAs = contextSrv.isEditor; + this.canSaveAs = this.dashboard.meta.canEdit && contextSrv.hasEditPermissionInFolders; this.canSave = this.dashboard.meta.canSave; this.canDelete = this.dashboard.meta.canSave; From b16626c3b5974aaed997fd28ce7708844bf785bc Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 12 Apr 2018 16:31:47 +0300 Subject: [PATCH 0562/3000] graph histogram: fix invisible highest value bucket --- public/app/plugins/panel/graph/graph.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 07ce0fed49f..2de53b6dce0 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -443,7 +443,8 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { // Expand ticks for pretty view min = Math.floor(min / tickStep) * tickStep; - max = Math.ceil(max / tickStep) * tickStep; + // 1.01 is 101% - ensure we have enough space for last bar + max = Math.ceil(max * 1.01 / tickStep) * tickStep; ticks = []; for (let i = min; i <= max; i += tickStep) { From fc718b8a9a1229248a360c29c5b310b6fc5448ff Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 30 Apr 2018 16:17:37 +0200 Subject: [PATCH 0563/3000] table: fix for padding The table-panel-wrapper class got removed when clicking on the panel menu which resulted in extra padding for the .panel-content div. This fixes that by setting the table-specific css class lower down in the html. --- public/app/plugins/panel/table/module.ts | 4 ++-- public/sass/components/_panel_table.scss | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 27eab205f09..51caed86c25 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -218,13 +218,13 @@ class TablePanelCtrl extends MetricsPanelCtrl { } function renderPanel() { - var panelElem = elem.parents('.panel'); + var panelElem = elem.parents('.panel-content'); var rootElem = elem.find('.table-panel-scroll'); var tbodyElem = elem.find('tbody'); var footerElem = elem.find('.table-panel-footer'); elem.css({ 'font-size': panel.fontSize }); - panelElem.addClass('table-panel-wrapper'); + panelElem.addClass('table-panel-content'); appendTableRows(tbodyElem); appendPaginationControls(footerElem); diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index f120fcc8b35..8e0ecf15896 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -1,7 +1,6 @@ -.table-panel-wrapper { - .panel-content { - padding: 0; - } +.table-panel-content { + padding: 0; + .panel-title-container { padding-bottom: 4px; } From fa7d7ed5df2030a84bb8cf3eb221cf248c92c618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 30 Apr 2018 16:21:04 +0200 Subject: [PATCH 0564/3000] Initial Baby Step to refactoring settings from global vars to instance (#11777) * wip: start on refactoring settings * settings: progress on settings refactor * refactor: progress on settings refactoring * fix: fixed failing test * settings: moved smtp settings from global to instance --- pkg/api/admin.go | 2 +- pkg/api/http_server.go | 5 +- pkg/cmd/grafana-cli/commands/commands.go | 3 +- pkg/cmd/grafana-server/server.go | 17 +- .../imguploader/azureblobuploader_test.go | 3 +- .../imguploader/gcsuploader_test.go | 3 +- pkg/components/imguploader/imguploader.go | 11 +- .../imguploader/imguploader_test.go | 27 +-- pkg/components/imguploader/s3uploader_test.go | 3 +- pkg/metrics/settings.go | 2 +- pkg/plugins/dashboard_importer_test.go | 4 +- pkg/plugins/dashboards_test.go | 4 +- pkg/plugins/plugins.go | 2 +- pkg/plugins/plugins_test.go | 6 +- pkg/services/cleanup/cleanup.go | 51 +++--- pkg/services/notifications/mailer.go | 27 +-- pkg/services/notifications/notifications.go | 18 +- .../notifications/notifications_test.go | 9 +- .../send_email_integration_test.go | 9 +- pkg/services/sqlstore/sqlstore.go | 4 +- pkg/setting/setting.go | 163 ++++++++++-------- pkg/setting/setting_quota.go | 4 +- pkg/setting/setting_smtp.go | 30 ++-- pkg/setting/setting_test.go | 49 ++++-- pkg/social/social.go | 2 +- pkg/tracing/tracing.go | 2 +- pkg/tsdb/influxdb/response_parser_test.go | 3 +- pkg/tsdb/interval_test.go | 3 +- 28 files changed, 263 insertions(+), 203 deletions(-) diff --git a/pkg/api/admin.go b/pkg/api/admin.go index 52d271ce69b..54a86724f0c 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -12,7 +12,7 @@ import ( func AdminGetSettings(c *m.ReqContext) { settings := make(map[string]interface{}) - for _, section := range setting.Cfg.Sections() { + for _, section := range setting.Raw.Sections() { jsonSec := make(map[string]interface{}) settings[section.Name()] = jsonSec diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 8d1d0dc0a60..fa27eabbf24 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -35,9 +35,10 @@ type HTTPServer struct { context context.Context streamManager *live.StreamManager cache *gocache.Cache - RouteRegister RouteRegister `inject:""` + httpSrv *http.Server - httpSrv *http.Server + RouteRegister RouteRegister `inject:""` + Bus bus.Bus `inject:""` } func (hs *HTTPServer) Init() { diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index d8f01bbdcab..43484749670 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -15,7 +15,8 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli return func(context *cli.Context) { cmd := &contextCommandLine{context} - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ Config: cmd.String("config"), HomePath: cmd.String("homepath"), Args: flag.Args(), diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 86cbe51dd22..911cf092b47 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -49,6 +49,7 @@ func NewGrafanaServer() *GrafanaServerImpl { shutdownFn: shutdownFn, childRoutines: childRoutines, log: log.New("server"), + cfg: setting.NewCfg(), } } @@ -57,28 +58,29 @@ type GrafanaServerImpl struct { shutdownFn context.CancelFunc childRoutines *errgroup.Group log log.Logger + cfg *setting.Cfg RouteRegister api.RouteRegister `inject:""` HttpServer *api.HTTPServer `inject:""` } func (g *GrafanaServerImpl) Start() error { - g.initLogging() + g.loadConfiguration() g.writePIDFile() // initSql sqlstore.NewEngine() // TODO: this should return an error sqlstore.EnsureAdminUser() - metrics.Init(setting.Cfg) + metrics.Init(g.cfg.Raw) login.Init() social.NewOAuthService() - if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil { + if err := provisioning.Init(g.context, setting.HomePath, g.cfg.Raw); err != nil { return fmt.Errorf("Failed to provision Grafana from config. error: %v", err) } - tracingCloser, err := tracing.Init(setting.Cfg) + tracingCloser, err := tracing.Init(g.cfg.Raw) if err != nil { return fmt.Errorf("Tracing settings is not valid. error: %v", err) } @@ -86,6 +88,7 @@ func (g *GrafanaServerImpl) Start() error { serviceGraph := inject.Graph{} serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) + serviceGraph.Provide(&inject.Object{Value: g.cfg}) serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()}) serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) serviceGraph.Provide(&inject.Object{Value: api.HTTPServer{}}) @@ -138,8 +141,8 @@ func (g *GrafanaServerImpl) Start() error { return g.startHttpServer() } -func (g *GrafanaServerImpl) initLogging() { - err := setting.NewConfigContext(&setting.CommandLineArgs{ +func (g *GrafanaServerImpl) loadConfiguration() { + err := g.cfg.Load(&setting.CommandLineArgs{ Config: *configFile, HomePath: *homePath, Args: flag.Args(), @@ -151,7 +154,7 @@ func (g *GrafanaServerImpl) initLogging() { } g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0)) - setting.LogConfigurationInfo() + g.cfg.LogConfigSources() } func (g *GrafanaServerImpl) startHttpServer() error { diff --git a/pkg/components/imguploader/azureblobuploader_test.go b/pkg/components/imguploader/azureblobuploader_test.go index ca978f70e3d..c0c7889a155 100644 --- a/pkg/components/imguploader/azureblobuploader_test.go +++ b/pkg/components/imguploader/azureblobuploader_test.go @@ -10,7 +10,8 @@ import ( func TestUploadToAzureBlob(t *testing.T) { SkipConvey("[Integration test] for external_image_store.azure_blob", t, func() { - err := setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + err := cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) So(err, ShouldBeNil) diff --git a/pkg/components/imguploader/gcsuploader_test.go b/pkg/components/imguploader/gcsuploader_test.go index bdc21084dbf..58cb21c184c 100644 --- a/pkg/components/imguploader/gcsuploader_test.go +++ b/pkg/components/imguploader/gcsuploader_test.go @@ -10,7 +10,8 @@ import ( func TestUploadToGCS(t *testing.T) { SkipConvey("[Integration test] for external_image_store.gcs", t, func() { - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index 52a31f9f606..93f69cadd46 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -3,9 +3,10 @@ package imguploader import ( "context" "fmt" - "github.com/grafana/grafana/pkg/log" "regexp" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" ) @@ -24,7 +25,7 @@ func NewImageUploader() (ImageUploader, error) { switch setting.ImageUploadProvider { case "s3": - s3sec, err := setting.Cfg.GetSection("external_image_storage.s3") + s3sec, err := setting.Raw.GetSection("external_image_storage.s3") if err != nil { return nil, err } @@ -51,7 +52,7 @@ func NewImageUploader() (ImageUploader, error) { return NewS3Uploader(region, bucket, path, "public-read", accessKey, secretKey), nil case "webdav": - webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav") + webdavSec, err := setting.Raw.GetSection("external_image_storage.webdav") if err != nil { return nil, err } @@ -67,7 +68,7 @@ func NewImageUploader() (ImageUploader, error) { return NewWebdavImageUploader(url, username, password, public_url) case "gcs": - gcssec, err := setting.Cfg.GetSection("external_image_storage.gcs") + gcssec, err := setting.Raw.GetSection("external_image_storage.gcs") if err != nil { return nil, err } @@ -78,7 +79,7 @@ func NewImageUploader() (ImageUploader, error) { return NewGCSUploader(keyFile, bucketName, path), nil case "azure_blob": - azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob") + azureBlobSec, err := setting.Raw.GetSection("external_image_storage.azure_blob") if err != nil { return nil, err } diff --git a/pkg/components/imguploader/imguploader_test.go b/pkg/components/imguploader/imguploader_test.go index b272a45e7a5..570e36a47e3 100644 --- a/pkg/components/imguploader/imguploader_test.go +++ b/pkg/components/imguploader/imguploader_test.go @@ -11,14 +11,15 @@ import ( func TestImageUploaderFactory(t *testing.T) { Convey("Can create image uploader for ", t, func() { Convey("S3ImageUploader config", func() { - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) setting.ImageUploadProvider = "s3" Convey("with bucket url https://foo.bar.baz.s3-us-east-2.amazonaws.com", func() { - s3sec, err := setting.Cfg.GetSection("external_image_storage.s3") + s3sec, err := setting.Raw.GetSection("external_image_storage.s3") So(err, ShouldBeNil) s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com") s3sec.NewKey("access_key", "access_key") @@ -37,7 +38,7 @@ func TestImageUploaderFactory(t *testing.T) { }) Convey("with bucket url https://s3.amazonaws.com/mybucket", func() { - s3sec, err := setting.Cfg.GetSection("external_image_storage.s3") + s3sec, err := setting.Raw.GetSection("external_image_storage.s3") So(err, ShouldBeNil) s3sec.NewKey("bucket_url", "https://s3.amazonaws.com/my.bucket.com") s3sec.NewKey("access_key", "access_key") @@ -56,7 +57,7 @@ func TestImageUploaderFactory(t *testing.T) { }) Convey("with bucket url https://s3-us-west-2.amazonaws.com/mybucket", func() { - s3sec, err := setting.Cfg.GetSection("external_image_storage.s3") + s3sec, err := setting.Raw.GetSection("external_image_storage.s3") So(err, ShouldBeNil) s3sec.NewKey("bucket_url", "https://s3-us-west-2.amazonaws.com/my.bucket.com") s3sec.NewKey("access_key", "access_key") @@ -77,13 +78,14 @@ func TestImageUploaderFactory(t *testing.T) { Convey("Webdav uploader", func() { var err error - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) setting.ImageUploadProvider = "webdav" - webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav") + webdavSec, err := cfg.Raw.GetSection("external_image_storage.webdav") So(err, ShouldBeNil) webdavSec.NewKey("url", "webdavUrl") webdavSec.NewKey("username", "username") @@ -103,13 +105,14 @@ func TestImageUploaderFactory(t *testing.T) { Convey("GCS uploader", func() { var err error - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) setting.ImageUploadProvider = "gcs" - gcpSec, err := setting.Cfg.GetSection("external_image_storage.gcs") + gcpSec, err := cfg.Raw.GetSection("external_image_storage.gcs") So(err, ShouldBeNil) gcpSec.NewKey("key_file", "/etc/secrets/project-79a52befa3f6.json") gcpSec.NewKey("bucket", "project-grafana-east") @@ -124,13 +127,14 @@ func TestImageUploaderFactory(t *testing.T) { }) Convey("AzureBlobUploader config", func() { - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) setting.ImageUploadProvider = "azure_blob" Convey("with container name", func() { - azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob") + azureBlobSec, err := cfg.Raw.GetSection("external_image_storage.azure_blob") So(err, ShouldBeNil) azureBlobSec.NewKey("account_name", "account_name") azureBlobSec.NewKey("account_key", "account_key") @@ -150,7 +154,8 @@ func TestImageUploaderFactory(t *testing.T) { Convey("Local uploader", func() { var err error - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) diff --git a/pkg/components/imguploader/s3uploader_test.go b/pkg/components/imguploader/s3uploader_test.go index b02d4676b5e..0e43740ef9b 100644 --- a/pkg/components/imguploader/s3uploader_test.go +++ b/pkg/components/imguploader/s3uploader_test.go @@ -10,7 +10,8 @@ import ( func TestUploadToS3(t *testing.T) { SkipConvey("[Integration test] for external_image_store.s3", t, func() { - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go index 5e51f85768a..c21e7279b7e 100644 --- a/pkg/metrics/settings.go +++ b/pkg/metrics/settings.go @@ -46,7 +46,7 @@ func ReadSettings(file *ini.File) *MetricSettings { } func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphitebridge.Config, error) { - graphiteSection, err := setting.Cfg.GetSection("metrics.graphite") + graphiteSection, err := setting.Raw.GetSection("metrics.graphite") if err != nil { return nil, nil } diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index d8460a1875c..6f31b49f99d 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -87,8 +87,8 @@ func TestDashboardImport(t *testing.T) { func pluginScenario(desc string, t *testing.T, fn func()) { Convey("Given a plugin", t, func() { - setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.test-app") + setting.Raw = ini.Empty() + sec, _ := setting.Raw.NewSection("plugin.test-app") sec.NewKey("path", "../../tests/test-app") pm := &PluginManager{} diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go index 241e41d7bb2..c422a1431c0 100644 --- a/pkg/plugins/dashboards_test.go +++ b/pkg/plugins/dashboards_test.go @@ -14,8 +14,8 @@ import ( func TestPluginDashboards(t *testing.T) { Convey("When asking plugin dashboard info", t, func() { - setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.test-app") + setting.Raw = ini.Empty() + sec, _ := setting.Raw.NewSection("plugin.test-app") sec.NewKey("path", "../../tests/test-app") pm := &PluginManager{} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index aa4131ae06d..5096bf5cebc 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -132,7 +132,7 @@ func (pm *PluginManager) Run(ctx context.Context) error { } func checkPluginPaths() error { - for _, section := range setting.Cfg.Sections() { + for _, section := range setting.Raw.Sections() { if strings.HasPrefix(section.Name(), "plugin.") { path := section.Key("path").String() if path != "" { diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 7566d054b7f..fa68ae4389d 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -13,7 +13,7 @@ func TestPluginScans(t *testing.T) { Convey("When scanning for plugins", t, func() { setting.StaticRootPath, _ = filepath.Abs("../../public/") - setting.Cfg = ini.Empty() + setting.Raw = ini.Empty() pm := &PluginManager{} err := pm.Init() @@ -28,8 +28,8 @@ func TestPluginScans(t *testing.T) { }) Convey("When reading app plugin definition", t, func() { - setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.nginx-app") + setting.Raw = ini.Empty() + sec, _ := setting.Raw.NewSection("plugin.nginx-app") sec.NewKey("path", "../../tests/test-app") pm := &PluginManager{} diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index ef474fd2eb2..69bc7695dea 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -16,42 +16,43 @@ import ( type CleanUpService struct { log log.Logger + Cfg *setting.Cfg `inject:""` } func init() { registry.RegisterService(&CleanUpService{}) } -func (service *CleanUpService) Init() error { - service.log = log.New("cleanup") +func (srv *CleanUpService) Init() error { + srv.log = log.New("cleanup") return nil } -func (service *CleanUpService) Run(ctx context.Context) error { - service.cleanUpTmpFiles() +func (srv *CleanUpService) Run(ctx context.Context) error { + srv.cleanUpTmpFiles() ticker := time.NewTicker(time.Minute * 10) for { select { case <-ticker.C: - service.cleanUpTmpFiles() - service.deleteExpiredSnapshots() - service.deleteExpiredDashboardVersions() - service.deleteOldLoginAttempts() + srv.cleanUpTmpFiles() + srv.deleteExpiredSnapshots() + srv.deleteExpiredDashboardVersions() + srv.deleteOldLoginAttempts() case <-ctx.Done(): return ctx.Err() } } } -func (service *CleanUpService) cleanUpTmpFiles() { - if _, err := os.Stat(setting.ImagesDir); os.IsNotExist(err) { +func (srv *CleanUpService) cleanUpTmpFiles() { + if _, err := os.Stat(srv.Cfg.ImagesDir); os.IsNotExist(err) { return } - files, err := ioutil.ReadDir(setting.ImagesDir) + files, err := ioutil.ReadDir(srv.Cfg.ImagesDir) if err != nil { - service.log.Error("Problem reading image dir", "error", err) + srv.log.Error("Problem reading image dir", "error", err) return } @@ -63,36 +64,36 @@ func (service *CleanUpService) cleanUpTmpFiles() { } for _, file := range toDelete { - fullPath := path.Join(setting.ImagesDir, file.Name()) + fullPath := path.Join(srv.Cfg.ImagesDir, file.Name()) err := os.Remove(fullPath) if err != nil { - service.log.Error("Failed to delete temp file", "file", file.Name(), "error", err) + srv.log.Error("Failed to delete temp file", "file", file.Name(), "error", err) } } - service.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files)) + srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files)) } -func (service *CleanUpService) deleteExpiredSnapshots() { +func (srv *CleanUpService) deleteExpiredSnapshots() { cmd := m.DeleteExpiredSnapshotsCommand{} if err := bus.Dispatch(&cmd); err != nil { - service.log.Error("Failed to delete expired snapshots", "error", err.Error()) + srv.log.Error("Failed to delete expired snapshots", "error", err.Error()) } else { - service.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows) + srv.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows) } } -func (service *CleanUpService) deleteExpiredDashboardVersions() { +func (srv *CleanUpService) deleteExpiredDashboardVersions() { cmd := m.DeleteExpiredVersionsCommand{} if err := bus.Dispatch(&cmd); err != nil { - service.log.Error("Failed to delete expired dashboard versions", "error", err.Error()) + srv.log.Error("Failed to delete expired dashboard versions", "error", err.Error()) } else { - service.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows) + srv.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows) } } -func (service *CleanUpService) deleteOldLoginAttempts() { - if setting.DisableBruteForceLoginProtection { +func (srv *CleanUpService) deleteOldLoginAttempts() { + if srv.Cfg.DisableBruteForceLoginProtection { return } @@ -100,8 +101,8 @@ func (service *CleanUpService) deleteOldLoginAttempts() { OlderThan: time.Now().Add(time.Minute * -10), } if err := bus.Dispatch(&cmd); err != nil { - service.log.Error("Problem deleting expired login attempts", "error", err.Error()) + srv.log.Error("Problem deleting expired login attempts", "error", err.Error()) } else { - service.log.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows) + srv.log.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows) } } diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 37169661d73..4730ef7f0f1 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -17,8 +17,8 @@ import ( gomail "gopkg.in/mail.v2" ) -func send(msg *Message) (int, error) { - dialer, err := createDialer() +func (ns *NotificationService) send(msg *Message) (int, error) { + dialer, err := ns.createDialer() if err != nil { return 0, err } @@ -42,8 +42,8 @@ func send(msg *Message) (int, error) { return len(msg.To), nil } -func createDialer() (*gomail.Dialer, error) { - host, port, err := net.SplitHostPort(setting.Smtp.Host) +func (ns *NotificationService) createDialer() (*gomail.Dialer, error) { + host, port, err := net.SplitHostPort(ns.Cfg.Smtp.Host) if err != nil { return nil, err @@ -54,30 +54,31 @@ func createDialer() (*gomail.Dialer, error) { } tlsconfig := &tls.Config{ - InsecureSkipVerify: setting.Smtp.SkipVerify, + InsecureSkipVerify: ns.Cfg.Smtp.SkipVerify, ServerName: host, } - if setting.Smtp.CertFile != "" { - cert, err := tls.LoadX509KeyPair(setting.Smtp.CertFile, setting.Smtp.KeyFile) + if ns.Cfg.Smtp.CertFile != "" { + cert, err := tls.LoadX509KeyPair(ns.Cfg.Smtp.CertFile, ns.Cfg.Smtp.KeyFile) if err != nil { return nil, fmt.Errorf("Could not load cert or key file. error: %v", err) } tlsconfig.Certificates = []tls.Certificate{cert} } - d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password) + d := gomail.NewDialer(host, iPort, ns.Cfg.Smtp.User, ns.Cfg.Smtp.Password) d.TLSConfig = tlsconfig - if setting.Smtp.EhloIdentity != "" { - d.LocalName = setting.Smtp.EhloIdentity + + if ns.Cfg.Smtp.EhloIdentity != "" { + d.LocalName = ns.Cfg.Smtp.EhloIdentity } else { d.LocalName = setting.InstanceName } return d, nil } -func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) { - if !setting.Smtp.Enabled { +func (ns *NotificationService) buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) { + if !ns.Cfg.Smtp.Enabled { return nil, m.ErrSmtpNotEnabled } @@ -121,7 +122,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) { return &Message{ To: cmd.To, - From: fmt.Sprintf("%s <%s>", setting.Smtp.FromName, setting.Smtp.FromAddress), + From: fmt.Sprintf("%s <%s>", ns.Cfg.Smtp.FromName, ns.Cfg.Smtp.FromAddress), Subject: subject, Body: buffer.String(), EmbededFiles: cmd.EmbededFiles, diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index ad776057ad7..ee54e7269f7 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -28,7 +28,9 @@ func init() { } type NotificationService struct { - Bus bus.Bus `inject:""` + Bus bus.Bus `inject:""` + Cfg *setting.Cfg `inject:""` + mailQueue chan *Message webhookQueue chan *Webhook log log.Logger @@ -54,13 +56,13 @@ func (ns *NotificationService) Init() error { "Subject": subjectTemplateFunc, }) - templatePattern := filepath.Join(setting.StaticRootPath, setting.Smtp.TemplatesPattern) + templatePattern := filepath.Join(setting.StaticRootPath, ns.Cfg.Smtp.TemplatesPattern) _, err := mailTemplates.ParseGlob(templatePattern) if err != nil { return err } - if !util.IsEmail(setting.Smtp.FromAddress) { + if !util.IsEmail(ns.Cfg.Smtp.FromAddress) { return errors.New("Invalid email address for SMTP from_address config") } @@ -81,7 +83,7 @@ func (ns *NotificationService) Run(ctx context.Context) error { ns.log.Error("Failed to send webrequest ", "error", err) } case msg := <-ns.mailQueue: - num, err := send(msg) + num, err := ns.send(msg) tos := strings.Join(msg.To, "; ") info := "" if err != nil { @@ -117,7 +119,7 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string { } func (ns *NotificationService) sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error { - message, err := buildEmailMessage(&m.SendEmailCommand{ + message, err := ns.buildEmailMessage(&m.SendEmailCommand{ Data: cmd.Data, Info: cmd.Info, Template: cmd.Template, @@ -130,12 +132,12 @@ func (ns *NotificationService) sendEmailCommandHandlerSync(ctx context.Context, return err } - _, err = send(message) + _, err = ns.send(message) return err } func (ns *NotificationService) sendEmailCommandHandler(cmd *m.SendEmailCommand) error { - message, err := buildEmailMessage(cmd) + message, err := ns.buildEmailMessage(cmd) if err != nil { return err @@ -205,7 +207,7 @@ func (ns *NotificationService) signUpStartedHandler(evt *events.SignUpStarted) e } func (ns *NotificationService) signUpCompletedHandler(evt *events.SignUpCompleted) error { - if evt.Email == "" || !setting.Smtp.SendWelcomeEmailOnSignUp { + if evt.Email == "" || !ns.Cfg.Smtp.SendWelcomeEmailOnSignUp { return nil } diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index a86bd3b19ed..504c10c22ec 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -19,13 +19,14 @@ func TestNotifications(t *testing.T) { Convey("Given the notifications service", t, func() { setting.StaticRootPath = "../../../public/" - setting.Smtp.Enabled = true - setting.Smtp.TemplatesPattern = "emails/*.html" - setting.Smtp.FromAddress = "from@address.com" - setting.Smtp.FromName = "Grafana Admin" ns := &NotificationService{} ns.Bus = bus.New() + ns.Cfg = setting.NewCfg() + ns.Cfg.Smtp.Enabled = true + ns.Cfg.Smtp.TemplatesPattern = "emails/*.html" + ns.Cfg.Smtp.FromAddress = "from@address.com" + ns.Cfg.Smtp.FromName = "Grafana Admin" err := ns.Init() So(err, ShouldBeNil) diff --git a/pkg/services/notifications/send_email_integration_test.go b/pkg/services/notifications/send_email_integration_test.go index a9f37018a3a..201f86036d3 100644 --- a/pkg/services/notifications/send_email_integration_test.go +++ b/pkg/services/notifications/send_email_integration_test.go @@ -13,14 +13,15 @@ import ( func TestEmailIntegrationTest(t *testing.T) { SkipConvey("Given the notifications service", t, func() { setting.StaticRootPath = "../../../public/" - setting.Smtp.Enabled = true - setting.Smtp.TemplatesPattern = "emails/*.html" - setting.Smtp.FromAddress = "from@address.com" - setting.Smtp.FromName = "Grafana Admin" setting.BuildVersion = "4.0.0" ns := &NotificationService{} ns.Bus = bus.New() + ns.Cfg = setting.NewCfg() + ns.Cfg.Smtp.Enabled = true + ns.Cfg.Smtp.TemplatesPattern = "emails/*.html" + ns.Cfg.Smtp.FromAddress = "from@address.com" + ns.Cfg.Smtp.FromName = "Grafana Admin" err := ns.Init() So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index e4be3208c86..b804d8b1621 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -168,7 +168,7 @@ func getEngine() (*xorm.Engine, error) { engine.SetMaxOpenConns(DbCfg.MaxOpenConn) engine.SetMaxIdleConns(DbCfg.MaxIdleConn) engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime)) - debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false) + debugSql := setting.Raw.Section("database").Key("log_queries").MustBool(false) if !debugSql { engine.SetLogger(&xorm.DiscardLogger{}) } else { @@ -181,7 +181,7 @@ func getEngine() (*xorm.Engine, error) { } func LoadConfig() { - sec := setting.Cfg.Section("database") + sec := setting.Raw.Section("database") cfgURL := sec.Key("url").String() if len(cfgURL) != 0 { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 922eea607d1..40d7522f775 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -137,7 +137,7 @@ var ( SessionConnMaxLifetime int64 // Global setting objects. - Cfg *ini.File + Raw *ini.File ConfRootPath string IsWindows bool @@ -160,9 +160,6 @@ var ( LdapConfigFile string LdapAllowSignup = true - // SMTP email settings - Smtp SmtpSettings - // QUOTA Quota QuotaSettings @@ -187,6 +184,16 @@ var ( ImageUploadProvider string ) +type Cfg struct { + Raw *ini.File + + // SMTP email settings + Smtp SmtpSettings + + ImagesDir string + DisableBruteForceLoginProtection bool +} + type CommandLineArgs struct { Config string HomePath string @@ -228,9 +235,9 @@ func shouldRedactURLKey(s string) bool { return strings.Contains(uppercased, "DATABASE_URL") } -func applyEnvVariableOverrides() error { +func applyEnvVariableOverrides(file *ini.File) error { appliedEnvOverrides = make([]string, 0) - for _, section := range Cfg.Sections() { + for _, section := range file.Sections() { for _, key := range section.Keys() { sectionName := strings.ToUpper(strings.Replace(section.Name(), ".", "_", -1)) keyName := strings.ToUpper(strings.Replace(key.Name(), ".", "_", -1)) @@ -264,9 +271,9 @@ func applyEnvVariableOverrides() error { return nil } -func applyCommandLineDefaultProperties(props map[string]string) { +func applyCommandLineDefaultProperties(props map[string]string, file *ini.File) { appliedCommandLineProperties = make([]string, 0) - for _, section := range Cfg.Sections() { + for _, section := range file.Sections() { for _, key := range section.Keys() { keyString := fmt.Sprintf("default.%s.%s", section.Name(), key.Name()) value, exists := props[keyString] @@ -281,8 +288,8 @@ func applyCommandLineDefaultProperties(props map[string]string) { } } -func applyCommandLineProperties(props map[string]string) { - for _, section := range Cfg.Sections() { +func applyCommandLineProperties(props map[string]string, file *ini.File) { + for _, section := range file.Sections() { sectionName := section.Name() + "." if section.Name() == ini.DEFAULT_SECTION { sectionName = "" @@ -341,15 +348,15 @@ func evalEnvVarExpression(value string) string { }) } -func evalConfigValues() { - for _, section := range Cfg.Sections() { +func evalConfigValues(file *ini.File) { + for _, section := range file.Sections() { for _, key := range section.Keys() { key.SetValue(evalEnvVarExpression(key.Value())) } } } -func loadSpecifedConfigFile(configFile string) error { +func loadSpecifedConfigFile(configFile string, masterFile *ini.File) error { if configFile == "" { configFile = filepath.Join(HomePath, CustomInitPath) // return without error if custom file does not exist @@ -371,9 +378,9 @@ func loadSpecifedConfigFile(configFile string) error { continue } - defaultSec, err := Cfg.GetSection(section.Name()) + defaultSec, err := masterFile.GetSection(section.Name()) if err != nil { - defaultSec, _ = Cfg.NewSection(section.Name()) + defaultSec, _ = masterFile.NewSection(section.Name()) } defaultKey, err := defaultSec.GetKey(key.Name()) if err != nil { @@ -387,7 +394,7 @@ func loadSpecifedConfigFile(configFile string) error { return nil } -func loadConfiguration(args *CommandLineArgs) error { +func loadConfiguration(args *CommandLineArgs) (*ini.File, error) { var err error // load config defaults @@ -401,44 +408,44 @@ func loadConfiguration(args *CommandLineArgs) error { } // load defaults - Cfg, err = ini.Load(defaultConfigFile) + parsedFile, err := ini.Load(defaultConfigFile) if err != nil { fmt.Println(fmt.Sprintf("Failed to parse defaults.ini, %v", err)) os.Exit(1) - return err + return nil, err } - Cfg.BlockMode = false + parsedFile.BlockMode = false // command line props commandLineProps := getCommandLineProperties(args.Args) // load default overrides - applyCommandLineDefaultProperties(commandLineProps) + applyCommandLineDefaultProperties(commandLineProps, parsedFile) // load specified config file - err = loadSpecifedConfigFile(args.Config) + err = loadSpecifedConfigFile(args.Config, parsedFile) if err != nil { - initLogging() + initLogging(parsedFile) log.Fatal(3, err.Error()) } // apply environment overrides - err = applyEnvVariableOverrides() + err = applyEnvVariableOverrides(parsedFile) if err != nil { - return err + return nil, err } // apply command line overrides - applyCommandLineProperties(commandLineProps) + applyCommandLineProperties(commandLineProps, parsedFile) // evaluate config values containing environment variables - evalConfigValues() + evalConfigValues(parsedFile) // update data path and logging config - DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath) - initLogging() + DataPath = makeAbsolute(parsedFile.Section("paths").Key("data").String(), HomePath) + initLogging(parsedFile) - return err + return parsedFile, err } func pathExists(path string) bool { @@ -484,23 +491,33 @@ func validateStaticRootPath() error { return nil } -func NewConfigContext(args *CommandLineArgs) error { +func NewCfg() *Cfg { + return &Cfg{} +} + +func (cfg *Cfg) Load(args *CommandLineArgs) error { setHomePath(args) - err := loadConfiguration(args) + + iniFile, err := loadConfiguration(args) if err != nil { return err } + cfg.Raw = iniFile + + // Temporary keep global, to make refactor in steps + Raw = cfg.Raw + ApplicationName = "Grafana" if Enterprise { ApplicationName += " Enterprise" } - Env = Cfg.Section("").Key("app_mode").MustString("development") - InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name") - PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath) - ProvisioningPath = makeAbsolute(Cfg.Section("paths").Key("provisioning").String(), HomePath) - server := Cfg.Section("server") + Env = iniFile.Section("").Key("app_mode").MustString("development") + InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name") + PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath) + ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath) + server := iniFile.Section("server") AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server) Protocol = HTTP @@ -528,27 +545,28 @@ func NewConfigContext(args *CommandLineArgs) error { } // read data proxy settings - dataproxy := Cfg.Section("dataproxy") + dataproxy := iniFile.Section("dataproxy") DataProxyLogging = dataproxy.Key("logging").MustBool(false) // read security settings - security := Cfg.Section("security") + security := iniFile.Section("security") SecretKey = security.Key("secret_key").String() LogInRememberDays = security.Key("login_remember_days").MustInt() CookieUserName = security.Key("cookie_username").String() CookieRememberName = security.Key("cookie_remember_name").String() DisableGravatar = security.Key("disable_gravatar").MustBool(true) - DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) + cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) + DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection // read snapshots settings - snapshots := Cfg.Section("snapshots") + snapshots := iniFile.Section("snapshots") ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String() ExternalSnapshotName = snapshots.Key("external_snapshot_name").String() ExternalEnabled = snapshots.Key("external_enabled").MustBool(true) SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true) // read dashboard settings - dashboards := Cfg.Section("dashboards") + dashboards := iniFile.Section("dashboards") DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) // read data source proxy white list @@ -561,7 +579,7 @@ func NewConfigContext(args *CommandLineArgs) error { AdminUser = security.Key("admin_user").String() AdminPassword = security.Key("admin_password").String() - users := Cfg.Section("users") + users := iniFile.Section("users") AllowUserSignUp = users.Key("allow_sign_up").MustBool(true) AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) @@ -575,17 +593,17 @@ func NewConfigContext(args *CommandLineArgs) error { ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false) // auth - auth := Cfg.Section("auth") + auth := iniFile.Section("auth") DisableLoginForm = auth.Key("disable_login_form").MustBool(false) DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) // anonymous access - AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false) - AnonymousOrgName = Cfg.Section("auth.anonymous").Key("org_name").String() - AnonymousOrgRole = Cfg.Section("auth.anonymous").Key("org_role").String() + AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false) + AnonymousOrgName = iniFile.Section("auth.anonymous").Key("org_name").String() + AnonymousOrgRole = iniFile.Section("auth.anonymous").Key("org_role").String() // auth proxy - authProxy := Cfg.Section("auth.proxy") + authProxy := iniFile.Section("auth.proxy") AuthProxyEnabled = authProxy.Key("enabled").MustBool(false) AuthProxyHeaderName = authProxy.Key("header_name").String() AuthProxyHeaderProperty = authProxy.Key("header_property").String() @@ -594,63 +612,64 @@ func NewConfigContext(args *CommandLineArgs) error { AuthProxyWhitelist = authProxy.Key("whitelist").String() // basic auth - authBasic := Cfg.Section("auth.basic") + authBasic := iniFile.Section("auth.basic") BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) // global plugin settings - PluginAppsSkipVerifyTLS = Cfg.Section("plugins").Key("app_tls_skip_verify_insecure").MustBool(false) + PluginAppsSkipVerifyTLS = iniFile.Section("plugins").Key("app_tls_skip_verify_insecure").MustBool(false) // PhantomJS rendering - ImagesDir = filepath.Join(DataPath, "png") + cfg.ImagesDir = filepath.Join(DataPath, "png") + ImagesDir = cfg.ImagesDir PhantomDir = filepath.Join(HomePath, "tools/phantomjs") - analytics := Cfg.Section("analytics") + analytics := iniFile.Section("analytics") ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) CheckForUpdates = analytics.Key("check_for_updates").MustBool(true) GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String() GoogleTagManagerId = analytics.Key("google_tag_manager_id").String() - ldapSec := Cfg.Section("auth.ldap") + ldapSec := iniFile.Section("auth.ldap") LdapEnabled = ldapSec.Key("enabled").MustBool(false) LdapConfigFile = ldapSec.Key("config_file").String() LdapAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) - alerting := Cfg.Section("alerting") + alerting := iniFile.Section("alerting") AlertingEnabled = alerting.Key("enabled").MustBool(true) ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true) - explore := Cfg.Section("explore") + explore := iniFile.Section("explore") ExploreEnabled = explore.Key("enabled").MustBool(false) - readSessionConfig() - readSmtpSettings() - readQuotaSettings() + cfg.readSessionConfig() + cfg.readSmtpSettings() + cfg.readQuotaSettings() - if VerifyEmailEnabled && !Smtp.Enabled { + if VerifyEmailEnabled && !cfg.Smtp.Enabled { log.Warn("require_email_validation is enabled but smtp is disabled") } // check old key name - GrafanaComUrl = Cfg.Section("grafana_net").Key("url").MustString("") + GrafanaComUrl = iniFile.Section("grafana_net").Key("url").MustString("") if GrafanaComUrl == "" { - GrafanaComUrl = Cfg.Section("grafana_com").Key("url").MustString("https://grafana.com") + GrafanaComUrl = iniFile.Section("grafana_com").Key("url").MustString("https://grafana.com") } - imageUploadingSection := Cfg.Section("external_image_storage") + imageUploadingSection := iniFile.Section("external_image_storage") ImageUploadProvider = imageUploadingSection.Key("provider").MustString("") return nil } -func readSessionConfig() { - sec := Cfg.Section("session") +func (cfg *Cfg) readSessionConfig() { + sec := cfg.Raw.Section("session") SessionOptions = session.Options{} SessionOptions.Provider = sec.Key("provider").In("memory", []string{"memory", "file", "redis", "mysql", "postgres", "memcache"}) SessionOptions.ProviderConfig = strings.Trim(sec.Key("provider_config").String(), "\" ") SessionOptions.CookieName = sec.Key("cookie_name").MustString("grafana_sess") SessionOptions.CookiePath = AppSubUrl SessionOptions.Secure = sec.Key("cookie_secure").MustBool() - SessionOptions.Gclifetime = Cfg.Section("session").Key("gc_interval_time").MustInt64(86400) - SessionOptions.Maxlifetime = Cfg.Section("session").Key("session_life_time").MustInt64(86400) + SessionOptions.Gclifetime = cfg.Raw.Section("session").Key("gc_interval_time").MustInt64(86400) + SessionOptions.Maxlifetime = cfg.Raw.Section("session").Key("session_life_time").MustInt64(86400) SessionOptions.IDLength = 16 if SessionOptions.Provider == "file" { @@ -662,21 +681,21 @@ func readSessionConfig() { SessionOptions.CookiePath = "/" } - SessionConnMaxLifetime = Cfg.Section("session").Key("conn_max_lifetime").MustInt64(14400) + SessionConnMaxLifetime = cfg.Raw.Section("session").Key("conn_max_lifetime").MustInt64(14400) } -func initLogging() { +func initLogging(file *ini.File) { // split on comma - LogModes = strings.Split(Cfg.Section("log").Key("mode").MustString("console"), ",") + LogModes = strings.Split(file.Section("log").Key("mode").MustString("console"), ",") // also try space if len(LogModes) == 1 { - LogModes = strings.Split(Cfg.Section("log").Key("mode").MustString("console"), " ") + LogModes = strings.Split(file.Section("log").Key("mode").MustString("console"), " ") } - LogsPath = makeAbsolute(Cfg.Section("paths").Key("logs").String(), HomePath) - log.ReadLoggingConfig(LogModes, LogsPath, Cfg) + LogsPath = makeAbsolute(file.Section("paths").Key("logs").String(), HomePath) + log.ReadLoggingConfig(LogModes, LogsPath, file) } -func LogConfigurationInfo() { +func (cfg *Cfg) LogConfigSources() { var text bytes.Buffer for _, file := range configFiles { diff --git a/pkg/setting/setting_quota.go b/pkg/setting/setting_quota.go index 49769d9930f..c3a509219db 100644 --- a/pkg/setting/setting_quota.go +++ b/pkg/setting/setting_quota.go @@ -63,9 +63,9 @@ type QuotaSettings struct { Global *GlobalQuota } -func readQuotaSettings() { +func (cfg *Cfg) readQuotaSettings() { // set global defaults. - quota := Cfg.Section("quota") + quota := cfg.Raw.Section("quota") Quota.Enabled = quota.Key("enabled").MustBool(false) // per ORG Limits diff --git a/pkg/setting/setting_smtp.go b/pkg/setting/setting_smtp.go index 9d8b8a529a5..5df774dc691 100644 --- a/pkg/setting/setting_smtp.go +++ b/pkg/setting/setting_smtp.go @@ -16,20 +16,20 @@ type SmtpSettings struct { TemplatesPattern string } -func readSmtpSettings() { - sec := Cfg.Section("smtp") - Smtp.Enabled = sec.Key("enabled").MustBool(false) - Smtp.Host = sec.Key("host").String() - Smtp.User = sec.Key("user").String() - Smtp.Password = sec.Key("password").String() - Smtp.CertFile = sec.Key("cert_file").String() - Smtp.KeyFile = sec.Key("key_file").String() - Smtp.FromAddress = sec.Key("from_address").String() - Smtp.FromName = sec.Key("from_name").String() - Smtp.EhloIdentity = sec.Key("ehlo_identity").String() - Smtp.SkipVerify = sec.Key("skip_verify").MustBool(false) +func (cfg *Cfg) readSmtpSettings() { + sec := cfg.Raw.Section("smtp") + cfg.Smtp.Enabled = sec.Key("enabled").MustBool(false) + cfg.Smtp.Host = sec.Key("host").String() + cfg.Smtp.User = sec.Key("user").String() + cfg.Smtp.Password = sec.Key("password").String() + cfg.Smtp.CertFile = sec.Key("cert_file").String() + cfg.Smtp.KeyFile = sec.Key("key_file").String() + cfg.Smtp.FromAddress = sec.Key("from_address").String() + cfg.Smtp.FromName = sec.Key("from_name").String() + cfg.Smtp.EhloIdentity = sec.Key("ehlo_identity").String() + cfg.Smtp.SkipVerify = sec.Key("skip_verify").MustBool(false) - emails := Cfg.Section("emails") - Smtp.SendWelcomeEmailOnSignUp = emails.Key("welcome_email_on_sign_up").MustBool(false) - Smtp.TemplatesPattern = emails.Key("templates_pattern").MustString("emails/*.html") + emails := cfg.Raw.Section("emails") + cfg.Smtp.SendWelcomeEmailOnSignUp = emails.Key("welcome_email_on_sign_up").MustBool(false) + cfg.Smtp.TemplatesPattern = emails.Key("templates_pattern").MustString("emails/*.html") } diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 2da728b7298..9de22c86811 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -15,7 +15,8 @@ func TestLoadingSettings(t *testing.T) { skipStaticRootValidation = true Convey("Given the default ini files", func() { - err := NewConfigContext(&CommandLineArgs{HomePath: "../../"}) + cfg := NewCfg() + err := cfg.Load(&CommandLineArgs{HomePath: "../../"}) So(err, ShouldBeNil) So(AdminUser, ShouldEqual, "admin") @@ -23,7 +24,9 @@ func TestLoadingSettings(t *testing.T) { Convey("Should be able to override via environment variables", func() { os.Setenv("GF_SECURITY_ADMIN_USER", "superduper") - NewConfigContext(&CommandLineArgs{HomePath: "../../"}) + + cfg := NewCfg() + cfg.Load(&CommandLineArgs{HomePath: "../../"}) So(AdminUser, ShouldEqual, "superduper") So(DataPath, ShouldEqual, filepath.Join(HomePath, "data")) @@ -32,21 +35,27 @@ func TestLoadingSettings(t *testing.T) { Convey("Should replace password when defined in environment", func() { os.Setenv("GF_SECURITY_ADMIN_PASSWORD", "supersecret") - NewConfigContext(&CommandLineArgs{HomePath: "../../"}) + + cfg := NewCfg() + cfg.Load(&CommandLineArgs{HomePath: "../../"}) So(appliedEnvOverrides, ShouldContain, "GF_SECURITY_ADMIN_PASSWORD=*********") }) Convey("Should return an error when url is invalid", func() { os.Setenv("GF_DATABASE_URL", "postgres.%31://grafana:secret@postgres:5432/grafana") - err := NewConfigContext(&CommandLineArgs{HomePath: "../../"}) + + cfg := NewCfg() + err := cfg.Load(&CommandLineArgs{HomePath: "../../"}) So(err, ShouldNotBeNil) }) Convey("Should replace password in URL when url environment is defined", func() { os.Setenv("GF_DATABASE_URL", "mysql://user:secret@localhost:3306/database") - NewConfigContext(&CommandLineArgs{HomePath: "../../"}) + + cfg := NewCfg() + cfg.Load(&CommandLineArgs{HomePath: "../../"}) So(appliedEnvOverrides, ShouldContain, "GF_DATABASE_URL=mysql://user:-redacted-@localhost:3306/database") }) @@ -61,14 +70,16 @@ func TestLoadingSettings(t *testing.T) { Convey("Should be able to override via command line", func() { if runtime.GOOS == "windows" { - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Args: []string{`cfg:paths.data=c:\tmp\data`, `cfg:paths.logs=c:\tmp\logs`}, }) So(DataPath, ShouldEqual, `c:\tmp\data`) So(LogsPath, ShouldEqual, `c:\tmp\logs`) } else { - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Args: []string{"cfg:paths.data=/tmp/data", "cfg:paths.logs=/tmp/logs"}, }) @@ -79,7 +90,8 @@ func TestLoadingSettings(t *testing.T) { }) Convey("Should be able to override defaults via command line", func() { - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Args: []string{ "cfg:default.server.domain=test2", @@ -92,7 +104,8 @@ func TestLoadingSettings(t *testing.T) { Convey("Defaults can be overridden in specified config file", func() { if runtime.GOOS == "windows" { - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"), Args: []string{`cfg:default.paths.data=c:\tmp\data`}, @@ -100,7 +113,8 @@ func TestLoadingSettings(t *testing.T) { So(DataPath, ShouldEqual, `c:\tmp\override`) } else { - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Config: filepath.Join(HomePath, "tests/config-files/override.ini"), Args: []string{"cfg:default.paths.data=/tmp/data"}, @@ -112,7 +126,8 @@ func TestLoadingSettings(t *testing.T) { Convey("Command line overrides specified config file", func() { if runtime.GOOS == "windows" { - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"), Args: []string{`cfg:paths.data=c:\tmp\data`}, @@ -120,7 +135,8 @@ func TestLoadingSettings(t *testing.T) { So(DataPath, ShouldEqual, `c:\tmp\data`) } else { - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Config: filepath.Join(HomePath, "tests/config-files/override.ini"), Args: []string{"cfg:paths.data=/tmp/data"}, @@ -133,7 +149,8 @@ func TestLoadingSettings(t *testing.T) { Convey("Can use environment variables in config values", func() { if runtime.GOOS == "windows" { os.Setenv("GF_DATA_PATH", `c:\tmp\env_override`) - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Args: []string{"cfg:paths.data=${GF_DATA_PATH}"}, }) @@ -141,7 +158,8 @@ func TestLoadingSettings(t *testing.T) { So(DataPath, ShouldEqual, `c:\tmp\env_override`) } else { os.Setenv("GF_DATA_PATH", "/tmp/env_override") - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", Args: []string{"cfg:paths.data=${GF_DATA_PATH}"}, }) @@ -151,7 +169,8 @@ func TestLoadingSettings(t *testing.T) { }) Convey("instance_name default to hostname even if hostname env is empty", func() { - NewConfigContext(&CommandLineArgs{ + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ HomePath: "../../", }) diff --git a/pkg/social/social.go b/pkg/social/social.go index 8f0618b7f74..adbe5a912d9 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -58,7 +58,7 @@ func NewOAuthService() { allOauthes := []string{"github", "google", "generic_oauth", "grafananet", "grafana_com"} for _, name := range allOauthes { - sec := setting.Cfg.Section("auth." + name) + sec := setting.Raw.Section("auth." + name) info := &setting.OAuthInfo{ ClientId: sec.Key("client_id").String(), ClientSecret: sec.Key("client_secret").String(), diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go index 921996d155d..79b01f70c9b 100644 --- a/pkg/tracing/tracing.go +++ b/pkg/tracing/tracing.go @@ -32,7 +32,7 @@ func Init(file *ini.File) (io.Closer, error) { func parseSettings(file *ini.File) *TracingSettings { settings := &TracingSettings{} - var section, err = setting.Cfg.GetSection("tracing.jaeger") + var section, err = setting.Raw.GetSection("tracing.jaeger") if err != nil { return settings } diff --git a/pkg/tsdb/influxdb/response_parser_test.go b/pkg/tsdb/influxdb/response_parser_test.go index a517cf4d71f..d8ec6e145c7 100644 --- a/pkg/tsdb/influxdb/response_parser_test.go +++ b/pkg/tsdb/influxdb/response_parser_test.go @@ -13,7 +13,8 @@ func TestInfluxdbResponseParser(t *testing.T) { Convey("Response parser", func() { parser := &ResponseParser{} - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../../", }) diff --git a/pkg/tsdb/interval_test.go b/pkg/tsdb/interval_test.go index 1e36e5428fe..941b08dd554 100644 --- a/pkg/tsdb/interval_test.go +++ b/pkg/tsdb/interval_test.go @@ -10,7 +10,8 @@ import ( func TestInterval(t *testing.T) { Convey("Default interval ", t, func() { - setting.NewConfigContext(&setting.CommandLineArgs{ + cfg := setting.NewCfg() + cfg.Load(&setting.CommandLineArgs{ HomePath: "../../", }) From 0fc4da810fc6412c5e6c8dd7d71656db4dc80df8 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Apr 2018 16:33:27 +0200 Subject: [PATCH 0565/3000] changelog: notes about closing #11498 [skip ci] --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a82a8d0498..866cb216757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 5.2.0 (unreleased) + +### Minor + +* **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) + + # 5.1.0 (2018-04-26) * **Folders**: Default permissions on folder are not shown as inherited in its dashboards [#11668](https://github.com/grafana/grafana/issues/11668) @@ -51,13 +58,13 @@ * **Units**: Use B/s instead Bps for Bytes per second [#9342](https://github.com/grafana/grafana/pull/9342), thx [@mayli](https://github.com/mayli) * **Units**: Radiation units [#11001](https://github.com/grafana/grafana/issues/11001), thx [@victorclaessen](https://github.com/victorclaessen) * **Units**: Timeticks unit [#11183](https://github.com/grafana/grafana/pull/11183), thx [@jtyr](https://github.com/jtyr) -* **Units**: Concentration units and "Normal cubic metre" [#11211](https://github.com/grafana/grafana/issues/11211), thx [@flopp999](https://github.com/flopp999) +* **Units**: Concentration units and "Normal cubic metre" [#11211](https://github.com/grafana/grafana/issues/11211), thx [@flopp999](https://github.com/flopp999) * **Units**: New currency - Czech koruna [#11384](https://github.com/grafana/grafana/pull/11384), thx [@Rohlik](https://github.com/Rohlik) * **Avatar**: Fix DISABLE_GRAVATAR option [#11095](https://github.com/grafana/grafana/issues/11095) * **Heatmap**: Disable log scale when using time time series buckets [#10792](https://github.com/grafana/grafana/issues/10792) * **Provisioning**: Remove `id` from json when provisioning dashboards, [#11138](https://github.com/grafana/grafana/issues/11138) -* **Prometheus**: tooltip for legend format not showing properly [#11516](https://github.com/grafana/grafana/issues/11516), thx [@svenklemm](https://github.com/svenklemm) -* **Playlist**: Empty playlists cannot be deleted [#11133](https://github.com/grafana/grafana/issues/11133), thx [@kichristensen](https://github.com/kichristensen) +* **Prometheus**: tooltip for legend format not showing properly [#11516](https://github.com/grafana/grafana/issues/11516), thx [@svenklemm](https://github.com/svenklemm) +* **Playlist**: Empty playlists cannot be deleted [#11133](https://github.com/grafana/grafana/issues/11133), thx [@kichristensen](https://github.com/kichristensen) * **Switch Orgs**: Alphabetic order in Switch Organization modal [#11556](https://github.com/grafana/grafana/issues/11556) * **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm) * **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747) From 05b0bfafe4a7b4587d3fcba70c8e8e60e66afaf1 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 27 Apr 2018 18:21:20 +0200 Subject: [PATCH 0566/3000] Explore: Add entry to panel menu to jump to Explore * panel container menu gets new Explore entry (between Edit and Share) * entry only shows if datasource has `supportsExplore` set to true (set for Prometheus only for now) * click on Explore entry changes url to `/explore/state` via location provider * `state` is a JSON representation of the panel queries * datasources implement `getExploreState()` how to turn a panel config into explore initial state * Explore can parse the state and initialize its query expressions * ReactContainer now forwards route parameters as props to component * `pluginlist` and `singlestat` panel subclasses needed to be adapted because `panel_ctrl` now has the location provider as a property already --- public/app/containers/Explore/Explore.tsx | 18 ++++++++++++++++-- public/app/containers/Explore/QueryRows.tsx | 19 +++++++++++++++---- public/app/features/panel/panel_ctrl.ts | 18 ++++++++++++++++++ .../datasource/prometheus/datasource.ts | 10 ++++++++++ public/app/plugins/panel/pluginlist/module.ts | 2 +- public/app/plugins/panel/singlestat/module.ts | 2 +- public/app/routes/ReactContainer.tsx | 1 + public/app/routes/routes.ts | 2 +- 8 files changed, 63 insertions(+), 9 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index eae3f4d0a1f..ba1cb6807a6 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -38,6 +38,19 @@ function makeTimeSeriesList(dataList, options) { }); } +function parseInitialQueries(initial) { + if (!initial) { + return []; + } + try { + const parsed = JSON.parse(initial); + return parsed.queries.map(q => q.query); + } catch (e) { + console.error(e); + return []; + } +} + interface IExploreState { datasource: any; datasourceError: any; @@ -58,6 +71,7 @@ export class Explore extends React.Component { constructor(props) { super(props); + const initialQueries = parseInitialQueries(props.routeParams.initial); this.state = { datasource: null, datasourceError: null, @@ -65,7 +79,7 @@ export class Explore extends React.Component { graphResult: null, latency: 0, loading: false, - queries: ensureQueries(), + queries: ensureQueries(initialQueries), requestOptions: null, showingGraph: true, showingTable: true, @@ -77,7 +91,7 @@ export class Explore extends React.Component { const datasource = await this.props.datasourceSrv.get(); const testResult = await datasource.testDatasource(); if (testResult.status === 'success') { - this.setState({ datasource, datasourceError: null, datasourceLoading: false }); + this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit()); } else { this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false }); } diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index a0a9981368d..3940d16b2f6 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -6,13 +6,16 @@ class QueryRow extends PureComponent { constructor(props) { super(props); this.state = { - query: '', + edited: false, + query: props.query || '', }; } handleChangeQuery = value => { const { index, onChangeQuery } = this.props; - this.setState({ query: value }); + const { query } = this.state; + const edited = query !== value; + this.setState({ edited, query: value }); if (onChangeQuery) { onChangeQuery(value, index); } @@ -41,6 +44,7 @@ class QueryRow extends PureComponent { render() { const { request } = this.props; + const { edited, query } = this.state; return (
    @@ -52,7 +56,12 @@ class QueryRow extends PureComponent {
    - +
    ); @@ -63,7 +72,9 @@ export default class QueryRows extends PureComponent { render() { const { className = '', queries, ...handlers } = this.props; return ( -
    {queries.map((q, index) => )}
    +
    + {queries.map((q, index) => )} +
    ); } } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index da8adc4f908..cdd2bc2be60 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -22,6 +22,7 @@ export class PanelCtrl { editorTabs: any; $scope: any; $injector: any; + $location: any; $timeout: any; fullscreen: boolean; inspector: any; @@ -35,6 +36,7 @@ export class PanelCtrl { constructor($scope, $injector) { this.$injector = $injector; + this.$location = $injector.get('$location'); this.$scope = $scope; this.$timeout = $injector.get('$timeout'); this.editorTabIndex = 0; @@ -97,6 +99,12 @@ export class PanelCtrl { this.changeView(false, false); } + explore() { + // TS hack :< + const initialState = JSON.stringify(this['datasource'].getExploreState(this.panel)); + this.$location.url(`/explore/${initialState}`); + } + initEditMode() { this.editorTabs = []; this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); @@ -154,6 +162,16 @@ export class PanelCtrl { }); } + // TS hack :< + if ('datasource' in this && this['datasource'].supportsExplore) { + menu.push({ + text: 'Explore', + click: 'ctrl.explore();', + icon: 'fa fa-fw fa-rocket', + shortcut: 'x', + }); + } + menu.push({ text: 'Share', click: 'ctrl.sharePanel();', diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 2a8b3069a53..221bffbda67 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -19,6 +19,7 @@ export class PrometheusDatasource { type: string; editorSrc: string; name: string; + supportsExplore: boolean; supportMetrics: boolean; url: string; directUrl: string; @@ -34,6 +35,7 @@ export class PrometheusDatasource { this.type = 'prometheus'; this.editorSrc = 'app/features/prometheus/partials/query.editor.html'; this.name = instanceSettings.name; + this.supportsExplore = true; this.supportMetrics = true; this.url = instanceSettings.url; this.directUrl = instanceSettings.directUrl; @@ -323,6 +325,14 @@ export class PrometheusDatasource { }); } + getExploreState(panel) { + if (!panel.targets) { + return {}; + } + const queries = panel.targets.map(t => ({ query: t.expr, format: t.format })); + return { queries }; + } + getPrometheusTime(date, roundUp) { if (_.isString(date)) { date = dateMath.parse(date, roundUp); diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index e97b1a8fbf9..acfa69b171c 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -12,7 +12,7 @@ class PluginListCtrl extends PanelCtrl { panelDefaults = {}; /** @ngInject */ - constructor($scope, $injector, private backendSrv, private $location) { + constructor($scope, $injector, private backendSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index c7f523a9591..28d3f308d68 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -77,7 +77,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { }; /** @ngInject */ - constructor($scope, $injector, private $location, private linkSrv) { + constructor($scope, $injector, private linkSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index d6d34372090..db6938cc878 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -29,6 +29,7 @@ export function reactContainer($route, $location, backendSrv: BackendSrv, dataso const props = { backendSrv: backendSrv, datasourceSrv: datasourceSrv, + routeParams: $route.current.params, }; ReactDOM.render(WrapInProvider(store, component, props), elem[0]); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 49690561728..6a61315f956 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -111,7 +111,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'FolderDashboardsCtrl', controllerAs: 'ctrl', }) - .when('/explore', { + .when('/explore/:initial?', { template: '', resolve: { component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Explore'), From 8a53ec610bf0dc6cc5b69e675958b94482331661 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sun, 29 Apr 2018 14:02:32 +0200 Subject: [PATCH 0567/3000] Fix url encoding, expand template vars, fix TS hacks * moved datasource related functions to panel sub-class * expand panel template vars for url * added keybindings for x -> Explore * url encoding for explore state --- public/app/containers/Explore/Explore.tsx | 3 ++- public/app/core/services/keybindingSrv.ts | 14 ++++++++++- public/app/core/utils/location_util.ts | 11 +++++---- .../app/features/panel/metrics_panel_ctrl.ts | 19 +++++++++++++++ public/app/features/panel/panel_ctrl.ts | 24 +++++++------------ .../datasource/prometheus/datasource.ts | 15 ++++++++---- 6 files changed, 60 insertions(+), 26 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index ba1cb6807a6..40261ee635a 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -10,6 +10,7 @@ import Graph from './Graph'; import Table from './Table'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query'; +import { decodePathComponent } from 'app/core/utils/location_util'; function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { @@ -43,7 +44,7 @@ function parseInitialQueries(initial) { return []; } try { - const parsed = JSON.parse(initial); + const parsed = JSON.parse(decodePathComponent(initial)); return parsed.queries.map(q => q.query); } catch (e) { console.error(e); diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 55d968fd981..94bf9efb31b 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; +import { encodePathComponent } from 'app/core/utils/location_util'; import Mousetrap from 'mousetrap'; import 'mousetrap-global-bind'; @@ -13,7 +14,7 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor(private $rootScope, private $location) { + constructor(private $rootScope, private $location, private datasourceSrv) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -176,6 +177,17 @@ export class KeybindingSrv { } }); + this.bind('x', async () => { + if (dashboard.meta.focusPanelId) { + const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); + const datasource = await this.datasourceSrv.get(panel.datasource); + if (datasource && datasource.supportsExplore) { + const exploreState = encodePathComponent(JSON.stringify(datasource.getExploreState(panel))); + this.$location.url(`/explore/${exploreState}`); + } + } + }); + // delete panel this.bind('p r', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { diff --git a/public/app/core/utils/location_util.ts b/public/app/core/utils/location_util.ts index f8d6aa4ee5f..735272285ff 100644 --- a/public/app/core/utils/location_util.ts +++ b/public/app/core/utils/location_util.ts @@ -1,6 +1,11 @@ import config from 'app/core/config'; -const _stripBaseFromUrl = url => { +// Slash encoding for angular location provider, see https://github.com/angular/angular.js/issues/10479 +const SLASH = ''; +export const decodePathComponent = (pc: string) => decodeURIComponent(pc).replace(new RegExp(SLASH, 'g'), '/'); +export const encodePathComponent = (pc: string) => encodeURIComponent(pc.replace(/\//g, SLASH)); + +export const stripBaseFromUrl = url => { const appSubUrl = config.appSubUrl; const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; const urlWithoutBase = @@ -9,6 +14,4 @@ const _stripBaseFromUrl = url => { return urlWithoutBase; }; -export default { - stripBaseFromUrl: _stripBaseFromUrl, -}; +export default { stripBaseFromUrl }; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 9e9598e1732..acf46a193e8 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -6,6 +6,7 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; +import { encodePathComponent } from 'app/core/utils/location_util'; import { metricsTabDirective } from './metrics_tab'; @@ -309,6 +310,24 @@ class MetricsPanelCtrl extends PanelCtrl { this.refresh(); } + getAdditionalMenuItems() { + const items = []; + if (this.datasource.supportsExplore) { + items.push({ + text: 'Explore', + click: 'ctrl.explore();', + icon: 'fa fa-fw fa-rocket', + shortcut: 'x', + }); + } + return items; + } + + explore() { + const exploreState = encodePathComponent(JSON.stringify(this.datasource.getExploreState(this.panel))); + this.$location.url(`/explore/${exploreState}`); + } + addQuery(target) { target.refId = this.dashboard.getNextQueryLetter(this.panel); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index cdd2bc2be60..67725ec5fec 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -99,12 +99,6 @@ export class PanelCtrl { this.changeView(false, false); } - explore() { - // TS hack :< - const initialState = JSON.stringify(this['datasource'].getExploreState(this.panel)); - this.$location.url(`/explore/${initialState}`); - } - initEditMode() { this.editorTabs = []; this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); @@ -162,16 +156,6 @@ export class PanelCtrl { }); } - // TS hack :< - if ('datasource' in this && this['datasource'].supportsExplore) { - menu.push({ - text: 'Explore', - click: 'ctrl.explore();', - icon: 'fa fa-fw fa-rocket', - shortcut: 'x', - }); - } - menu.push({ text: 'Share', click: 'ctrl.sharePanel();', @@ -179,6 +163,9 @@ export class PanelCtrl { shortcut: 'p s', }); + // Additional items from sub-class + menu.push(...this.getAdditionalMenuItems()); + let extendedMenu = this.getExtendedMenu(); menu.push({ text: 'More ...', @@ -227,6 +214,11 @@ export class PanelCtrl { return menu; } + // Override in sub-class to add items before extended menu + getAdditionalMenuItems() { + return []; + } + otherPanelInFullscreenMode() { return this.dashboard.meta.fullscreen && !this.fullscreen; } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 221bffbda67..5d9510b4238 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -326,11 +326,18 @@ export class PrometheusDatasource { } getExploreState(panel) { - if (!panel.targets) { - return {}; + let state = {}; + if (panel.targets) { + const queries = panel.targets.map(t => ({ + query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr), + format: t.format, + })); + state = { + ...state, + queries, + }; } - const queries = panel.targets.map(t => ({ query: t.expr, format: t.format })); - return { queries }; + return state; } getPrometheusTime(date, roundUp) { From 253b2cc081dc3fe2f81da2feb22b490f09494537 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 1 May 2018 05:00:56 +0900 Subject: [PATCH 0568/3000] add test for prometheus table column title --- .../prometheus/specs/result_transformer.jest.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts index abcc46d7ea8..64b983fc8a7 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts @@ -47,6 +47,18 @@ describe('Prometheus Result Transformer', () => { { text: 'Value' }, ]); }); + + it('should column title include refId if response count is more than 2', () => { + var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result, 2, "B"); + expect(table.type).toBe('table'); + expect(table.columns).toEqual([ + { text: 'Time', type: 'time' }, + { text: '__name__' }, + { text: 'instance' }, + { text: 'job' }, + { text: 'Value #B' }, + ]); + }); }); describe('When resultFormat is table and instant = true', () => { From 13e015fe3f6ec77f3e77b7d61997c98e43c67d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 1 May 2018 14:13:38 +0200 Subject: [PATCH 0569/3000] fix: improved handling of http server shutdown --- pkg/api/http_server.go | 10 ++++++++++ pkg/cmd/grafana-server/server.go | 15 +++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index fa27eabbf24..38858606579 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -60,6 +60,16 @@ func (hs *HTTPServer) Start(ctx context.Context) error { hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath) hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron} + + // handle http shutdown on server context done + go func() { + <-ctx.Done() + if err := hs.httpSrv.Shutdown(context.Background()); err != nil { + hs.log.Error("Failed to shutdown server", "error", err) + } + hs.log.Info("Stopped HTTP Server") + }() + switch setting.Protocol { case setting.HTTP: err = hs.httpSrv.ListenAndServe() diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 911cf092b47..71a1560215f 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -92,6 +92,8 @@ func (g *GrafanaServerImpl) Start() error { serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()}) serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) serviceGraph.Provide(&inject.Object{Value: api.HTTPServer{}}) + + // self registered services services := registry.GetServices() // Add all services to dependency graph @@ -172,15 +174,12 @@ func (g *GrafanaServerImpl) startHttpServer() error { func (g *GrafanaServerImpl) Shutdown(code int, reason string) { g.log.Info("Shutdown started", "code", code, "reason", reason) - err := g.HttpServer.Shutdown(g.context) - if err != nil { - g.log.Error("Failed to shutdown server", "error", err) - } - + // call cancel func on root context g.shutdownFn() - err = g.childRoutines.Wait() - if err != nil && err != context.Canceled { - g.log.Error("Server shutdown completed with an error", "error", err) + + // wait for chid routines + if err := g.childRoutines.Wait(); err != nil && err != context.Canceled { + g.log.Error("Server shutdown completed", "error", err) } } From 2b93cbbf04004af09dc140ba2aa577b7ffb536c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 1 May 2018 14:18:10 +0200 Subject: [PATCH 0570/3000] --amend --- pkg/cmd/grafana-server/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 71a1560215f..30bb0b2003a 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -177,7 +177,7 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) { // call cancel func on root context g.shutdownFn() - // wait for chid routines + // wait for child routines if err := g.childRoutines.Wait(); err != nil && err != context.Canceled { g.log.Error("Server shutdown completed", "error", err) } From 3dd073f98d3fc2bc5e4b5639d226aa04cb68db0f Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 2 May 2018 09:56:53 +0200 Subject: [PATCH 0571/3000] fixed so all buttons are styled not just small ones, fixes #11616 --- public/sass/components/_timepicker.scss | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index 2d7a12c3d01..9b71e8e7c05 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -71,12 +71,10 @@ td { padding: 1px; } - button.btn-sm { + button { @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl); - font-size: $font-size-sm; background-image: none; border: none; - padding: 5px 11px; color: $text-color; &.active span { color: $blue; @@ -86,6 +84,10 @@ color: $orange; font-weight: bold; } + &.btn-sm { + font-size: $font-size-sm; + padding: 5px 11px; + } } } @@ -103,10 +105,10 @@ } .fa-chevron-left::before { - content: "\f053"; + content: '\f053'; } .fa-chevron-right::before { - content: "\f054"; + content: '\f054'; } .glyphicon-chevron-right { From 1f21b3e23b569817e143464f12514f8fc10344c4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 2 May 2018 10:54:00 +0200 Subject: [PATCH 0572/3000] remove jest it.only to not skip important tests --- public/app/features/dashboard/specs/dashboard_model.jest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.jest.ts index feede679018..6f0b45c9ba8 100644 --- a/public/app/features/dashboard/specs/dashboard_model.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_model.jest.ts @@ -405,7 +405,7 @@ describe('DashboardModel', function() { }); }); - it.only('should move panels below down', function() { + it('should move panels below down', function() { expect(dashboard.panels[4].gridPos).toMatchObject({ x: 0, y: 9, From 6dcb9e696d46095c5e963dc9c77bd137c8115550 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 2 May 2018 11:19:22 +0200 Subject: [PATCH 0573/3000] changelog: add notes about closing #11625 [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 866cb216757..b977edaadf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### Minor * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) - +* **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) # 5.1.0 (2018-04-26) From 64283408ee610e6bd1832abf37a946ca25505c81 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 2 May 2018 12:43:25 +0300 Subject: [PATCH 0574/3000] scroll: fix scrolling on mobile Chrome (#11710) --- public/sass/pages/_dashboard.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index cf32522df7f..aeb9e28975b 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -45,6 +45,8 @@ div.flot-text { height: calc(100% - 27px); position: relative; overflow: hidden; + // Fixes scrolling on mobile devices + overflow-y: scroll; } .panel-title-container { From de0d409a2399bcecb998a5bdca066b51dc0a7eac Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 2 May 2018 14:06:46 +0200 Subject: [PATCH 0575/3000] Revert "Opportunities to unindent code (unindent)" --- pkg/api/common.go | 6 +- pkg/components/dynmap/dynmap.go | 54 ++++++++------- pkg/components/simplejson/simplejson.go | 6 +- pkg/middleware/dashboard_redirect.go | 31 ++++----- pkg/tsdb/mssql/mssql.go | 23 +++---- pkg/tsdb/mysql/mysql.go | 23 +++---- pkg/tsdb/postgres/postgres.go | 23 +++---- pkg/tsdb/sql_engine.go | 89 ++++++++++++------------- 8 files changed, 131 insertions(+), 124 deletions(-) diff --git a/pkg/api/common.go b/pkg/api/common.go index cd64c57dc92..97f41ff7c72 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -99,8 +99,10 @@ func Error(status int, message string, err error) *NormalResponse { data["message"] = message } - if err != nil && setting.Env != setting.PROD { - data["error"] = err.Error() + if err != nil { + if setting.Env != setting.PROD { + data["error"] = err.Error() + } } resp := JSON(status, data) diff --git a/pkg/components/dynmap/dynmap.go b/pkg/components/dynmap/dynmap.go index 6d3546f3bc5..96effb24332 100644 --- a/pkg/components/dynmap/dynmap.go +++ b/pkg/components/dynmap/dynmap.go @@ -639,24 +639,26 @@ func (v *Value) Object() (*Object, error) { valid = true } - if !valid { - return nil, ErrNotObject - } - obj := new(Object) - obj.valid = valid - - m := make(map[string]*Value) - if valid { - for key, element := range v.data.(map[string]interface{}) { - m[key] = &Value{element, true} + obj := new(Object) + obj.valid = valid + + m := make(map[string]*Value) + + if valid { + for key, element := range v.data.(map[string]interface{}) { + m[key] = &Value{element, true} + + } } + + obj.data = v.data + obj.m = m + + return obj, nil } - obj.data = v.data - obj.m = m - - return obj, nil + return nil, ErrNotObject } // Attempts to typecast the current value into an object arrau. @@ -676,19 +678,23 @@ func (v *Value) ObjectArray() ([]*Object, error) { // Unsure if this is a good way to use slices, it's probably not var slice []*Object - if !valid { - return nil, ErrNotObjectArray - } - for _, element := range v.data.([]interface{}) { - childValue := Value{element, true} - childObject, err := childValue.Object() + if valid { - if err != nil { - return nil, ErrNotObjectArray + for _, element := range v.data.([]interface{}) { + childValue := Value{element, true} + childObject, err := childValue.Object() + + if err != nil { + return nil, ErrNotObjectArray + } + slice = append(slice, childObject) } - slice = append(slice, childObject) + + return slice, nil } - return slice, nil + + return nil, ErrNotObjectArray + } // Attempts to typecast the current value into a string. diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go index 15293b0cd93..85e2f955943 100644 --- a/pkg/components/simplejson/simplejson.go +++ b/pkg/components/simplejson/simplejson.go @@ -168,8 +168,10 @@ func (j *Json) GetPath(branch ...string) *Json { // js.Get("top_level").Get("array").GetIndex(1).Get("key").Int() func (j *Json) GetIndex(index int) *Json { a, err := j.Array() - if err == nil && len(a) > index { - return &Json{a[index]} + if err == nil { + if len(a) > index { + return &Json{a[index]} + } } return &Json{nil} } diff --git a/pkg/middleware/dashboard_redirect.go b/pkg/middleware/dashboard_redirect.go index 1111929c2f6..2edf04d543e 100644 --- a/pkg/middleware/dashboard_redirect.go +++ b/pkg/middleware/dashboard_redirect.go @@ -24,12 +24,12 @@ func RedirectFromLegacyDashboardURL() macaron.Handler { return func(c *m.ReqContext) { slug := c.Params("slug") - if slug == "" { - return - } - if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { - url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) - c.Redirect(url, 301) + if slug != "" { + if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { + url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) + c.Redirect(url, 301) + return + } } } } @@ -39,16 +39,17 @@ func RedirectFromLegacyDashboardSoloURL() macaron.Handler { slug := c.Params("slug") renderRequest := c.QueryBool("render") - if slug == "" { - return - } - if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { - if renderRequest && strings.Contains(url, setting.AppSubUrl) { - url = strings.Replace(url, setting.AppSubUrl, "", 1) + if slug != "" { + if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil { + if renderRequest && strings.Contains(url, setting.AppSubUrl) { + url = strings.Replace(url, setting.AppSubUrl, "", 1) + } + + url = strings.Replace(url, "/d/", "/d-solo/", 1) + url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) + c.Redirect(url, 301) + return } - url = strings.Replace(url, "/d/", "/d-solo/", 1) - url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery) - c.Redirect(url, 301) } } } diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index 221670f1bdb..eb71259b46b 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -298,19 +298,18 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. key := elem.Value.(string) result.Series = append(result.Series, pointsBySeries[key]) - if !fillMissing { - break - } - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + if fillMissing { + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } } } diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 57986eb7c04..7eceaffdb09 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -309,19 +309,18 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core. key := elem.Value.(string) result.Series = append(result.Series, pointsBySeries[key]) - if !fillMissing { - break - } - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + if fillMissing { + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } } } diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index f66c09b5724..fdf09216e51 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -289,19 +289,18 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co key := elem.Value.(string) result.Series = append(result.Series, pointsBySeries[key]) - if !fillMissing { - break - } - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + if fillMissing { + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } } } diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index ecf46ac689d..274e5b05dc1 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -141,51 +141,50 @@ func (e *DefaultSqlEngine) Query( // ConvertSqlTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds // to make native datetime types and epoch dates work in annotation and table queries. func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) { - if timeIndex < 0 { - return - } - switch value := values[timeIndex].(type) { - case time.Time: - values[timeIndex] = EpochPrecisionToMs(float64(value.UnixNano())) - case *time.Time: - if value != nil { - values[timeIndex] = EpochPrecisionToMs(float64((*value).UnixNano())) - } - case int64: - values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) - case *int64: - if value != nil { - values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) - } - case uint64: - values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) - case *uint64: - if value != nil { - values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) - } - case int32: - values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) - case *int32: - if value != nil { - values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) - } - case uint32: - values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) - case *uint32: - if value != nil { - values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) - } - case float64: - values[timeIndex] = EpochPrecisionToMs(value) - case *float64: - if value != nil { - values[timeIndex] = EpochPrecisionToMs(*value) - } - case float32: - values[timeIndex] = EpochPrecisionToMs(float64(value)) - case *float32: - if value != nil { - values[timeIndex] = EpochPrecisionToMs(float64(*value)) + if timeIndex >= 0 { + switch value := values[timeIndex].(type) { + case time.Time: + values[timeIndex] = EpochPrecisionToMs(float64(value.UnixNano())) + case *time.Time: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(float64((*value).UnixNano())) + } + case int64: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *int64: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case uint64: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *uint64: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case int32: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *int32: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case uint32: + values[timeIndex] = int64(EpochPrecisionToMs(float64(value))) + case *uint32: + if value != nil { + values[timeIndex] = int64(EpochPrecisionToMs(float64(*value))) + } + case float64: + values[timeIndex] = EpochPrecisionToMs(value) + case *float64: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(*value) + } + case float32: + values[timeIndex] = EpochPrecisionToMs(float64(value)) + case *float32: + if value != nil { + values[timeIndex] = EpochPrecisionToMs(float64(*value)) + } } } } From 14bb7832af9a43f58cc0bf53eb7e4abe9bf44085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 19:54:07 +0200 Subject: [PATCH 0576/3000] Metrics package now follows new service interface & registration (#11787) * refactoring: metrics package now follows new service interface & registration * fix: minor fix, make sure metrics service is imported, by grafana-server --- pkg/cmd/grafana-server/server.go | 3 +- pkg/metrics/init.go | 38 ----------------- pkg/metrics/metrics.go | 24 +---------- pkg/metrics/service.go | 71 ++++++++++++++++++++++++++++++++ pkg/metrics/settings.go | 58 +++++++++++--------------- 5 files changed, 96 insertions(+), 98 deletions(-) delete mode 100644 pkg/metrics/init.go create mode 100644 pkg/metrics/service.go diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 30bb0b2003a..f20195b563a 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" - "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" @@ -33,6 +32,7 @@ import ( // self registering services _ "github.com/grafana/grafana/pkg/extensions" + _ "github.com/grafana/grafana/pkg/metrics" _ "github.com/grafana/grafana/pkg/plugins" _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" @@ -72,7 +72,6 @@ func (g *GrafanaServerImpl) Start() error { sqlstore.NewEngine() // TODO: this should return an error sqlstore.EnsureAdminUser() - metrics.Init(g.cfg.Raw) login.Init() social.NewOAuthService() diff --git a/pkg/metrics/init.go b/pkg/metrics/init.go deleted file mode 100644 index 833b148d319..00000000000 --- a/pkg/metrics/init.go +++ /dev/null @@ -1,38 +0,0 @@ -package metrics - -import ( - "context" - - ini "gopkg.in/ini.v1" - - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/metrics/graphitebridge" -) - -var metricsLogger log.Logger = log.New("metrics") - -type logWrapper struct { - logger log.Logger -} - -func (lw *logWrapper) Println(v ...interface{}) { - lw.logger.Info("graphite metric bridge", v...) -} - -func Init(file *ini.File) { - cfg := ReadSettings(file) - internalInit(cfg) -} - -func internalInit(settings *MetricSettings) { - initMetricVars(settings) - - if settings.GraphiteBridgeConfig != nil { - bridge, err := graphitebridge.NewBridge(settings.GraphiteBridgeConfig) - if err != nil { - metricsLogger.Error("failed to create graphite bridge", "error", err) - } else { - go bridge.Run(context.Background()) - } - } -} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index e3640378f7e..83505826910 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -279,7 +279,7 @@ func init() { }, []string{"version"}) } -func initMetricVars(settings *MetricSettings) { +func initMetricVars() { prometheus.MustRegister( M_Instance_Start, M_Page_Status, @@ -316,28 +316,6 @@ func initMetricVars(settings *MetricSettings) { M_StatTotal_Playlists, M_Grafana_Version) - go instrumentationLoop(settings) -} - -func instrumentationLoop(settings *MetricSettings) chan struct{} { - M_Instance_Start.Inc() - - // set the total stats gauges before we publishing metrics - updateTotalStats() - - onceEveryDayTick := time.NewTicker(time.Hour * 24) - everyMinuteTicker := time.NewTicker(time.Minute) - defer onceEveryDayTick.Stop() - defer everyMinuteTicker.Stop() - - for { - select { - case <-onceEveryDayTick.C: - sendUsageStats() - case <-everyMinuteTicker.C: - updateTotalStats() - } - } } func updateTotalStats() { diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go new file mode 100644 index 00000000000..ec38e0acfec --- /dev/null +++ b/pkg/metrics/service.go @@ -0,0 +1,71 @@ +package metrics + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics/graphitebridge" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/setting" +) + +var metricsLogger log.Logger = log.New("metrics") + +type logWrapper struct { + logger log.Logger +} + +func (lw *logWrapper) Println(v ...interface{}) { + lw.logger.Info("graphite metric bridge", v...) +} + +func init() { + registry.RegisterService(&InternalMetricsService{}) + initMetricVars() +} + +type InternalMetricsService struct { + Cfg *setting.Cfg `inject:""` + + enabled bool + intervalSeconds int64 + graphiteCfg *graphitebridge.Config +} + +func (im *InternalMetricsService) Init() error { + return im.readSettings() +} + +func (im *InternalMetricsService) Run(ctx context.Context) error { + // Start Graphite Bridge + if im.graphiteCfg != nil { + bridge, err := graphitebridge.NewBridge(im.graphiteCfg) + if err != nil { + metricsLogger.Error("failed to create graphite bridge", "error", err) + } else { + go bridge.Run(ctx) + } + } + + M_Instance_Start.Inc() + + // set the total stats gauges before we publishing metrics + updateTotalStats() + + onceEveryDayTick := time.NewTicker(time.Hour * 24) + everyMinuteTicker := time.NewTicker(time.Minute) + defer onceEveryDayTick.Stop() + defer everyMinuteTicker.Stop() + + for { + select { + case <-onceEveryDayTick.C: + sendUsageStats() + case <-everyMinuteTicker.C: + updateTotalStats() + case <-ctx.Done(): + return ctx.Err() + } + } +} diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go index c21e7279b7e..58b84a7192f 100644 --- a/pkg/metrics/settings.go +++ b/pkg/metrics/settings.go @@ -1,67 +1,53 @@ package metrics import ( + "fmt" "strings" "time" "github.com/grafana/grafana/pkg/metrics/graphitebridge" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" - ini "gopkg.in/ini.v1" ) -type MetricSettings struct { - Enabled bool - IntervalSeconds int64 - GraphiteBridgeConfig *graphitebridge.Config -} - -func ReadSettings(file *ini.File) *MetricSettings { - var settings = &MetricSettings{ - Enabled: false, +func (im *InternalMetricsService) readSettings() error { + var section, err = im.Cfg.Raw.GetSection("metrics") + if err != nil { + return fmt.Errorf("Unable to find metrics config section %v", err) } - var section, err = file.GetSection("metrics") - if err != nil { - metricsLogger.Crit("Unable to find metrics config section", "error", err) + im.enabled = section.Key("enabled").MustBool(false) + im.intervalSeconds = section.Key("interval_seconds").MustInt64(10) + + if !im.enabled { return nil } - settings.Enabled = section.Key("enabled").MustBool(false) - settings.IntervalSeconds = section.Key("interval_seconds").MustInt64(10) - - if !settings.Enabled { - return settings + if err := im.parseGraphiteSettings(); err != nil { + return fmt.Errorf("Unable to parse metrics graphite section, %v", err) } - cfg, err := parseGraphiteSettings(settings, file) - if err != nil { - metricsLogger.Crit("Unable to parse metrics graphite section", "error", err) - return nil - } - - settings.GraphiteBridgeConfig = cfg - - return settings + return nil } -func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphitebridge.Config, error) { - graphiteSection, err := setting.Raw.GetSection("metrics.graphite") +func (im *InternalMetricsService) parseGraphiteSettings() error { + graphiteSection, err := im.Cfg.Raw.GetSection("metrics.graphite") + if err != nil { - return nil, nil + return nil } address := graphiteSection.Key("address").String() if address == "" { - return nil, nil + return nil } - cfg := &graphitebridge.Config{ + bridgeCfg := &graphitebridge.Config{ URL: address, Prefix: graphiteSection.Key("prefix").MustString("prod.grafana.%(instance_name)s"), CountersAsDelta: true, Gatherer: prometheus.DefaultGatherer, - Interval: time.Duration(settings.IntervalSeconds) * time.Second, + Interval: time.Duration(im.intervalSeconds) * time.Second, Timeout: 10 * time.Second, Logger: &logWrapper{logger: metricsLogger}, ErrorHandling: graphitebridge.ContinueOnError, @@ -74,6 +60,8 @@ func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphiteb prefix = "prod.grafana.%(instance_name)s." } - cfg.Prefix = strings.Replace(prefix, "%(instance_name)s", safeInstanceName, -1) - return cfg, nil + bridgeCfg.Prefix = strings.Replace(prefix, "%(instance_name)s", safeInstanceName, -1) + + im.graphiteCfg = bridgeCfg + return nil } From 053c2039bb407a33d2af36609279213a6bc4d5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 1 May 2018 15:51:15 +0200 Subject: [PATCH 0577/3000] refactor: provisioning service refactoring --- pkg/api/http_server.go | 11 ++++- pkg/cmd/grafana-server/server.go | 8 +--- .../provisioning/dashboards/config_reader.go | 2 +- .../provisioning/dashboards/dashboard.go | 7 +--- pkg/services/provisioning/provisioning.go | 40 ++++++++++++------- pkg/setting/setting.go | 18 +++++---- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 38858606579..f978a8c220c 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -26,9 +26,14 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" ) +func init() { + registry.RegisterService(&HTTPServer{}) +} + type HTTPServer struct { log log.Logger macaron *macaron.Macaron @@ -41,12 +46,14 @@ type HTTPServer struct { Bus bus.Bus `inject:""` } -func (hs *HTTPServer) Init() { +func (hs *HTTPServer) Init() error { hs.log = log.New("http.server") hs.cache = gocache.New(5*time.Minute, 10*time.Minute) + + return nil } -func (hs *HTTPServer) Start(ctx context.Context) error { +func (hs *HTTPServer) Run(ctx context.Context) error { var err error hs.context = ctx diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index f20195b563a..05d10ea1afd 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/provisioning" "golang.org/x/sync/errgroup" @@ -37,6 +36,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" _ "github.com/grafana/grafana/pkg/services/notifications" + _ "github.com/grafana/grafana/pkg/services/provisioning" _ "github.com/grafana/grafana/pkg/services/search" ) @@ -75,10 +75,6 @@ func (g *GrafanaServerImpl) Start() error { login.Init() social.NewOAuthService() - if err := provisioning.Init(g.context, setting.HomePath, g.cfg.Raw); err != nil { - return fmt.Errorf("Failed to provision Grafana from config. error: %v", err) - } - tracingCloser, err := tracing.Init(g.cfg.Raw) if err != nil { return fmt.Errorf("Tracing settings is not valid. error: %v", err) @@ -116,7 +112,7 @@ func (g *GrafanaServerImpl) Start() error { g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name()) if err := service.Init(); err != nil { - return fmt.Errorf("Service init failed %v", err) + return fmt.Errorf("Service init failed: %v", err) } } diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 8ac79df0fac..4f9577f82db 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -69,7 +69,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { parsedDashboards, err := cr.parseConfigs(file) if err != nil { - + return nil, err } if len(parsedDashboards) > 0 { diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index a5349517bbe..a856565bf01 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -10,19 +10,16 @@ import ( type DashboardProvisioner struct { cfgReader *configReader log log.Logger - ctx context.Context } -func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) { +func NewDashboardProvisioner(configDirectory string) *DashboardProvisioner { log := log.New("provisioning.dashboard") d := &DashboardProvisioner{ cfgReader: &configReader{path: configDirectory, log: log}, log: log, - ctx: ctx, } - err := d.Provision(ctx) - return d, err + return d } func (provider *DashboardProvisioner) Provision(ctx context.Context) error { diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 71385994bb5..9044ae97389 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -2,30 +2,40 @@ package provisioning import ( "context" + "fmt" "path" - "path/filepath" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/datasources" - ini "gopkg.in/ini.v1" + "github.com/grafana/grafana/pkg/setting" ) -func Init(ctx context.Context, homePath string, cfg *ini.File) error { - provisioningPath := makeAbsolute(cfg.Section("paths").Key("provisioning").String(), homePath) +func init() { + registry.RegisterService(&ProvisioningService{}) +} - datasourcePath := path.Join(provisioningPath, "datasources") +type ProvisioningService struct { + Cfg *setting.Cfg `inject:""` +} + +func (ps *ProvisioningService) Init() error { + datasourcePath := path.Join(ps.Cfg.ProvisioningPath, "datasources") if err := datasources.Provision(datasourcePath); err != nil { + return fmt.Errorf("Datasource provisioning error: %v", err) + } + + return nil +} + +func (ps *ProvisioningService) Run(ctx context.Context) error { + dashboardPath := path.Join(ps.Cfg.ProvisioningPath, "dashboards") + dashProvisioner := dashboards.NewDashboardProvisioner(dashboardPath) + + if err := dashProvisioner.Provision(ctx); err != nil { return err } - dashboardPath := path.Join(provisioningPath, "dashboards") - _, err := dashboards.Provision(ctx, dashboardPath) - return err -} - -func makeAbsolute(path string, root string) string { - if filepath.IsAbs(path) { - return path - } - return filepath.Join(root, path) + <-ctx.Done() + return ctx.Err() } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 40d7522f775..44d14e166d0 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -52,12 +52,11 @@ var ( ApplicationName string // Paths - LogsPath string - HomePath string - DataPath string - PluginsPath string - ProvisioningPath string - CustomInitPath = "conf/custom.ini" + LogsPath string + HomePath string + DataPath string + PluginsPath string + CustomInitPath = "conf/custom.ini" // Log settings. LogModes []string @@ -187,6 +186,9 @@ var ( type Cfg struct { Raw *ini.File + // Paths + ProvisioningPath string + // SMTP email settings Smtp SmtpSettings @@ -516,7 +518,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { Env = iniFile.Section("").Key("app_mode").MustString("development") InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name") PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath) - ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath) + cfg.ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath) server := iniFile.Section("server") AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server) @@ -719,6 +721,6 @@ func (cfg *Cfg) LogConfigSources() { logger.Info("Path Data", "path", DataPath) logger.Info("Path Logs", "path", LogsPath) logger.Info("Path Plugins", "path", PluginsPath) - logger.Info("Path Provisioning", "path", ProvisioningPath) + logger.Info("Path Provisioning", "path", cfg.ProvisioningPath) logger.Info("App mode " + Env) } From d04ad835e2d902a1d8fae33a871aa43befbbae04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 14:14:42 +0200 Subject: [PATCH 0578/3000] refactoring: lots of refactoring around server shutdown flows, making sure process is terminated when background service has crashed --- pkg/api/http_server.go | 9 +------ pkg/cmd/grafana-server/main.go | 31 ++++++--------------- pkg/cmd/grafana-server/server.go | 46 +++++++++++++++++--------------- 3 files changed, 33 insertions(+), 53 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index f978a8c220c..2cf8f3796ae 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -64,7 +64,7 @@ func (hs *HTTPServer) Run(ctx context.Context) error { hs.streamManager.Run(ctx) listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) - hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath) + hs.log.Info("HTTP Server Listen", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath) hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron} @@ -74,7 +74,6 @@ func (hs *HTTPServer) Run(ctx context.Context) error { if err := hs.httpSrv.Shutdown(context.Background()); err != nil { hs.log.Error("Failed to shutdown server", "error", err) } - hs.log.Info("Stopped HTTP Server") }() switch setting.Protocol { @@ -113,12 +112,6 @@ func (hs *HTTPServer) Run(ctx context.Context) error { return err } -func (hs *HTTPServer) Shutdown(ctx context.Context) error { - err := hs.httpSrv.Shutdown(ctx) - hs.log.Info("Stopped HTTP server") - return err -} - func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error { if certfile == "" { return fmt.Errorf("cert_file cannot be empty when using HTTPS") diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 466e97ff2d6..23cac74ccdb 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -81,29 +81,20 @@ func main() { setting.Enterprise, _ = strconv.ParseBool(enterprise) metrics.M_Grafana_Version.WithLabelValues(version).Set(1) - shutdownCompleted := make(chan int) + server := NewGrafanaServer() - go listenToSystemSignals(server, shutdownCompleted) + go listenToSystemSignals(server) - go func() { - code := 0 - if err := server.Start(); err != nil { - log.Error2("Startup failed", "error", err) - code = 1 - } + err := server.Start() - exitChan <- code - }() - - code := <-shutdownCompleted - log.Info2("Grafana shutdown completed.", "code", code) + trace.Stop() log.Close() - os.Exit(code) + + server.Exit(err) } -func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) { - var code int +func listenToSystemSignals(server *GrafanaServerImpl) { signalChan := make(chan os.Signal, 1) ignoreChan := make(chan os.Signal, 1) @@ -112,12 +103,6 @@ func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int select { case sig := <-signalChan: - trace.Stop() // Stops trace if profiling has been enabled - server.Shutdown(0, fmt.Sprintf("system signal: %s", sig)) - shutdownCompleted <- 0 - case code = <-exitChan: - trace.Stop() // Stops trace if profiling has been enabled - server.Shutdown(code, "startup error") - shutdownCompleted <- code + server.Shutdown(fmt.Sprintf("System signal: %s", sig)) } } diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 05d10ea1afd..aff40111ae9 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -54,11 +54,12 @@ func NewGrafanaServer() *GrafanaServerImpl { } type GrafanaServerImpl struct { - context context.Context - shutdownFn context.CancelFunc - childRoutines *errgroup.Group - log log.Logger - cfg *setting.Cfg + context context.Context + shutdownFn context.CancelFunc + childRoutines *errgroup.Group + log log.Logger + cfg *setting.Cfg + shutdownReason string RouteRegister api.RouteRegister `inject:""` HttpServer *api.HTTPServer `inject:""` @@ -135,7 +136,8 @@ func (g *GrafanaServerImpl) Start() error { } sendSystemdNotification("READY=1") - return g.startHttpServer() + + return g.childRoutines.Wait() } func (g *GrafanaServerImpl) loadConfiguration() { @@ -154,28 +156,28 @@ func (g *GrafanaServerImpl) loadConfiguration() { g.cfg.LogConfigSources() } -func (g *GrafanaServerImpl) startHttpServer() error { - g.HttpServer.Init() - - err := g.HttpServer.Start(g.context) - - if err != nil { - return fmt.Errorf("Fail to start server. error: %v", err) - } - - return nil -} - -func (g *GrafanaServerImpl) Shutdown(code int, reason string) { - g.log.Info("Shutdown started", "code", code, "reason", reason) +func (g *GrafanaServerImpl) Shutdown(reason string) { + g.log.Info("Shutdown started", "reason", reason) + g.shutdownReason = reason // call cancel func on root context g.shutdownFn() // wait for child routines - if err := g.childRoutines.Wait(); err != nil && err != context.Canceled { - g.log.Error("Server shutdown completed", "error", err) + g.childRoutines.Wait() +} + +func (g *GrafanaServerImpl) Exit(reason error) { + // default exit code is 1 + code := 1 + + if reason == context.Canceled && g.shutdownReason != "" { + reason = fmt.Errorf(g.shutdownReason) + code = 0 } + + g.log.Error("Server shutdown", "reason", reason) + os.Exit(code) } func (g *GrafanaServerImpl) writePIDFile() { From 23655315b877c18d0a73e0669e4b8cfea39a2255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 18:10:21 +0200 Subject: [PATCH 0579/3000] fix: fixed race condition between http.Server ListenAndServe & Shutdown, now service crash during startup correctly closes http server every time --- pkg/api/http_server.go | 2 ++ pkg/cmd/grafana-server/main.go | 2 +- pkg/cmd/grafana-server/server.go | 34 ++++++++++++++++++++++++-------- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 2cf8f3796ae..f30e5602484 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -71,6 +71,8 @@ func (hs *HTTPServer) Run(ctx context.Context) error { // handle http shutdown on server context done go func() { <-ctx.Done() + // Hacky fix for race condition between ListenAndServe and Shutdown + time.Sleep(time.Millisecond * 100) if err := hs.httpSrv.Shutdown(context.Background()); err != nil { hs.log.Error("Failed to shutdown server", "error", err) } diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 23cac74ccdb..02ea7a7f8f1 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -86,7 +86,7 @@ func main() { go listenToSystemSignals(server) - err := server.Start() + err := server.Run() trace.Stop() log.Close() diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index aff40111ae9..43f6bed1646 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -54,18 +54,19 @@ func NewGrafanaServer() *GrafanaServerImpl { } type GrafanaServerImpl struct { - context context.Context - shutdownFn context.CancelFunc - childRoutines *errgroup.Group - log log.Logger - cfg *setting.Cfg - shutdownReason string + context context.Context + shutdownFn context.CancelFunc + childRoutines *errgroup.Group + log log.Logger + cfg *setting.Cfg + shutdownReason string + shutdownInProgress bool RouteRegister api.RouteRegister `inject:""` HttpServer *api.HTTPServer `inject:""` } -func (g *GrafanaServerImpl) Start() error { +func (g *GrafanaServerImpl) Run() error { g.loadConfiguration() g.writePIDFile() @@ -129,8 +130,24 @@ func (g *GrafanaServerImpl) Start() error { } g.childRoutines.Go(func() error { + // Skip starting new service is we are shutting down + // Ccan happen when service crash during startup + if g.shutdownInProgress { + return nil + } + err := service.Run(g.context) - g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) + + // If error is not canceled then the service crashed + if err != context.Canceled { + g.log.Error("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) + } else { + g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) + } + + // Mark that we are in shutdown mode + // So more services are not started + g.shutdownInProgress = true return err }) } @@ -159,6 +176,7 @@ func (g *GrafanaServerImpl) loadConfiguration() { func (g *GrafanaServerImpl) Shutdown(reason string) { g.log.Info("Shutdown started", "reason", reason) g.shutdownReason = reason + g.shutdownInProgress = true // call cancel func on root context g.shutdownFn() From e3ea6c683c928bcb84c5710b447b4b8f1ec087ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 18:27:54 +0200 Subject: [PATCH 0580/3000] fix: comment spell fix --- pkg/cmd/grafana-server/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 43f6bed1646..e03018a4167 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -130,8 +130,8 @@ func (g *GrafanaServerImpl) Run() error { } g.childRoutines.Go(func() error { - // Skip starting new service is we are shutting down - // Ccan happen when service crash during startup + // Skip starting new service when shutting down + // Can happen when service stop/return during startup if g.shutdownInProgress { return nil } From b5e70d4607996120e78eca384ce5eb3643b6ff04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 19:00:16 +0200 Subject: [PATCH 0581/3000] fix: removed unused channel --- pkg/cmd/grafana-server/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 02ea7a7f8f1..c7ea6bb432b 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -39,7 +39,6 @@ var enterprise string var configFile = flag.String("config", "", "path to config file") var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory") var pidFile = flag.String("pidfile", "", "path to pid file") -var exitChan = make(chan int) func main() { v := flag.Bool("v", false, "prints current version and exits") From c40a50829d622f38c51462e13daec5d92bd75326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 21:30:15 +0200 Subject: [PATCH 0582/3000] fix: removed manully added http server from inject graph as it is now a self registered service --- pkg/cmd/grafana-server/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index e03018a4167..0f364076a15 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -88,7 +88,6 @@ func (g *GrafanaServerImpl) Run() error { serviceGraph.Provide(&inject.Object{Value: g.cfg}) serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()}) serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) - serviceGraph.Provide(&inject.Object{Value: api.HTTPServer{}}) // self registered services services := registry.GetServices() From 764fa15e2415c9394d1064dc2256de33df691c12 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 2 May 2018 23:03:37 +0200 Subject: [PATCH 0583/3000] dont shadow format passed in as function parameter --- public/app/features/templating/template_srv.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index f6274a80165..e7c1dc7f102 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -179,16 +179,16 @@ export class TemplateSrv { return target; } - var variable, systemValue, value; + var variable, systemValue, value, fmt; this.regex.lastIndex = 0; return target.replace(this.regex, (match, var1, var2, fmt2, var3, fmt3) => { variable = this.index[var1 || var2 || var3]; - format = fmt2 || fmt3 || format; + fmt = fmt2 || fmt3 || format; if (scopedVars) { value = scopedVars[var1 || var2 || var3]; if (value) { - return this.formatValue(value.value, format, variable); + return this.formatValue(value.value, fmt, variable); } } @@ -198,7 +198,7 @@ export class TemplateSrv { systemValue = this.grafanaVariables[variable.current.value]; if (systemValue) { - return this.formatValue(systemValue, format, variable); + return this.formatValue(systemValue, fmt, variable); } value = variable.current.value; @@ -210,7 +210,7 @@ export class TemplateSrv { } } - var res = this.formatValue(value, format, variable); + var res = this.formatValue(value, fmt, variable); return res; }); } From fc7d8761588c7dde089b8b0707b239b6d776f6eb Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 3 May 2018 00:17:18 +0200 Subject: [PATCH 0584/3000] try to fix table --- docs/sources/guides/whats-new-in-v5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-1.md b/docs/sources/guides/whats-new-in-v5-1.md index 66b04ce3d50..ecab274bd32 100644 --- a/docs/sources/guides/whats-new-in-v5-1.md +++ b/docs/sources/guides/whats-new-in-v5-1.md @@ -100,7 +100,7 @@ In the table below you can see some examples and you can find all different opti Filter Option | Example | Raw | Interpolated | Description ------------ | ------------- | ------------- | ------------- | ------------- `glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob -`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.|test2)` | Formats multi-value variable into a regex string +`regex` | ${servers:regex} | `'test.', 'test2'` | ```(test\\.|test2)``` | Formats multi-value variable into a regex string `pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string @@ -122,4 +122,4 @@ More information in the [Provisioning documentation](/features/datasources/prome ## Changelog Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list -of new features, changes, and bug fixes. \ No newline at end of file +of new features, changes, and bug fixes. From 7f8dacd0879ff31e1d016ee1b066733062ca4c26 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 3 May 2018 00:19:47 +0200 Subject: [PATCH 0585/3000] use ascii code for pipe symbol to not mess up markdown table --- docs/sources/guides/whats-new-in-v5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-1.md b/docs/sources/guides/whats-new-in-v5-1.md index ecab274bd32..19317074d6e 100644 --- a/docs/sources/guides/whats-new-in-v5-1.md +++ b/docs/sources/guides/whats-new-in-v5-1.md @@ -100,8 +100,8 @@ In the table below you can see some examples and you can find all different opti Filter Option | Example | Raw | Interpolated | Description ------------ | ------------- | ------------- | ------------- | ------------- `glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob -`regex` | ${servers:regex} | `'test.', 'test2'` | ```(test\\.|test2)``` | Formats multi-value variable into a regex string -`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string +`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.|test2)` | Formats multi-value variable into a regex string +`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string ## Improved workflow for provisioned dashboards From f52920aa01c4c304b393a677b3194c321ca7caaa Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 3 May 2018 00:23:35 +0200 Subject: [PATCH 0586/3000] pipe escape try #3 --- docs/sources/guides/whats-new-in-v5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-1.md b/docs/sources/guides/whats-new-in-v5-1.md index 19317074d6e..64b9a73eb9b 100644 --- a/docs/sources/guides/whats-new-in-v5-1.md +++ b/docs/sources/guides/whats-new-in-v5-1.md @@ -100,8 +100,8 @@ In the table below you can see some examples and you can find all different opti Filter Option | Example | Raw | Interpolated | Description ------------ | ------------- | ------------- | ------------- | ------------- `glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob -`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.|test2)` | Formats multi-value variable into a regex string -`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string +`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.`|`test2)` | Formats multi-value variable into a regex string +`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.`|`test2` | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string ## Improved workflow for provisioned dashboards From fc0a4b34a1a91b0b5615123c797a67875c97e9aa Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 3 May 2018 01:25:52 +0200 Subject: [PATCH 0587/3000] Add missing items to Gopkg.lock --- Gopkg.lock | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Gopkg.lock b/Gopkg.lock index 3a7466c312a..41fc92313d1 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -111,6 +111,18 @@ ] revision = "270bc3860bb94dd3a3ffd047377d746c5e276726" +[[projects]] + branch = "master" + name = "github.com/facebookgo/inject" + packages = ["."] + revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b" + +[[projects]] + branch = "master" + name = "github.com/facebookgo/structtag" + packages = ["."] + revision = "217e25fb96916cc60332e399c9aa63f5c422ceed" + [[projects]] name = "github.com/fatih/color" packages = ["."] @@ -649,6 +661,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "2bd5b309496d57e2189a1cc28f5c1c41398c19729ba0cf53c8cbb17ea3f706b5" + inputs-digest = "bd54a1a836599d90b36d4ac1af56d716ef9ca5be4865e217bddd49e3d32a1997" solver-name = "gps-cdcl" solver-version = 1 From 83d599670da3832982553327003dbb5f606e6bfe Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 3 May 2018 11:54:02 +0300 Subject: [PATCH 0588/3000] scroll: remove firefox scrollbars --- public/sass/pages/_dashboard.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index aeb9e28975b..471e90ed9cf 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -44,9 +44,8 @@ div.flot-text { padding: $panel-padding; height: calc(100% - 27px); position: relative; - overflow: hidden; // Fixes scrolling on mobile devices - overflow-y: scroll; + overflow: auto; } .panel-title-container { From d518ed5330e85d7ac192f52c6432d6f65024fbd8 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 3 May 2018 15:46:21 +0200 Subject: [PATCH 0589/3000] changelog: add notes for ##11754, #11758, #11710 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b977edaadf0..3772812f254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) + +# 5.1.1 (unreleased) + +* **LDAP**: LDAP login with MariaDB/MySQL database and dn>100 chars not possible [#11754](https://github.com/grafana/grafana/issues/11754) +* **Build**: AppVeyor Windows build missing version and commit info [#11758](https://github.com/grafana/grafana/issues/11758) +* **Scroll**: Scroll can't start in graphs on Chrome mobile [#11710](https://github.com/grafana/grafana/issues/11710) + # 5.1.0 (2018-04-26) * **Folders**: Default permissions on folder are not shown as inherited in its dashboards [#11668](https://github.com/grafana/grafana/issues/11668) From 8a9da4ba66f472cee8f4c10791eda18da63a3017 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 3 May 2018 18:42:14 +0200 Subject: [PATCH 0590/3000] changelog: notes about closing #11690 [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3772812f254..f0c59ccc310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) - +* **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda) # 5.1.1 (unreleased) From a806f542c64a61cf41d7f9fc0620b6a69100100e Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 3 May 2018 18:42:58 +0200 Subject: [PATCH 0591/3000] test if default variable interpolation is effective when no specific format is specified --- .../app/features/templating/specs/template_srv.jest.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 5290a883c48..59915776b4f 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -136,6 +136,11 @@ describe('templateSrv', function() { var target = _templateSrv.replace('this=${test:pipe}', {}); expect(target).toBe('this=value1|value2'); }); + + it('should replace ${test:pipe} with piped value and $test with globbed value', function() { + var target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); + expect(target).toBe('value1|value2,{value1,value2}'); + }); }); describe('variable with all option', function() { @@ -164,6 +169,11 @@ describe('templateSrv', function() { var target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.{value1,value2}.filters'); }); + + it('should replace ${test:pipe} with piped value and $test with globbed value', function() { + var target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); + expect(target).toBe('value1|value2,{value1,value2}'); + }); }); describe('variable with all option and custom value', function() { From 4d2e6b4a34a0f675131de7776c30fe2b2b07fa3a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 3 May 2018 19:13:57 +0200 Subject: [PATCH 0592/3000] changelog: add notes about closing #11800 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c59ccc310..5dd958e237a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) * **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda) +* **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm) # 5.1.1 (unreleased) From c897485958ff6932bb7277ed85913fad8a1172c0 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 4 May 2018 10:30:42 +0200 Subject: [PATCH 0593/3000] fixed text color in light theme --- public/sass/components/_timepicker.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index 9b71e8e7c05..e4d8f4555e0 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -77,7 +77,7 @@ border: none; color: $text-color; &.active span { - color: $blue; + color: $query-blue; font-weight: bold; } .text-info { @@ -88,6 +88,12 @@ font-size: $font-size-sm; padding: 5px 11px; } + &:hover { + color: $text-color-strong; + } + &[disabled] { + color: $text-color; + } } } From 515eab240538b68932e0ee67def5b52b4b25987c Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 4 May 2018 11:46:17 +0200 Subject: [PATCH 0594/3000] added left:unset to counter left:0 in recent react-select release --- public/sass/components/_form_select_box.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_form_select_box.scss b/public/sass/components/_form_select_box.scss index beee2db15ab..0401f06eba8 100644 --- a/public/sass/components/_form_select_box.scss +++ b/public/sass/components/_form_select_box.scss @@ -102,5 +102,6 @@ $select-option-selected-bg: $dropdownLinkBackgroundActive; .gf-form-input--form-dropdown-right { .Select-menu-outer { right: 0; + left: unset; } } From 8523b1e4105568f63a484f8c7d627e074b300bfa Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 4 May 2018 15:03:30 +0200 Subject: [PATCH 0595/3000] changelog: add notes about closing #11616 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd958e237a..1ae35bed3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) * **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda) * **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm) +* **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616) # 5.1.1 (unreleased) From ed4dc241cc1dc1e48eaef72cf06642cd32f7ef68 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 5 May 2018 10:47:44 +0200 Subject: [PATCH 0596/3000] escape pipe symbol same way as in templating docs --- docs/sources/guides/whats-new-in-v5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-1.md b/docs/sources/guides/whats-new-in-v5-1.md index 64b9a73eb9b..d992fd9062a 100644 --- a/docs/sources/guides/whats-new-in-v5-1.md +++ b/docs/sources/guides/whats-new-in-v5-1.md @@ -100,8 +100,8 @@ In the table below you can see some examples and you can find all different opti Filter Option | Example | Raw | Interpolated | Description ------------ | ------------- | ------------- | ------------- | ------------- `glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob -`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.`|`test2)` | Formats multi-value variable into a regex string -`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.`|`test2` | Formats multi-value variable into a pipe-separated string +`regex` | ${servers:regex} | `'test.', 'test2'` | (test\.|test2) | Formats multi-value variable into a regex string +`pipe` | ${servers:pipe} | `'test.', 'test2'` | test.|test2 | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string ## Improved workflow for provisioned dashboards From 2ee59ccad84fdaae580c5706c776c32f2da62bd4 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 7 May 2018 08:03:30 +0200 Subject: [PATCH 0597/3000] Add panel scrolling docs (#11826) --- docs/sources/plugins/developing/apps.md | 2 +- .../sources/plugins/developing/datasources.md | 2 +- docs/sources/plugins/developing/panels.md | 29 ++++++++++++------- .../sources/plugins/developing/plugin.json.md | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/sources/plugins/developing/apps.md b/docs/sources/plugins/developing/apps.md index a3fc35066f6..155f97461c9 100644 --- a/docs/sources/plugins/developing/apps.md +++ b/docs/sources/plugins/developing/apps.md @@ -5,7 +5,7 @@ type = "docs" [menu.docs] name = "Developing App Plugins" parent = "developing" -weight = 6 +weight = 4 +++ # Grafana Apps diff --git a/docs/sources/plugins/developing/datasources.md b/docs/sources/plugins/developing/datasources.md index 09a005ba714..064f3a850ae 100644 --- a/docs/sources/plugins/developing/datasources.md +++ b/docs/sources/plugins/developing/datasources.md @@ -5,7 +5,7 @@ type = "docs" [menu.docs] name = "Developing Datasource Plugins" parent = "developing" -weight = 6 +weight = 5 +++ # Datasources diff --git a/docs/sources/plugins/developing/panels.md b/docs/sources/plugins/developing/panels.md index 26db69c7c94..d679288e2d2 100644 --- a/docs/sources/plugins/developing/panels.md +++ b/docs/sources/plugins/developing/panels.md @@ -1,16 +1,11 @@ ---- -page_title: Plugin panel -page_description: Panel plugins for Grafana -page_keywords: grafana, plugins, documentation ---- - - +++ -title = "Installing Plugins" +title = "Developing Panel Plugins" +keywords = ["grafana", "plugins", "panel", "documentation"] type = "docs" [menu.docs] +name = "Developing Panel Plugins" parent = "developing" -weight = 1 +weight = 4 +++ @@ -20,7 +15,21 @@ Panels are the main building blocks of dashboards. ## Panel development -Examples + +### Scrolling +The grafana dashboard framework controls the panel height. To enable a scrollbar within the panel the PanelCtrl needs to set the scrollable static variable: + +```javascript +export class MyPanelCtrl extends PanelCtrl { + static scrollable = true; + ... +``` + +In this case, make sure the template has a single `
    ...
    ` root. The plugin loader will modifiy that element adding a scrollbar. + + + +### Examples - [clock-panel](https://github.com/grafana/clock-panel) - [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts) diff --git a/docs/sources/plugins/developing/plugin.json.md b/docs/sources/plugins/developing/plugin.json.md index 7de5e91986f..2d21a665207 100644 --- a/docs/sources/plugins/developing/plugin.json.md +++ b/docs/sources/plugins/developing/plugin.json.md @@ -5,7 +5,7 @@ type = "docs" [menu.docs] name = "plugin.json Schema" parent = "developing" -weight = 6 +weight = 8 +++ # Plugin.json From 1fbac909cb6f3334af1788683bbc1819398f4904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Podlipsk=C3=BD?= Date: Mon, 7 May 2018 08:11:48 +0200 Subject: [PATCH 0598/3000] Remove preceding `/` from public JS path (#11804) --- scripts/webpack/webpack.common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index c2909b1c8d3..01ef1bc56d3 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -12,7 +12,7 @@ module.exports = { output: { path: path.resolve(__dirname, '../../public/build'), filename: '[name].[hash].js', - publicPath: "/public/build/", + publicPath: "public/build/", }, resolve: { extensions: ['.ts', '.tsx', '.es6', '.js', '.json'], From 217d43e512e0f33be6411835e80886ea315b05f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 10:13:39 +0200 Subject: [PATCH 0599/3000] Update ROADMAP.md --- ROADMAP.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e7bed99489e..adfde503432 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,25 +2,17 @@ This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. But it will give you an idea of our current vision and plan. - + ### Short term (1-2 months) -- v5.1 - - Build speed improvements & integration test execution - - Kubernetes friendly docker container - - Enterprise LDAP - - Provisioning workflow - - MSSQL datasource - -### Mid term (2-4 months) - -- v5.2 - - Azure monitor backend rewrite - Elasticsearch alerting - First login registration view - - Backend plugins? (alert notifiers, auth) - Crossplatform builds - - IFQL Initial support + - Backend service refactorings + +### Mid term (2-4 months) + - Multi-Stat panel + - Explore UI ### Long term (4 - 8 months) From ec7703bad7c4d4dec1315b0df299e8989af850a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 10:14:04 +0200 Subject: [PATCH 0600/3000] Update ROADMAP.md --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index adfde503432..ad351729309 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,4 +1,4 @@ -# Roadmap (2018-02-22) +# Roadmap (2018-05-06) This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. But it will give you an idea of our current vision and plan. From 871b85f199992fc80a96233d14ab57b68f693d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 10:16:39 +0200 Subject: [PATCH 0601/3000] Update ROADMAP.md --- ROADMAP.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index ad351729309..fa3b837585a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,6 +13,8 @@ But it will give you an idea of our current vision and plan. ### Mid term (2-4 months) - Multi-Stat panel - Explore UI + - React Panels + - Templating Query Editor UI Plugin hook ### Long term (4 - 8 months) From 5d54bc00e1a958d994c27ee6a76b1909d82e83af Mon Sep 17 00:00:00 2001 From: Florian Plattner Date: Mon, 7 May 2018 10:22:54 +0200 Subject: [PATCH 0602/3000] Fix/improved csv output (#11740) * fix: initial cleanup and implementation * feat: finish special character escaping * feat: updates fileExport to generate RFC-4180 compliant CSV * chore: replace html decoder with the lodash version and final cleanup * fix: restore character html decoding --- public/app/core/specs/file_export.jest.ts | 100 +++++++++++--- public/app/core/utils/file_export.ts | 158 ++++++++++++++-------- 2 files changed, 185 insertions(+), 73 deletions(-) diff --git a/public/app/core/specs/file_export.jest.ts b/public/app/core/specs/file_export.jest.ts index bbb894094ff..82097227b97 100644 --- a/public/app/core/specs/file_export.jest.ts +++ b/public/app/core/specs/file_export.jest.ts @@ -30,17 +30,17 @@ describe('file_export', () => { it('should export points in proper order', () => { let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); const expectedText = - 'Series;Time;Value\n' + - 'series_1;1500026100;1\n' + - 'series_1;1500026200;2\n' + - 'series_1;1500026300;null\n' + - 'series_1;1500026400;null\n' + - 'series_1;1500026500;null\n' + - 'series_1;1500026600;6\n' + - 'series_2;1500026100;11\n' + - 'series_2;1500026200;12\n' + - 'series_2;1500026300;13\n' + - 'series_2;1500026500;15\n'; + '"Series";"Time";"Value"\r\n' + + '"series_1";"1500026100";1\r\n' + + '"series_1";"1500026200";2\r\n' + + '"series_1";"1500026300";null\r\n' + + '"series_1";"1500026400";null\r\n' + + '"series_1";"1500026500";null\r\n' + + '"series_1";"1500026600";6\r\n' + + '"series_2";"1500026100";11\r\n' + + '"series_2";"1500026200";12\r\n' + + '"series_2";"1500026300";13\r\n' + + '"series_2";"1500026500";15'; expect(text).toBe(expectedText); }); @@ -50,15 +50,79 @@ describe('file_export', () => { it('should export points in proper order', () => { let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); const expectedText = - 'Time;series_1;series_2\n' + - '1500026100;1;11\n' + - '1500026200;2;12\n' + - '1500026300;null;13\n' + - '1500026400;null;null\n' + - '1500026500;null;15\n' + - '1500026600;6;null\n'; + '"Time";"series_1";"series_2"\r\n' + + '"1500026100";1;11\r\n' + + '"1500026200";2;12\r\n' + + '"1500026300";null;13\r\n' + + '"1500026400";null;null\r\n' + + '"1500026500";null;15\r\n' + + '"1500026600";6;null'; expect(text).toBe(expectedText); }); }); + + describe('when exporting table data to csv', () => { + + it('should properly escape special characters and quote all string values', () => { + const inputTable = { + columns: [ + { title: 'integer_value' }, + { text: 'string_value' }, + { title: 'float_value' }, + { text: 'boolean_value' }, + ], + rows: [ + [123, 'some_string', 1.234, true], + [0o765, 'some string with " in the middle', 1e-2, false], + [0o765, 'some string with "" in the middle', 1e-2, false], + [0o765, 'some string with """ in the middle', 1e-2, false], + [0o765, '"some string with " at the beginning', 1e-2, false], + [0o765, 'some string with " at the end"', 1e-2, false], + [0x123, 'some string with \n in the middle', 10.01, false], + [0b1011, 'some string with ; in the middle', -12.34, true], + [123, 'some string with ;; in the middle', -12.34, true], + ], + }; + + const returnedText = fileExport.convertTableDataToCsv(inputTable, false); + + const expectedText = + '"integer_value";"string_value";"float_value";"boolean_value"\r\n' + + '123;"some_string";1.234;true\r\n' + + '501;"some string with "" in the middle";0.01;false\r\n' + + '501;"some string with """" in the middle";0.01;false\r\n' + + '501;"some string with """""" in the middle";0.01;false\r\n' + + '501;"""some string with "" at the beginning";0.01;false\r\n' + + '501;"some string with "" at the end""";0.01;false\r\n' + + '291;"some string with \n in the middle";10.01;false\r\n' + + '11;"some string with ; in the middle";-12.34;true\r\n' + + '123;"some string with ;; in the middle";-12.34;true'; + + expect(returnedText).toBe(expectedText); + }); + + it('should decode HTML encoded characters', function() { + const inputTable = { + columns: [ + { text: 'string_value' }, + ], + rows: [ + ['"&ä'], + ['"some html"'], + ['some text'] + ], + }; + + const returnedText = fileExport.convertTableDataToCsv(inputTable, false); + + const expectedText = + '"string_value"\r\n' + + '"""&ä"\r\n' + + '"""some html"""\r\n' + + '"some text"'; + + expect(returnedText).toBe(expectedText); + }); + }); }); diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 670326fc068..f25d340a0be 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -1,59 +1,108 @@ -import _ from 'lodash'; +import { isBoolean, isNumber, sortedUniq, sortedIndexOf, unescape as htmlUnescaped } from 'lodash'; import moment from 'moment'; import { saveAs } from 'file-saver'; +import { isNullOrUndefined } from 'util'; const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; const POINT_TIME_INDEX = 1; const POINT_VALUE_INDEX = 0; +const END_COLUMN = ';'; +const END_ROW = '\r\n'; +const QUOTE = '"'; +const EXPORT_FILENAME = 'grafana_data_export.csv'; + +function csvEscaped(text) { + if (!text) { + return text; + } + + return text.split(QUOTE).join(QUOTE + QUOTE); +} + +const domParser = new DOMParser(); +function htmlDecoded(text) { + if (!text) { + return text; + } + + const regexp = /&[^;]+;/g; + function htmlDecoded(value) { + const parsedDom = domParser.parseFromString(value, 'text/html'); + return parsedDom.body.textContent; + } + return text.replace(regexp, htmlDecoded).replace(regexp, htmlDecoded); +} + +function formatSpecialHeader(useExcelHeader) { + return useExcelHeader ? `sep=${END_COLUMN}${END_ROW}` : ''; +} + +function formatRow(row, addEndRowDelimiter = true) { + let text = ''; + for (let i = 0; i < row.length; i += 1) { + if (isBoolean(row[i]) || isNullOrUndefined(row[i])) { + text += row[i]; + } else if (isNumber(row[i])) { + text += row[i].toLocaleString(); + } else { + text += `${QUOTE}${csvEscaped(htmlUnescaped(htmlDecoded(row[i])))}${QUOTE}`; + } + + if (i < row.length - 1) { + text += END_COLUMN; + } + } + return addEndRowDelimiter ? text + END_ROW : text; +} + export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n'; - _.each(seriesList, function(series) { - _.each(series.datapoints, function(dp) { - text += - series.alias + ';' + moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat) + ';' + dp[POINT_VALUE_INDEX] + '\n'; - }); - }); + let text = formatSpecialHeader(excel) + formatRow(['Series', 'Time', 'Value']); + for (let seriesIndex = 0; seriesIndex < seriesList.length; seriesIndex += 1) { + for (let i = 0; i < seriesList[seriesIndex].datapoints.length; i += 1) { + text += formatRow( + [ + seriesList[seriesIndex].alias, + moment(seriesList[seriesIndex].datapoints[i][POINT_TIME_INDEX]).format(dateTimeFormat), + seriesList[seriesIndex].datapoints[i][POINT_VALUE_INDEX], + ], + i < seriesList[seriesIndex].datapoints.length - 1 || seriesIndex < seriesList.length - 1 + ); + } + } return text; } export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - var text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); - saveSaveBlob(text, 'grafana_data_export.csv'); + let text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); + saveSaveBlob(text, EXPORT_FILENAME); } export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - let text = (excel ? 'sep=;\n' : '') + 'Time;'; // add header - _.each(seriesList, function(series) { - text += series.alias + ';'; - }); - text = text.substring(0, text.length - 1); - text += '\n'; - + let text = + formatSpecialHeader(excel) + + formatRow( + ['Time'].concat( + seriesList.map(function(val) { + return val.alias; + }) + ) + ); // process data seriesList = mergeSeriesByTime(seriesList); - var dataArr = [[]]; - var sIndex = 1; - _.each(seriesList, function(series) { - var cIndex = 0; - dataArr.push([]); - _.each(series.datapoints, function(dp) { - dataArr[0][cIndex] = moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat); - dataArr[sIndex][cIndex] = dp[POINT_VALUE_INDEX]; - cIndex++; - }); - sIndex++; - }); // make text - for (var i = 0; i < dataArr[0].length; i++) { - text += dataArr[0][i] + ';'; - for (var j = 1; j < dataArr.length; j++) { - text += dataArr[j][i] + ';'; - } - text = text.substring(0, text.length - 1); - text += '\n'; + for (let i = 0; i < seriesList[0].datapoints.length; i += 1) { + const timestamp = moment(seriesList[0].datapoints[i][POINT_TIME_INDEX]).format(dateTimeFormat); + text += formatRow( + [timestamp].concat( + seriesList.map(function(series) { + return series.datapoints[i][POINT_VALUE_INDEX]; + }) + ), + i < seriesList[0].datapoints.length - 1 + ); } return text; @@ -71,15 +120,15 @@ function mergeSeriesByTime(seriesList) { timestamps.push(seriesPoints[j][POINT_TIME_INDEX]); } } - timestamps = _.sortedUniq(timestamps.sort()); + timestamps = sortedUniq(timestamps.sort()); for (let i = 0; i < seriesList.length; i++) { let seriesPoints = seriesList[i].datapoints; - let seriesTimestamps = _.map(seriesPoints, p => p[POINT_TIME_INDEX]); + let seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]); let extendedSeries = []; let pointIndex; for (let j = 0; j < timestamps.length; j++) { - pointIndex = _.sortedIndexOf(seriesTimestamps, timestamps[j]); + pointIndex = sortedIndexOf(seriesTimestamps, timestamps[j]); if (pointIndex !== -1) { extendedSeries.push(seriesPoints[pointIndex]); } else { @@ -93,27 +142,26 @@ function mergeSeriesByTime(seriesList) { export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); - saveSaveBlob(text, 'grafana_data_export.csv'); + saveSaveBlob(text, EXPORT_FILENAME); +} + +export function convertTableDataToCsv(table, excel = false) { + let text = formatSpecialHeader(excel); + // add headline + text += formatRow(table.columns.map(val => val.title || val.text)); + // process data + for (let i = 0; i < table.rows.length; i += 1) { + text += formatRow(table.rows[i], i < table.rows.length - 1); + } + return text; } export function exportTableDataToCsv(table, excel = false) { - var text = excel ? 'sep=;\n' : ''; - // add header - _.each(table.columns, function(column) { - text += (column.title || column.text) + ';'; - }); - text += '\n'; - // process data - _.each(table.rows, function(row) { - _.each(row, function(value) { - text += value + ';'; - }); - text += '\n'; - }); - saveSaveBlob(text, 'grafana_data_export.csv'); + let text = convertTableDataToCsv(table, excel); + saveSaveBlob(text, EXPORT_FILENAME); } export function saveSaveBlob(payload, fname) { - var blob = new Blob([payload], { type: 'text/csv;charset=utf-8' }); + let blob = new Blob([payload], { type: 'text/csv;charset=utf-8;header=present;' }); saveAs(blob, fname); } From b4ad044044800b0e9687aeba6850c03dfbce6ff9 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 7 May 2018 04:33:33 -0400 Subject: [PATCH 0603/3000] better handling for special chars in db config (#11662) --- pkg/services/sqlstore/sqlstore.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index b804d8b1621..cac0c54226c 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -123,7 +123,7 @@ func getEngine() (*xorm.Engine, error) { } cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true", - DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name) + url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Pwd), protocol, DbCfg.Host, url.PathEscape(DbCfg.Name)) if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" { tlsCert, err := makeCert("custom", DbCfg) @@ -142,13 +142,17 @@ func getEngine() (*xorm.Engine, error) { if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 { port = fields[1] } - if DbCfg.Pwd == "" { - DbCfg.Pwd = "''" - } - if DbCfg.User == "" { - DbCfg.User = "''" - } - cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode, DbCfg.ClientCertPath, DbCfg.ClientKeyPath, DbCfg.CaCertPath) + cnnstr = fmt.Sprintf("user='%s' password='%s' host='%s' port='%s' dbname='%s' sslmode='%s' sslcert='%s' sslkey='%s' sslrootcert='%s'", + strings.Replace(DbCfg.User, `'`, `\'`, -1), + strings.Replace(DbCfg.Pwd, `'`, `\'`, -1), + strings.Replace(host, `'`, `\'`, -1), + strings.Replace(port, `'`, `\'`, -1), + strings.Replace(DbCfg.Name, `'`, `\'`, -1), + strings.Replace(DbCfg.SslMode, `'`, `\'`, -1), + strings.Replace(DbCfg.ClientCertPath, `'`, `\'`, -1), + strings.Replace(DbCfg.ClientKeyPath, `'`, `\'`, -1), + strings.Replace(DbCfg.CaCertPath, `'`, `\'`, -1), + ) case "sqlite3": if !filepath.IsAbs(DbCfg.Path) { DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path) From 543c7fe587d85384b150dad1939101424430a676 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 7 May 2018 04:39:16 -0400 Subject: [PATCH 0604/3000] support additional fields in authproxy (#11661) --- docs/sources/installation/configuration.md | 4 ++++ pkg/middleware/auth_proxy.go | 11 +++++++++++ pkg/setting/setting.go | 9 +++++++++ 3 files changed, 24 insertions(+) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index b7fe9040574..baec76df5d9 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -659,6 +659,10 @@ Set to `true` to enable auto sign up of users who do not exist in Grafana DB. De Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header. +### headers + +Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`. +
    ## [session] diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index 36b059e4ae7..144a0ae3a69 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "net/mail" + "reflect" "strings" "time" @@ -111,6 +112,16 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { return true } + for _, field := range []string{"Name", "Email", "Login"} { + if setting.AuthProxyHeaders[field] == "" { + continue + } + + if val := ctx.Req.Header.Get(setting.AuthProxyHeaders[field]); val != "" { + reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val) + } + } + // add/update user in grafana cmd := &m.UpsertUserCommand{ ReqContext: ctx, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 40d7522f775..4db1a2ad7a1 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -125,6 +125,7 @@ var ( AuthProxyAutoSignUp bool AuthProxyLdapSyncTtl int AuthProxyWhitelist string + AuthProxyHeaders map[string]string // Basic Auth BasicAuthEnabled bool @@ -611,6 +612,14 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { AuthProxyLdapSyncTtl = authProxy.Key("ldap_sync_ttl").MustInt() AuthProxyWhitelist = authProxy.Key("whitelist").String() + AuthProxyHeaders = make(map[string]string) + for _, propertyAndHeader := range util.SplitString(authProxy.Key("headers").String()) { + split := strings.SplitN(propertyAndHeader, ":", 2) + if len(split) == 2 { + AuthProxyHeaders[split[0]] = split[1] + } + } + // basic auth authBasic := iniFile.Section("auth.basic") BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) From afec9ec5be0a845619839f255556ccc85f5bd07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 11:16:59 +0200 Subject: [PATCH 0605/3000] Update ROADMAP.md --- ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index fa3b837585a..7b9c043fef1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,13 +6,13 @@ But it will give you an idea of our current vision and plan. ### Short term (1-2 months) - Elasticsearch alerting - - First login registration view - Crossplatform builds - Backend service refactorings + - Explore UI + - First login registration view ### Mid term (2-4 months) - Multi-Stat panel - - Explore UI - React Panels - Templating Query Editor UI Plugin hook From c3cc60b080a7fb56eae39cebe5dfdbed5f729ef1 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 7 May 2018 12:32:18 +0200 Subject: [PATCH 0606/3000] Support for local Docker builds * add a git-ignored `local/` folder for Makefile/Dockerfile * import `local/Makefile` to root Makefile * add `.dockerignore` --- .dockerignore | 18 ++++++++++++++++++ .gitignore | 2 ++ Makefile | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..c79fe777899 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.awcache +.dockerignore +.git +.gitignore +.github +data* +dist +docker +docs +dump.rdb +node_modules +/local +/tmp +/vendor +*.yml +*.md +/vendor +/tmp diff --git a/.gitignore b/.gitignore index cf13dac6d9b..953c98d04aa 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,9 @@ docker-compose.yaml /conf/provisioning/**/custom.yaml profile.cov /grafana +/local .notouch +/Makefile.local /pkg/cmd/grafana-cli/grafana-cli /pkg/cmd/grafana-server/grafana-server /pkg/cmd/grafana-server/debug diff --git a/Makefile b/Makefile index 6f7beb837d8..c1d755d247d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +-include local/Makefile + all: deps build deps-go: From 249c1e8d3dbbca74e7a060fccc1952c7179d94b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 14:02:38 +0200 Subject: [PATCH 0607/3000] fix: loading of css url (images/fonts) --- scripts/webpack/webpack.dev.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 26af661bf9d..a2c60cf15c4 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -85,7 +85,7 @@ module.exports = merge(common, { ] }, require('./sass.rule.js')({ - sourceMap: true, minimize: false, preserveUrl: true + sourceMap: true, minimize: false, preserveUrl: false }, extractSass), { test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, From b804f6d9994f9105af4b394a6c060b890924ea2d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 7 May 2018 14:09:04 +0200 Subject: [PATCH 0608/3000] changelog: 5.1.1 update add notes about closing #11743 and add release data for 5.1.1 [skip ci] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae35bed3a8..74a9f292000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,12 @@ * **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm) * **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616) -# 5.1.1 (unreleased) +# 5.1.1 (2018-05-07) * **LDAP**: LDAP login with MariaDB/MySQL database and dn>100 chars not possible [#11754](https://github.com/grafana/grafana/issues/11754) * **Build**: AppVeyor Windows build missing version and commit info [#11758](https://github.com/grafana/grafana/issues/11758) * **Scroll**: Scroll can't start in graphs on Chrome mobile [#11710](https://github.com/grafana/grafana/issues/11710) +* **Units**: Revert renaming of unit key ppm [#11743](https://github.com/grafana/grafana/issues/11743) # 5.1.0 (2018-04-26) From 29c9d3f74c29d654cdaf5b1e9bda2237a8338c37 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 7 May 2018 08:18:21 -0400 Subject: [PATCH 0609/3000] fix root_url in docs & comments (#11819) * fix root_url in docs & comments * include ports in docker-compose config --- docker/blocks/apache_proxy/docker-compose.yaml | 2 +- docker/blocks/nginx_proxy/docker-compose.yaml | 2 +- docs/sources/installation/behind_proxy.md | 4 ++-- docs/sources/tutorials/iis.md | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docker/blocks/apache_proxy/docker-compose.yaml b/docker/blocks/apache_proxy/docker-compose.yaml index 2aec3d4bc4f..86d4befadd6 100644 --- a/docker/blocks/apache_proxy/docker-compose.yaml +++ b/docker/blocks/apache_proxy/docker-compose.yaml @@ -2,7 +2,7 @@ # http://localhost:3000 (Grafana running locally) # # Please note that you'll need to change the root_url in the Grafana configuration: -# root_url = %(protocol)s://%(domain)s:/grafana/ +# root_url = %(protocol)s://%(domain)s:10081/grafana/ apacheproxy: build: blocks/apache_proxy diff --git a/docker/blocks/nginx_proxy/docker-compose.yaml b/docker/blocks/nginx_proxy/docker-compose.yaml index 7c3447ade5c..a0ceceb83ac 100644 --- a/docker/blocks/nginx_proxy/docker-compose.yaml +++ b/docker/blocks/nginx_proxy/docker-compose.yaml @@ -2,7 +2,7 @@ # http://localhost:3000 (Grafana running locally) # # Please note that you'll need to change the root_url in the Grafana configuration: -# root_url = %(protocol)s://%(domain)s:/grafana/ +# root_url = %(protocol)s://%(domain)s:10080/grafana/ nginxproxy: build: blocks/nginx_proxy diff --git a/docs/sources/installation/behind_proxy.md b/docs/sources/installation/behind_proxy.md index f1a00a5b1cc..89711aecb46 100644 --- a/docs/sources/installation/behind_proxy.md +++ b/docs/sources/installation/behind_proxy.md @@ -53,7 +53,7 @@ server { ```bash [server] domain = foo.bar -root_url = %(protocol)s://%(domain)s:/grafana +root_url = %(protocol)s://%(domain)s/grafana/ ``` #### Nginx configuration with sub path @@ -98,7 +98,7 @@ Given: ```bash [server] domain = localhost:8080 - root_url = %(protocol)s://%(domain)s:/grafana + root_url = %(protocol)s://%(domain)s/grafana/ ``` Create an Inbound Rule for the parent website (localhost:8080 in this example) in IIS Manager with the following settings: diff --git a/docs/sources/tutorials/iis.md b/docs/sources/tutorials/iis.md index 63a41d67c16..896181c4a9f 100644 --- a/docs/sources/tutorials/iis.md +++ b/docs/sources/tutorials/iis.md @@ -16,7 +16,7 @@ Example: - Parent site: http://localhost:8080 - Grafana: http://localhost:3000 -Grafana as a subpath: http://localhost:8080/grafana +Grafana as a subpath: http://localhost:8080/grafana ## Setup @@ -33,7 +33,7 @@ Given that the subpath should be `grafana` and the parent site is `localhost:808 ```bash [server] domain = localhost:8080 -root_url = %(protocol)s://%(domain)s:/grafana +root_url = %(protocol)s://%(domain)s/grafana/ ``` Restart the Grafana server after changing the config file. @@ -74,11 +74,11 @@ When navigating to the grafana url (`http://localhost:8080/grafana` in the examp 1. The `root_url` setting in the Grafana config file does not match the parent url with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files): - `; root_url = %(protocol)s://%(domain)s:/grafana` + `; root_url = %(protocol)s://%(domain)s/grafana/` 2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS: - `root_url = %(protocol)s://%(domain)s:/grafana` + `root_url = %(protocol)s://%(domain)s/grafana/` pattern in Inbound Rule: `wrongsubpath(/)?(.*)` From 23c88a3b3f5c215a196d977c17a00d9b3903c6df Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 7 May 2018 14:57:18 +0200 Subject: [PATCH 0610/3000] docs: update installation instructions targeting v5.1.1 stable --- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 10 +++++----- docs/sources/installation/windows.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index dccb880ec74..83d26351295 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,7 +15,7 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [grafana_5.1.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb) +Stable for Debian-based Linux | [grafana_5.1.1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.1_amd64.deb) @@ -27,9 +27,9 @@ installation. ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.1_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.0_amd64.deb +sudo dpkg -i grafana_5.1.1_amd64.deb ``` @@ -28,7 +28,7 @@ installation. You can install Grafana using Yum directly. ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.1-1.x86_64.rpm ``` @@ -27,9 +27,9 @@ installation. ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.1_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.2_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.1_amd64.deb +sudo dpkg -i grafana_5.1.2_amd64.deb ``` @@ -28,7 +28,7 @@ installation. You can install Grafana using Yum directly. ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.1-1.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.2-1.x86_64.rpm ``` @@ -27,9 +27,9 @@ installation. ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.2_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.3_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.2_amd64.deb +sudo dpkg -i grafana_5.1.3_amd64.deb ``` @@ -28,7 +28,7 @@ installation. You can install Grafana using Yum directly. ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.2-1.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3-1.x86_64.rpm ``` - - - - - - diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index aec08d72258..0a27df75164 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -1,4 +1,4 @@ -@import '~react-grid-layout-grafana/css/styles.css'; +@import '~react-grid-layout/css/styles.css'; @import '~react-resizable/css/styles.css'; .panel-in-fullscreen { @@ -44,11 +44,6 @@ border-right: 2px solid $gray-1; border-bottom: 2px solid $gray-1; } - // temp fix since we use old commit of grid component - // this can be removed when we revert to non fork grid component - .react-grid-item > .react-resizable-handle { - background-image: url('../img/resize-handle-white.svg'); - } } .theme-light { diff --git a/yarn.lock b/yarn.lock index cdd71528baa..f58731040c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -405,17 +405,17 @@ angular-native-dragdrop@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/angular-native-dragdrop/-/angular-native-dragdrop-1.2.2.tgz#d646c6b75b131c48073c3f6e36a225b2726d8bae" -angular-route@^1.6.6: - version "1.6.10" - resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.10.tgz#4247a32eab19495624623e96c1626dfba17ebf21" +angular-route@1.6.6: + version "1.6.6" + resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.6.tgz#8c11748aa195c717b1b615a7e746442bfc7c61f4" -angular-sanitize@^1.6.6: - version "1.6.10" - resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.10.tgz#635a362afb2dd040179f17d3a5455962b2c1918f" +angular-sanitize@1.6.6: + version "1.6.6" + resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.6.tgz#0fd065a19931517fbece66596d325d72b6e06041" -angular@^1.6.6: - version "1.6.10" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.10.tgz#eed3080a34d29d0f681ff119b18ce294e3f74826" +angular@1.6.6: + version "1.6.6" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.6.tgz#fd5a3cfb437ce382d854ee01120797978527cb64" ansi-align@^2.0.0: version "2.0.0" @@ -8898,22 +8898,22 @@ react-dom@^16.2.0: object-assign "^4.1.1" prop-types "^15.6.0" -"react-draggable@^2.2.6 || ^3.0.3", react-draggable@^3.0.3: +react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.5.tgz#c031e0ed4313531f9409d6cd84c8ebcec0ddfe2d" dependencies: classnames "^2.2.5" prop-types "^15.6.0" -react-grid-layout-grafana@0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/react-grid-layout-grafana/-/react-grid-layout-grafana-0.16.0.tgz#12242153fcd0bb80a26af8e41694bc2fde788b3a" +react-grid-layout@0.16.6: + version "0.16.6" + resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.6.tgz#9b2407a2b946c2260ebaf66f13b556e1da4efeb2" dependencies: classnames "2.x" lodash.isequal "^4.0.0" prop-types "15.x" - react-draggable "^3.0.3" - react-resizable "^1.7.5" + react-draggable "3.x" + react-resizable "1.x" react-highlight-words@^0.10.0: version "0.10.0" @@ -8973,7 +8973,7 @@ react-reconciler@^0.7.0: object-assign "^4.1.1" prop-types "^15.6.0" -react-resizable@^1.7.5: +react-resizable@1.x: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" dependencies: From a1e6c31ec12a43f7cd7605031e9cbce7c2c667d6 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 29 May 2018 14:00:46 +0200 Subject: [PATCH 0757/3000] devenv: script for setting up default datasources --- .../bulk-testing/bulk-dashboards.yaml | 9 ++ devenv/dashboards/generate-bulk-dashboards.sh | 15 ---- devenv/datasources/default/default.yaml | 82 +++++++++++++++++++ devenv/setup.sh | 61 ++++++++++++++ 4 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 devenv/dashboards/bulk-testing/bulk-dashboards.yaml delete mode 100755 devenv/dashboards/generate-bulk-dashboards.sh create mode 100644 devenv/datasources/default/default.yaml create mode 100755 devenv/setup.sh diff --git a/devenv/dashboards/bulk-testing/bulk-dashboards.yaml b/devenv/dashboards/bulk-testing/bulk-dashboards.yaml new file mode 100644 index 00000000000..7838e4bc342 --- /dev/null +++ b/devenv/dashboards/bulk-testing/bulk-dashboards.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 + +providers: + - name: 'Bulk dashboards' + folder: 'Bulk dashboards' + type: file + options: + path: /home/carl/go/src/github.com/grafana/grafana/devenv/dashboards/bulk-testing + diff --git a/devenv/dashboards/generate-bulk-dashboards.sh b/devenv/dashboards/generate-bulk-dashboards.sh deleted file mode 100755 index 079a5a9c520..00000000000 --- a/devenv/dashboards/generate-bulk-dashboards.sh +++ /dev/null @@ -1,15 +0,0 @@ -#/bin/bash - -if ! type "jsonnet" > /dev/null; then - echo "you need you install jsonnet to run this script" - echo "follow the instructions on https://github.com/google/jsonnet" - exit 1 -fi - -COUNTER=0 -MAX=400 -while [ $COUNTER -lt $MAX ]; do - jsonnet -o "bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" - let COUNTER=COUNTER+1 -done - diff --git a/devenv/datasources/default/default.yaml b/devenv/datasources/default/default.yaml new file mode 100644 index 00000000000..b721c093f3a --- /dev/null +++ b/devenv/datasources/default/default.yaml @@ -0,0 +1,82 @@ +apiVersion: 1 + +datasources: + - name: Graphite + type: graphite + access: proxy + url: http://localhost:8080 + jsonData: + graphiteVersion: "1.1" + + - name: Prometheus + type: prometheus + access: proxy + isDefault: true + url: http://localhost:9090 + + - name: InfluxDB + type: influxdb + access: proxy + database: site + user: grafana + password: grafana + url: http://localhost:8086 + jsonData: + timeInterval: "15s" + + - name: OpenTsdb + type: opentsdb + access: proxy + url: http://localhost:4242 + jsonData: + tsdbResolution: 1 + tsdbVersion: 1 + + - name: Elastic + type: elasticsearch + access: proxy + database: "[metrics-]YYYY.MM.DD" + url: http://localhost:9200 + jsonData: + interval: Daily + timeField: "@timestamp" + + - name: MySQL + type: mysql + url: localhost:3306 + database: grafana + user: grafana + password: password + + - name: MSSQL + type: mssql + url: localhost:1433 + database: grafana + user: grafana + password: "Password!" + + - name: Postgres + type: postgres + url: localhost:5432 + database: grafana + user: grafana + password: password + jsonData: + sslmode: "disable" + + - name: Cloudwatch + type: cloudwatch + editable: true + jsonData: + authType: credentials + defaultRegion: eu-west-2 + + - name: Cloudwatch keys + type: cloudwatch + editable: true + jsonData: + authType: keys + defaultRegion: eu-west-2 + secureJsonData: + accessKey: AKIAJL347VWN6MK63N2A + secretKey: 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 diff --git a/devenv/setup.sh b/devenv/setup.sh new file mode 100755 index 00000000000..d6f8f969e75 --- /dev/null +++ b/devenv/setup.sh @@ -0,0 +1,61 @@ +#/bin/bash + +bulkDashboard() { + + requiresJsonnet + + COUNTER=0 + MAX=400 + while [ $COUNTER -lt $MAX ]; do + jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" + let COUNTER=COUNTER+1 + done + + ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml +} + +requiresJsonnet() { + if ! type "jsonnet" > /dev/null; then + echo "you need you install jsonnet to run this script" + echo "follow the instructions on https://github.com/google/jsonnet" + exit 1 + fi +} + +defaultDashboards() { + echo "not implemented yet" +} + +defaultDatasources() { + echo "setting up all default datasources using provisioning" + + ln -s -f -r ./datasources/default/default.yaml ../conf/provisioning/datasources/custom.yaml +} + +usage() { + echo -e "install.sh\n\tThis script installs my basic setup for a debian laptop\n" + echo "Usage:" + echo " bulk-dashboards - create and provisioning 400 dashboards" + echo " default-datasources - provisiong all core datasources" +} + +main() { + local cmd=$1 + + if [[ -z "$cmd" ]]; then + usage + exit 1 + fi + + if [[ $cmd == "bulk-dashboards" ]]; then + bulkDashboard + elif [[ $cmd == "default-datasources" ]]; then + defaultDatasources + elif [[ $cmd == "default-dashboards" ]]; then + bulkDashboard + else + usage + fi +} + +main "$@" \ No newline at end of file From be34417b3aa85c5eddfcff044ecd3df38c56c905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 May 2018 14:02:52 +0200 Subject: [PATCH 0758/3000] fix: refactoring PR #11996 and fixing issue #11551 16706hashkey in json editors --- public/app/features/annotations/editor_ctrl.ts | 4 ++++ public/app/features/annotations/partials/editor.html | 4 ++-- public/app/features/dashboard/save_provisioned_modal.ts | 2 +- public/app/features/dashboard/settings/settings.ts | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/public/app/features/annotations/editor_ctrl.ts b/public/app/features/annotations/editor_ctrl.ts index 169e2e4c2bb..34b9635ec85 100644 --- a/public/app/features/annotations/editor_ctrl.ts +++ b/public/app/features/annotations/editor_ctrl.ts @@ -70,6 +70,10 @@ export class AnnotationsEditorCtrl { this.mode = 'list'; } + move(index, dir) { + _.move(this.annotations, index, index + dir); + } + add() { this.annotations.push(this.currentAnnotation); this.reset(); diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index e1410ad0fea..65ee7e52bd0 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -33,8 +33,8 @@ {{annotation.datasource || 'Default'}} - - + + diff --git a/public/app/features/dashboard/save_provisioned_modal.ts b/public/app/features/dashboard/save_provisioned_modal.ts index ba96ce0b0b9..3f2dcd0f57b 100644 --- a/public/app/features/dashboard/save_provisioned_modal.ts +++ b/public/app/features/dashboard/save_provisioned_modal.ts @@ -48,7 +48,7 @@ export class SaveProvisionedDashboardModalCtrl { constructor(dashboardSrv) { this.dash = dashboardSrv.getCurrent().getSaveModelClone(); delete this.dash.id; - this.dashboardJson = JSON.stringify(this.dash, null, 2); + this.dashboardJson = angular.toJson(this.dash, true); } save() { diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 5acbbcf29c5..457cac5af72 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -2,6 +2,7 @@ import { coreModule, appEvents, contextSrv } from 'app/core/core'; import { DashboardModel } from '../dashboard_model'; import $ from 'jquery'; import _ from 'lodash'; +import angular from 'angular'; import config from 'app/core/config'; export class SettingsCtrl { @@ -118,7 +119,7 @@ export class SettingsCtrl { this.viewId = this.$location.search().editview; if (this.viewId) { - this.json = JSON.stringify(this.dashboard.getSaveModelClone(), null, 2); + this.json = angular.toJson(this.dashboard.getSaveModelClone(), true); } if (this.viewId === 'settings' && this.dashboard.meta.canMakeEditable) { From 4c9b146bda91ad3a37923c3dcd478109553cd3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 29 May 2018 14:11:05 +0200 Subject: [PATCH 0759/3000] PR: minor change to PR #12004 before merge --- public/sass/components/_panel_singlestat.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/sass/components/_panel_singlestat.scss b/public/sass/components/_panel_singlestat.scss index faaa6fc2447..af11de3b835 100644 --- a/public/sass/components/_panel_singlestat.scss +++ b/public/sass/components/_panel_singlestat.scss @@ -7,7 +7,6 @@ .singlestat-panel-value-container { line-height: 1; - display: table-cell; position: absolute; z-index: 1; font-size: 3em; @@ -16,7 +15,6 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - padding-bottom: 10px; } .singlestat-panel-prefix { From 3ba3fd9a598f73ef719f5497f658ab52dd5e909f Mon Sep 17 00:00:00 2001 From: Christophe Le Guern Date: Tue, 29 May 2018 14:26:33 +0200 Subject: [PATCH 0760/3000] Add new regions to handleGetRegions function (#12082) As public/app/plugins/datasource/cloudwatch/partials/config.html and this file differ between the AWS regions available, I've updated the latest so they share the same data. In that way, the regions() method in dashboards returns the same list as the frontend does. --- pkg/tsdb/cloudwatch/metric_find_query.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index a7d33645b9b..136ee241c2e 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -230,8 +230,8 @@ func parseMultiSelectValue(input string) []string { // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) { regions := []string{ - "ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", - "eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", + "ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1", + "eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", } result := make([]suggestData, 0) From 79575ea124e07fcd106da646787318f8de1f29a7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 29 May 2018 14:28:04 +0200 Subject: [PATCH 0761/3000] changelog: add notes about closing #11494 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0d650c1aee..7c16f4f6e5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * **Login**: Add optional option sign out url for generic oauth [#9847](https://github.com/grafana/grafana/issues/9847), thx [@roidelapluie](https://github.com/roidelapluie) * **Login**: Use proxy server from environment variable if available [#9703](https://github.com/grafana/grafana/issues/9703), thx [@iyeonok](https://github.com/iyeonok) * **Invite users**: Friendlier error message when smtp is not configured [#12087](https://github.com/grafana/grafana/issues/12087), thx [@thurt](https://github.com/thurt) +* **Graphite**: Don't send distributed tracing headers when using direct/browser access mode [#11494](https://github.com/grafana/grafana/issues/11494) # 5.1.3 (2018-05-16) From 1411709db1c8ce65fd45906fcbc43c7757256084 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 29 May 2018 14:07:37 +0200 Subject: [PATCH 0762/3000] provisioning: place testfiles within testdata folder --- .../all-properties/not.yaml.txt => devenv/README.md | 0 devenv/datasources/default/default.yaml | 9 --------- .../provisioning/datasources/config_reader_test.go | 12 ++++++------ .../all-properties/all-properties.yaml | 0 .../all-properties/not.yaml.txt} | 0 .../all-properties/sample.yaml | 0 .../all-properties/second.yaml | 0 .../broken-yaml/broken.yaml | 0 .../broken-yaml/commented.yaml | 0 .../double-default/default-1.yaml | 0 .../double-default/default-2.yaml | 0 .../insert-two-delete-two/one-datasources.yaml | 0 .../insert-two-delete-two/two-datasources.yml | 0 .../two-datasources/two-datasources.yaml | 0 .../version-0/version-0.yaml | 0 .../testdata/zero-datasources/placeholder-for-git | 0 16 files changed, 6 insertions(+), 15 deletions(-) rename pkg/services/provisioning/datasources/test-configs/all-properties/not.yaml.txt => devenv/README.md (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/all-properties/all-properties.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs/zero-datasources/placeholder-for-git => testdata/all-properties/not.yaml.txt} (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/all-properties/sample.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/all-properties/second.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/broken-yaml/broken.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/broken-yaml/commented.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/double-default/default-1.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/double-default/default-2.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/insert-two-delete-two/one-datasources.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/insert-two-delete-two/two-datasources.yml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/two-datasources/two-datasources.yaml (100%) rename pkg/services/provisioning/datasources/{test-configs => testdata}/version-0/version-0.yaml (100%) create mode 100644 pkg/services/provisioning/datasources/testdata/zero-datasources/placeholder-for-git diff --git a/pkg/services/provisioning/datasources/test-configs/all-properties/not.yaml.txt b/devenv/README.md similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/all-properties/not.yaml.txt rename to devenv/README.md diff --git a/devenv/datasources/default/default.yaml b/devenv/datasources/default/default.yaml index b721c093f3a..dc2310f15aa 100644 --- a/devenv/datasources/default/default.yaml +++ b/devenv/datasources/default/default.yaml @@ -71,12 +71,3 @@ datasources: authType: credentials defaultRegion: eu-west-2 - - name: Cloudwatch keys - type: cloudwatch - editable: true - jsonData: - authType: keys - defaultRegion: eu-west-2 - secureJsonData: - accessKey: AKIAJL347VWN6MK63N2A - secretKey: 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 diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go index 89ecc5a0b68..2e407dbe4de 100644 --- a/pkg/services/provisioning/datasources/config_reader_test.go +++ b/pkg/services/provisioning/datasources/config_reader_test.go @@ -13,12 +13,12 @@ import ( var ( logger log.Logger = log.New("fake.log") - twoDatasourcesConfig = "./test-configs/two-datasources" - twoDatasourcesConfigPurgeOthers = "./test-configs/insert-two-delete-two" - doubleDatasourcesConfig = "./test-configs/double-default" - allProperties = "./test-configs/all-properties" - versionZero = "./test-configs/version-0" - brokenYaml = "./test-configs/broken-yaml" + twoDatasourcesConfig = "testdata/two-datasources" + twoDatasourcesConfigPurgeOthers = "testdata/insert-two-delete-two" + doubleDatasourcesConfig = "testdata/double-default" + allProperties = "testdata/all-properties" + versionZero = "testdata/version-0" + brokenYaml = "testdata/broken-yaml" fakeRepo *fakeRepository ) diff --git a/pkg/services/provisioning/datasources/test-configs/all-properties/all-properties.yaml b/pkg/services/provisioning/datasources/testdata/all-properties/all-properties.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/all-properties/all-properties.yaml rename to pkg/services/provisioning/datasources/testdata/all-properties/all-properties.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/zero-datasources/placeholder-for-git b/pkg/services/provisioning/datasources/testdata/all-properties/not.yaml.txt similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/zero-datasources/placeholder-for-git rename to pkg/services/provisioning/datasources/testdata/all-properties/not.yaml.txt diff --git a/pkg/services/provisioning/datasources/test-configs/all-properties/sample.yaml b/pkg/services/provisioning/datasources/testdata/all-properties/sample.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/all-properties/sample.yaml rename to pkg/services/provisioning/datasources/testdata/all-properties/sample.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/all-properties/second.yaml b/pkg/services/provisioning/datasources/testdata/all-properties/second.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/all-properties/second.yaml rename to pkg/services/provisioning/datasources/testdata/all-properties/second.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/broken-yaml/broken.yaml b/pkg/services/provisioning/datasources/testdata/broken-yaml/broken.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/broken-yaml/broken.yaml rename to pkg/services/provisioning/datasources/testdata/broken-yaml/broken.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/broken-yaml/commented.yaml b/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/broken-yaml/commented.yaml rename to pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/double-default/default-1.yaml b/pkg/services/provisioning/datasources/testdata/double-default/default-1.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/double-default/default-1.yaml rename to pkg/services/provisioning/datasources/testdata/double-default/default-1.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/double-default/default-2.yaml b/pkg/services/provisioning/datasources/testdata/double-default/default-2.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/double-default/default-2.yaml rename to pkg/services/provisioning/datasources/testdata/double-default/default-2.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/insert-two-delete-two/one-datasources.yaml b/pkg/services/provisioning/datasources/testdata/insert-two-delete-two/one-datasources.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/insert-two-delete-two/one-datasources.yaml rename to pkg/services/provisioning/datasources/testdata/insert-two-delete-two/one-datasources.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/insert-two-delete-two/two-datasources.yml b/pkg/services/provisioning/datasources/testdata/insert-two-delete-two/two-datasources.yml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/insert-two-delete-two/two-datasources.yml rename to pkg/services/provisioning/datasources/testdata/insert-two-delete-two/two-datasources.yml diff --git a/pkg/services/provisioning/datasources/test-configs/two-datasources/two-datasources.yaml b/pkg/services/provisioning/datasources/testdata/two-datasources/two-datasources.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/two-datasources/two-datasources.yaml rename to pkg/services/provisioning/datasources/testdata/two-datasources/two-datasources.yaml diff --git a/pkg/services/provisioning/datasources/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/datasources/testdata/version-0/version-0.yaml similarity index 100% rename from pkg/services/provisioning/datasources/test-configs/version-0/version-0.yaml rename to pkg/services/provisioning/datasources/testdata/version-0/version-0.yaml diff --git a/pkg/services/provisioning/datasources/testdata/zero-datasources/placeholder-for-git b/pkg/services/provisioning/datasources/testdata/zero-datasources/placeholder-for-git new file mode 100644 index 00000000000..e69de29bb2d From b253284accef14e4ad5fa0d89ee55c8837cb5047 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 29 May 2018 16:52:02 +0200 Subject: [PATCH 0763/3000] devenv: improve readme --- devenv/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/devenv/README.md b/devenv/README.md index e69de29bb2d..4ec6f672f25 100644 --- a/devenv/README.md +++ b/devenv/README.md @@ -0,0 +1,11 @@ +This folder contains useful scripts and configuration for... + +* Configuring datasources in Grafana +* Provision example dashboards in Grafana +* Run preconfiured datasources as docker containers + +want to know more? run setup! + +```bash +./setup.sh +``` From f32e3a29609ad311595ec7e6b87b6c740d3ec270 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 29 May 2018 17:22:52 +0200 Subject: [PATCH 0764/3000] changelog: note about closing #11858 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c16f4f6e5b..9eb6125492d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * **Login**: Use proxy server from environment variable if available [#9703](https://github.com/grafana/grafana/issues/9703), thx [@iyeonok](https://github.com/iyeonok) * **Invite users**: Friendlier error message when smtp is not configured [#12087](https://github.com/grafana/grafana/issues/12087), thx [@thurt](https://github.com/thurt) * **Graphite**: Don't send distributed tracing headers when using direct/browser access mode [#11494](https://github.com/grafana/grafana/issues/11494) +* **Sidenav**: Show create dashboard link for viewers if at least editor in one folder [#11858](https://github.com/grafana/grafana/issues/11858) # 5.1.3 (2018-05-16) From c7acbcdaf5e28092a2be9d44c348d2a767bc7e3b Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 30 May 2018 08:46:44 +0200 Subject: [PATCH 0765/3000] provisioning: enable relative path's this commit enable relatives path for provisioning dashboards. this enables easier dev setups --- .../bulk-testing/bulk-dashboards.yaml | 2 +- .../provisioning/dashboards/file_reader.go | 8 +- .../dashboards/file_reader_test.go | 75 ++++++++++++------- 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/devenv/dashboards/bulk-testing/bulk-dashboards.yaml b/devenv/dashboards/bulk-testing/bulk-dashboards.yaml index 7838e4bc342..e0ba8a88e68 100644 --- a/devenv/dashboards/bulk-testing/bulk-dashboards.yaml +++ b/devenv/dashboards/bulk-testing/bulk-dashboards.yaml @@ -5,5 +5,5 @@ providers: folder: 'Bulk dashboards' type: file options: - path: /home/carl/go/src/github.com/grafana/grafana/devenv/dashboards/bulk-testing + path: devenv/dashboards/bulk-testing diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index e5186e12f06..93846f5c474 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -47,9 +47,15 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade log.Error("Cannot read directory", "error", err) } + absPath, err := filepath.Abs(path) + if err != nil { + log.Error("Could not create absolute path ", "path", path) + absPath = path //if .Abs return an error we fallback to path + } + return &fileReader{ Cfg: cfg, - Path: path, + Path: absPath, log: log, dashboardService: dashboards.NewProvisioningService(), }, nil diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 084fae1310a..a04fbb23f82 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -15,14 +15,57 @@ import ( ) var ( - defaultDashboards = "./testdata/test-dashboards/folder-one" - brokenDashboards = "./testdata/test-dashboards/broken-dashboards" - oneDashboard = "./testdata/test-dashboards/one-dashboard" - containingId = "./testdata/test-dashboards/containing-id" + defaultDashboards = "testdata/test-dashboards/folder-one" + brokenDashboards = "testdata/test-dashboards/broken-dashboards" + oneDashboard = "testdata/test-dashboards/one-dashboard" + containingId = "testdata/test-dashboards/containing-id" fakeService *fakeDashboardProvisioningService ) +func TestCreatingNewDashboardFileReader(t *testing.T) { + Convey("creating new dashboard file reader", t, func() { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + Options: map[string]interface{}{}, + } + + Convey("using path parameter", func() { + cfg.Options["path"] = defaultDashboards + reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + So(err, ShouldBeNil) + So(reader.Path, ShouldNotEqual, "") + }) + + Convey("using folder as options", func() { + cfg.Options["folder"] = defaultDashboards + reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + So(err, ShouldBeNil) + So(reader.Path, ShouldNotEqual, "") + }) + + Convey("using full path", func() { + cfg.Options["folder"] = "/var/lib/grafana/dashboards" + reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + So(err, ShouldBeNil) + + So(reader.Path, ShouldEqual, "/var/lib/grafana/dashboards") + So(filepath.IsAbs(reader.Path), ShouldBeTrue) + }) + + Convey("using relative path", func() { + cfg.Options["folder"] = defaultDashboards + reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + So(err, ShouldBeNil) + + So(filepath.IsAbs(reader.Path), ShouldBeTrue) + }) + }) +} + func TestDashboardFileReader(t *testing.T) { Convey("Dashboard file reader", t, func() { bus.ClearBusHandlers() @@ -170,30 +213,6 @@ func TestDashboardFileReader(t *testing.T) { }) }) - Convey("Can use bpth path and folder as dashboard path", func() { - cfg := &DashboardsAsConfig{ - Name: "Default", - Type: "file", - OrgId: 1, - Folder: "", - Options: map[string]interface{}{}, - } - - Convey("using path parameter", func() { - cfg.Options["path"] = defaultDashboards - reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) - So(err, ShouldBeNil) - So(reader.Path, ShouldEqual, defaultDashboards) - }) - - Convey("using folder as options", func() { - cfg.Options["folder"] = defaultDashboards - reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) - So(err, ShouldBeNil) - So(reader.Path, ShouldEqual, defaultDashboards) - }) - }) - Reset(func() { dashboards.NewProvisioningService = origNewDashboardProvisioningService }) From 48fc5edda19a7c426d70a494786a46378722a6b2 Mon Sep 17 00:00:00 2001 From: Kim Christensen Date: Wed, 30 May 2018 09:22:16 +0200 Subject: [PATCH 0766/3000] Support InfluxDB count distinct aggregation (#11658) influxdb: support count distinct aggregation --- pkg/tsdb/influxdb/query_part_test.go | 8 + .../plugins/datasource/influxdb/query_part.ts | 23 +++ .../influxdb/specs/query_part.jest.ts | 144 ++++++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go index d23865174c8..cd0863cee9b 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -76,5 +76,13 @@ func TestInfluxdbQueryPart(t *testing.T) { res := part.Render(query, queryContext, "mean(value)") So(res, ShouldEqual, `mean(value) AS "test"`) }) + + Convey("render count distinct", func() { + part, err := NewQueryPart("count", []string{}) + So(err, ShouldBeNil) + + res := part.Render(query, queryContext, "distinct(value)") + So(res, ShouldEqual, `count(distinct(value))`) + }) }) } diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts index ce5588abe53..2a2f9f2a4ef 100644 --- a/public/app/plugins/datasource/influxdb/query_part.ts +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -44,6 +44,28 @@ function replaceAggregationAddStrategy(selectParts, partModel) { for (var i = 0; i < selectParts.length; i++) { var part = selectParts[i]; if (part.def.category === categories.Aggregations) { + if (part.def.type === partModel.def.type) { + return; + } + // count distinct is allowed + if (part.def.type === 'count' && partModel.def.type === 'distinct') { + break; + } + // remove next aggregation if distinct was replaced + if (part.def.type === 'distinct') { + var morePartsAvailable = selectParts.length >= i + 2; + if (partModel.def.type !== 'count' && morePartsAvailable) { + var nextPart = selectParts[i + 1]; + if (nextPart.def.category === categories.Aggregations) { + selectParts.splice(i + 1, 1); + } + } else if (partModel.def.type === 'count') { + if (!morePartsAvailable || selectParts[i + 1].def.type !== 'count') { + selectParts.splice(i + 1, 0, partModel); + } + return; + } + } selectParts[i] = partModel; return; } @@ -434,4 +456,5 @@ export default { getCategories: function() { return categories; }, + replaceAggregationAdd: replaceAggregationAddStrategy, }; diff --git a/public/app/plugins/datasource/influxdb/specs/query_part.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_part.jest.ts index cabe8bc9b6f..e9e6d216c1e 100644 --- a/public/app/plugins/datasource/influxdb/specs/query_part.jest.ts +++ b/public/app/plugins/datasource/influxdb/specs/query_part.jest.ts @@ -40,5 +40,149 @@ describe('InfluxQueryPart', () => { expect(part.text).toBe('alias(test)'); expect(part.render('mean(value)')).toBe('mean(value) AS "test"'); }); + + it('should nest distinct when count is selected', () => { + var selectParts = [ + queryPart.create({ + type: 'field', + category: queryPart.getCategories().Fields, + }), + queryPart.create({ + type: 'count', + category: queryPart.getCategories().Aggregations, + }), + ]; + var partModel = queryPart.create({ + type: 'distinct', + category: queryPart.getCategories().Aggregations, + }); + + queryPart.replaceAggregationAdd(selectParts, partModel); + + expect(selectParts[1].text).toBe('distinct()'); + expect(selectParts[2].text).toBe('count()'); + }); + + it('should convert to count distinct when distinct is selected and count added', () => { + var selectParts = [ + queryPart.create({ + type: 'field', + category: queryPart.getCategories().Fields, + }), + queryPart.create({ + type: 'distinct', + category: queryPart.getCategories().Aggregations, + }), + ]; + var partModel = queryPart.create({ + type: 'count', + category: queryPart.getCategories().Aggregations, + }); + + queryPart.replaceAggregationAdd(selectParts, partModel); + + expect(selectParts[1].text).toBe('distinct()'); + expect(selectParts[2].text).toBe('count()'); + }); + + it('should replace count distinct if an aggregation is selected', () => { + var selectParts = [ + queryPart.create({ + type: 'field', + category: queryPart.getCategories().Fields, + }), + queryPart.create({ + type: 'distinct', + category: queryPart.getCategories().Aggregations, + }), + queryPart.create({ + type: 'count', + category: queryPart.getCategories().Aggregations, + }), + ]; + var partModel = queryPart.create({ + type: 'mean', + category: queryPart.getCategories().Selectors, + }); + + queryPart.replaceAggregationAdd(selectParts, partModel); + + expect(selectParts[1].text).toBe('mean()'); + expect(selectParts).toHaveLength(2); + }); + + it('should not allowed nested counts when count distinct is selected', () => { + var selectParts = [ + queryPart.create({ + type: 'field', + category: queryPart.getCategories().Fields, + }), + queryPart.create({ + type: 'distinct', + category: queryPart.getCategories().Aggregations, + }), + queryPart.create({ + type: 'count', + category: queryPart.getCategories().Aggregations, + }), + ]; + var partModel = queryPart.create({ + type: 'count', + category: queryPart.getCategories().Aggregations, + }); + + queryPart.replaceAggregationAdd(selectParts, partModel); + + expect(selectParts[1].text).toBe('distinct()'); + expect(selectParts[2].text).toBe('count()'); + expect(selectParts).toHaveLength(3); + }); + + it('should not remove count distinct when distinct is added', () => { + var selectParts = [ + queryPart.create({ + type: 'field', + category: queryPart.getCategories().Fields, + }), + queryPart.create({ + type: 'distinct', + category: queryPart.getCategories().Aggregations, + }), + queryPart.create({ + type: 'count', + category: queryPart.getCategories().Aggregations, + }), + ]; + var partModel = queryPart.create({ + type: 'distinct', + category: queryPart.getCategories().Aggregations, + }); + + queryPart.replaceAggregationAdd(selectParts, partModel); + + expect(selectParts[1].text).toBe('distinct()'); + expect(selectParts[2].text).toBe('count()'); + expect(selectParts).toHaveLength(3); + }); + + it('should remove distinct when sum aggregation is selected', () => { + var selectParts = [ + queryPart.create({ + type: 'field', + category: queryPart.getCategories().Fields, + }), + queryPart.create({ + type: 'distinct', + category: queryPart.getCategories().Aggregations, + }), + ]; + var partModel = queryPart.create({ + type: 'sum', + category: queryPart.getCategories().Aggregations, + }); + queryPart.replaceAggregationAdd(selectParts, partModel); + + expect(selectParts[1].text).toBe('sum()'); + }); }); }); From f2942d94a5b3c8d48616e2ee77f53e20f50420ff Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 30 May 2018 09:26:15 +0200 Subject: [PATCH 0767/3000] changelog: add notes about closing #11645 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb6125492d..3597b1b6a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616) * **Discord**: Alert notification channel type for Discord, [#7964](https://github.com/grafana/grafana/issues/7964) thx [@jereksel](https://github.com/jereksel), * **InfluxDB**: Support SELECT queries in templating query, [#5013](https://github.com/grafana/grafana/issues/5013) +* **InfluxDB**: Support count distinct aggregation [#11645](https://github.com/grafana/grafana/issues/11645), thx [@kichristensen](https://github.com/kichristensen) * **Dashboard**: JSON Model under dashboard settings can now be updated & changes saved, [#1429](https://github.com/grafana/grafana/issues/1429), thx [@jereksel](https://github.com/jereksel) * **Security**: Fix XSS vulnerabilities in dashboard links [#11813](https://github.com/grafana/grafana/pull/11813) * **Singlestat**: Fix "time of last point" shows local time when dashboard timezone set to UTC [#10338](https://github.com/grafana/grafana/issues/10338) From ac1dda3b3a522d3174fd2035c4e562312994e92d Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 30 May 2018 12:07:51 +0200 Subject: [PATCH 0768/3000] Fix CSS to hide grid controls in fullscreen/low-activity views * there was a comma missing to hide the handles, fixed now * added new styles to hide header interaction in full screen panels --- public/sass/components/_dashboard_grid.scss | 14 ++++++++++++++ public/sass/components/_view_states.scss | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index 0a27df75164..f1908ca8786 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -18,6 +18,20 @@ height: 100% !important; transform: translate(0px, 0px) !important; } + + // Disable grid interaction indicators in fullscreen panels + + .panel-header:hover { + background-color: inherit; + } + + .panel-title-container { + cursor: pointer; + } + + .react-resizable-handle { + display: none; + } } @include media-breakpoint-down(sm) { diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss index b1fa47d0c0a..c14590b4ec9 100644 --- a/public/sass/components/_view_states.scss +++ b/public/sass/components/_view_states.scss @@ -10,7 +10,8 @@ .playlist-active, .user-activity-low { - .react-resizable-handle .add-row-panel-hint, + .react-resizable-handle, + .add-row-panel-hint, .dash-row-menu-container, .navbar-button--refresh, .navbar-buttons--zoom, From f69654fcd5dd20b264e38af988a4a69673de76bb Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 30 May 2018 13:13:29 +0200 Subject: [PATCH 0769/3000] Restrict Explore UI to Editor and Admin roles Access is restricted via not showing in the following places: * hide from sidemenu * hide from panel header menu * disable keybinding `x` Also adds a `roles` property to reactContainer routes that will be checked if `roles` is set, and on failure redirects to `/`. --- pkg/api/index.go | 2 +- public/app/core/services/keybindingSrv.ts | 33 ++++++++++--------- .../app/features/panel/metrics_panel_ctrl.ts | 4 ++- public/app/routes/ReactContainer.tsx | 20 +++++++++-- public/app/routes/routes.ts | 1 + 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index f082f03b5f6..acf0c30c907 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -128,7 +128,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { Children: dashboardChildNavs, }) - if setting.ExploreEnabled { + if setting.ExploreEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) { data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Explore", Id: "explore", diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 25d00ab37f1..b1021c90adc 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -14,7 +14,7 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv) { + constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -177,21 +177,24 @@ export class KeybindingSrv { } }); - this.bind('x', async () => { - if (dashboard.meta.focusPanelId) { - const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); - const datasource = await this.datasourceSrv.get(panel.datasource); - if (datasource && datasource.supportsExplore) { - const range = this.timeSrv.timeRangeForUrl(); - const state = { - ...datasource.getExploreState(panel), - range, - }; - const exploreState = encodePathComponent(JSON.stringify(state)); - this.$location.url(`/explore/${exploreState}`); + // jump to explore if permissions allow + if (this.contextSrv.isEditor) { + this.bind('x', async () => { + if (dashboard.meta.focusPanelId) { + const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); + const datasource = await this.datasourceSrv.get(panel.datasource); + if (datasource && datasource.supportsExplore) { + const range = this.timeSrv.timeRangeForUrl(); + const state = { + ...datasource.getExploreState(panel), + range, + }; + const exploreState = encodePathComponent(JSON.stringify(state)); + this.$location.url(`/explore/${exploreState}`); + } } - } - }); + }); + } // delete panel this.bind('p r', () => { diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 3c48119ba3a..cf1b2cd49bc 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -16,6 +16,7 @@ class MetricsPanelCtrl extends PanelCtrl { datasourceName: any; $q: any; $timeout: any; + contextSrv: any; datasourceSrv: any; timeSrv: any; templateSrv: any; @@ -37,6 +38,7 @@ class MetricsPanelCtrl extends PanelCtrl { // make metrics tab the default this.editorTabIndex = 1; this.$q = $injector.get('$q'); + this.contextSrv = $injector.get('contextSrv'); this.datasourceSrv = $injector.get('datasourceSrv'); this.timeSrv = $injector.get('timeSrv'); this.templateSrv = $injector.get('templateSrv'); @@ -312,7 +314,7 @@ class MetricsPanelCtrl extends PanelCtrl { getAdditionalMenuItems() { const items = []; - if (this.datasource && this.datasource.supportsExplore) { + if (this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) { items.push({ text: 'Explore', click: 'ctrl.explore();', diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index db6938cc878..b161a5e7a87 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -6,6 +6,7 @@ import coreModule from 'app/core/core_module'; import { store } from 'app/stores/store'; import { BackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { ContextSrv } from 'app/core/services/context_srv'; function WrapInProvider(store, Component, props) { return ( @@ -16,16 +17,31 @@ function WrapInProvider(store, Component, props) { } /** @ngInject */ -export function reactContainer($route, $location, backendSrv: BackendSrv, datasourceSrv: DatasourceSrv) { +export function reactContainer( + $route, + $location, + backendSrv: BackendSrv, + datasourceSrv: DatasourceSrv, + contextSrv: ContextSrv +) { return { restrict: 'E', template: '', link(scope, elem) { - let component = $route.current.locals.component; + // Check permissions for this component + const { roles } = $route.current.locals; + if (roles && roles.length) { + if (!roles.some(r => contextSrv.hasRole(r))) { + $location.url('/'); + } + } + + let { component } = $route.current.locals; // Dynamic imports return whole module, need to extract default export if (component.default) { component = component.default; } + const props = { backendSrv: backendSrv, datasourceSrv: datasourceSrv, diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index b10084d1941..568b3438b38 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -113,6 +113,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/explore/:initial?', { template: '', resolve: { + roles: () => ['Editor', 'Admin'], component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Wrapper'), }, }) From 7224ca6c622547124fcab828919872fde93efca6 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 30 May 2018 13:24:09 +0200 Subject: [PATCH 0770/3000] Fix panel menu test --- public/app/features/panel/specs/metrics_panel_ctrl.jest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts b/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts index f2e5199b57d..79564e2a123 100644 --- a/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts +++ b/public/app/features/panel/specs/metrics_panel_ctrl.jest.ts @@ -24,8 +24,9 @@ describe('MetricsPanelCtrl', () => { }); }); - describe('and has datasource set that supports explore', () => { + describe('and has datasource set that supports explore and user has powers', () => { beforeEach(() => { + ctrl.contextSrv = { isEditor: true }; ctrl.datasource = { supportsExplore: true }; additionalItems = ctrl.getAdditionalMenuItems(); }); From 21ecaae6ff2f91b5b58e008f81a32d30bd06d74d Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 30 May 2018 14:30:01 +0200 Subject: [PATCH 0771/3000] changelog: Second epochs are now correctly converted to ms. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3597b1b6a1c..3a86eeba75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * **Invite users**: Friendlier error message when smtp is not configured [#12087](https://github.com/grafana/grafana/issues/12087), thx [@thurt](https://github.com/thurt) * **Graphite**: Don't send distributed tracing headers when using direct/browser access mode [#11494](https://github.com/grafana/grafana/issues/11494) * **Sidenav**: Show create dashboard link for viewers if at least editor in one folder [#11858](https://github.com/grafana/grafana/issues/11858) +* **SQL**: Second epochs are now correctly converted to ms. [#12085](https://github.com/grafana/grafana/pull/12085) # 5.1.3 (2018-05-16) From 827fb7e8de3bf075a2af13f8f6940abbee5eb584 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 30 May 2018 15:24:47 +0200 Subject: [PATCH 0772/3000] Fix karma tests that rely on MetricsPanelCtrl --- public/app/features/panel/metrics_panel_ctrl.ts | 4 ++-- public/test/specs/helpers.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index cf1b2cd49bc..cbda8c874db 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -1,9 +1,9 @@ -import config from 'app/core/config'; import $ from 'jquery'; import _ from 'lodash'; + +import config from 'app/core/config'; import kbn from 'app/core/utils/kbn'; import { PanelCtrl } from 'app/features/panel/panel_ctrl'; - import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; import { encodePathComponent } from 'app/core/utils/location_util'; diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 276d9867ec4..dd8bd39846e 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -11,6 +11,7 @@ export function ControllerTestContext() { this.$element = {}; this.$sanitize = {}; this.annotationsSrv = {}; + this.contextSrv = {}; this.timeSrv = new TimeSrvStub(); this.templateSrv = new TemplateSrvStub(); this.datasourceSrv = { @@ -27,6 +28,7 @@ export function ControllerTestContext() { this.providePhase = function(mocks) { return angularMocks.module(function($provide) { + $provide.value('contextSrv', self.contextSrv); $provide.value('datasourceSrv', self.datasourceSrv); $provide.value('annotationsSrv', self.annotationsSrv); $provide.value('timeSrv', self.timeSrv); From 50d1519a916a5526d02e7cb3621b97b5db8505e2 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 30 May 2018 13:55:30 +0200 Subject: [PATCH 0773/3000] build: mysql integration testing on ci. --- .circleci/config.yml | 26 +++++++++++++++++++++++++ docker/blocks/mysql/docker-compose.yaml | 2 +- docker/blocks/mysql_tests/Dockerfile | 4 ++-- pkg/tsdb/mysql/mysql_test.go | 6 +++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c92a68bf99d..d9cc03b9527 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,6 +12,26 @@ aliases: version: 2 jobs: + mysql-integration-test: + docker: + - image: circleci/golang:1.10 + - image: circleci/mysql:5.6-ram + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: grafana_tests + MYSQL_USER: grafana + MYSQL_PASSWORD: password + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: sudo apt update + - run: sudo apt install -y mysql-client + - run: dockerize -wait tcp://127.0.0.1:3306 -timeout 120s + - run: cat docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass + - run: + name: mysql integration tests + command: 'GRAFANA_TEST_DB=mysql go test ./pkg/...' + codespell: docker: - image: circleci/python @@ -188,6 +208,8 @@ workflows: filters: *filter-not-release - test-backend: filters: *filter-not-release + - mysql-integration-test: + filters: *filter-not-release - deploy-master: requires: - build-all @@ -195,6 +217,7 @@ workflows: - test-frontend - codespell - gometalinter + - mysql-integration-test filters: branches: only: master @@ -210,6 +233,8 @@ workflows: filters: *filter-only-release - test-backend: filters: *filter-only-release + - mysql-integration-test: + filters: *filter-only-release - deploy-release: requires: - build-all @@ -217,4 +242,5 @@ workflows: - test-frontend - codespell - gometalinter + - mysql-integration-test filters: *filter-only-release diff --git a/docker/blocks/mysql/docker-compose.yaml b/docker/blocks/mysql/docker-compose.yaml index 53ff9da62a7..381b04a53c8 100644 --- a/docker/blocks/mysql/docker-compose.yaml +++ b/docker/blocks/mysql/docker-compose.yaml @@ -1,5 +1,5 @@ mysql: - image: mysql:latest + image: mysql:5.6 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: grafana diff --git a/docker/blocks/mysql_tests/Dockerfile b/docker/blocks/mysql_tests/Dockerfile index fa91fa3c023..89e16bc2ed6 100644 --- a/docker/blocks/mysql_tests/Dockerfile +++ b/docker/blocks/mysql_tests/Dockerfile @@ -1,3 +1,3 @@ -FROM mysql:latest +FROM mysql:5.6 ADD setup.sql /docker-entrypoint-initdb.d -CMD ["mysqld"] \ No newline at end of file +CMD ["mysqld"] diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 29c5b72b408..5650de237c5 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -601,7 +601,7 @@ func TestMySQL(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": `SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values ORDER BY 1`, + "rawSql": `SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values ORDER BY 1,2`, "format": "time_series", }), RefId: "A", @@ -615,8 +615,8 @@ func TestMySQL(t *testing.T) { So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 2) - So(queryResult.Series[0].Name, ShouldEqual, "Metric B - value one") - So(queryResult.Series[1].Name, ShouldEqual, "Metric A - value one") + So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") + So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") }) Convey("When doing a metric query grouping by time should return correct series", func() { From e33b17fac666e03135fdeb1c5b9a0227e85e1ff2 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 30 May 2018 09:40:45 +0200 Subject: [PATCH 0774/3000] build: integration testing postegres on ci. --- .circleci/config.yml | 25 ++++++++++++++++++++++ docker/blocks/postgres/docker-compose.yaml | 4 ++-- docker/blocks/postgres_tests/Dockerfile | 4 ++-- docker/blocks/postgres_tests/setup.sql | 2 +- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d9cc03b9527..46404e4e650 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,6 +32,25 @@ jobs: name: mysql integration tests command: 'GRAFANA_TEST_DB=mysql go test ./pkg/...' + postgres-integration-test: + docker: + - image: circleci/golang:1.10 + - image: circleci/postgres:9.3-ram + environment: + POSTGRES_USER: grafanatest + POSTGRES_PASSWORD: grafanatest + POSTGRES_DB: grafanatest + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: sudo apt update + - run: sudo apt install -y postgresql-client + - run: dockerize -wait tcp://127.0.0.1:5432 -timeout 120s + - run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f docker/blocks/postgres_tests/setup.sql' + - run: + name: postgres integration tests + command: 'GRAFANA_TEST_DB=postgres go test ./pkg/...' + codespell: docker: - image: circleci/python @@ -210,6 +229,8 @@ workflows: filters: *filter-not-release - mysql-integration-test: filters: *filter-not-release + - postgres-integration-test: + filters: *filter-not-release - deploy-master: requires: - build-all @@ -218,6 +239,7 @@ workflows: - codespell - gometalinter - mysql-integration-test + - postgres-integration-test filters: branches: only: master @@ -235,6 +257,8 @@ workflows: filters: *filter-only-release - mysql-integration-test: filters: *filter-only-release + - postgres-integration-test: + filters: *filter-only-release - deploy-release: requires: - build-all @@ -243,4 +267,5 @@ workflows: - codespell - gometalinter - mysql-integration-test + - postgres-integration-test filters: *filter-only-release diff --git a/docker/blocks/postgres/docker-compose.yaml b/docker/blocks/postgres/docker-compose.yaml index 566df7b8877..27736042f7b 100644 --- a/docker/blocks/postgres/docker-compose.yaml +++ b/docker/blocks/postgres/docker-compose.yaml @@ -1,5 +1,5 @@ postgrestest: - image: postgres:latest + image: postgres:9.3 environment: POSTGRES_USER: grafana POSTGRES_PASSWORD: password @@ -13,4 +13,4 @@ network_mode: bridge environment: FD_DATASOURCE: postgres - FD_PORT: 5432 \ No newline at end of file + FD_PORT: 5432 diff --git a/docker/blocks/postgres_tests/Dockerfile b/docker/blocks/postgres_tests/Dockerfile index afe4d199651..df188e1094d 100644 --- a/docker/blocks/postgres_tests/Dockerfile +++ b/docker/blocks/postgres_tests/Dockerfile @@ -1,3 +1,3 @@ -FROM postgres:latest +FROM postgres:9.3 ADD setup.sql /docker-entrypoint-initdb.d -CMD ["postgres"] \ No newline at end of file +CMD ["postgres"] diff --git a/docker/blocks/postgres_tests/setup.sql b/docker/blocks/postgres_tests/setup.sql index b182b7c292d..3b8a48f938d 100644 --- a/docker/blocks/postgres_tests/setup.sql +++ b/docker/blocks/postgres_tests/setup.sql @@ -1,3 +1,3 @@ CREATE DATABASE grafanadstest; REVOKE CONNECT ON DATABASE grafanadstest FROM PUBLIC; -GRANT CONNECT ON DATABASE grafanadstest TO grafanatest; \ No newline at end of file +GRANT CONNECT ON DATABASE grafanadstest TO grafanatest; From b379b2833760a24a5f4221f178255dfcbb6f1254 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 30 May 2018 15:16:31 +0200 Subject: [PATCH 0775/3000] build: only runs db related tests on db. --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 46404e4e650..e898ad9e214 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,7 +30,7 @@ jobs: - run: cat docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass - run: name: mysql integration tests - command: 'GRAFANA_TEST_DB=mysql go test ./pkg/...' + command: 'GRAFANA_TEST_DB=mysql go test ./pkg/services/sqlstore/... ./pkg/tsdb/mysql/... ' postgres-integration-test: docker: @@ -49,7 +49,7 @@ jobs: - run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f docker/blocks/postgres_tests/setup.sql' - run: name: postgres integration tests - command: 'GRAFANA_TEST_DB=postgres go test ./pkg/...' + command: 'GRAFANA_TEST_DB=postgres go test ./pkg/services/sqlstore/... ./pkg/tsdb/postgres/...' codespell: docker: From b894b5e669f94424b83b364238d2e7b254954989 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 30 May 2018 18:09:57 +0200 Subject: [PATCH 0776/3000] Fix singlestat threshold tooltip (#12109) fix singlestat threshold tooltip --- public/app/plugins/panel/singlestat/editor.html | 2 +- public/app/plugins/panel/singlestat/module.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/singlestat/editor.html b/public/app/plugins/panel/singlestat/editor.html index f444cd0170c..15f4e6a9efa 100644 --- a/public/app/plugins/panel/singlestat/editor.html +++ b/public/app/plugins/panel/singlestat/editor.html @@ -61,7 +61,7 @@
    diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index b73a3bb32bd..20c4dcfeb70 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -714,11 +714,13 @@ function getColorForValue(data, value) { if (!_.isFinite(value)) { return null; } + for (var i = data.thresholds.length; i > 0; i--) { if (value >= data.thresholds[i - 1]) { return data.colorMap[i]; } } + return _.first(data.colorMap); } From a4b1dd036d04cd372a7475be425b9c53467f15c7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 30 May 2018 18:11:47 +0200 Subject: [PATCH 0777/3000] changelog: add notes about closing #11971 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a86eeba75e..3d77986b290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * **Graphite**: Don't send distributed tracing headers when using direct/browser access mode [#11494](https://github.com/grafana/grafana/issues/11494) * **Sidenav**: Show create dashboard link for viewers if at least editor in one folder [#11858](https://github.com/grafana/grafana/issues/11858) * **SQL**: Second epochs are now correctly converted to ms. [#12085](https://github.com/grafana/grafana/pull/12085) +* **Singlestat**: Fix singlestat threshold tooltip [#11971](https://github.com/grafana/grafana/issues/11971) # 5.1.3 (2018-05-16) From 82ba27b5f22c60153f620430937392151c3d312f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 30 May 2018 21:31:31 +0200 Subject: [PATCH 0778/3000] changelog: add notes about closing #11771 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d77986b290..5b756ea0102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * **Sidenav**: Show create dashboard link for viewers if at least editor in one folder [#11858](https://github.com/grafana/grafana/issues/11858) * **SQL**: Second epochs are now correctly converted to ms. [#12085](https://github.com/grafana/grafana/pull/12085) * **Singlestat**: Fix singlestat threshold tooltip [#11971](https://github.com/grafana/grafana/issues/11971) +* **Dashboard**: Hide grid controls in fullscreen/low-activity views [#11771](https://github.com/grafana/grafana/issues/11771) # 5.1.3 (2018-05-16) From d5aeae3a90e2cd7b1318b2d62a7e4516aabff9a0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 08:27:29 +0200 Subject: [PATCH 0779/3000] test: fixes broken test on windows --- pkg/services/provisioning/dashboards/file_reader_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index a04fbb23f82..87e9ec6d226 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -3,6 +3,7 @@ package dashboards import ( "os" "path/filepath" + "runtime" "testing" "time" @@ -52,7 +53,9 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) So(err, ShouldBeNil) - So(reader.Path, ShouldEqual, "/var/lib/grafana/dashboards") + if runtime.GOOS != "windows" { + So(reader.Path, ShouldEqual, "/var/lib/grafana/dashboards") + } So(filepath.IsAbs(reader.Path), ShouldBeTrue) }) From 47d388437740d930f3273f99338a2721ec8a9225 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 21 May 2018 09:03:32 +0200 Subject: [PATCH 0780/3000] provisioning: follow symlinked folders fixes #11958 --- .../provisioning/dashboards/file_reader.go | 5 +++ .../dashboards/file_reader_linux_test.go | 39 +++++++++++++++++++ .../testdata/test-dashboards/symlink | 1 + 3 files changed, 45 insertions(+) create mode 100644 pkg/services/provisioning/dashboards/file_reader_linux_test.go create mode 120000 pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 93846f5c474..628c63de3a8 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -47,6 +47,11 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade log.Error("Cannot read directory", "error", err) } + path, err := filepath.EvalSymlinks(path) + if err != nil { + log.Error("Failed to read content of symlinked path: %s", path) + } + absPath, err := filepath.Abs(path) if err != nil { log.Error("Could not create absolute path ", "path", path) diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_linux_test.go new file mode 100644 index 00000000000..9d4cdae8609 --- /dev/null +++ b/pkg/services/provisioning/dashboards/file_reader_linux_test.go @@ -0,0 +1,39 @@ +// +build linux + +package dashboards + +import ( + "path/filepath" + "testing" + + "github.com/grafana/grafana/pkg/log" +) + +var ( + symlinkedFolder = "testdata/test-dashboards/symlink" +) + +func TestProvsionedSymlinkedFolder(t *testing.T) { + cfg := &DashboardsAsConfig{ + Name: "Default", + Type: "file", + OrgId: 1, + Folder: "", + Options: map[string]interface{}{"path": symlinkedFolder}, + } + + reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + if err != nil { + t.Error("expected err to be nil") + } + + want, err := filepath.Abs(containingId) + + if err != nil { + t.Errorf("expected err to be nill") + } + + if reader.Path != want { + t.Errorf("got %s want %s", reader.Path, want) + } +} diff --git a/pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink b/pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink new file mode 120000 index 00000000000..42e166e6959 --- /dev/null +++ b/pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink @@ -0,0 +1 @@ +containing-id/ \ No newline at end of file From 2bd4c14e5f4d0a525dd7f7b692484f8fbb8fc9bc Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 09:53:15 +0200 Subject: [PATCH 0781/3000] make path absolute before following symlink --- .../provisioning/dashboards/file_reader.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 628c63de3a8..a1ba4dbf8e2 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -47,20 +47,21 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade log.Error("Cannot read directory", "error", err) } - path, err := filepath.EvalSymlinks(path) + copy := path + path, err := filepath.Abs(path) + if err != nil { + log.Error("Could not create absolute path ", "path", path) + path = copy //if .Abs return an error we fallback to path + } + + path, err = filepath.EvalSymlinks(path) if err != nil { log.Error("Failed to read content of symlinked path: %s", path) } - absPath, err := filepath.Abs(path) - if err != nil { - log.Error("Could not create absolute path ", "path", path) - absPath = path //if .Abs return an error we fallback to path - } - return &fileReader{ Cfg: cfg, - Path: absPath, + Path: path, log: log, dashboardService: dashboards.NewProvisioningService(), }, nil From 0c45ee63a9bf360ded82e3a229fd0a142187c797 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 31 May 2018 11:26:24 +0200 Subject: [PATCH 0782/3000] Guard /explore by editor role on the backend --- pkg/api/api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/api/api.go b/pkg/api/api.go index 01189f7a81e..c205e7d3e2f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -77,6 +77,9 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/dashboards/", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index) + r.Get("/explore/", reqEditorRole, Index) + r.Get("/explore/*", reqEditorRole, Index) + r.Get("/playlists/", reqSignedIn, Index) r.Get("/playlists/*", reqSignedIn, Index) r.Get("/alerting/", reqSignedIn, Index) From 44f5b92fbcd77330f28e61bdcb84d0b9499b6b47 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 11:38:29 +0200 Subject: [PATCH 0783/3000] provisioning: only provision if json file is newer then db --- pkg/services/provisioning/dashboards/file_reader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 93846f5c474..cd4598794bc 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -159,7 +159,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil } provisionedData, alreadyProvisioned := provisionedDashboardRefs[path] - upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix() + upToDate := alreadyProvisioned && provisionedData.Updated >= resolvedFileInfo.ModTime().Unix() dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId) if err != nil { From 938deae4b467c2fcf4f35304dba7968e514f49a8 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 31 May 2018 15:24:01 +0200 Subject: [PATCH 0784/3000] changelog: add notes about closing #11515 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b756ea0102..280d4429778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * **SQL**: Second epochs are now correctly converted to ms. [#12085](https://github.com/grafana/grafana/pull/12085) * **Singlestat**: Fix singlestat threshold tooltip [#11971](https://github.com/grafana/grafana/issues/11971) * **Dashboard**: Hide grid controls in fullscreen/low-activity views [#11771](https://github.com/grafana/grafana/issues/11771) +* **Dashboard**: Validate uid when importing dashboards [#11515](https://github.com/grafana/grafana/issues/11515) # 5.1.3 (2018-05-16) From 37f9bdfc8ce15f061d30c613e9c849a8907a6a54 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 31 May 2018 15:40:57 +0200 Subject: [PATCH 0785/3000] save modal ux improvements (#11822) changes to save modal when saving an updated dashboard Changed time range and variables are now not saved by default, you'll need to actively choose if you want to save updated time range and or variables. --- .../app/features/dashboard/dashboard_model.ts | 26 +++++- public/app/features/dashboard/save_modal.ts | 65 ++++++++++++-- .../dashboard/specs/dashboard_model.jest.ts | 59 ++++++++++++ .../dashboard/specs/save_modal.jest.ts | 90 +++++++++++++++++++ 4 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 public/app/features/dashboard/specs/save_modal.jest.ts diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 8a300a80341..a37e753bd89 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -22,8 +22,10 @@ export class DashboardModel { editable: any; graphTooltip: any; time: any; + originalTime: any; timepicker: any; templating: any; + originalTemplating: any; annotations: any; refresh: any; snapshot: any; @@ -68,8 +70,12 @@ export class DashboardModel { this.editable = data.editable !== false; this.graphTooltip = data.graphTooltip || 0; this.time = data.time || { from: 'now-6h', to: 'now' }; + this.originalTime = _.cloneDeep(this.time); this.timepicker = data.timepicker || {}; this.templating = this.ensureListExist(data.templating); + this.originalTemplating = _.map(this.templating.list, variable => { + return { name: variable.name, current: _.clone(variable.current) }; + }); this.annotations = this.ensureListExist(data.annotations); this.refresh = data.refresh; this.snapshot = data.snapshot; @@ -130,7 +136,12 @@ export class DashboardModel { } // cleans meta data and other non persistent state - getSaveModelClone() { + getSaveModelClone(options?) { + let defaults = _.defaults(options || {}, { + saveVariables: false, + saveTimerange: false, + }); + // make clone var copy: any = {}; for (var property in this) { @@ -142,10 +153,23 @@ export class DashboardModel { } // get variable save models + //console.log(this.templating.list); copy.templating = { list: _.map(this.templating.list, variable => (variable.getSaveModel ? variable.getSaveModel() : variable)), }; + if (!defaults.saveVariables && copy.templating.list.length === this.originalTemplating.length) { + for (let i = 0; i < copy.templating.list.length; i++) { + if (copy.templating.list[i].name === this.originalTemplating[i].name) { + copy.templating.list[i].current = this.originalTemplating[i].current; + } + } + } + + if (!defaults.saveTimerange) { + copy.time = this.originalTime; + } + // get panel save models copy.panels = _.chain(this.panels) .filter(panel => panel.type !== 'add-panel') diff --git a/public/app/features/dashboard/save_modal.ts b/public/app/features/dashboard/save_modal.ts index 33165758555..1c364fbc55f 100644 --- a/public/app/features/dashboard/save_modal.ts +++ b/public/app/features/dashboard/save_modal.ts @@ -1,4 +1,5 @@ import coreModule from 'app/core/core_module'; +import _ from 'lodash'; const template = ` -
    Add a note to describe your changes
    -
    +
    +
    + + + + +
    @@ -48,14 +59,51 @@ const template = ` export class SaveDashboardModalCtrl { message: string; + saveVariables = false; + saveTimerange = false; + templating: any; + time: any; + originalTime: any; + current = []; + originalCurrent = []; max: number; saveForm: any; dismiss: () => void; + timeChange = false; + variableChange = false; /** @ngInject */ constructor(private dashboardSrv) { this.message = ''; this.max = 64; + this.templating = dashboardSrv.dash.templating.list; + + this.compareTemplating(); + this.compareTime(); + } + + compareTime() { + if (_.isEqual(this.dashboardSrv.dash.time, this.dashboardSrv.dash.originalTime)) { + this.timeChange = false; + } else { + this.timeChange = true; + } + } + + compareTemplating() { + if (this.dashboardSrv.dash.templating.list.length > 0) { + for (let i = 0; i < this.dashboardSrv.dash.templating.list.length; i++) { + if ( + this.dashboardSrv.dash.templating.list[i].current.text !== + this.dashboardSrv.dash.originalTemplating[i].current.text + ) { + return (this.variableChange = true); + } + } + return (this.variableChange = false); + } else { + return (this.variableChange = false); + } } save() { @@ -63,9 +111,14 @@ export class SaveDashboardModalCtrl { return; } + var options = { + saveVariables: this.saveVariables, + saveTimerange: this.saveTimerange, + message: this.message, + }; + var dashboard = this.dashboardSrv.getCurrent(); - var saveModel = dashboard.getSaveModelClone(); - var options = { message: this.message }; + var saveModel = dashboard.getSaveModelClone(options); return this.dashboardSrv.save(saveModel, options).then(this.dismiss); } diff --git a/public/app/features/dashboard/specs/dashboard_model.jest.ts b/public/app/features/dashboard/specs/dashboard_model.jest.ts index 6f0b45c9ba8..3fb5b3c3b73 100644 --- a/public/app/features/dashboard/specs/dashboard_model.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_model.jest.ts @@ -434,4 +434,63 @@ describe('DashboardModel', function() { }); }); }); + + describe('save variables and timeline', () => { + let model; + + beforeEach(() => { + model = new DashboardModel({ + templating: { + list: [ + { + name: 'Server', + current: { + selected: true, + text: 'server_001', + value: 'server_001', + }, + }, + ], + }, + time: { + from: 'now-6h', + to: 'now', + }, + }); + model.templating.list[0] = { + name: 'Server', + current: { + selected: true, + text: 'server_002', + value: 'server_002', + }, + }; + model.time = { + from: 'now-3h', + to: 'now', + }; + }); + + it('should not save variables and timeline', () => { + let options = { + saveVariables: false, + saveTimerange: false, + }; + let saveModel = model.getSaveModelClone(options); + + expect(saveModel.templating.list[0].current.text).toBe('server_001'); + expect(saveModel.time.from).toBe('now-6h'); + }); + + it('should save variables and timeline', () => { + let options = { + saveVariables: true, + saveTimerange: true, + }; + let saveModel = model.getSaveModelClone(options); + + expect(saveModel.templating.list[0].current.text).toBe('server_002'); + expect(saveModel.time.from).toBe('now-3h'); + }); + }); }); diff --git a/public/app/features/dashboard/specs/save_modal.jest.ts b/public/app/features/dashboard/specs/save_modal.jest.ts new file mode 100644 index 00000000000..05281cc6097 --- /dev/null +++ b/public/app/features/dashboard/specs/save_modal.jest.ts @@ -0,0 +1,90 @@ +import { SaveDashboardModalCtrl } from '../save_modal'; + +jest.mock('app/core/services/context_srv', () => ({})); + +describe('SaveDashboardModal', () => { + describe('save modal checkboxes', () => { + it('should show checkboxes', () => { + let fakeDashboardSrv = { + dash: { + templating: { + list: [ + { + current: { + selected: true, + tags: Array(0), + text: 'server_001', + value: 'server_001', + }, + name: 'Server', + }, + ], + }, + originalTemplating: [ + { + current: { + selected: true, + text: 'server_002', + value: 'server_002', + }, + name: 'Server', + }, + ], + time: { + from: 'now-3h', + to: 'now', + }, + originalTime: { + from: 'now-6h', + to: 'now', + }, + }, + }; + let modal = new SaveDashboardModalCtrl(fakeDashboardSrv); + + expect(modal.timeChange).toBe(true); + expect(modal.variableChange).toBe(true); + }); + + it('should hide checkboxes', () => { + let fakeDashboardSrv = { + dash: { + templating: { + list: [ + { + current: { + selected: true, + //tags: Array(0), + text: 'server_002', + value: 'server_002', + }, + name: 'Server', + }, + ], + }, + originalTemplating: [ + { + current: { + selected: true, + text: 'server_002', + value: 'server_002', + }, + name: 'Server', + }, + ], + time: { + from: 'now-3h', + to: 'now', + }, + originalTime: { + from: 'now-3h', + to: 'now', + }, + }, + }; + let modal = new SaveDashboardModalCtrl(fakeDashboardSrv); + expect(modal.timeChange).toBe(false); + expect(modal.variableChange).toBe(false); + }); + }); +}); From 67410f7a4d13be8a491f4deb5192b967faf63235 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 31 May 2018 15:48:19 +0200 Subject: [PATCH 0786/3000] changelog: add notes about closing #10748, #8805 [skip ci] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 280d4429778..1fa0d9173f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 5.2.0 (unreleased) +### Breaking change + +* **Dashboard**: Modified time range and variables are now not saved by default [#10748](https://github.com/grafana/grafana/issues/10748), [#8805](https://github.com/grafana/grafana/issues/8805) + ### Minor * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) From a2cd05f6dbecb9b15455ad57aa8d97c24bdc53f4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 31 May 2018 16:31:53 +0200 Subject: [PATCH 0787/3000] changelog: update [skip ci] --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa0d9173f7..7ad20e5e5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,8 @@ # 5.2.0 (unreleased) -### Breaking change - -* **Dashboard**: Modified time range and variables are now not saved by default [#10748](https://github.com/grafana/grafana/issues/10748), [#8805](https://github.com/grafana/grafana/issues/8805) - ### Minor +* **Dashboard**: Modified time range and variables are now not saved by default [#10748](https://github.com/grafana/grafana/issues/10748), [#8805](https://github.com/grafana/grafana/issues/8805) * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) * **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda) From dcac63936bbff64fb0e9277b216a0e1e27955d51 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 31 May 2018 19:02:20 +0200 Subject: [PATCH 0788/3000] elasticsearch: minor refactor Handle all replacements if interval template variables in the client. Fix issue with client and different versions. Adds better tests of the client --- pkg/tsdb/elasticsearch/client/client.go | 128 +++----- pkg/tsdb/elasticsearch/client/client_test.go | 279 ++++++++++++------ pkg/tsdb/elasticsearch/client/models.go | 3 + .../elasticsearch/client/search_request.go | 11 +- .../client/search_request_test.go | 12 +- pkg/tsdb/elasticsearch/time_series_query.go | 12 +- .../elasticsearch/time_series_query_test.go | 2 +- 7 files changed, 252 insertions(+), 195 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index 3a2d31b42c6..8ae45599484 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" "time" @@ -25,6 +26,10 @@ var ( clientLog = log.New(loggerName) ) +var newDatasourceHttpClient = func(ds *models.DataSource) (*http.Client, error) { + return ds.GetHttpClient() +} + // Client represents a client which can interact with elasticsearch api type Client interface { GetVersion() int @@ -57,23 +62,18 @@ var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb return nil, err } - bc := &baseClientImpl{ - ctx: ctx, - ds: ds, - version: version, - timeField: timeField, - indices: indices, - } - clientLog.Debug("Creating new client", "version", version, "timeField", timeField, "indices", strings.Join(indices, ", ")) switch version { - case 2: - return newV2Client(bc) - case 5: - return newV5Client(bc) - case 56: - return newV56Client(bc) + case 2, 5, 56: + return &baseClientImpl{ + ctx: ctx, + ds: ds, + version: version, + timeField: timeField, + indices: indices, + timeRange: timeRange, + }, nil } return nil, fmt.Errorf("elasticsearch version=%d is not supported", version) @@ -93,6 +93,7 @@ type baseClientImpl struct { version int timeField string indices []string + timeRange *tsdb.TimeRange } func (c *baseClientImpl) GetVersion() int { @@ -114,11 +115,20 @@ func (c *baseClientImpl) getSettings() *simplejson.Json { } type multiRequest struct { - header map[string]interface{} - body interface{} + header map[string]interface{} + body interface{} + interval tsdb.Interval } func (c *baseClientImpl) executeBatchRequest(uriPath string, requests []*multiRequest) (*http.Response, error) { + bytes, err := c.encodeBatchRequests(requests) + if err != nil { + return nil, err + } + return c.executeRequest(http.MethodPost, uriPath, bytes) +} + +func (c *baseClientImpl) encodeBatchRequests(requests []*multiRequest) ([]byte, error) { clientLog.Debug("Encoding batch requests to json", "batch requests", len(requests)) start := time.Now() @@ -134,13 +144,18 @@ func (c *baseClientImpl) executeBatchRequest(uriPath string, requests []*multiRe if err != nil { return nil, err } - payload.WriteString(string(reqBody) + "\n") + + body := string(reqBody) + body = strings.Replace(body, "$__interval_ms", strconv.FormatInt(r.interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1) + body = strings.Replace(body, "$__interval", r.interval.Text, -1) + + payload.WriteString(body + "\n") } elapsed := time.Now().Sub(start) clientLog.Debug("Encoded batch requests to json", "took", elapsed) - return c.executeRequest(http.MethodPost, uriPath, payload.Bytes()) + return payload.Bytes(), nil } func (c *baseClientImpl) executeRequest(method, uriPath string, body []byte) (*http.Response, error) { @@ -173,7 +188,7 @@ func (c *baseClientImpl) executeRequest(method, uriPath string, body []byte) (*h req.SetBasicAuth(c.ds.User, c.ds.Password) } - httpClient, err := c.ds.GetHttpClient() + httpClient, err := newDatasourceHttpClient(c.ds) if err != nil { return nil, err } @@ -220,77 +235,26 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque multiRequests := []*multiRequest{} for _, searchReq := range searchRequests { - multiRequests = append(multiRequests, &multiRequest{ + mr := multiRequest{ header: map[string]interface{}{ "search_type": "query_then_fetch", "ignore_unavailable": true, "index": strings.Join(c.indices, ","), }, - body: searchReq, - }) - } + body: searchReq, + interval: searchReq.Interval, + } - return multiRequests -} + if c.version == 2 { + mr.header["search_type"] = "count" + } -type v2Client struct { - baseClient -} + if c.version >= 56 { + maxConcurrentShardRequests := c.getSettings().Get("maxConcurrentShardRequests").MustInt(256) + mr.header["max_concurrent_shard_requests"] = maxConcurrentShardRequests + } -func newV2Client(bc baseClient) (*v2Client, error) { - c := v2Client{ - baseClient: bc, - } - - return &c, nil -} - -func (c *v2Client) createMultiSearchRequests(searchRequests []*SearchRequest) []*multiRequest { - multiRequests := c.baseClient.createMultiSearchRequests(searchRequests) - - for _, mr := range multiRequests { - mr.header["search_type"] = "count" - } - - return multiRequests -} - -type v5Client struct { - baseClient -} - -func newV5Client(bc baseClient) (*v5Client, error) { - c := v5Client{ - baseClient: bc, - } - - return &c, nil -} - -type v56Client struct { - *v5Client - maxConcurrentShardRequests int -} - -func newV56Client(bc baseClient) (*v56Client, error) { - v5Client := v5Client{ - baseClient: bc, - } - maxConcurrentShardRequests := bc.getSettings().Get("maxConcurrentShardRequests").MustInt(256) - - c := v56Client{ - v5Client: &v5Client, - maxConcurrentShardRequests: maxConcurrentShardRequests, - } - - return &c, nil -} - -func (c *v56Client) createMultiSearchRequests(searchRequests []*SearchRequest) []*multiRequest { - multiRequests := c.v5Client.createMultiSearchRequests(searchRequests) - - for _, mr := range multiRequests { - mr.header["max_concurrent_shard_requests"] = c.maxConcurrentShardRequests + multiRequests = append(multiRequests, &mr) } return multiRequests diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go index d557ceb28b1..11d1cdb1d71 100644 --- a/pkg/tsdb/elasticsearch/client/client_test.go +++ b/pkg/tsdb/elasticsearch/client/client_test.go @@ -1,10 +1,17 @@ package es import ( + "bytes" + "context" + "fmt" + "io/ioutil" "net/http" + "net/http/httptest" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" @@ -85,131 +92,213 @@ func TestClient(t *testing.T) { }) }) - Convey("v2", func() { - ds := &models.DataSource{ - JsonData: simplejson.NewFromAny(map[string]interface{}{ - "esVersion": 2, - }), + Convey("Given a fake http client", func() { + var responseBuffer *bytes.Buffer + var req *http.Request + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + req = r + buf, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("Failed to read response body, err=%v", err) + } + responseBuffer = bytes.NewBuffer(buf) + })) + + currentNewDatasourceHttpClient := newDatasourceHttpClient + + newDatasourceHttpClient = func(ds *models.DataSource) (*http.Client, error) { + return ts.Client(), nil } - c, err := newV2Client(newFakeBaseClient(ds, []string{"test-*"})) - So(err, ShouldBeNil) - So(c, ShouldNotBeNil) + from := time.Date(2018, 5, 15, 17, 50, 0, 0, time.UTC) + to := time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC) + fromStr := fmt.Sprintf("%d", from.UnixNano()/int64(time.Millisecond)) + toStr := fmt.Sprintf("%d", to.UnixNano()/int64(time.Millisecond)) + timeRange := tsdb.NewTimeRange(fromStr, toStr) - Convey("When creating multisearch requests should have correct headers", func() { - multiRequests := c.createMultiSearchRequests([]*SearchRequest{ - {Index: "test-*"}, - }) - So(multiRequests, ShouldHaveLength, 1) - header := multiRequests[0].header - So(header, ShouldHaveLength, 3) - So(header["index"], ShouldEqual, "test-*") - So(header["ignore_unavailable"], ShouldEqual, true) - So(header["search_type"], ShouldEqual, "count") - }) - }) - - Convey("v5", func() { - ds := &models.DataSource{ - JsonData: simplejson.NewFromAny(map[string]interface{}{ - "esVersion": 5, - }), - } - - c, err := newV5Client(newFakeBaseClient(ds, []string{"test-*"})) - So(err, ShouldBeNil) - So(c, ShouldNotBeNil) - - Convey("When creating multisearch requests should have correct headers", func() { - multiRequests := c.createMultiSearchRequests([]*SearchRequest{ - {Index: "test-*"}, - }) - So(multiRequests, ShouldHaveLength, 1) - header := multiRequests[0].header - So(header, ShouldHaveLength, 3) - So(header["index"], ShouldEqual, "test-*") - So(header["ignore_unavailable"], ShouldEqual, true) - So(header["search_type"], ShouldEqual, "query_then_fetch") - }) - }) - - Convey("v5.6", func() { - Convey("With default settings", func() { + Convey("and a v2.x client", func() { ds := models.DataSource{ + Database: "[metrics-]YYYY.MM.DD", + Url: ts.URL, JsonData: simplejson.NewFromAny(map[string]interface{}{ - "esVersion": 56, + "esVersion": 2, + "timeField": "@timestamp", + "interval": "Daily", }), } - c, err := newV56Client(newFakeBaseClient(&ds, []string{"test-*"})) + c, err := NewClient(context.Background(), &ds, timeRange) So(err, ShouldBeNil) So(c, ShouldNotBeNil) - Convey("When creating multisearch requests should have correct headers", func() { - multiRequests := c.createMultiSearchRequests([]*SearchRequest{ - {Index: "test-*"}, + Convey("When executing multi search", func() { + ms, err := createMultisearchForTest(c) + So(err, ShouldBeNil) + c.ExecuteMultisearch(ms) + + Convey("Should send correct request and payload", func() { + So(req, ShouldNotBeNil) + So(req.Method, ShouldEqual, http.MethodPost) + So(req.URL.Path, ShouldEqual, "/_msearch") + + So(responseBuffer, ShouldNotBeNil) + + headerBytes, err := responseBuffer.ReadBytes('\n') + So(err, ShouldBeNil) + bodyBytes := responseBuffer.Bytes() + + jHeader, err := simplejson.NewJson(headerBytes) + So(err, ShouldBeNil) + + jBody, err := simplejson.NewJson(bodyBytes) + So(err, ShouldBeNil) + + fmt.Println("body", string(headerBytes)) + + So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15") + So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true) + So(jHeader.Get("search_type").MustString(), ShouldEqual, "count") + So(jHeader.Get("max_concurrent_shard_requests").MustInt(10), ShouldEqual, 10) + + Convey("and replace $__interval variable", func() { + So(jBody.GetPath("aggs", "2", "aggs", "1", "avg", "script").MustString(), ShouldEqual, "15000*@hostname") + }) + + Convey("and replace $__interval_ms variable", func() { + So(jBody.GetPath("aggs", "2", "date_histogram", "interval").MustString(), ShouldEqual, "15s") + }) }) - So(multiRequests, ShouldHaveLength, 1) - header := multiRequests[0].header - So(header, ShouldHaveLength, 4) - So(header["index"], ShouldEqual, "test-*") - So(header["ignore_unavailable"], ShouldEqual, true) - So(header["search_type"], ShouldEqual, "query_then_fetch") - So(header["max_concurrent_shard_requests"], ShouldEqual, 256) }) }) - Convey("With custom settings", func() { + Convey("and a v5.x client", func() { ds := models.DataSource{ + Database: "[metrics-]YYYY.MM.DD", + Url: ts.URL, + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "esVersion": 5, + "maxConcurrentShardRequests": 100, + "timeField": "@timestamp", + "interval": "Daily", + }), + } + + c, err := NewClient(context.Background(), &ds, timeRange) + So(err, ShouldBeNil) + So(c, ShouldNotBeNil) + + Convey("When executing multi search", func() { + ms, err := createMultisearchForTest(c) + So(err, ShouldBeNil) + c.ExecuteMultisearch(ms) + + Convey("Should send correct request and payload", func() { + So(req, ShouldNotBeNil) + So(req.Method, ShouldEqual, http.MethodPost) + So(req.URL.Path, ShouldEqual, "/_msearch") + + So(responseBuffer, ShouldNotBeNil) + + headerBytes, err := responseBuffer.ReadBytes('\n') + So(err, ShouldBeNil) + bodyBytes := responseBuffer.Bytes() + + jHeader, err := simplejson.NewJson(headerBytes) + So(err, ShouldBeNil) + + jBody, err := simplejson.NewJson(bodyBytes) + So(err, ShouldBeNil) + + fmt.Println("body", string(headerBytes)) + + So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15") + So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true) + So(jHeader.Get("search_type").MustString(), ShouldEqual, "query_then_fetch") + So(jHeader.Get("max_concurrent_shard_requests").MustInt(10), ShouldEqual, 10) + + Convey("and replace $__interval variable", func() { + So(jBody.GetPath("aggs", "2", "aggs", "1", "avg", "script").MustString(), ShouldEqual, "15000*@hostname") + }) + + Convey("and replace $__interval_ms variable", func() { + So(jBody.GetPath("aggs", "2", "date_histogram", "interval").MustString(), ShouldEqual, "15s") + }) + }) + }) + }) + + Convey("and a v5.6 client", func() { + ds := models.DataSource{ + Database: "[metrics-]YYYY.MM.DD", + Url: ts.URL, JsonData: simplejson.NewFromAny(map[string]interface{}{ "esVersion": 56, "maxConcurrentShardRequests": 100, + "timeField": "@timestamp", + "interval": "Daily", }), } - c, err := newV56Client(newFakeBaseClient(&ds, []string{"test-*"})) + c, err := NewClient(context.Background(), &ds, timeRange) So(err, ShouldBeNil) So(c, ShouldNotBeNil) - Convey("When creating multisearch requests should have correct headers", func() { - multiRequests := c.createMultiSearchRequests([]*SearchRequest{ - {Index: "test-*"}, + + Convey("When executing multi search", func() { + ms, err := createMultisearchForTest(c) + So(err, ShouldBeNil) + c.ExecuteMultisearch(ms) + + Convey("Should send correct request and payload", func() { + So(req, ShouldNotBeNil) + So(req.Method, ShouldEqual, http.MethodPost) + So(req.URL.Path, ShouldEqual, "/_msearch") + + So(responseBuffer, ShouldNotBeNil) + + headerBytes, err := responseBuffer.ReadBytes('\n') + So(err, ShouldBeNil) + bodyBytes := responseBuffer.Bytes() + + jHeader, err := simplejson.NewJson(headerBytes) + So(err, ShouldBeNil) + + jBody, err := simplejson.NewJson(bodyBytes) + So(err, ShouldBeNil) + + fmt.Println("body", string(headerBytes)) + + So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15") + So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true) + So(jHeader.Get("search_type").MustString(), ShouldEqual, "query_then_fetch") + So(jHeader.Get("max_concurrent_shard_requests").MustInt(), ShouldEqual, 100) + + Convey("and replace $__interval variable", func() { + So(jBody.GetPath("aggs", "2", "aggs", "1", "avg", "script").MustString(), ShouldEqual, "15000*@hostname") + }) + + Convey("and replace $__interval_ms variable", func() { + So(jBody.GetPath("aggs", "2", "date_histogram", "interval").MustString(), ShouldEqual, "15s") + }) }) - So(multiRequests, ShouldHaveLength, 1) - header := multiRequests[0].header - So(header, ShouldHaveLength, 4) - So(header["index"], ShouldEqual, "test-*") - So(header["ignore_unavailable"], ShouldEqual, true) - So(header["search_type"], ShouldEqual, "query_then_fetch") - So(header["max_concurrent_shard_requests"], ShouldEqual, 100) }) }) + + Reset(func() { + newDatasourceHttpClient = currentNewDatasourceHttpClient + }) }) }) } -type fakeBaseClient struct { - *baseClientImpl - ds *models.DataSource -} +func createMultisearchForTest(c Client) (*MultiSearchRequest, error) { + msb := c.MultiSearch() + s := msb.Search(tsdb.Interval{Value: 15 * time.Second, Text: "15s"}) + s.Agg().DateHistogram("2", "@timestamp", func(a *DateHistogramAgg, ab AggBuilder) { + a.Interval = "$__interval" -func newFakeBaseClient(ds *models.DataSource, indices []string) baseClient { - return &fakeBaseClient{ - baseClientImpl: &baseClientImpl{ - ds: ds, - indices: indices, - }, - ds: ds, - } -} - -func (c *fakeBaseClient) executeBatchRequest(uriPath string, requests []*multiRequest) (*http.Response, error) { - return nil, nil -} - -func (c *fakeBaseClient) executeRequest(method, uriPath string, body []byte) (*http.Response, error) { - return nil, nil -} - -func (c *fakeBaseClient) executeMultisearch(searchRequests []*SearchRequest) ([]*SearchResponse, error) { - return nil, nil + ab.Metric("1", "avg", "@hostname", func(a *MetricAggregation) { + a.Settings["script"] = "$__interval_ms*@hostname" + }) + }) + return msb.Build() } diff --git a/pkg/tsdb/elasticsearch/client/models.go b/pkg/tsdb/elasticsearch/client/models.go index 2f4f5dcd162..a5810a9b109 100644 --- a/pkg/tsdb/elasticsearch/client/models.go +++ b/pkg/tsdb/elasticsearch/client/models.go @@ -2,11 +2,14 @@ package es import ( "encoding/json" + + "github.com/grafana/grafana/pkg/tsdb" ) // SearchRequest represents a search request type SearchRequest struct { Index string + Interval tsdb.Interval Size int Sort map[string]interface{} Query *Query diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index a582d8ec247..2b833ce78d3 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -2,11 +2,14 @@ package es import ( "strings" + + "github.com/grafana/grafana/pkg/tsdb" ) // SearchRequestBuilder represents a builder which can build a search request type SearchRequestBuilder struct { version int + interval tsdb.Interval index string size int sort map[string]interface{} @@ -16,9 +19,10 @@ type SearchRequestBuilder struct { } // NewSearchRequestBuilder create a new search request builder -func NewSearchRequestBuilder(version int) *SearchRequestBuilder { +func NewSearchRequestBuilder(version int, interval tsdb.Interval) *SearchRequestBuilder { builder := &SearchRequestBuilder{ version: version, + interval: interval, sort: make(map[string]interface{}), customProps: make(map[string]interface{}), aggBuilders: make([]AggBuilder, 0), @@ -30,6 +34,7 @@ func NewSearchRequestBuilder(version int) *SearchRequestBuilder { func (b *SearchRequestBuilder) Build() (*SearchRequest, error) { sr := SearchRequest{ Index: b.index, + Interval: b.interval, Size: b.size, Sort: b.sort, CustomProps: b.customProps, @@ -128,8 +133,8 @@ func NewMultiSearchRequestBuilder(version int) *MultiSearchRequestBuilder { } // Search initiates and returns a new search request builder -func (m *MultiSearchRequestBuilder) Search() *SearchRequestBuilder { - b := NewSearchRequestBuilder(m.version) +func (m *MultiSearchRequestBuilder) Search(interval tsdb.Interval) *SearchRequestBuilder { + b := NewSearchRequestBuilder(m.version, interval) m.requestBuilders = append(m.requestBuilders, b) return b } diff --git a/pkg/tsdb/elasticsearch/client/search_request_test.go b/pkg/tsdb/elasticsearch/client/search_request_test.go index d93f8826442..b026578d64f 100644 --- a/pkg/tsdb/elasticsearch/client/search_request_test.go +++ b/pkg/tsdb/elasticsearch/client/search_request_test.go @@ -3,8 +3,10 @@ package es import ( "encoding/json" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" ) @@ -13,7 +15,7 @@ func TestSearchRequest(t *testing.T) { Convey("Test elasticsearch search request", t, func() { timeField := "@timestamp" Convey("Given new search request builder for es version 5", func() { - b := NewSearchRequestBuilder(5) + b := NewSearchRequestBuilder(5, tsdb.Interval{Value: 15 * time.Second, Text: "15s"}) Convey("When building search request", func() { sr, err := b.Build() @@ -388,7 +390,7 @@ func TestSearchRequest(t *testing.T) { }) Convey("Given new search request builder for es version 2", func() { - b := NewSearchRequestBuilder(2) + b := NewSearchRequestBuilder(2, tsdb.Interval{Value: 15 * time.Second, Text: "15s"}) Convey("When adding doc value field", func() { b.AddDocValueField(timeField) @@ -447,7 +449,7 @@ func TestMultiSearchRequest(t *testing.T) { b := NewMultiSearchRequestBuilder(0) Convey("When adding one search request", func() { - b.Search() + b.Search(tsdb.Interval{Value: 15 * time.Second, Text: "15s"}) Convey("When building search request should contain one search request", func() { mr, err := b.Build() @@ -457,8 +459,8 @@ func TestMultiSearchRequest(t *testing.T) { }) Convey("When adding two search requests", func() { - b.Search() - b.Search() + b.Search(tsdb.Interval{Value: 15 * time.Second, Text: "15s"}) + b.Search(tsdb.Interval{Value: 15 * time.Second, Text: "15s"}) Convey("When building search request should contain two search requests", func() { mr, err := b.Build() diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go index ef59c62c1dc..c9bb05dd09a 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/time_series_query.go @@ -3,8 +3,6 @@ package elasticsearch import ( "fmt" "strconv" - "strings" - "time" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/tsdb" @@ -47,7 +45,7 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) { } interval := e.intervalCalculator.Calculate(e.tsdbQuery.TimeRange, minInterval) - b := ms.Search() + b := ms.Search(interval) b.Size(0) filters := b.Query().Bool().Filter() filters.AddDateRangeFilter(e.client.GetTimeField(), to, from, es.DateFormatEpochMS) @@ -78,7 +76,7 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) { for _, bucketAgg := range q.BucketAggs { switch bucketAgg.Type { case "date_histogram": - aggBuilder = addDateHistogramAgg(aggBuilder, bucketAgg, from, to, interval) + aggBuilder = addDateHistogramAgg(aggBuilder, bucketAgg, from, to) case "histogram": aggBuilder = addHistogramAgg(aggBuilder, bucketAgg) case "filters": @@ -125,7 +123,7 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) { return rp.getTimeSeries() } -func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFrom, timeTo string, interval tsdb.Interval) es.AggBuilder { +func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFrom, timeTo string) es.AggBuilder { aggBuilder.DateHistogram(bucketAgg.ID, bucketAgg.Field, func(a *es.DateHistogramAgg, b es.AggBuilder) { a.Interval = bucketAgg.Settings.Get("interval").MustString("auto") a.MinDocCount = bucketAgg.Settings.Get("min_doc_count").MustInt(0) @@ -136,10 +134,6 @@ func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFro a.Interval = "$__interval" } - a.Interval = strings.Replace(a.Interval, "$interval", interval.Text, -1) - a.Interval = strings.Replace(a.Interval, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1) - a.Interval = strings.Replace(a.Interval, "$__interval", interval.Text, -1) - if missing, err := bucketAgg.Settings.Get("missing").String(); err == nil { a.Missing = &missing } diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index e2af4de749a..49bf5f5bc75 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -268,7 +268,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { So(firstLevel.Aggregation.Type, ShouldEqual, "date_histogram") hAgg := firstLevel.Aggregation.Aggregation.(*es.DateHistogramAgg) So(hAgg.Field, ShouldEqual, "@timestamp") - So(hAgg.Interval, ShouldEqual, "15s") + So(hAgg.Interval, ShouldEqual, "$__interval") So(hAgg.MinDocCount, ShouldEqual, 2) }) From b8ff3b1e3fa2fa061aaad41f2e44edb15272150b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 31 May 2018 19:05:32 +0200 Subject: [PATCH 0789/3000] remove dead code --- pkg/tsdb/elasticsearch/client/client.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index 8ae45599484..0cd3eb0d21b 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -79,14 +79,6 @@ var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb return nil, fmt.Errorf("elasticsearch version=%d is not supported", version) } -type baseClient interface { - Client - getSettings() *simplejson.Json - executeBatchRequest(uriPath string, requests []*multiRequest) (*http.Response, error) - executeRequest(method, uriPath string, body []byte) (*http.Response, error) - createMultiSearchRequests(searchRequests []*SearchRequest) []*multiRequest -} - type baseClientImpl struct { ctx context.Context ds *models.DataSource From c817aecd660ea563e7bdb8f723e8bb39e3ad3c53 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 14:13:34 +0200 Subject: [PATCH 0790/3000] provisioning: only update dashboard if hash of json changed --- .../dashboards/bulk-testing/bulkdash.jsonnet | 2 +- devenv/setup.sh | 6 +-- pkg/models/dashboards.go | 1 + .../provisioning/dashboards/file_reader.go | 45 ++++++++++++++++--- .../sqlstore/migrations/dashboard_mig.go | 4 ++ pkg/util/md5.go | 26 +++++++++++ pkg/util/md5_test.go | 17 +++++++ 7 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 pkg/util/md5.go create mode 100644 pkg/util/md5_test.go diff --git a/devenv/dashboards/bulk-testing/bulkdash.jsonnet b/devenv/dashboards/bulk-testing/bulkdash.jsonnet index 17b3f8983af..4c82fd36f69 100644 --- a/devenv/dashboards/bulk-testing/bulkdash.jsonnet +++ b/devenv/dashboards/bulk-testing/bulkdash.jsonnet @@ -1137,4 +1137,4 @@ "title": "Big Dashboard", "uid": "000000003", "version": 16 -} \ No newline at end of file +} diff --git a/devenv/setup.sh b/devenv/setup.sh index d6f8f969e75..0a8958131fb 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -5,10 +5,10 @@ bulkDashboard() { requiresJsonnet COUNTER=0 - MAX=400 + MAX=4 while [ $COUNTER -lt $MAX ]; do jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" - let COUNTER=COUNTER+1 + let COUNTER=COUNTER+1 done ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml @@ -58,4 +58,4 @@ main() { fi } -main "$@" \ No newline at end of file +main "$@" diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index eb44c1bc582..4b84d840113 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -254,6 +254,7 @@ type DashboardProvisioning struct { DashboardId int64 Name string ExternalId string + CheckSum string Updated int64 } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index cd4598794bc..e1cb92abc83 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -4,12 +4,14 @@ import ( "context" "errors" "fmt" + "io/ioutil" "os" "path/filepath" "strings" "time" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/bus" @@ -161,13 +163,18 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil provisionedData, alreadyProvisioned := provisionedDashboardRefs[path] upToDate := alreadyProvisioned && provisionedData.Updated >= resolvedFileInfo.ModTime().Unix() - dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId) + jsonFile, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId) if err != nil { fr.log.Error("failed to load dashboard from ", "file", path, "error", err) return provisioningMetadata, nil } + if provisionedData != nil && jsonFile.checkSum == provisionedData.CheckSum { + upToDate = true + } + // keeps track of what uid's and title's we have already provisioned + dash := jsonFile.dashboard provisioningMetadata.uid = dash.Dashboard.Uid provisioningMetadata.title = dash.Dashboard.Title @@ -185,7 +192,13 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil } fr.log.Debug("saving new dashboard", "file", path) - dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()} + dp := &models.DashboardProvisioning{ + ExternalId: path, + Name: fr.Cfg.Name, + Updated: resolvedFileInfo.ModTime().Unix(), + CheckSum: jsonFile.checkSum, + } + _, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp) return provisioningMetadata, err } @@ -283,14 +296,30 @@ func validateWalkablePath(fileInfo os.FileInfo) (bool, error) { return true, nil } -func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) { +type dashboardJsonFile struct { + dashboard *dashboards.SaveDashboardDTO + checkSum string + lastModified time.Time +} + +func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboardJsonFile, error) { reader, err := os.Open(path) if err != nil { return nil, err } defer reader.Close() - data, err := simplejson.NewFromReader(reader) + all, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + + checkSum, err := util.Md5SumString(string(all)) + if err != nil { + return nil, err + } + + data, err := simplejson.NewJson(all) if err != nil { return nil, err } @@ -300,7 +329,11 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, return nil, err } - return dash, nil + return &dashboardJsonFile{ + dashboard: dash, + checkSum: checkSum, + lastModified: lastModified, + }, nil } type provisioningMetadata struct { @@ -328,7 +361,6 @@ func (checker provisioningSanityChecker) track(pm provisioningMetadata) { if len(pm.title) > 0 { checker.titleUsage[pm.title] += 1 } - } func (checker provisioningSanityChecker) logWarnings(log log.Logger) { @@ -343,5 +375,4 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) { log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider) } } - } diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 170498c4bd9..b770afb1b4e 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -211,4 +211,8 @@ func addDashboardMigration(mg *Migrator) { "name": "name", "external_id": "external_id", }) + + mg.AddMigration("Add check_sum column", NewAddColumnMigration(dashboardExtrasTableV2, &Column{ + Name: "check_sum", Type: DB_NVarchar, Length: 32, Nullable: true, + })) } diff --git a/pkg/util/md5.go b/pkg/util/md5.go new file mode 100644 index 00000000000..2473a1a406c --- /dev/null +++ b/pkg/util/md5.go @@ -0,0 +1,26 @@ +package util + +import ( + "crypto/md5" + "encoding/hex" + "io" + "strings" +) + +// Md5Sum calculates the md5sum of a stream +func Md5Sum(reader io.Reader) (string, error) { + var returnMD5String string + hash := md5.New() + if _, err := io.Copy(hash, reader); err != nil { + return returnMD5String, err + } + hashInBytes := hash.Sum(nil)[:16] + returnMD5String = hex.EncodeToString(hashInBytes) + return returnMD5String, nil +} + +// Md5Sum calculates the md5sum of a string +func Md5SumString(input string) (string, error) { + buffer := strings.NewReader(input) + return Md5Sum(buffer) +} diff --git a/pkg/util/md5_test.go b/pkg/util/md5_test.go new file mode 100644 index 00000000000..1338d42bb51 --- /dev/null +++ b/pkg/util/md5_test.go @@ -0,0 +1,17 @@ +package util + +import "testing" + +func TestMd5Sum(t *testing.T) { + input := "dont hash passwords with md5" + + have, err := Md5SumString(input) + if err != nil { + t.Fatal("expected err to be nil") + } + + want := "2d6a56c82d09d374643b926d3417afba" + if have != want { + t.Fatalf("expected: %s got: %s", want, have) + } +} From 333af6fd9b5a01f5bda81928f79a7bb0779df245 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 May 2018 19:35:46 +0200 Subject: [PATCH 0791/3000] provisioning: makes the interval for polling for changes configurable --- devenv/setup.sh | 2 +- pkg/services/provisioning/dashboards/config_reader.go | 4 ++++ pkg/services/provisioning/dashboards/config_reader_test.go | 2 ++ pkg/services/provisioning/dashboards/file_reader.go | 4 +--- .../test-configs/dashboards-from-disk/dev-dashboards.yaml | 1 + .../testdata/test-configs/version-0/version-0.yaml | 1 + pkg/services/provisioning/dashboards/types.go | 5 +++++ 7 files changed, 15 insertions(+), 4 deletions(-) diff --git a/devenv/setup.sh b/devenv/setup.sh index 0a8958131fb..c75973ae3ce 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -5,7 +5,7 @@ bulkDashboard() { requiresJsonnet COUNTER=0 - MAX=4 + MAX=400 while [ $COUNTER -lt $MAX ]; do jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" let COUNTER=COUNTER+1 diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 4f9577f82db..f8b6070c704 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -81,6 +81,10 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { if dashboards[i].OrgId == 0 { dashboards[i].OrgId = 1 } + + if dashboards[i].IntervalSeconds == 0 { + dashboards[i].IntervalSeconds = 3 + } } return dashboards, nil diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index 25732089dcd..b49cd258005 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -68,6 +68,7 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(len(ds.Options), ShouldEqual, 1) So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds.DisableDeletion, ShouldBeTrue) + So(ds.IntervalSeconds, ShouldEqual, 10) ds2 := cfg[1] So(ds2.Name, ShouldEqual, "default") @@ -78,4 +79,5 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(len(ds2.Options), ShouldEqual, 1) So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds2.DisableDeletion, ShouldBeFalse) + So(ds2.IntervalSeconds, ShouldEqual, 3) } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index e1cb92abc83..a25b0208ad3 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -21,8 +21,6 @@ import ( ) var ( - checkDiskForChangesInterval = time.Second * 3 - ErrFolderNameMissing = errors.New("Folder name missing") ) @@ -68,7 +66,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error { fr.log.Error("failed to search for dashboards", "error", err) } - ticker := time.NewTicker(checkDiskForChangesInterval) + ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.IntervalSeconds)) running := false diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml index e9776d69010..5ea2a0a4f75 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -6,6 +6,7 @@ providers: folder: 'developers' editable: true disableDeletion: true + intervalSeconds: 10 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml index 979e762d4d4..bdbb06079fd 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml @@ -3,6 +3,7 @@ folder: 'developers' editable: true disableDeletion: true + intervalSeconds: 10 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 4a55351d3e4..424e5e35f4a 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -17,6 +17,7 @@ type DashboardsAsConfig struct { Editable bool Options map[string]interface{} DisableDeletion bool + IntervalSeconds int64 } type DashboardsAsConfigV0 struct { @@ -27,6 +28,7 @@ type DashboardsAsConfigV0 struct { Editable bool `json:"editable" yaml:"editable"` Options map[string]interface{} `json:"options" yaml:"options"` DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` + IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` } type ConfigVersion struct { @@ -45,6 +47,7 @@ type DashboardProviderConfigs struct { Editable bool `json:"editable" yaml:"editable"` Options map[string]interface{} `json:"options" yaml:"options"` DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` + IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` } func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) { @@ -75,6 +78,7 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig Editable: v.Editable, Options: v.Options, DisableDeletion: v.DisableDeletion, + IntervalSeconds: v.IntervalSeconds, }) } @@ -93,6 +97,7 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { Editable: v.Editable, Options: v.Options, DisableDeletion: v.DisableDeletion, + IntervalSeconds: v.IntervalSeconds, }) } From 13a9701581a38e6c18ee3bc39709498bd4672be3 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 1 Jun 2018 10:13:48 +0200 Subject: [PATCH 0792/3000] Update CHANGELOG.md - added Prometheus query date alignment --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad20e5e5bd..6d7e46d6cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * **Dashboard**: Modified time range and variables are now not saved by default [#10748](https://github.com/grafana/grafana/issues/10748), [#8805](https://github.com/grafana/grafana/issues/8805) * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) +* **Prometheus**: Query dates are now step-aligned [#10434](https://github.com/grafana/grafana/pull/10434) * **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda) * **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm) * **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616) From 18e4271abdabaa111feabf656d3f1bc0f8bf0355 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 10:34:57 +0200 Subject: [PATCH 0793/3000] added span with folder title that is shown for recently and starred, created a new class for folder title --- public/app/core/components/search/search_results.html | 2 +- public/sass/components/_search.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 7435f8d0b7e..9f266ed3a6b 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -33,7 +33,7 @@ -
    {{::item.title}}
    +
    {{::item.title}} {{::item.folderTitle}}
    diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 8338a5d72ae..b00168505fa 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -208,6 +208,12 @@ color: $list-item-link-color; } +.search-item__body-folder-title { + color: $text-color-weak; + font-style: italic; + padding-left: 0.25rem; +} + .search-item__icon { padding: 5px; flex: 0 0 auto; From ce75afa413defc682fab6a22f02dad45c644f7ce Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 1 Jun 2018 12:26:20 +0200 Subject: [PATCH 0794/3000] docs: update alerting docs with alerting support for elasticsearch --- docs/sources/alerting/rules.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index bcca3c6b2fb..fa7332e7145 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -27,7 +27,9 @@ and the conditions that need to be met for the alert to change state and trigger ## Execution The alert rules are evaluated in the Grafana backend in a scheduler and query execution engine that is part -of core Grafana. Only some data sources are supported right now. They include `Graphite`, `Prometheus`, `InfluxDB`, `OpenTSDB`, `MySQL`, `Postgres` and `Cloudwatch`. +of core Grafana. Only some data sources are supported right now. They include `Graphite`, `Prometheus`, `Elasticsearch`, `InfluxDB`, `OpenTSDB`, `MySQL`, `Postgres` and `Cloudwatch`. + +> Alerting support for Elasticsearch is only available in Grafana v5.2 and above. ### Clustering @@ -152,6 +154,8 @@ filters = alerting.scheduler:debug \ tsdb.prometheus:debug \ tsdb.opentsdb:debug \ tsdb.influxdb:debug \ + tsdb.elasticsearch:debug \ + tsdb.elasticsearch.client:debug \ ``` If you want to log raw query sent to your TSDB and raw response in log you also have to set grafana.ini option `app_mode` to From 0c269d64d02c80c0d1a58b2cc12be186dd16eddc Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 14:36:40 +0200 Subject: [PATCH 0795/3000] Alert panel filters (#11712) alert list panel: filter alerts by name, dashboard, folder, tags --- docs/sources/http_api/alerting.md | 11 +++- pkg/api/alerting.go | 64 +++++++++++++++++-- pkg/api/alerting_test.go | 55 ++++++++++++++++ pkg/models/alert.go | 13 ++-- pkg/services/sqlstore/alert.go | 12 +++- pkg/services/sqlstore/alert_test.go | 15 +++-- .../dashboard/folder_picker/folder_picker.ts | 12 +++- .../app/plugins/panel/alertlist/editor.html | 24 +++++++ public/app/plugins/panel/alertlist/module.ts | 29 +++++++++ 9 files changed, 210 insertions(+), 25 deletions(-) diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index 4d52105cf3c..e4fe0dad3ff 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -35,10 +35,15 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk `/api/alerts?dashboardId=1` - - **dashboardId** – Return alerts for a specified dashboard. - - **panelId** – Return alerts for a specified panel on a dashboard. - - **limit** - Limit response to x number of alerts. + - **dashboardId** – Limit response to alerts in specified dashboard(s). You can specify multiple dashboards, e.g. dashboardId=23&dashboardId=35. + - **panelId** – Limit response to alert for a specified panel on a dashboard. + - **query** - Limit response to alerts having a name like this value. - **state** - Return alerts with one or more of the following alert states: `ALL`,`no_data`, `paused`, `alerting`, `ok`, `pending`. To specify multiple states use the following format: `?state=paused&state=alerting` + - **limit** - Limit response to *X* number of alerts. + - **folderId** – Limit response to alerts of dashboards in specified folder(s). You can specify multiple folders, e.g. folderId=23&folderId=35. + - **dashboardQuery** - Limit response to alerts having a dashboard name like this value. + - **dashboardTag** - Limit response to alerts of dashboards with specified tags. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. dashboardTag=tag1&dashboardTag=tag2. + **Example Response**: diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index a9a3773ceb1..961fc11b2dc 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -2,12 +2,14 @@ package api import ( "fmt" + "strconv" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/search" ) func ValidateOrgAlert(c *m.ReqContext) { @@ -46,12 +48,64 @@ func GetAlertStatesForDashboard(c *m.ReqContext) Response { // GET /api/alerts func GetAlerts(c *m.ReqContext) Response { + dashboardQuery := c.Query("dashboardQuery") + dashboardTags := c.QueryStrings("dashboardTag") + stringDashboardIDs := c.QueryStrings("dashboardId") + stringFolderIDs := c.QueryStrings("folderId") + + dashboardIDs := make([]int64, 0) + for _, id := range stringDashboardIDs { + dashboardID, err := strconv.ParseInt(id, 10, 64) + if err == nil { + dashboardIDs = append(dashboardIDs, dashboardID) + } + } + + if dashboardQuery != "" || len(dashboardTags) > 0 || len(stringFolderIDs) > 0 { + folderIDs := make([]int64, 0) + for _, id := range stringFolderIDs { + folderID, err := strconv.ParseInt(id, 10, 64) + if err == nil { + folderIDs = append(folderIDs, folderID) + } + } + + searchQuery := search.Query{ + Title: dashboardQuery, + Tags: dashboardTags, + SignedInUser: c.SignedInUser, + Limit: 1000, + OrgId: c.OrgId, + DashboardIds: dashboardIDs, + Type: string(search.DashHitDB), + FolderIds: folderIDs, + Permission: m.PERMISSION_EDIT, + } + + err := bus.Dispatch(&searchQuery) + if err != nil { + return Error(500, "List alerts failed", err) + } + + for _, d := range searchQuery.Result { + if d.Type == search.DashHitDB && d.Id > 0 { + dashboardIDs = append(dashboardIDs, d.Id) + } + } + + // if we didn't find any dashboards, return empty result + if len(dashboardIDs) == 0 { + return JSON(200, []*m.AlertListItemDTO{}) + } + } + query := m.GetAlertsQuery{ - OrgId: c.OrgId, - DashboardId: c.QueryInt64("dashboardId"), - PanelId: c.QueryInt64("panelId"), - Limit: c.QueryInt64("limit"), - User: c.SignedInUser, + OrgId: c.OrgId, + DashboardIDs: dashboardIDs, + PanelId: c.QueryInt64("panelId"), + Limit: c.QueryInt64("limit"), + User: c.SignedInUser, + Query: c.Query("query"), } states := c.QueryStrings("state") diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 9302ef7beca..abfdfb66322 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/search" . "github.com/smartystreets/goconvey/convey" ) @@ -64,6 +65,60 @@ func TestAlertingApiEndpoint(t *testing.T) { }) }) }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1", "/api/alerts", m.ROLE_EDITOR, func(sc *scenarioContext) { + var searchQuery *search.Query + bus.AddHandler("test", func(query *search.Query) error { + searchQuery = query + return nil + }) + + var getAlertsQuery *m.GetAlertsQuery + bus.AddHandler("test", func(query *m.GetAlertsQuery) error { + getAlertsQuery = query + return nil + }) + + sc.handlerFunc = GetAlerts + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(searchQuery, ShouldBeNil) + So(getAlertsQuery, ShouldNotBeNil) + }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1&dashboardId=2&folderId=3&dashboardTag=abc&dashboardQuery=dbQuery&limit=5&query=alertQuery", "/api/alerts", m.ROLE_EDITOR, func(sc *scenarioContext) { + var searchQuery *search.Query + bus.AddHandler("test", func(query *search.Query) error { + searchQuery = query + query.Result = search.HitList{ + &search.Hit{Id: 1}, + &search.Hit{Id: 2}, + } + return nil + }) + + var getAlertsQuery *m.GetAlertsQuery + bus.AddHandler("test", func(query *m.GetAlertsQuery) error { + getAlertsQuery = query + return nil + }) + + sc.handlerFunc = GetAlerts + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + So(searchQuery, ShouldNotBeNil) + So(searchQuery.DashboardIds[0], ShouldEqual, 1) + So(searchQuery.DashboardIds[1], ShouldEqual, 2) + So(searchQuery.FolderIds[0], ShouldEqual, 3) + So(searchQuery.Tags[0], ShouldEqual, "abc") + So(searchQuery.Title, ShouldEqual, "dbQuery") + + So(getAlertsQuery, ShouldNotBeNil) + So(getAlertsQuery.DashboardIDs[0], ShouldEqual, 1) + So(getAlertsQuery.DashboardIDs[1], ShouldEqual, 2) + So(getAlertsQuery.Limit, ShouldEqual, 5) + So(getAlertsQuery.Query, ShouldEqual, "alertQuery") + }) }) } diff --git a/pkg/models/alert.go b/pkg/models/alert.go index b72d87e94b2..fba2aa63df9 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -161,12 +161,13 @@ type SetAlertStateCommand struct { //Queries type GetAlertsQuery struct { - OrgId int64 - State []string - DashboardId int64 - PanelId int64 - Limit int64 - User *SignedInUser + OrgId int64 + State []string + DashboardIDs []int64 + PanelId int64 + Limit int64 + Query string + User *SignedInUser Result []*AlertListItemDTO } diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index b0ca50eb67d..58ec7e2857a 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -82,8 +82,16 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { builder.Write(`WHERE alert.org_id = ?`, query.OrgId) - if query.DashboardId != 0 { - builder.Write(` AND alert.dashboard_id = ?`, query.DashboardId) + if len(strings.TrimSpace(query.Query)) > 0 { + builder.Write(" AND alert.name "+dialect.LikeStr()+" ?", "%"+query.Query+"%") + } + + if len(query.DashboardIDs) > 0 { + builder.sql.WriteString(` AND alert.dashboard_id IN (?` + strings.Repeat(",?", len(query.DashboardIDs)-1) + `) `) + + for _, dbID := range query.DashboardIDs { + builder.AddParams(dbID) + } } if query.PanelId != 0 { diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index 296d16c2f45..be48c7b2f52 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -3,10 +3,11 @@ package sqlstore import ( "testing" + "time" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" - "time" ) func mockTimeNow() { @@ -99,7 +100,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Can read properties", func() { - alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} + alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&alertQuery) alert := alertQuery.Result[0] @@ -109,7 +110,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Viewer cannot read alerts", func() { - alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}} + alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}} err2 := HandleAlertsQuery(&alertQuery) So(err2, ShouldBeNil) @@ -134,7 +135,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Alerts should be updated", func() { - query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} + query := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&query) So(err2, ShouldBeNil) @@ -183,7 +184,7 @@ func TestAlertingDataAccess(t *testing.T) { Convey("Should save 3 dashboards", func() { So(err, ShouldBeNil) - queryForDashboard := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} + queryForDashboard := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&queryForDashboard) So(err2, ShouldBeNil) @@ -197,7 +198,7 @@ func TestAlertingDataAccess(t *testing.T) { err = SaveAlerts(&cmd) Convey("should delete the missing alert", func() { - query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} + query := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&query) So(err2, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -232,7 +233,7 @@ func TestAlertingDataAccess(t *testing.T) { So(err, ShouldBeNil) Convey("Alerts should be removed", func() { - query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} + query := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}} err2 := HandleAlertsQuery(&query) So(testDash.Id, ShouldEqual, 1) diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts index b8ae18b14d3..69a09455c4d 100644 --- a/public/app/features/dashboard/folder_picker/folder_picker.ts +++ b/public/app/features/dashboard/folder_picker/folder_picker.ts @@ -12,6 +12,7 @@ export class FolderPickerCtrl { enterFolderCreation: any; exitFolderCreation: any; enableCreateNew: boolean; + enableReset: boolean; rootName = 'General'; folder: any; createNewFolder: boolean; @@ -58,6 +59,10 @@ export class FolderPickerCtrl { result.unshift({ title: '-- New Folder --', id: -1 }); } + if (this.enableReset && query === '' && this.initialTitle !== '') { + result.unshift({ title: this.initialTitle, id: null }); + } + return _.map(result, item => { return { text: item.title, value: item.id }; }); @@ -65,7 +70,9 @@ export class FolderPickerCtrl { } onFolderChange(option) { - if (option.value === -1) { + if (!option) { + option = { value: 0, text: this.rootName }; + } else if (option.value === -1) { this.createNewFolder = true; this.enterFolderCreation(); return; @@ -134,7 +141,7 @@ export class FolderPickerCtrl { this.onFolderLoad(); }); } else { - if (this.initialTitle) { + if (this.initialTitle && this.initialFolderId === null) { this.folder = { text: this.initialTitle, value: null }; } else { this.folder = { text: this.rootName, value: 0 }; @@ -171,6 +178,7 @@ export function folderPicker() { enterFolderCreation: '&', exitFolderCreation: '&', enableCreateNew: '@', + enableReset: '@', }, }; } diff --git a/public/app/plugins/panel/alertlist/editor.html b/public/app/plugins/panel/alertlist/editor.html index 36c989dd72c..c48b70e02c0 100644 --- a/public/app/plugins/panel/alertlist/editor.html +++ b/public/app/plugins/panel/alertlist/editor.html @@ -19,6 +19,30 @@
    +
    +
    Filter
    +
    + Alert name + +
    +
    + Dashboard title + +
    +
    + + +
    +
    + Dashboard tags + + +
    +
    State filter
    diff --git a/public/app/plugins/panel/alertlist/module.ts b/public/app/plugins/panel/alertlist/module.ts index 35fbaead3b1..55869ce626d 100644 --- a/public/app/plugins/panel/alertlist/module.ts +++ b/public/app/plugins/panel/alertlist/module.ts @@ -21,6 +21,7 @@ class AlertListPanel extends PanelCtrl { currentAlerts: any = []; alertHistory: any = []; noAlertsMessage: string; + // Set and populate defaults panelDefaults = { show: 'current', @@ -28,6 +29,9 @@ class AlertListPanel extends PanelCtrl { stateFilter: [], onlyAlertsOnDashboard: false, sortOrder: 1, + dashboardFilter: '', + nameFilter: '', + folderId: null, }; /** @ngInject */ @@ -89,6 +93,11 @@ class AlertListPanel extends PanelCtrl { }); } + onFolderChange(folder: any) { + this.panel.folderId = folder.id; + this.refresh(); + } + getStateChanges() { var params: any = { limit: this.panel.limit, @@ -110,6 +119,7 @@ class AlertListPanel extends PanelCtrl { al.info = alertDef.getAlertAnnotationInfo(al); return al; }); + this.noAlertsMessage = this.alertHistory.length === 0 ? 'No alerts in current time range' : ''; return this.alertHistory; @@ -121,10 +131,26 @@ class AlertListPanel extends PanelCtrl { state: this.panel.stateFilter, }; + if (this.panel.nameFilter) { + params.query = this.panel.nameFilter; + } + + if (this.panel.folderId >= 0) { + params.folderId = this.panel.folderId; + } + + if (this.panel.dashboardFilter) { + params.dashboardQuery = this.panel.dashboardFilter; + } + if (this.panel.onlyAlertsOnDashboard) { params.dashboardId = this.dashboard.id; } + if (this.panel.dashboardTags) { + params.dashboardTag = this.panel.dashboardTags; + } + return this.backendSrv.get(`/api/alerts`, params).then(res => { this.currentAlerts = this.sortResult( _.map(res, al => { @@ -135,6 +161,9 @@ class AlertListPanel extends PanelCtrl { return al; }) ); + if (this.currentAlerts.length > this.panel.limit) { + this.currentAlerts = this.currentAlerts.slice(0, this.panel.limit); + } this.noAlertsMessage = this.currentAlerts.length === 0 ? 'No alerts' : ''; return this.currentAlerts; From b67872bc35c63eb6debf2ac121673442d0a3f948 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 1 Jun 2018 14:49:14 +0200 Subject: [PATCH 0796/3000] changelog: add notes about closing #11500, #8168, #6541 [skip ci] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7e46d6cf4..7ef36a8796f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 5.2.0 (unreleased) +### New Features + +* **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541) + ### Minor * **Dashboard**: Modified time range and variables are now not saved by default [#10748](https://github.com/grafana/grafana/issues/10748), [#8805](https://github.com/grafana/grafana/issues/8805) From 83a73327cfb42ed5a3bea73497a5b4c7303a020e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 15:16:22 +0200 Subject: [PATCH 0797/3000] removed italic --- public/sass/components/_search.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index b00168505fa..3b6c1fbcce6 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -210,7 +210,6 @@ .search-item__body-folder-title { color: $text-color-weak; - font-style: italic; padding-left: 0.25rem; } From f5cf92636451ef2bb80f86606e6e8b03cb28c962 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 1 Jun 2018 15:23:26 +0200 Subject: [PATCH 0798/3000] changelog: add notes about closing #5893 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef36a8796f..76e538a8e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### New Features +* **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95) * **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541) ### Minor From 75ee1e920890e2b7568407b0034cbddc01ebdce3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 08:13:20 +0200 Subject: [PATCH 0799/3000] renames intervalSeconds to updateIntervalSeconds --- docs/sources/administration/provisioning.md | 1 + .../provisioning/dashboards/config_reader.go | 4 +- .../dashboards/config_reader_test.go | 12 +-- .../provisioning/dashboards/file_reader.go | 2 +- .../dashboards-from-disk/dev-dashboards.yaml | 2 +- .../test-configs/version-0/version-0.yaml | 2 +- pkg/services/provisioning/dashboards/types.go | 80 +++++++++---------- 7 files changed, 53 insertions(+), 50 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 79b47aee9f6..888a0777796 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -197,6 +197,7 @@ providers: folder: '' type: file disableDeletion: false + updateIntervalSeconds: 3 #how often Grafana will scan for changed dashboards options: path: /var/lib/grafana/dashboards ``` diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index f8b6070c704..7508550838f 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -82,8 +82,8 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { dashboards[i].OrgId = 1 } - if dashboards[i].IntervalSeconds == 0 { - dashboards[i].IntervalSeconds = 3 + if dashboards[i].UpdateIntervalSeconds == 0 { + dashboards[i].UpdateIntervalSeconds = 3 } } diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index b49cd258005..df0d2ae038e 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -22,7 +22,7 @@ func TestDashboardsAsConfig(t *testing.T) { cfg, err := cfgProvider.readConfig() So(err, ShouldBeNil) - validateDashboardAsConfig(cfg) + validateDashboardAsConfig(t, cfg) }) Convey("Can read config file in version 0 format", func() { @@ -30,7 +30,7 @@ func TestDashboardsAsConfig(t *testing.T) { cfg, err := cfgProvider.readConfig() So(err, ShouldBeNil) - validateDashboardAsConfig(cfg) + validateDashboardAsConfig(t, cfg) }) Convey("Should skip invalid path", func() { @@ -56,7 +56,9 @@ func TestDashboardsAsConfig(t *testing.T) { }) }) } -func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { +func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) { + t.Helper() + So(len(cfg), ShouldEqual, 2) ds := cfg[0] @@ -68,7 +70,7 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(len(ds.Options), ShouldEqual, 1) So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds.DisableDeletion, ShouldBeTrue) - So(ds.IntervalSeconds, ShouldEqual, 10) + So(ds.UpdateIntervalSeconds, ShouldEqual, 10) ds2 := cfg[1] So(ds2.Name, ShouldEqual, "default") @@ -79,5 +81,5 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) { So(len(ds2.Options), ShouldEqual, 1) So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds2.DisableDeletion, ShouldBeFalse) - So(ds2.IntervalSeconds, ShouldEqual, 3) + So(ds2.UpdateIntervalSeconds, ShouldEqual, 3) } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index a25b0208ad3..89416d2596c 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -66,7 +66,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error { fr.log.Error("failed to search for dashboards", "error", err) } - ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.IntervalSeconds)) + ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.UpdateIntervalSeconds)) running := false diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml index 5ea2a0a4f75..e26c329f87c 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -6,7 +6,7 @@ providers: folder: 'developers' editable: true disableDeletion: true - intervalSeconds: 10 + updateIntervalSeconds: 10 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml index bdbb06079fd..69a317fb396 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml @@ -3,7 +3,7 @@ folder: 'developers' editable: true disableDeletion: true - intervalSeconds: 10 + updateIntervalSeconds: 10 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 424e5e35f4a..a658b816c7d 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -10,25 +10,25 @@ import ( ) type DashboardsAsConfig struct { - Name string - Type string - OrgId int64 - Folder string - Editable bool - Options map[string]interface{} - DisableDeletion bool - IntervalSeconds int64 + Name string + Type string + OrgId int64 + Folder string + Editable bool + Options map[string]interface{} + DisableDeletion bool + UpdateIntervalSeconds int64 } type DashboardsAsConfigV0 struct { - Name string `json:"name" yaml:"name"` - Type string `json:"type" yaml:"type"` - OrgId int64 `json:"org_id" yaml:"org_id"` - Folder string `json:"folder" yaml:"folder"` - Editable bool `json:"editable" yaml:"editable"` - Options map[string]interface{} `json:"options" yaml:"options"` - DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` - IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"org_id" yaml:"org_id"` + Folder string `json:"folder" yaml:"folder"` + Editable bool `json:"editable" yaml:"editable"` + Options map[string]interface{} `json:"options" yaml:"options"` + DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` + UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"` } type ConfigVersion struct { @@ -40,14 +40,14 @@ type DashboardAsConfigV1 struct { } type DashboardProviderConfigs struct { - Name string `json:"name" yaml:"name"` - Type string `json:"type" yaml:"type"` - OrgId int64 `json:"orgId" yaml:"orgId"` - Folder string `json:"folder" yaml:"folder"` - Editable bool `json:"editable" yaml:"editable"` - Options map[string]interface{} `json:"options" yaml:"options"` - DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` - IntervalSeconds int64 `json:"intervalSeconds" yaml:"intervalSeconds"` + Name string `json:"name" yaml:"name"` + Type string `json:"type" yaml:"type"` + OrgId int64 `json:"orgId" yaml:"orgId"` + Folder string `json:"folder" yaml:"folder"` + Editable bool `json:"editable" yaml:"editable"` + Options map[string]interface{} `json:"options" yaml:"options"` + DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"` + UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"` } func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) { @@ -71,14 +71,14 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig for _, v := range v0 { r = append(r, &DashboardsAsConfig{ - Name: v.Name, - Type: v.Type, - OrgId: v.OrgId, - Folder: v.Folder, - Editable: v.Editable, - Options: v.Options, - DisableDeletion: v.DisableDeletion, - IntervalSeconds: v.IntervalSeconds, + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + DisableDeletion: v.DisableDeletion, + UpdateIntervalSeconds: v.UpdateIntervalSeconds, }) } @@ -90,14 +90,14 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig { for _, v := range dc.Providers { r = append(r, &DashboardsAsConfig{ - Name: v.Name, - Type: v.Type, - OrgId: v.OrgId, - Folder: v.Folder, - Editable: v.Editable, - Options: v.Options, - DisableDeletion: v.DisableDeletion, - IntervalSeconds: v.IntervalSeconds, + Name: v.Name, + Type: v.Type, + OrgId: v.OrgId, + Folder: v.Folder, + Editable: v.Editable, + Options: v.Options, + DisableDeletion: v.DisableDeletion, + UpdateIntervalSeconds: v.UpdateIntervalSeconds, }) } From 3f5078339c0193a416775e719fd5c8a0293229ab Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 08:27:03 +0200 Subject: [PATCH 0800/3000] tests: uses different paths depending on os --- .../provisioning/dashboards/file_reader_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 87e9ec6d226..bdc1e95aafe 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -49,13 +49,16 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { }) Convey("using full path", func() { - cfg.Options["folder"] = "/var/lib/grafana/dashboards" + fullPath := "/var/lib/grafana/dashboards" + if runtime.GOOS == "windows" { + fullPath = `c:\var\lib\grafana` + } + + cfg.Options["folder"] = fullPath reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) So(err, ShouldBeNil) - if runtime.GOOS != "windows" { - So(reader.Path, ShouldEqual, "/var/lib/grafana/dashboards") - } + So(reader.Path, ShouldEqual, fullPath) So(filepath.IsAbs(reader.Path), ShouldBeTrue) }) From f606654c50239fbc4616bcdd50c0441dd810ed1f Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 09:04:33 +0200 Subject: [PATCH 0801/3000] provisioning: adds fallback if evalsymlink/abs fails --- pkg/services/provisioning/dashboards/file_reader.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index a1ba4dbf8e2..8af23980531 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -51,7 +51,6 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade path, err := filepath.Abs(path) if err != nil { log.Error("Could not create absolute path ", "path", path) - path = copy //if .Abs return an error we fallback to path } path, err = filepath.EvalSymlinks(path) @@ -59,6 +58,11 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade log.Error("Failed to read content of symlinked path: %s", path) } + if path == "" { + path = copy + log.Info("falling back to original path due to EvalSymlink/Abs failure") + } + return &fileReader{ Cfg: cfg, Path: path, From feb5e20779379863687e624e7ddf52e1c503061d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Jun 2018 11:17:50 +0200 Subject: [PATCH 0802/3000] datasource: added option no-direct-access to ds-http-settings diretive, closes #12138 --- public/app/features/plugins/ds_edit_ctrl.ts | 4 ++++ public/app/features/plugins/partials/ds_http_settings.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index b98f0f48910..f86cc694255 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -204,10 +204,14 @@ coreModule.directive('datasourceHttpSettings', function() { scope: { current: '=', suggestUrl: '@', + noDirectAccess: '@', }, templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html', link: { pre: function($scope, elem, attrs) { + // do not show access option if direct access is disabled + $scope.showAccessOption = $scope.noDirectAccess !== 'true'; + $scope.getSuggestUrls = function() { return [$scope.suggestUrl]; }; diff --git a/public/app/features/plugins/partials/ds_http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html index b9f5683129c..b35aab0c099 100644 --- a/public/app/features/plugins/partials/ds_http_settings.html +++ b/public/app/features/plugins/partials/ds_http_settings.html @@ -22,7 +22,7 @@
    -
    +
    Access
    From 13c6f37ea581db9ecb04c859618847425d7cba46 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 1 Jun 2018 13:39:44 +0300 Subject: [PATCH 0803/3000] alerting: show alerts for user with Viewer role changelog: add notes about closing #11167 remove changelog note reformat alert_test.go --- pkg/api/alerting.go | 2 +- pkg/services/sqlstore/alert.go | 2 +- pkg/services/sqlstore/alert_test.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 961fc11b2dc..60013fe2b10 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -79,7 +79,7 @@ func GetAlerts(c *m.ReqContext) Response { DashboardIds: dashboardIDs, Type: string(search.DashHitDB), FolderIds: folderIDs, - Permission: m.PERMISSION_EDIT, + Permission: m.PERMISSION_VIEW, } err := bus.Dispatch(&searchQuery) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 58ec7e2857a..531a70b2101 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -116,7 +116,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { } if query.User.OrgRole != m.ROLE_ADMIN { - builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_EDIT) + builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_VIEW) } builder.Write(" ORDER BY name ASC") diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index be48c7b2f52..79fa99864e7 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -2,7 +2,6 @@ package sqlstore import ( "testing" - "time" "github.com/grafana/grafana/pkg/components/simplejson" @@ -110,11 +109,12 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Viewer cannot read alerts", func() { - alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}} + viewerUser := &m.SignedInUser{OrgRole: m.ROLE_VIEWER, OrgId: 1} + alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser} err2 := HandleAlertsQuery(&alertQuery) So(err2, ShouldBeNil) - So(alertQuery.Result, ShouldHaveLength, 0) + So(alertQuery.Result, ShouldHaveLength, 1) }) Convey("Alerts with same dashboard id and panel id should update", func() { From e562ae753b75210a56d98e3689179bebb318d0f7 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 4 Jun 2018 11:49:12 +0200 Subject: [PATCH 0804/3000] docs: docker secrets support. (#12141) Closes #12132 --- CHANGELOG.md | 1 + docs/sources/installation/docker.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e538a8e32..ecbc99608c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * **Singlestat**: Fix singlestat threshold tooltip [#11971](https://github.com/grafana/grafana/issues/11971) * **Dashboard**: Hide grid controls in fullscreen/low-activity views [#11771](https://github.com/grafana/grafana/issues/11771) * **Dashboard**: Validate uid when importing dashboards [#11515](https://github.com/grafana/grafana/issues/11515) +* **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) # 5.1.3 (2018-05-16) diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index e78796845c4..e7dee84b5f4 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -130,6 +130,18 @@ ID=$(id -u) # saves your user id in the ID variable docker run -d --user $ID --volume "$PWD/data:/var/lib/grafana" -p 3000:3000 grafana/grafana:5.1.0 ``` +## Reading secrets from files (support for Docker Secrets) + +It's possible to supply Grafana with configuration through files. This works well with [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) as the secrets by default gets mapped into `/run/secrets/` of the container. + +You can do this with any of the configuration options in conf/grafana.ini by setting `GF___FILE` to the path of the file holding the secret. + +Let's say you want to set the admin password this way. + +- Admin password secret: `/run/secrets/admin_password` +- Environment variable: `GF_SECURITY_ADMIN_PASSWORD_FILE=/run/secrets/admin_password` + + ## Migration from a previous version of the docker container to 5.1 or later The docker container for Grafana has seen a major rewrite for 5.1. From 7453df2662c569643e0d358c8e06ae99af89041e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Jun 2018 11:57:13 +0200 Subject: [PATCH 0805/3000] changelog: add notes about closing #11167 [skip ci] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecbc99608c4..9eda912e86a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,8 @@ * **Singlestat**: Fix singlestat threshold tooltip [#11971](https://github.com/grafana/grafana/issues/11971) * **Dashboard**: Hide grid controls in fullscreen/low-activity views [#11771](https://github.com/grafana/grafana/issues/11771) * **Dashboard**: Validate uid when importing dashboards [#11515](https://github.com/grafana/grafana/issues/11515) -* **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) +* **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) +* **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167) # 5.1.3 (2018-05-16) From 08ee1da6b128b8a3191768448118aec2ed564ef2 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 30 May 2018 11:29:44 +0200 Subject: [PATCH 0806/3000] InfluxDB IFQL datasource --- package.json | 1 + pkg/api/frontendsettings.go | 11 + pkg/models/datasource.go | 1 + public/app/core/table_model.ts | 4 + .../app/features/plugins/built_in_plugins.ts | 2 + .../datasource/influxdb-ifql/README.md | 26 ++ .../datasource/influxdb-ifql/datasource.ts | 255 +++++++++++++ .../influxdb-ifql/img/influxdb_logo.svg | 26 ++ .../datasource/influxdb-ifql/module.ts | 17 + .../partials/annotations.editor.html | 24 ++ .../influxdb-ifql/partials/config.html | 24 ++ .../influxdb-ifql/partials/query.editor.html | 24 ++ .../datasource/influxdb-ifql/plugin.json | 24 ++ .../datasource/influxdb-ifql/query_ctrl.ts | 17 + .../influxdb-ifql/response_parser.ts | 88 +++++ .../specs/response_parser.jest.ts | 63 ++++ .../specs/sample_response_csv.ts | 349 ++++++++++++++++++ yarn.lock | 4 + 18 files changed, 960 insertions(+) create mode 100644 public/app/plugins/datasource/influxdb-ifql/README.md create mode 100644 public/app/plugins/datasource/influxdb-ifql/datasource.ts create mode 100644 public/app/plugins/datasource/influxdb-ifql/img/influxdb_logo.svg create mode 100644 public/app/plugins/datasource/influxdb-ifql/module.ts create mode 100644 public/app/plugins/datasource/influxdb-ifql/partials/annotations.editor.html create mode 100644 public/app/plugins/datasource/influxdb-ifql/partials/config.html create mode 100644 public/app/plugins/datasource/influxdb-ifql/partials/query.editor.html create mode 100644 public/app/plugins/datasource/influxdb-ifql/plugin.json create mode 100644 public/app/plugins/datasource/influxdb-ifql/query_ctrl.ts create mode 100644 public/app/plugins/datasource/influxdb-ifql/response_parser.ts create mode 100644 public/app/plugins/datasource/influxdb-ifql/specs/response_parser.jest.ts create mode 100644 public/app/plugins/datasource/influxdb-ifql/specs/sample_response_csv.ts diff --git a/package.json b/package.json index df3da5812c1..5fd72357f6f 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "moment": "^2.18.1", "mousetrap": "^1.6.0", "mousetrap-global-bind": "^1.1.0", + "papaparse": "^4.4.0", "prismjs": "^1.6.0", "prop-types": "^15.6.0", "react": "^16.2.0", diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 5cd52122c3f..84524bad526 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -85,6 +85,13 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { dsMap["database"] = ds.Database dsMap["url"] = url } + + if ds.Type == m.DS_INFLUXDB_IFQL { + dsMap["username"] = ds.User + dsMap["password"] = ds.Password + dsMap["database"] = ds.Database + dsMap["url"] = url + } } if ds.Type == m.DS_ES { @@ -95,6 +102,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { dsMap["database"] = ds.Database } + if ds.Type == m.DS_INFLUXDB_IFQL { + dsMap["database"] = ds.Database + } + if ds.Type == m.DS_PROMETHEUS { // add unproxied server URL for link to Prometheus web UI dsMap["directUrl"] = ds.Url diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index b7e3e3eaa17..530f31242a9 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -12,6 +12,7 @@ const ( DS_GRAPHITE = "graphite" DS_INFLUXDB = "influxdb" DS_INFLUXDB_08 = "influxdb_08" + DS_INFLUXDB_IFQL = "influxdb-ifql" DS_ES = "elasticsearch" DS_OPENTSDB = "opentsdb" DS_CLOUDWATCH = "cloudwatch" diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 57800b3e48d..5716aac2be6 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -44,4 +44,8 @@ export default class TableModel { this.columnMap[col.text] = col; } } + + addRow(row) { + this.rows.push(row); + } } diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 6998321dd75..49be31e5474 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -4,6 +4,7 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul 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 influxdbIfqlPlugin from 'app/plugins/datasource/influxdb-ifql/module'; import * as mixedPlugin from 'app/plugins/datasource/mixed/module'; import * as mysqlPlugin from 'app/plugins/datasource/mysql/module'; import * as postgresPlugin from 'app/plugins/datasource/postgres/module'; @@ -30,6 +31,7 @@ const builtInPlugins = { 'app/plugins/datasource/opentsdb/module': opentsdbPlugin, 'app/plugins/datasource/grafana/module': grafanaPlugin, 'app/plugins/datasource/influxdb/module': influxdbPlugin, + 'app/plugins/datasource/influxdb-ifql/module': influxdbIfqlPlugin, 'app/plugins/datasource/mixed/module': mixedPlugin, 'app/plugins/datasource/mysql/module': mysqlPlugin, 'app/plugins/datasource/postgres/module': postgresPlugin, diff --git a/public/app/plugins/datasource/influxdb-ifql/README.md b/public/app/plugins/datasource/influxdb-ifql/README.md new file mode 100644 index 00000000000..91f82b2a89d --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/README.md @@ -0,0 +1,26 @@ +# InfluxDB (IFQL) Datasource [BETA] - Native Plugin + +Grafana ships with **built in** support for InfluxDB (>= 1.4.1). + +Use this datasource if you want to use IFQL to query your InfluxDB. +Feel free to run this datasource side-by-side with the non-IFQL datasource. +If you point both datasources to the same InfluxDB instance, you can switch query mode by switching the datasources. + +Read more about IFQL here: + +[https://github.com/influxdata/ifql](https://github.com/influxdata/ifql) + +Read more about InfluxDB here: + +[http://docs.grafana.org/datasources/influxdb/](http://docs.grafana.org/datasources/influxdb/) + +## Roadmap + +- Sync Grafana time ranges with `range()` +- Template variable expansion +- Syntax highlighting +- Tab completion (functions, values) +- Result helpers (result counts, table previews) +- Annotations support +- Alerting integration +- Explore UI integration diff --git a/public/app/plugins/datasource/influxdb-ifql/datasource.ts b/public/app/plugins/datasource/influxdb-ifql/datasource.ts new file mode 100644 index 00000000000..bd3cb7e2d5b --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/datasource.ts @@ -0,0 +1,255 @@ +import _ from 'lodash'; + +import * as dateMath from 'app/core/utils/datemath'; + +import { getTableModelFromResult, getTimeSeriesFromResult, parseResults } from './response_parser'; + +function serializeParams(params) { + if (!params) { + return ''; + } + + return _.reduce( + params, + (memo, value, key) => { + if (value === null || value === undefined) { + return memo; + } + memo.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + return memo; + }, + [] + ).join('&'); +} + +const MAX_SERIES = 20; +export default class InfluxDatasource { + type: string; + urls: any; + username: string; + password: string; + name: string; + orgName: string; + database: any; + basicAuth: any; + withCredentials: any; + interval: any; + supportAnnotations: boolean; + supportMetrics: boolean; + + /** @ngInject */ + constructor(instanceSettings, private backendSrv, private templateSrv) { + this.type = 'influxdb-ifql'; + this.urls = instanceSettings.url.split(',').map(url => url.trim()); + + this.username = instanceSettings.username; + this.password = instanceSettings.password; + this.name = instanceSettings.name; + this.orgName = instanceSettings.orgName || 'defaultorgname'; + this.database = instanceSettings.database; + this.basicAuth = instanceSettings.basicAuth; + this.withCredentials = instanceSettings.withCredentials; + this.interval = (instanceSettings.jsonData || {}).timeInterval; + this.supportAnnotations = true; + this.supportMetrics = true; + } + + query(options) { + const targets = _.cloneDeep(options.targets); + const queryTargets = targets.filter(t => t.query); + if (queryTargets.length === 0) { + return Promise.resolve({ data: [] }); + } + + // replace grafana variables + const timeFilter = this.getTimeFilter(options); + options.scopedVars.timeFilter = { value: timeFilter }; + + const queries = queryTargets.map(target => { + const { query, resultFormat } = target; + + // TODO replace templated variables + // allQueries = this.templateSrv.replace(allQueries, scopedVars); + + if (resultFormat === 'table') { + return ( + this._seriesQuery(query, options) + .then(response => parseResults(response.data)) + // Keep only first result from each request + .then(results => results[0]) + .then(getTableModelFromResult) + ); + } else { + return this._seriesQuery(query, options) + .then(response => parseResults(response.data)) + .then(results => results.map(getTimeSeriesFromResult)); + } + }); + + return Promise.all(queries).then((series: any) => { + let seriesList = _.flattenDeep(series).slice(0, MAX_SERIES); + return { data: seriesList }; + }); + } + + annotationQuery(options) { + if (!options.annotation.query) { + return Promise.reject({ + message: 'Query missing in annotation definition', + }); + } + + var timeFilter = this.getTimeFilter({ rangeRaw: options.rangeRaw }); + var query = options.annotation.query.replace('$timeFilter', timeFilter); + query = this.templateSrv.replace(query, null, 'regex'); + + return {}; + } + + targetContainsTemplate(target) { + for (let group of target.groupBy) { + for (let param of group.params) { + if (this.templateSrv.variableExists(param)) { + return true; + } + } + } + + for (let i in target.tags) { + if (this.templateSrv.variableExists(target.tags[i].value)) { + return true; + } + } + + return false; + } + + metricFindQuery(query: string, options?: any) { + var interpolated = this.templateSrv.replace(query, null, 'regex'); + + return this._seriesQuery(interpolated, options).then(_.curry(parseResults)(query)); + } + + _seriesQuery(query: string, options?: any) { + if (!query) { + return Promise.resolve({ data: '' }); + } + return this._influxRequest('POST', '/v1/query', { q: query }, options); + } + + testDatasource() { + const query = `from(db:"${this.database}") |> last()`; + + return this._influxRequest('POST', '/v1/query', { q: query }) + .then(res => { + if (res && res.trim()) { + return { status: 'success', message: 'Data source connected and database found.' }; + } + return { + status: 'error', + message: + 'Data source connected, but has no data. Verify the "Database" field and make sure the database has data.', + }; + }) + .catch(err => { + return { status: 'error', message: err.message }; + }); + } + + _influxRequest(method: string, url: string, data: any, options?: any) { + // TODO reinstante Round-robin + // const currentUrl = this.urls.shift(); + // this.urls.push(currentUrl); + const currentUrl = this.urls[0]; + + let params: any = { + orgName: this.orgName, + }; + + if (this.username) { + params.u = this.username; + params.p = this.password; + } + + if (options && options.database) { + params.db = options.database; + } else if (this.database) { + params.db = this.database; + } + + // data sent as GET param + _.extend(params, data); + data = null; + + let req: any = { + method: method, + url: currentUrl + url, + params: params, + data: data, + precision: 'ms', + inspect: { type: this.type }, + paramSerializer: serializeParams, + }; + + req.headers = req.headers || {}; + if (this.basicAuth || this.withCredentials) { + req.withCredentials = true; + } + if (this.basicAuth) { + req.headers.Authorization = this.basicAuth; + } + + return this.backendSrv.datasourceRequest(req).then( + result => { + return result; + }, + function(err) { + if (err.status !== 0 || err.status >= 300) { + if (err.data && err.data.error) { + throw { + message: 'InfluxDB Error: ' + err.data.error, + data: err.data, + config: err.config, + }; + } else { + throw { + message: 'Network Error: ' + err.statusText + '(' + err.status + ')', + data: err.data, + config: err.config, + }; + } + } + } + ); + } + + getTimeFilter(options) { + var from = this.getInfluxTime(options.rangeRaw.from, false); + var until = this.getInfluxTime(options.rangeRaw.to, true); + var fromIsAbsolute = from[from.length - 1] === 'ms'; + + if (until === 'now()' && !fromIsAbsolute) { + return 'time >= ' + from; + } + + return 'time >= ' + from + ' and time <= ' + until; + } + + getInfluxTime(date, roundUp) { + if (_.isString(date)) { + if (date === 'now') { + return 'now()'; + } + + var parts = /^now-(\d+)([d|h|m|s])$/.exec(date); + if (parts) { + var amount = parseInt(parts[1]); + var unit = parts[2]; + return 'now() - ' + amount + unit; + } + date = dateMath.parse(date, roundUp); + } + + return date.valueOf() + 'ms'; + } +} diff --git a/public/app/plugins/datasource/influxdb-ifql/img/influxdb_logo.svg b/public/app/plugins/datasource/influxdb-ifql/img/influxdb_logo.svg new file mode 100644 index 00000000000..3c0e379e0d7 --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/img/influxdb_logo.svg @@ -0,0 +1,26 @@ + + + + + + diff --git a/public/app/plugins/datasource/influxdb-ifql/module.ts b/public/app/plugins/datasource/influxdb-ifql/module.ts new file mode 100644 index 00000000000..5997a7d061b --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/module.ts @@ -0,0 +1,17 @@ +import InfluxDatasource from './datasource'; +import { InfluxQueryCtrl } from './query_ctrl'; + +class InfluxConfigCtrl { + static templateUrl = 'partials/config.html'; +} + +class InfluxAnnotationsQueryCtrl { + static templateUrl = 'partials/annotations.editor.html'; +} + +export { + InfluxDatasource as Datasource, + InfluxQueryCtrl as QueryCtrl, + InfluxConfigCtrl as ConfigCtrl, + InfluxAnnotationsQueryCtrl as AnnotationsQueryCtrl, +}; diff --git a/public/app/plugins/datasource/influxdb-ifql/partials/annotations.editor.html b/public/app/plugins/datasource/influxdb-ifql/partials/annotations.editor.html new file mode 100644 index 00000000000..48991426c1e --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/partials/annotations.editor.html @@ -0,0 +1,24 @@ + +
    +
    + +
    +
    + +
    Field mappings If your influxdb query returns more than one field you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.
    +
    +
    +
    + Text + +
    +
    + Tags + +
    +
    + Title (deprecated) + +
    +
    +
    diff --git a/public/app/plugins/datasource/influxdb-ifql/partials/config.html b/public/app/plugins/datasource/influxdb-ifql/partials/config.html new file mode 100644 index 00000000000..be6f0438efd --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/partials/config.html @@ -0,0 +1,24 @@ + + + +

    InfluxDB Details

    + +
    +
    +
    + Default Database + +
    +
    + +
    +
    + User + +
    +
    + Password + +
    +
    +
    \ No newline at end of file diff --git a/public/app/plugins/datasource/influxdb-ifql/partials/query.editor.html b/public/app/plugins/datasource/influxdb-ifql/partials/query.editor.html new file mode 100644 index 00000000000..31f5923cdb2 --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/partials/query.editor.html @@ -0,0 +1,24 @@ + + +
    + +
    +
    +
    + +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    \ No newline at end of file diff --git a/public/app/plugins/datasource/influxdb-ifql/plugin.json b/public/app/plugins/datasource/influxdb-ifql/plugin.json new file mode 100644 index 00000000000..b4eb764d556 --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/plugin.json @@ -0,0 +1,24 @@ +{ + "type": "datasource", + "name": "InfluxDB (IFQL) [BETA]", + "id": "influxdb-ifql", + "defaultMatchFormat": "regex values", + "metrics": true, + "annotations": false, + "alerting": false, + "queryOptions": { + "minInterval": true + }, + "info": { + "description": "InfluxDB Data Source for IFQL Queries for Grafana", + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/influxdb_logo.svg", + "large": "img/influxdb_logo.svg" + }, + "version": "5.1.0" + } +} \ No newline at end of file diff --git a/public/app/plugins/datasource/influxdb-ifql/query_ctrl.ts b/public/app/plugins/datasource/influxdb-ifql/query_ctrl.ts new file mode 100644 index 00000000000..950a3feb58e --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/query_ctrl.ts @@ -0,0 +1,17 @@ +import { QueryCtrl } from 'app/plugins/sdk'; + +export class InfluxQueryCtrl extends QueryCtrl { + static templateUrl = 'partials/query.editor.html'; + + resultFormats: any[]; + + /** @ngInject **/ + constructor($scope, $injector) { + super($scope, $injector); + this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }]; + } + + getCollapsedText() { + return this.target.query; + } +} diff --git a/public/app/plugins/datasource/influxdb-ifql/response_parser.ts b/public/app/plugins/datasource/influxdb-ifql/response_parser.ts new file mode 100644 index 00000000000..e2ef753392c --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/response_parser.ts @@ -0,0 +1,88 @@ +import Papa from 'papaparse'; +import groupBy from 'lodash/groupBy'; + +import TableModel from 'app/core/table_model'; + +const filterColumnKeys = key => key && key[0] !== '_' && key !== 'result' && key !== 'table'; + +const IGNORE_FIELDS_FOR_NAME = ['result', '', 'table']; +export const getNameFromRecord = record => { + // Measurement and field + const metric = [record._measurement, record._field]; + + // Add tags + const tags = Object.keys(record) + .filter(key => key[0] !== '_') + .filter(key => IGNORE_FIELDS_FOR_NAME.indexOf(key) === -1) + .map(key => `${key}=${record[key]}`); + + return [...metric, ...tags].join(' '); +}; + +const parseCSV = (input: string) => + Papa.parse(input, { + header: true, + comments: '#', + }).data; + +export const parseValue = (input: string) => { + const value = parseFloat(input); + return isNaN(value) ? null : value; +}; + +export const parseTime = (input: string) => Date.parse(input); + +export function parseResults(response: string): any[] { + return response.trim().split(/\n\s*\s/); +} + +export function getTableModelFromResult(result: string) { + const data = parseCSV(result); + + const table = new TableModel(); + if (data.length > 0) { + // First columns are fixed + const firstColumns = [ + { text: 'Time', id: '_time' }, + { text: 'Measurement', id: '_measurement' }, + { text: 'Field', id: '_field' }, + ]; + + // Dynamically add columns for tags + const firstRecord = data[0]; + const tags = Object.keys(firstRecord) + .filter(filterColumnKeys) + .map(key => ({ id: key, text: key })); + + const valueColumn = { id: '_value', text: 'Value' }; + const columns = [...firstColumns, ...tags, valueColumn]; + columns.forEach(c => table.addColumn(c)); + + // Add rows + data.forEach(record => { + const row = columns.map(c => record[c.id]); + table.addRow(row); + }); + } + + return table; +} + +export function getTimeSeriesFromResult(result: string) { + const data = parseCSV(result); + if (data.length === 0) { + return []; + } + + // Group results by table ID (assume one table per timeseries for now) + const tables = groupBy(data, 'table'); + const seriesList = Object.keys(tables) + .map(id => tables[id]) + .map(series => { + const datapoints = series.map(record => [parseValue(record._value), parseTime(record._time)]); + const alias = getNameFromRecord(series[0]); + return { datapoints, target: alias }; + }); + + return seriesList; +} diff --git a/public/app/plugins/datasource/influxdb-ifql/specs/response_parser.jest.ts b/public/app/plugins/datasource/influxdb-ifql/specs/response_parser.jest.ts new file mode 100644 index 00000000000..bac154c0760 --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/specs/response_parser.jest.ts @@ -0,0 +1,63 @@ +import { + getNameFromRecord, + getTableModelFromResult, + getTimeSeriesFromResult, + parseResults, + parseValue, +} from '../response_parser'; +import response from './sample_response_csv'; + +describe('influxdb ifql response parser', () => { + describe('parseResults()', () => { + it('expects three results', () => { + const results = parseResults(response); + expect(results.length).toBe(2); + }); + }); + + describe('getTableModelFromResult()', () => { + it('expects a table model', () => { + const results = parseResults(response); + const table = getTableModelFromResult(results[0]); + expect(table.columns.length).toBe(6); + expect(table.rows.length).toBe(300); + }); + }); + + describe('getTimeSeriesFromResult()', () => { + it('expects time series', () => { + const results = parseResults(response); + const series = getTimeSeriesFromResult(results[0]); + expect(series.length).toBe(50); + expect(series[0].datapoints.length).toBe(6); + }); + }); + + describe('getNameFromRecord()', () => { + it('expects name based on measurements and tags', () => { + const record = { + '': '', + result: '', + table: '0', + _start: '2018-06-02T06:35:25.651942602Z', + _stop: '2018-06-02T07:35:25.651942602Z', + _time: '2018-06-02T06:35:31Z', + _value: '0', + _field: 'usage_guest', + _measurement: 'cpu', + cpu: 'cpu-total', + host: 'kenobi-3.local', + }; + expect(getNameFromRecord(record)).toBe('cpu usage_guest cpu=cpu-total host=kenobi-3.local'); + }); + }); + + describe('parseValue()', () => { + it('parses a number', () => { + expect(parseValue('42.3')).toBe(42.3); + }); + it('parses a non-number to null', () => { + expect(parseValue('foo')).toBe(null); + }); + }); +}); diff --git a/public/app/plugins/datasource/influxdb-ifql/specs/sample_response_csv.ts b/public/app/plugins/datasource/influxdb-ifql/specs/sample_response_csv.ts new file mode 100644 index 00000000000..2c7c0194684 --- /dev/null +++ b/public/app/plugins/datasource/influxdb-ifql/specs/sample_response_csv.ts @@ -0,0 +1,349 @@ +const result = `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string +#partition,false,false,true,true,false,false,true,true,true,true +#default,_result,,,,,,,,, +,result,table,_start,_stop,_time,_value,_field,_measurement,cpu,host +,,0,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest,cpu,cpu-total,kenobi-3.local +,,0,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest,cpu,cpu-total,kenobi-3.local +,,0,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest,cpu,cpu-total,kenobi-3.local +,,0,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest,cpu,cpu-total,kenobi-3.local +,,0,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest,cpu,cpu-total,kenobi-3.local +,,0,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest,cpu,cpu-total,kenobi-3.local +,,1,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest_nice,cpu,cpu-total,kenobi-3.local +,,1,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest_nice,cpu,cpu-total,kenobi-3.local +,,1,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest_nice,cpu,cpu-total,kenobi-3.local +,,1,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest_nice,cpu,cpu-total,kenobi-3.local +,,1,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest_nice,cpu,cpu-total,kenobi-3.local +,,1,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest_nice,cpu,cpu-total,kenobi-3.local +,,2,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,81.87046761690422,usage_idle,cpu,cpu-total,kenobi-3.local +,,2,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,82.03398300849575,usage_idle,cpu,cpu-total,kenobi-3.local +,,2,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,76.26186906546727,usage_idle,cpu,cpu-total,kenobi-3.local +,,2,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,79.65465465465465,usage_idle,cpu,cpu-total,kenobi-3.local +,,2,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,70.72195853110168,usage_idle,cpu,cpu-total,kenobi-3.local +,,2,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,69.86746686671668,usage_idle,cpu,cpu-total,kenobi-3.local +,,3,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_iowait,cpu,cpu-total,kenobi-3.local +,,3,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_iowait,cpu,cpu-total,kenobi-3.local +,,3,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_iowait,cpu,cpu-total,kenobi-3.local +,,3,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_iowait,cpu,cpu-total,kenobi-3.local +,,3,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_iowait,cpu,cpu-total,kenobi-3.local +,,3,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_iowait,cpu,cpu-total,kenobi-3.local +,,4,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_irq,cpu,cpu-total,kenobi-3.local +,,4,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_irq,cpu,cpu-total,kenobi-3.local +,,4,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_irq,cpu,cpu-total,kenobi-3.local +,,4,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_irq,cpu,cpu-total,kenobi-3.local +,,4,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_irq,cpu,cpu-total,kenobi-3.local +,,4,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_irq,cpu,cpu-total,kenobi-3.local +,,5,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_nice,cpu,cpu-total,kenobi-3.local +,,5,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_nice,cpu,cpu-total,kenobi-3.local +,,5,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_nice,cpu,cpu-total,kenobi-3.local +,,5,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_nice,cpu,cpu-total,kenobi-3.local +,,5,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_nice,cpu,cpu-total,kenobi-3.local +,,5,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_nice,cpu,cpu-total,kenobi-3.local +,,6,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_softirq,cpu,cpu-total,kenobi-3.local +,,6,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_softirq,cpu,cpu-total,kenobi-3.local +,,6,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_softirq,cpu,cpu-total,kenobi-3.local +,,6,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_softirq,cpu,cpu-total,kenobi-3.local +,,6,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_softirq,cpu,cpu-total,kenobi-3.local +,,6,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_softirq,cpu,cpu-total,kenobi-3.local +,,7,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_steal,cpu,cpu-total,kenobi-3.local +,,7,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_steal,cpu,cpu-total,kenobi-3.local +,,7,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_steal,cpu,cpu-total,kenobi-3.local +,,7,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_steal,cpu,cpu-total,kenobi-3.local +,,7,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_steal,cpu,cpu-total,kenobi-3.local +,,7,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_steal,cpu,cpu-total,kenobi-3.local +,,8,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,6.25156289072268,usage_system,cpu,cpu-total,kenobi-3.local +,,8,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,8.045977011494253,usage_system,cpu,cpu-total,kenobi-3.local +,,8,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,8.79560219890055,usage_system,cpu,cpu-total,kenobi-3.local +,,8,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,8.408408408408409,usage_system,cpu,cpu-total,kenobi-3.local +,,8,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,11.64126904821384,usage_system,cpu,cpu-total,kenobi-3.local +,,8,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,13.078269567391848,usage_system,cpu,cpu-total,kenobi-3.local +,,9,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,11.877969492373094,usage_user,cpu,cpu-total,kenobi-3.local +,,9,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,9.920039980009996,usage_user,cpu,cpu-total,kenobi-3.local +,,9,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,14.942528735632184,usage_user,cpu,cpu-total,kenobi-3.local +,,9,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,11.936936936936936,usage_user,cpu,cpu-total,kenobi-3.local +,,9,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,17.636772420684487,usage_user,cpu,cpu-total,kenobi-3.local +,,9,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,17.05426356589147,usage_user,cpu,cpu-total,kenobi-3.local +,,10,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest,cpu,cpu0,kenobi-3.local +,,10,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest,cpu,cpu0,kenobi-3.local +,,10,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest,cpu,cpu0,kenobi-3.local +,,10,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest,cpu,cpu0,kenobi-3.local +,,10,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest,cpu,cpu0,kenobi-3.local +,,10,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest,cpu,cpu0,kenobi-3.local +,,11,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest_nice,cpu,cpu0,kenobi-3.local +,,11,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest_nice,cpu,cpu0,kenobi-3.local +,,11,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest_nice,cpu,cpu0,kenobi-3.local +,,11,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest_nice,cpu,cpu0,kenobi-3.local +,,11,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest_nice,cpu,cpu0,kenobi-3.local +,,11,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest_nice,cpu,cpu0,kenobi-3.local +,,12,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,73.1,usage_idle,cpu,cpu0,kenobi-3.local +,,12,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,69.03096903096903,usage_idle,cpu,cpu0,kenobi-3.local +,,12,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,63.63636363636363,usage_idle,cpu,cpu0,kenobi-3.local +,,12,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,67.86786786786787,usage_idle,cpu,cpu0,kenobi-3.local +,,12,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,57.4,usage_idle,cpu,cpu0,kenobi-3.local +,,12,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,57.8,usage_idle,cpu,cpu0,kenobi-3.local +,,13,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_iowait,cpu,cpu0,kenobi-3.local +,,13,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_iowait,cpu,cpu0,kenobi-3.local +,,13,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_iowait,cpu,cpu0,kenobi-3.local +,,13,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_iowait,cpu,cpu0,kenobi-3.local +,,13,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_iowait,cpu,cpu0,kenobi-3.local +,,13,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_iowait,cpu,cpu0,kenobi-3.local +,,14,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_irq,cpu,cpu0,kenobi-3.local +,,14,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_irq,cpu,cpu0,kenobi-3.local +,,14,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_irq,cpu,cpu0,kenobi-3.local +,,14,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_irq,cpu,cpu0,kenobi-3.local +,,14,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_irq,cpu,cpu0,kenobi-3.local +,,14,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_irq,cpu,cpu0,kenobi-3.local +,,15,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_nice,cpu,cpu0,kenobi-3.local +,,15,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_nice,cpu,cpu0,kenobi-3.local +,,15,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_nice,cpu,cpu0,kenobi-3.local +,,15,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_nice,cpu,cpu0,kenobi-3.local +,,15,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_nice,cpu,cpu0,kenobi-3.local +,,15,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_nice,cpu,cpu0,kenobi-3.local +,,16,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_softirq,cpu,cpu0,kenobi-3.local +,,16,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_softirq,cpu,cpu0,kenobi-3.local +,,16,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_softirq,cpu,cpu0,kenobi-3.local +,,16,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_softirq,cpu,cpu0,kenobi-3.local +,,16,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_softirq,cpu,cpu0,kenobi-3.local +,,16,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_softirq,cpu,cpu0,kenobi-3.local +,,17,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_steal,cpu,cpu0,kenobi-3.local +,,17,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_steal,cpu,cpu0,kenobi-3.local +,,17,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_steal,cpu,cpu0,kenobi-3.local +,,17,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_steal,cpu,cpu0,kenobi-3.local +,,17,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_steal,cpu,cpu0,kenobi-3.local +,,17,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_steal,cpu,cpu0,kenobi-3.local +,,18,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,9.6,usage_system,cpu,cpu0,kenobi-3.local +,,18,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,14.985014985014985,usage_system,cpu,cpu0,kenobi-3.local +,,18,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,14.185814185814186,usage_system,cpu,cpu0,kenobi-3.local +,,18,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,13.813813813813814,usage_system,cpu,cpu0,kenobi-3.local +,,18,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,17.9,usage_system,cpu,cpu0,kenobi-3.local +,,18,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,20,usage_system,cpu,cpu0,kenobi-3.local +,,19,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,17.3,usage_user,cpu,cpu0,kenobi-3.local +,,19,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,15.984015984015985,usage_user,cpu,cpu0,kenobi-3.local +,,19,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,22.17782217782218,usage_user,cpu,cpu0,kenobi-3.local +,,19,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,18.31831831831832,usage_user,cpu,cpu0,kenobi-3.local +,,19,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,24.7,usage_user,cpu,cpu0,kenobi-3.local +,,19,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,22.2,usage_user,cpu,cpu0,kenobi-3.local +,,20,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest,cpu,cpu1,kenobi-3.local +,,20,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest,cpu,cpu1,kenobi-3.local +,,20,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest,cpu,cpu1,kenobi-3.local +,,20,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest,cpu,cpu1,kenobi-3.local +,,20,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest,cpu,cpu1,kenobi-3.local +,,20,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest,cpu,cpu1,kenobi-3.local +,,21,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest_nice,cpu,cpu1,kenobi-3.local +,,21,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest_nice,cpu,cpu1,kenobi-3.local +,,21,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest_nice,cpu,cpu1,kenobi-3.local +,,21,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest_nice,cpu,cpu1,kenobi-3.local +,,21,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest_nice,cpu,cpu1,kenobi-3.local +,,21,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest_nice,cpu,cpu1,kenobi-3.local +,,22,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,89.8,usage_idle,cpu,cpu1,kenobi-3.local +,,22,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,91.8,usage_idle,cpu,cpu1,kenobi-3.local +,,22,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,87.11288711288711,usage_idle,cpu,cpu1,kenobi-3.local +,,22,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,89.48948948948949,usage_idle,cpu,cpu1,kenobi-3.local +,,22,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,83,usage_idle,cpu,cpu1,kenobi-3.local +,,22,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,80.1,usage_idle,cpu,cpu1,kenobi-3.local +,,23,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_iowait,cpu,cpu1,kenobi-3.local +,,23,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_iowait,cpu,cpu1,kenobi-3.local +,,23,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_iowait,cpu,cpu1,kenobi-3.local +,,23,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_iowait,cpu,cpu1,kenobi-3.local +,,23,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_iowait,cpu,cpu1,kenobi-3.local +,,23,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_iowait,cpu,cpu1,kenobi-3.local +,,24,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_irq,cpu,cpu1,kenobi-3.local +,,24,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_irq,cpu,cpu1,kenobi-3.local +,,24,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_irq,cpu,cpu1,kenobi-3.local +,,24,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_irq,cpu,cpu1,kenobi-3.local +,,24,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_irq,cpu,cpu1,kenobi-3.local +,,24,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_irq,cpu,cpu1,kenobi-3.local +,,25,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_nice,cpu,cpu1,kenobi-3.local +,,25,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_nice,cpu,cpu1,kenobi-3.local +,,25,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_nice,cpu,cpu1,kenobi-3.local +,,25,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_nice,cpu,cpu1,kenobi-3.local +,,25,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_nice,cpu,cpu1,kenobi-3.local +,,25,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_nice,cpu,cpu1,kenobi-3.local +,,26,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_softirq,cpu,cpu1,kenobi-3.local +,,26,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_softirq,cpu,cpu1,kenobi-3.local +,,26,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_softirq,cpu,cpu1,kenobi-3.local +,,26,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_softirq,cpu,cpu1,kenobi-3.local +,,26,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_softirq,cpu,cpu1,kenobi-3.local +,,26,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_softirq,cpu,cpu1,kenobi-3.local +,,27,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_steal,cpu,cpu1,kenobi-3.local +,,27,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_steal,cpu,cpu1,kenobi-3.local +,,27,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_steal,cpu,cpu1,kenobi-3.local +,,27,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_steal,cpu,cpu1,kenobi-3.local +,,27,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_steal,cpu,cpu1,kenobi-3.local +,,27,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_steal,cpu,cpu1,kenobi-3.local +,,28,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,3.5,usage_system,cpu,cpu1,kenobi-3.local +,,28,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,4,usage_system,cpu,cpu1,kenobi-3.local +,,28,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,4.895104895104895,usage_system,cpu,cpu1,kenobi-3.local +,,28,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,4.504504504504505,usage_system,cpu,cpu1,kenobi-3.local +,,28,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,6.3,usage_system,cpu,cpu1,kenobi-3.local +,,28,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,7.9,usage_system,cpu,cpu1,kenobi-3.local +,,29,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,6.7,usage_user,cpu,cpu1,kenobi-3.local +,,29,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,4.2,usage_user,cpu,cpu1,kenobi-3.local +,,29,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,7.992007992007992,usage_user,cpu,cpu1,kenobi-3.local +,,29,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,6.006006006006006,usage_user,cpu,cpu1,kenobi-3.local +,,29,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,10.7,usage_user,cpu,cpu1,kenobi-3.local +,,29,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,12,usage_user,cpu,cpu1,kenobi-3.local +,,30,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest,cpu,cpu2,kenobi-3.local +,,30,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest,cpu,cpu2,kenobi-3.local +,,30,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest,cpu,cpu2,kenobi-3.local +,,30,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest,cpu,cpu2,kenobi-3.local +,,30,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest,cpu,cpu2,kenobi-3.local +,,30,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest,cpu,cpu2,kenobi-3.local +,,31,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest_nice,cpu,cpu2,kenobi-3.local +,,31,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest_nice,cpu,cpu2,kenobi-3.local +,,31,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest_nice,cpu,cpu2,kenobi-3.local +,,31,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest_nice,cpu,cpu2,kenobi-3.local +,,31,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest_nice,cpu,cpu2,kenobi-3.local +,,31,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest_nice,cpu,cpu2,kenobi-3.local +,,32,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,75.17517517517517,usage_idle,cpu,cpu2,kenobi-3.local +,,32,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,74.82517482517483,usage_idle,cpu,cpu2,kenobi-3.local +,,32,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,67.9,usage_idle,cpu,cpu2,kenobi-3.local +,,32,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,72.47247247247248,usage_idle,cpu,cpu2,kenobi-3.local +,,32,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,61.63836163836164,usage_idle,cpu,cpu2,kenobi-3.local +,,32,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,62,usage_idle,cpu,cpu2,kenobi-3.local +,,33,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_iowait,cpu,cpu2,kenobi-3.local +,,33,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_iowait,cpu,cpu2,kenobi-3.local +,,33,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_iowait,cpu,cpu2,kenobi-3.local +,,33,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_iowait,cpu,cpu2,kenobi-3.local +,,33,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_iowait,cpu,cpu2,kenobi-3.local +,,33,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_iowait,cpu,cpu2,kenobi-3.local +,,34,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_irq,cpu,cpu2,kenobi-3.local +,,34,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_irq,cpu,cpu2,kenobi-3.local +,,34,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_irq,cpu,cpu2,kenobi-3.local +,,34,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_irq,cpu,cpu2,kenobi-3.local +,,34,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_irq,cpu,cpu2,kenobi-3.local +,,34,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_irq,cpu,cpu2,kenobi-3.local +,,35,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_nice,cpu,cpu2,kenobi-3.local +,,35,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_nice,cpu,cpu2,kenobi-3.local +,,35,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_nice,cpu,cpu2,kenobi-3.local +,,35,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_nice,cpu,cpu2,kenobi-3.local +,,35,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_nice,cpu,cpu2,kenobi-3.local +,,35,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_nice,cpu,cpu2,kenobi-3.local +,,36,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_softirq,cpu,cpu2,kenobi-3.local +,,36,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_softirq,cpu,cpu2,kenobi-3.local +,,36,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_softirq,cpu,cpu2,kenobi-3.local +,,36,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_softirq,cpu,cpu2,kenobi-3.local +,,36,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_softirq,cpu,cpu2,kenobi-3.local +,,36,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_softirq,cpu,cpu2,kenobi-3.local +,,37,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_steal,cpu,cpu2,kenobi-3.local +,,37,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_steal,cpu,cpu2,kenobi-3.local +,,37,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_steal,cpu,cpu2,kenobi-3.local +,,37,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_steal,cpu,cpu2,kenobi-3.local +,,37,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_steal,cpu,cpu2,kenobi-3.local +,,37,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_steal,cpu,cpu2,kenobi-3.local +,,38,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,8.208208208208209,usage_system,cpu,cpu2,kenobi-3.local +,,38,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,9.99000999000999,usage_system,cpu,cpu2,kenobi-3.local +,,38,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,11.2,usage_system,cpu,cpu2,kenobi-3.local +,,38,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,10.81081081081081,usage_system,cpu,cpu2,kenobi-3.local +,,38,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,14.785214785214785,usage_system,cpu,cpu2,kenobi-3.local +,,38,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,16.2,usage_system,cpu,cpu2,kenobi-3.local +,,39,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,16.616616616616618,usage_user,cpu,cpu2,kenobi-3.local +,,39,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,15.184815184815184,usage_user,cpu,cpu2,kenobi-3.local +,,39,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,20.9,usage_user,cpu,cpu2,kenobi-3.local +,,39,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,16.716716716716718,usage_user,cpu,cpu2,kenobi-3.local +,,39,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,23.576423576423576,usage_user,cpu,cpu2,kenobi-3.local +,,39,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,21.8,usage_user,cpu,cpu2,kenobi-3.local +,,40,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest,cpu,cpu3,kenobi-3.local +,,40,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest,cpu,cpu3,kenobi-3.local +,,40,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest,cpu,cpu3,kenobi-3.local +,,40,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest,cpu,cpu3,kenobi-3.local +,,40,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest,cpu,cpu3,kenobi-3.local +,,40,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest,cpu,cpu3,kenobi-3.local +,,41,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_guest_nice,cpu,cpu3,kenobi-3.local +,,41,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_guest_nice,cpu,cpu3,kenobi-3.local +,,41,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_guest_nice,cpu,cpu3,kenobi-3.local +,,41,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_guest_nice,cpu,cpu3,kenobi-3.local +,,41,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_guest_nice,cpu,cpu3,kenobi-3.local +,,41,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_guest_nice,cpu,cpu3,kenobi-3.local +,,42,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,89.4,usage_idle,cpu,cpu3,kenobi-3.local +,,42,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,92.5,usage_idle,cpu,cpu3,kenobi-3.local +,,42,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,86.4,usage_idle,cpu,cpu3,kenobi-3.local +,,42,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,88.78878878878879,usage_idle,cpu,cpu3,kenobi-3.local +,,42,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,80.83832335329342,usage_idle,cpu,cpu3,kenobi-3.local +,,42,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,79.57957957957957,usage_idle,cpu,cpu3,kenobi-3.local +,,43,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_iowait,cpu,cpu3,kenobi-3.local +,,43,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_iowait,cpu,cpu3,kenobi-3.local +,,43,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_iowait,cpu,cpu3,kenobi-3.local +,,43,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_iowait,cpu,cpu3,kenobi-3.local +,,43,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_iowait,cpu,cpu3,kenobi-3.local +,,43,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_iowait,cpu,cpu3,kenobi-3.local +,,44,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_irq,cpu,cpu3,kenobi-3.local +,,44,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_irq,cpu,cpu3,kenobi-3.local +,,44,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_irq,cpu,cpu3,kenobi-3.local +,,44,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_irq,cpu,cpu3,kenobi-3.local +,,44,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_irq,cpu,cpu3,kenobi-3.local +,,44,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_irq,cpu,cpu3,kenobi-3.local +,,45,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_nice,cpu,cpu3,kenobi-3.local +,,45,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_nice,cpu,cpu3,kenobi-3.local +,,45,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_nice,cpu,cpu3,kenobi-3.local +,,45,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_nice,cpu,cpu3,kenobi-3.local +,,45,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_nice,cpu,cpu3,kenobi-3.local +,,45,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_nice,cpu,cpu3,kenobi-3.local +,,46,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_softirq,cpu,cpu3,kenobi-3.local +,,46,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_softirq,cpu,cpu3,kenobi-3.local +,,46,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_softirq,cpu,cpu3,kenobi-3.local +,,46,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_softirq,cpu,cpu3,kenobi-3.local +,,46,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_softirq,cpu,cpu3,kenobi-3.local +,,46,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_softirq,cpu,cpu3,kenobi-3.local +,,47,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,0,usage_steal,cpu,cpu3,kenobi-3.local +,,47,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,0,usage_steal,cpu,cpu3,kenobi-3.local +,,47,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,0,usage_steal,cpu,cpu3,kenobi-3.local +,,47,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,0,usage_steal,cpu,cpu3,kenobi-3.local +,,47,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,0,usage_steal,cpu,cpu3,kenobi-3.local +,,47,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,0,usage_steal,cpu,cpu3,kenobi-3.local +,,48,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,3.7,usage_system,cpu,cpu3,kenobi-3.local +,,48,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,3.2,usage_system,cpu,cpu3,kenobi-3.local +,,48,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,4.9,usage_system,cpu,cpu3,kenobi-3.local +,,48,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,4.504504504504505,usage_system,cpu,cpu3,kenobi-3.local +,,48,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,7.584830339321357,usage_system,cpu,cpu3,kenobi-3.local +,,48,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,8.208208208208209,usage_system,cpu,cpu3,kenobi-3.local +,,49,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,6.9,usage_user,cpu,cpu3,kenobi-3.local +,,49,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,4.3,usage_user,cpu,cpu3,kenobi-3.local +,,49,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,8.7,usage_user,cpu,cpu3,kenobi-3.local +,,49,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,6.706706706706707,usage_user,cpu,cpu3,kenobi-3.local +,,49,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,11.57684630738523,usage_user,cpu,cpu3,kenobi-3.local +,,49,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,12.212212212212211,usage_user,cpu,cpu3,kenobi-3.local + +#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string,string,string,string,string +#partition,false,false,true,true,false,false,true,true,true,true,true,true,true +#default,_result,,,,,,,,,,,, +,result,table,_start,_stop,_time,_value,_field,_measurement,device,fstype,host,mode,path +,,50,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,9024180224,free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,50,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,9025056768,free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,50,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,9024774144,free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,50,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,9024638976,free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,50,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,9024299008,free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,50,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,9024036864,free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,51,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,4290025660,inodes_free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,51,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,4290025659,inodes_free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,51,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,4290025659,inodes_free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,51,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,4290025660,inodes_free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,51,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,4290025660,inodes_free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,51,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,4290025657,inodes_free,disk,disk1,hfs,kenobi-3.local,rw,/ +,,52,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,4294967279,inodes_total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,52,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,4294967279,inodes_total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,52,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,4294967279,inodes_total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,52,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,4294967279,inodes_total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,52,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,4294967279,inodes_total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,52,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,4294967279,inodes_total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,53,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,4941619,inodes_used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,53,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,4941620,inodes_used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,53,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,4941620,inodes_used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,53,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,4941619,inodes_used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,53,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,4941619,inodes_used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,53,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,4941622,inodes_used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,54,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,249804886016,total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,54,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,249804886016,total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,54,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,249804886016,total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,54,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,249804886016,total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,54,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,249804886016,total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,54,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,249804886016,total,disk,disk1,hfs,kenobi-3.local,rw,/ +,,55,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:23Z,240518561792,used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,55,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:33Z,240517685248,used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,55,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:43Z,240517967872,used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,55,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:54:53Z,240518103040,used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,55,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:03Z,240518443008,used,disk,disk1,hfs,kenobi-3.local,rw,/ +,,55,2018-06-01T12:54:13.516195939Z,2018-06-01T12:55:13.516195939Z,2018-06-01T12:55:13Z,240518705152,used,disk,disk1,hfs,kenobi-3.local,rw,/ + +`; + +export default result; diff --git a/yarn.lock b/yarn.lock index f58731040c6..97435f665fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7928,6 +7928,10 @@ pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" +papaparse@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-4.4.0.tgz#6bcdbda80873e00cfb0bdcd7a4571c72a9a40168" + parallel-transform@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" From e068be4c26dc2d969ca4b0cc70bb00e2ee4d85a1 Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Sat, 12 May 2018 21:11:58 -0400 Subject: [PATCH 0807/3000] Feature for repeated alerting in grafana --- pkg/api/dtos/alerting.go | 3 +++ pkg/models/alert.go | 9 ++++++++ pkg/services/alerting/extractor.go | 2 ++ pkg/services/alerting/notifiers/base.go | 5 ++++- pkg/services/alerting/result_handler.go | 1 + pkg/services/alerting/rule.go | 6 +++++ pkg/services/sqlstore/alert.go | 22 ++++++++++++++++++- pkg/services/sqlstore/migrations/alert_mig.go | 3 +++ .../app/features/alerting/alert_tab_ctrl.ts | 3 +++ .../features/alerting/partials/alert_tab.html | 3 +++ 10 files changed, 55 insertions(+), 2 deletions(-) diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index d30f2697f3f..64dd619a4eb 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -21,6 +21,9 @@ type AlertRule struct { ExecutionError string `json:"executionError"` Url string `json:"url"` CanEdit bool `json:"canEdit"` + NotifyOnce bool `json:"notifyOnce"` + NotifyEval uint64 `json:"notifyEval"` + NotifyFreq uint64 `json:"notifyFrequency"` } type AlertNotification struct { diff --git a/pkg/models/alert.go b/pkg/models/alert.go index fba2aa63df9..56ceeb2cbf7 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -72,6 +72,9 @@ type Alert struct { Silenced bool ExecutionError string Frequency int64 + NotifyOnce bool + NotifyFreq uint64 + NotifyEval uint64 EvalData *simplejson.Json NewStateDate time.Time @@ -95,6 +98,8 @@ func (this *Alert) ContainsUpdates(other *Alert) bool { result := false result = result || this.Name != other.Name result = result || this.Message != other.Message + result = result || this.NotifyOnce != other.NotifyOnce + result = result || (!other.NotifyOnce && this.NotifyFreq != other.NotifyFreq) if this.Settings != nil && other.Settings != nil { json1, err1 := this.Settings.Encode() @@ -159,6 +164,10 @@ type SetAlertStateCommand struct { Timestamp time.Time } +type IncAlertEvalCommand struct { + AlertId int64 +} + //Queries type GetAlertsQuery struct { OrgId int64 diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index e1c1bfacb2e..f820e546a93 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -122,6 +122,8 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, Handler: jsonAlert.Get("handler").MustInt64(), Message: jsonAlert.Get("message").MustString(), Frequency: frequency, + NotifyOnce: jsonAlert.Get("notifyOnce").MustBool(), + NotifyFreq: jsonAlert.Get("notifyFrequency").MustUint64(), } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 51676efdfd5..498bae3a6e6 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -32,7 +32,10 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model func defaultShouldNotify(context *alerting.EvalContext) bool { // Only notify on state change. - if context.PrevAlertState == context.Rule.State { + if context.PrevAlertState == context.Rule.State && context.Rule.NotifyOnce { + return false + } + if !context.Rule.NotifyOnce && context.Rule.NotifyEval != 0 { return false } // Do not notify when we become OK for the first time. diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index c57b28c7c3e..56d299001f0 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,6 +88,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } + bus.Dispatch(&m.IncAlertEvalCommand{AlertId: evalContext.Rule.Id}) handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 018d138dbe4..0003fe791e3 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -23,6 +23,9 @@ type Rule struct { State m.AlertStateType Conditions []Condition Notifications []int64 + NotifyOnce bool + NotifyFreq uint64 + NotifyEval uint64 } type ValidationError struct { @@ -97,6 +100,9 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Name = ruleDef.Name model.Message = ruleDef.Message model.Frequency = ruleDef.Frequency + model.NotifyOnce = ruleDef.NotifyOnce + model.NotifyFreq = ruleDef.NotifyFreq + model.NotifyEval = ruleDef.NotifyEval model.State = ruleDef.State model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 58ec7e2857a..9ab28be84ee 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -22,6 +22,7 @@ func init() { bus.AddHandler("sql", GetAlertStatesForDashboard) bus.AddHandler("sql", PauseAlert) bus.AddHandler("sql", PauseAllAlerts) + bus.AddHandler("sql", IncAlertEval) } func GetAlertById(query *m.GetAlertByIdQuery) error { @@ -188,7 +189,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS if alertToUpdate.ContainsUpdates(alert) { alert.Updated = timeNow() alert.State = alertToUpdate.State - sess.MustCols("message") + sess.MustCols("message", "notify_freq", "notify_once") _, err := sess.Id(alert.Id).Update(alert) if err != nil { return err @@ -343,3 +344,22 @@ func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error return err } + +func IncAlertEval(cmd *m.IncAlertEvalCommand) error { + return inTransaction(func(sess *DBSession) error { + alert := m.Alert{} + + if _, err := sess.Id(cmd.AlertId).Get(&alert); err != nil { + return err + } + + alert.NotifyEval = (alert.NotifyEval + 1) % alert.NotifyFreq + + sess.MustCols("notify_eval") + if _, err := sess.Id(cmd.AlertId).Update(alert); err != nil { + return err + } + + return nil + }) +} diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 2a364d5f464..3452e5710cc 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -29,6 +29,9 @@ func addAlertMigrations(mg *Migrator) { {Name: "state_changes", Type: DB_Int, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, + {Name: "notify_once", Type: DB_Bool, Nullable: false}, + {Name: "notify_freq", Type: DB_Int, Nullable: false}, + {Name: "notify_eval", Type: DB_Int, Nullable: false}, }, Indices: []*Index{ {Cols: []string{"org_id", "id"}, Type: IndexType}, diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 79baa1e3f5a..f0d965ae81e 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -167,6 +167,9 @@ export class AlertTabCtrl { alert.noDataState = alert.noDataState || 'no_data'; alert.executionErrorState = alert.executionErrorState || 'alerting'; alert.frequency = alert.frequency || '60s'; + alert.notifyFrequency = alert.notifyFrequency || 10; + alert.notifyOnce = alert.notifyOnce == null ? true : alert.notifyOnce; + alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index cb101672aa4..084aeb2036a 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -31,6 +31,9 @@ Evaluate every + {{ ctrl.alert.notifyOnce ? 'Notify on state change' : 'Notify every' }} + + evaluations
    From 3cb0e27e1c474e5d203eb32428b2f39ee5fb3216 Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Sat, 19 May 2018 16:21:00 -0400 Subject: [PATCH 0808/3000] Revert changes post code review and move them to notification page --- pkg/api/dtos/alerting.go | 25 +++---- pkg/models/alert.go | 9 --- pkg/models/alert_notifications.go | 71 ++++++++++++++----- pkg/services/alerting/eval_context.go | 15 ++++ pkg/services/alerting/extractor.go | 2 - pkg/services/alerting/interfaces.go | 2 + pkg/services/alerting/notifier.go | 12 +++- .../alerting/notifiers/alertmanager.go | 2 +- pkg/services/alerting/notifiers/base.go | 25 +++++-- pkg/services/alerting/notifiers/dingding.go | 2 +- pkg/services/alerting/notifiers/discord.go | 2 +- pkg/services/alerting/notifiers/email.go | 2 +- pkg/services/alerting/notifiers/hipchat.go | 2 +- pkg/services/alerting/notifiers/kafka.go | 2 +- pkg/services/alerting/notifiers/line.go | 2 +- pkg/services/alerting/notifiers/opsgenie.go | 2 +- pkg/services/alerting/notifiers/pagerduty.go | 2 +- pkg/services/alerting/notifiers/pushover.go | 2 +- pkg/services/alerting/notifiers/sensu.go | 2 +- pkg/services/alerting/notifiers/slack.go | 2 +- pkg/services/alerting/notifiers/teams.go | 2 +- pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/alerting/notifiers/threema.go | 2 +- pkg/services/alerting/notifiers/victorops.go | 2 +- pkg/services/alerting/notifiers/webhook.go | 2 +- pkg/services/alerting/result_handler.go | 1 - pkg/services/alerting/rule.go | 6 -- pkg/services/sqlstore/alert.go | 22 +----- pkg/services/sqlstore/alert_notification.go | 58 +++++++++++++-- pkg/services/sqlstore/migrations/alert_mig.go | 27 ++++++- .../app/features/alerting/alert_tab_ctrl.ts | 3 - .../alerting/notification_edit_ctrl.ts | 2 + .../features/alerting/partials/alert_tab.html | 3 - .../alerting/partials/notification_edit.html | 5 ++ 34 files changed, 215 insertions(+), 107 deletions(-) diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 64dd619a4eb..5e0196c20d1 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -21,18 +21,17 @@ type AlertRule struct { ExecutionError string `json:"executionError"` Url string `json:"url"` CanEdit bool `json:"canEdit"` - NotifyOnce bool `json:"notifyOnce"` - NotifyEval uint64 `json:"notifyEval"` - NotifyFreq uint64 `json:"notifyFrequency"` } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + NotifyOnce bool `json:"notifyOnce"` + Frequency bool `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type AlertTestCommand struct { @@ -62,9 +61,11 @@ type EvalMatch struct { } type NotificationTestCommand struct { - Name string `json:"name"` - Type string `json:"type"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name"` + Type string `json:"type"` + NotifyOnce bool `json:"notifyOnce"` + Frequency time.Duration `json:"frequency"` + Settings *simplejson.Json `json:"settings"` } type PauseAlertCommand struct { diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 56ceeb2cbf7..fba2aa63df9 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -72,9 +72,6 @@ type Alert struct { Silenced bool ExecutionError string Frequency int64 - NotifyOnce bool - NotifyFreq uint64 - NotifyEval uint64 EvalData *simplejson.Json NewStateDate time.Time @@ -98,8 +95,6 @@ func (this *Alert) ContainsUpdates(other *Alert) bool { result := false result = result || this.Name != other.Name result = result || this.Message != other.Message - result = result || this.NotifyOnce != other.NotifyOnce - result = result || (!other.NotifyOnce && this.NotifyFreq != other.NotifyFreq) if this.Settings != nil && other.Settings != nil { json1, err1 := this.Settings.Encode() @@ -164,10 +159,6 @@ type SetAlertStateCommand struct { Timestamp time.Time } -type IncAlertEvalCommand struct { - AlertId int64 -} - //Queries type GetAlertsQuery struct { OrgId int64 diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 87b515f370c..cba62a51527 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -7,32 +7,38 @@ import ( ) type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + NotifyOnce bool `json:"notifyOnce"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + NotifyOnce bool `json:"notifyOnce" binding:"Required"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + NotifyOnce string `json:"notifyOnce" binding:"Required"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings" binding:"Required"` OrgId int64 `json:"-"` Result *AlertNotification @@ -63,3 +69,34 @@ type GetAllAlertNotificationsQuery struct { Result []*AlertNotification } + +type NotificationJournal struct { + Id int64 + OrgId int64 + AlertId int64 + NotifierId int64 + SentAt time.Time + Success bool +} + +type RecordNotificationJournalCommand struct { + OrgId int64 + AlertId int64 + NotifierId int64 + SentAt time.Time + Success bool +} + +type GetLatestNotificationQuery struct { + OrgId int64 + AlertId int64 + NotifierId int64 + + Result *NotificationJournal +} + +type CleanNotificationJournalCommand struct { + OrgId int64 + AlertId int64 + NotifierId int64 +} diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d0441d379b7..b451d188a64 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -143,3 +143,18 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return m.AlertStateOK } + +func (c *EvalContext) LastNotify(notifierId int64) *time.Time { + cmd := &m.GetLatestNotificationQuery{ + OrgId: c.Rule.OrgId, + AlertId: c.Rule.Id, + NotifierId: notifierId, + } + if err := bus.Dispatch(cmd); err != nil { + c.log.Warn("Could not determine last time alert", + c.Rule.Name, "notified") + return nil + } + + return &cmd.Result.SentAt +} diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index f820e546a93..e1c1bfacb2e 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -122,8 +122,6 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, Handler: jsonAlert.Get("handler").MustInt64(), Message: jsonAlert.Get("message").MustString(), Frequency: frequency, - NotifyOnce: jsonAlert.Get("notifyOnce").MustBool(), - NotifyFreq: jsonAlert.Get("notifyFrequency").MustUint64(), } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 18f969ba1b9..8842b35fba2 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -19,6 +19,8 @@ type Notifier interface { GetNotifierId() int64 GetIsDefault() bool + GetNotifyOnce() bool + GetFrequency() time.Duration } type NotifierSlice []Notifier diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 2ea68cf5085..53923a420fe 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -66,7 +66,17 @@ func (n *notificationService) sendNotifications(context *EvalContext, notifiers not := notifier //avoid updating scope variable in go routine n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() - g.Go(func() error { return not.Notify(context) }) + g.Go(func() error { + success := not.Notify(context) == nil + cmd := &m.RecordNotificationJournalCommand{ + OrgId: context.Rule.OrgId, + AlertId: context.Rule.Id, + NotifierId: not.GetNotifierId(), + SentAt: time.Now(), + Success: success, + } + return bus.Dispatch(cmd) + }) } return g.Wait() diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index d449167de13..3eeb25986e0 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -33,7 +33,7 @@ func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, err } return &AlertmanagerNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.prometheus-alertmanager"), }, nil diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 498bae3a6e6..e9e32020c6d 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -1,6 +1,8 @@ package notifiers import ( + "time" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" @@ -12,9 +14,11 @@ type NotifierBase struct { Id int64 IsDeault bool UploadImage bool + NotifyOnce bool + Frequency time.Duration } -func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase { +func NewNotifierBase(id int64, isDefault bool, name, notifierType string, notifyOnce bool, frequency time.Duration, model *simplejson.Json) NotifierBase { uploadImage := true value, exist := model.CheckGet("uploadImage") if exist { @@ -27,15 +31,17 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model IsDeault: isDefault, Type: notifierType, UploadImage: uploadImage, + NotifyOnce: notifyOnce, + Frequency: frequency, } } -func defaultShouldNotify(context *alerting.EvalContext) bool { +func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequency time.Duration, lastNotify *time.Time) bool { // Only notify on state change. - if context.PrevAlertState == context.Rule.State && context.Rule.NotifyOnce { + if context.PrevAlertState == context.Rule.State && notifyOnce { return false } - if !context.Rule.NotifyOnce && context.Rule.NotifyEval != 0 { + if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } // Do not notify when we become OK for the first time. @@ -46,7 +52,8 @@ func defaultShouldNotify(context *alerting.EvalContext) bool { } func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) + lastNotify := context.LastNotify(n.Id) + return defaultShouldNotify(context, n.NotifyOnce, n.Frequency, lastNotify) } func (n *NotifierBase) GetType() string { @@ -64,3 +71,11 @@ func (n *NotifierBase) GetNotifierId() int64 { func (n *NotifierBase) GetIsDefault() bool { return n.IsDeault } + +func (n *NotifierBase) GetNotifyOnce() bool { + return n.NotifyOnce +} + +func (n *NotifierBase) GetFrequency() time.Duration { + return n.Frequency +} diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 14eacef5831..78446c56f88 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -32,7 +32,7 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.dingding"), }, nil diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 3ffa7484870..693ed31e206 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -39,7 +39,7 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &DiscordNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), WebhookURL: url, log: log.New("alerting.notifier.discord"), }, nil diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 562ffbe1269..234a4f8e756 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -52,7 +52,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }) return &EmailNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Addresses: addresses, log: log.New("alerting.notifier.email"), }, nil diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 58e1b7bd71e..4eb5b78811e 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -59,7 +59,7 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err roomId := model.Settings.Get("roomid").MustString() return &HipChatNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, ApiKey: apikey, RoomId: roomId, diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index 92f6489106b..0dab556d5e1 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -43,7 +43,7 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &KafkaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Endpoint: endpoint, Topic: topic, log: log.New("alerting.notifier.kafka"), diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 4814662f3a9..0ee252e6447 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -39,7 +39,7 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &LineNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Token: token, log: log.New("alerting.notifier.line"), }, nil diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index f0f5142cf05..991afd5ce9b 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -56,7 +56,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &OpsGenieNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), ApiKey: apiKey, ApiUrl: apiUrl, AutoClose: autoClose, diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 02219b2203d..afa0ba63eca 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -51,7 +51,7 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &PagerdutyNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Key: key, AutoResolve: autoResolve, log: log.New("alerting.notifier.pagerduty"), diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index cbe9e16801a..09dfd6f0f9b 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -99,7 +99,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) return nil, alerting.ValidationError{Reason: "API token not given"} } return &PushoverNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), UserKey: userKey, ApiToken: apiToken, Priority: priority, diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index 9f77801d458..e6b94d3223e 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -51,7 +51,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &SensuNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, User: model.Settings.Get("username").MustString(), Source: model.Settings.Get("source").MustString(), diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index a8139b62726..fbbe4b3e59d 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -78,7 +78,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { uploadImage := model.Settings.Get("uploadImage").MustBool(true) return &SlackNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, Recipient: recipient, Mention: mention, diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 7f62340d0e1..362a367e1f2 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -33,7 +33,7 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &TeamsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.teams"), }, nil diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index ca24c996914..97696b2290c 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -78,7 +78,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &TelegramNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), BotToken: botToken, ChatID: chatId, UploadImage: uploadImage, diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index e4ffffc9108..e7fb39f27db 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -106,7 +106,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &ThreemaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), GatewayID: gatewayID, RecipientID: recipientID, APISecret: apiSecret, diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index a753ca3cbf6..c6c1cf76047 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -51,7 +51,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e } return &VictoropsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), URL: url, AutoResolve: autoResolve, log: log.New("alerting.notifier.victorops"), diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 4c97ed2b75e..26989873e9e 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -47,7 +47,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, User: model.Settings.Get("username").MustString(), Password: model.Settings.Get("password").MustString(), diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 56d299001f0..c57b28c7c3e 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,7 +88,6 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } - bus.Dispatch(&m.IncAlertEvalCommand{AlertId: evalContext.Rule.Id}) handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 0003fe791e3..018d138dbe4 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -23,9 +23,6 @@ type Rule struct { State m.AlertStateType Conditions []Condition Notifications []int64 - NotifyOnce bool - NotifyFreq uint64 - NotifyEval uint64 } type ValidationError struct { @@ -100,9 +97,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Name = ruleDef.Name model.Message = ruleDef.Message model.Frequency = ruleDef.Frequency - model.NotifyOnce = ruleDef.NotifyOnce - model.NotifyFreq = ruleDef.NotifyFreq - model.NotifyEval = ruleDef.NotifyEval model.State = ruleDef.State model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 9ab28be84ee..58ec7e2857a 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -22,7 +22,6 @@ func init() { bus.AddHandler("sql", GetAlertStatesForDashboard) bus.AddHandler("sql", PauseAlert) bus.AddHandler("sql", PauseAllAlerts) - bus.AddHandler("sql", IncAlertEval) } func GetAlertById(query *m.GetAlertByIdQuery) error { @@ -189,7 +188,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS if alertToUpdate.ContainsUpdates(alert) { alert.Updated = timeNow() alert.State = alertToUpdate.State - sess.MustCols("message", "notify_freq", "notify_once") + sess.MustCols("message") _, err := sess.Id(alert.Id).Update(alert) if err != nil { return err @@ -344,22 +343,3 @@ func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error return err } - -func IncAlertEval(cmd *m.IncAlertEvalCommand) error { - return inTransaction(func(sess *DBSession) error { - alert := m.Alert{} - - if _, err := sess.Id(cmd.AlertId).Get(&alert); err != nil { - return err - } - - alert.NotifyEval = (alert.NotifyEval + 1) % alert.NotifyFreq - - sess.MustCols("notify_eval") - if _, err := sess.Id(cmd.AlertId).Update(alert); err != nil { - return err - } - - return nil - }) -} diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 651241f7714..8bb17143042 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -17,6 +17,9 @@ func init() { bus.AddHandler("sql", DeleteAlertNotification) bus.AddHandler("sql", GetAlertNotificationsToSend) bus.AddHandler("sql", GetAllAlertNotifications) + bus.AddHandler("sql", RecordNotificationJournal) + bus.AddHandler("sql", GetLatestNotification) + bus.AddHandler("sql", CleanNotificationJournal) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -138,13 +141,15 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - Created: time.Now(), - Updated: time.Now(), - IsDefault: cmd.IsDefault, + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + NotifyOnce: cmd.NotifyOnce, + Frequency: cmd.Frequency, + Created: time.Now(), + Updated: time.Now(), + IsDefault: cmd.IsDefault, } if _, err = sess.Insert(alertNotification); err != nil { @@ -192,3 +197,42 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { return nil }) } + +func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { + return inTransaction(func(sess *DBSession) error { + journalEntry := &m.NotificationJournal{ + OrgId: cmd.OrgId, + AlertId: cmd.AlertId, + NotifierId: cmd.NotifierId, + SentAt: cmd.SentAt, + Success: cmd.Success, + } + + if _, err := sess.Insert(journalEntry); err != nil { + return err + } + + return nil + }) +} + +func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { + return inTransaction(func(sess *DBSession) error { + notificationJournal := &m.NotificationJournal{} + _, err := sess.OrderBy("notification_journal.sent_at").Desc().Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + if err != nil { + return err + } + + cmd.Result = notificationJournal + return nil + }) +} + +func CleanNotificationJournal(cmd *m.CleanNotificationJournalCommand) error { + return inTransaction(func(sess *DBSession) error { + sql := "DELETE FROM notification_journal WHERE notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?" + _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) + return err + }) +} diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 3452e5710cc..d045f611fb2 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -29,9 +29,6 @@ func addAlertMigrations(mg *Migrator) { {Name: "state_changes", Type: DB_Int, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, - {Name: "notify_once", Type: DB_Bool, Nullable: false}, - {Name: "notify_freq", Type: DB_Int, Nullable: false}, - {Name: "notify_eval", Type: DB_Int, Nullable: false}, }, Indices: []*Index{ {Cols: []string{"org_id", "id"}, Type: IndexType}, @@ -68,8 +65,32 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{ Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0", })) + mg.AddMigration("Add column frequency", NewAddColumnMigration(alert_notification, &Column{ + Name: "frequency", Type: DB_BigInt, Nullable: true, + })) + mg.AddMigration("Add column notify_once", NewAddColumnMigration(alert_notification, &Column{ + Name: "notify_once", Type: DB_Bool, Nullable: false, Default: "1", + })) mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) + notification_journal := Table{ + Name: "notification_journal", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "alert_id", Type: DB_BigInt, Nullable: false}, + {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, + {Name: "sent_at", Type: DB_DateTime, Nullable: false}, + {Name: "success", Type: DB_Bool, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, + }, + } + + mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) + mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) + mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{ {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "message", Type: DB_Text, Nullable: false}, diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index f0d965ae81e..79baa1e3f5a 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -167,9 +167,6 @@ export class AlertTabCtrl { alert.noDataState = alert.noDataState || 'no_data'; alert.executionErrorState = alert.executionErrorState || 'alerting'; alert.frequency = alert.frequency || '60s'; - alert.notifyFrequency = alert.notifyFrequency || 10; - alert.notifyOnce = alert.notifyOnce == null ? true : alert.notifyOnce; - alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 18b1c4d1d55..2fd185bee29 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -11,6 +11,7 @@ export class AlertNotificationEditCtrl { model: any; defaults: any = { type: 'email', + notifyOnce: true, settings: { httpMethod: 'POST', autoResolve: true, @@ -102,6 +103,7 @@ export class AlertNotificationEditCtrl { var payload = { name: this.model.name, type: this.model.type, + frequency: this.model.frequency, settings: this.model.settings, }; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 084aeb2036a..cb101672aa4 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -31,9 +31,6 @@ Evaluate every - {{ ctrl.alert.notifyOnce ? 'Notify on state change' : 'Notify every' }} - - evaluations
    diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index d20b9031a8f..ccdb9ef1073 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -18,6 +18,11 @@
    + Date: Sun, 20 May 2018 12:12:10 -0400 Subject: [PATCH 0809/3000] Fix multiple bugs --- pkg/api/alerting.go | 57 ++++++++++++++++--- pkg/api/dtos/alerting.go | 19 ++++--- pkg/models/alert_notifications.go | 6 +- pkg/services/alerting/eval_context.go | 4 +- pkg/services/alerting/notifiers/base.go | 1 + pkg/services/alerting/notifiers/base_test.go | 13 +++-- pkg/services/sqlstore/alert_notification.go | 28 +++++++-- .../alerting/partials/notification_edit.html | 3 +- 8 files changed, 95 insertions(+), 36 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 961fc11b2dc..c5b47270f4d 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -193,12 +193,15 @@ func GetAlertNotifications(c *m.ReqContext) Response { for _, notification := range query.Result { result = append(result, &dtos.AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: notification.Frequency.String(), + NotifyOnce: notification.NotifyOnce, + Settings: notification.Settings, }) } @@ -215,7 +218,19 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } - return JSON(200, query.Result) + result := &dtos.AlertNotification{ + Id: query.Result.Id, + Name: query.Result.Name, + Type: query.Result.Type, + IsDefault: query.Result.IsDefault, + Created: query.Result.Created, + Updated: query.Result.Updated, + Frequency: query.Result.Frequency.String(), + NotifyOnce: query.Result.NotifyOnce, + Settings: query.Result.Settings, + } + + return JSON(200, result) } func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response { @@ -225,7 +240,19 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma return Error(500, "Failed to create alert notification", err) } - return JSON(200, cmd.Result) + result := &dtos.AlertNotification{ + Id: cmd.Result.Id, + Name: cmd.Result.Name, + Type: cmd.Result.Type, + IsDefault: cmd.Result.IsDefault, + Created: cmd.Result.Created, + Updated: cmd.Result.Updated, + Frequency: cmd.Result.Frequency.String(), + NotifyOnce: cmd.Result.NotifyOnce, + Settings: cmd.Result.Settings, + } + + return JSON(200, result) } func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response { @@ -235,7 +262,19 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma return Error(500, "Failed to update alert notification", err) } - return JSON(200, cmd.Result) + result := &dtos.AlertNotification{ + Id: cmd.Result.Id, + Name: cmd.Result.Name, + Type: cmd.Result.Type, + IsDefault: cmd.Result.IsDefault, + Created: cmd.Result.Created, + Updated: cmd.Result.Updated, + Frequency: cmd.Result.Frequency.String(), + NotifyOnce: cmd.Result.NotifyOnce, + Settings: cmd.Result.Settings, + } + + return JSON(200, result) } func DeleteAlertNotification(c *m.ReqContext) Response { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 5e0196c20d1..7d4201fba87 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -24,14 +24,15 @@ type AlertRule struct { } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - NotifyOnce bool `json:"notifyOnce"` - Frequency bool `json:"frequency"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + NotifyOnce bool `json:"notifyOnce"` + Frequency string `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Settings *simplejson.Json `json:"settings"` } type AlertTestCommand struct { @@ -64,7 +65,7 @@ type NotificationTestCommand struct { Name string `json:"name"` Type string `json:"type"` NotifyOnce bool `json:"notifyOnce"` - Frequency time.Duration `json:"frequency"` + Frequency string `json:"frequency"` Settings *simplejson.Json `json:"settings"` } diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index cba62a51527..6715eb21395 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -22,8 +22,8 @@ type AlertNotification struct { type CreateAlertNotificationCommand struct { Name string `json:"name" binding:"Required"` Type string `json:"type" binding:"Required"` - NotifyOnce bool `json:"notifyOnce" binding:"Required"` - Frequency time.Duration `json:"frequency"` + NotifyOnce bool `json:"notifyOnce"` + Frequency string `json:"frequency"` IsDefault bool `json:"isDefault"` Settings *simplejson.Json `json:"settings"` @@ -35,7 +35,7 @@ type UpdateAlertNotificationCommand struct { Id int64 `json:"id" binding:"Required"` Name string `json:"name" binding:"Required"` Type string `json:"type" binding:"Required"` - NotifyOnce string `json:"notifyOnce" binding:"Required"` + NotifyOnce bool `json:"notifyOnce"` Frequency string `json:"frequency"` IsDefault bool `json:"isDefault"` Settings *simplejson.Json `json:"settings" binding:"Required"` diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index b451d188a64..3817f4b4a3c 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -151,8 +151,8 @@ func (c *EvalContext) LastNotify(notifierId int64) *time.Time { NotifierId: notifierId, } if err := bus.Dispatch(cmd); err != nil { - c.log.Warn("Could not determine last time alert", - c.Rule.Name, "notified") + c.log.Warn("Could not determine last time alert notifier fired", + "Alert name", c.Rule.Name, "Error", err) return nil } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index e9e32020c6d..734b5e56b28 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -41,6 +41,7 @@ func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequen if context.PrevAlertState == context.Rule.State && notifyOnce { return false } + // Do not notify if interval has not elapsed if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index b7142d144cc..5f2d4989063 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -3,6 +3,7 @@ package notifiers import ( "context" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" @@ -18,19 +19,19 @@ func TestBaseNotifier(t *testing.T) { Convey("can parse false value", func() { bJson.Set("uploadImage", false) - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeFalse) }) Convey("can parse true value", func() { bJson.Set("uploadImage", true) - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeTrue) }) Convey("default value should be true for backwards compatibility", func() { - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeTrue) }) }) @@ -41,7 +42,8 @@ func TestBaseNotifier(t *testing.T) { State: m.AlertStatePending, }) context.Rule.State = m.AlertStateOK - So(defaultShouldNotify(context), ShouldBeFalse) + timeNow := time.Now() + So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeFalse) }) Convey("ok -> alerting", func() { @@ -49,7 +51,8 @@ func TestBaseNotifier(t *testing.T) { State: m.AlertStateOK, }) context.Rule.State = m.AlertStateAlerting - So(defaultShouldNotify(context), ShouldBeTrue) + timeNow := time.Now() + So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeTrue) }) }) }) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 8bb17143042..a2cfa37ce5c 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -56,7 +56,9 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro alert_notification.created, alert_notification.updated, alert_notification.settings, - alert_notification.is_default + alert_notification.is_default, + alert_notification.notify_once, + alert_notification.frequency FROM alert_notification `) @@ -94,7 +96,9 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS alert_notification.created, alert_notification.updated, alert_notification.settings, - alert_notification.is_default + alert_notification.is_default, + alert_notification.notify_once, + alert_notification.frequency FROM alert_notification `) @@ -140,19 +144,24 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } + frequency, err_convert := time.ParseDuration(cmd.Frequency) + if err_convert != nil { + return err + } + alertNotification := &m.AlertNotification{ OrgId: cmd.OrgId, Name: cmd.Name, Type: cmd.Type, Settings: cmd.Settings, NotifyOnce: cmd.NotifyOnce, - Frequency: cmd.Frequency, + Frequency: frequency, Created: time.Now(), Updated: time.Now(), IsDefault: cmd.IsDefault, } - if _, err = sess.Insert(alertNotification); err != nil { + if _, err = sess.MustCols("notify_once").Insert(alertNotification); err != nil { return err } @@ -184,8 +193,15 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Name = cmd.Name current.Type = cmd.Type current.IsDefault = cmd.IsDefault + current.NotifyOnce = cmd.NotifyOnce - sess.UseBool("is_default") + frequency, err_convert := time.ParseDuration(cmd.Frequency) + if err_convert != nil { + return err + } + current.Frequency = frequency + + sess.UseBool("is_default", "notify_once") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err @@ -219,7 +235,7 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { notificationJournal := &m.NotificationJournal{} - _, err := sess.OrderBy("notification_journal.sent_at").Desc().Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + _, err := sess.Desc("notification_journal.sent_at").Limit(1).Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) if err != nil { return err } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index ccdb9ef1073..dd56564cb95 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -20,8 +20,7 @@
    Date: Sun, 20 May 2018 16:08:42 -0400 Subject: [PATCH 0810/3000] Fix tests --- pkg/services/sqlstore/alert_notification.go | 8 +++++ .../sqlstore/alert_notification_test.go | 32 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index a2cfa37ce5c..0ecd6a18818 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -144,6 +144,10 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } + if cmd.Frequency == "" { + return fmt.Errorf("Alert notification frequency required") + } + frequency, err_convert := time.ParseDuration(cmd.Frequency) if err_convert != nil { return err @@ -195,6 +199,10 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.IsDefault = cmd.IsDefault current.NotifyOnce = cmd.NotifyOnce + if cmd.Frequency == "" { + return fmt.Errorf("Alert notification frequency required") + } + frequency, err_convert := time.ParseDuration(cmd.Frequency) if err_convert != nil { return err diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 2dbf9de5ca8..01c6c3aebd6 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -26,10 +26,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can save Alert Notification", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - Settings: simplejson.New(), + Name: "ops", + Type: "email", + OrgId: 1, + NotifyOnce: true, + Frequency: "10s", + Settings: simplejson.New(), } err = CreateAlertNotificationCommand(cmd) @@ -45,11 +47,13 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can update alert notification", func() { newCmd := &m.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - Settings: simplejson.New(), - Id: cmd.Result.Id, + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + NotifyOnce: true, + Frequency: "10s", + Settings: simplejson.New(), + Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) @@ -58,12 +62,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can search using an array of ids", func() { - cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()} - cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()} - cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()} - cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()} + cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()} + otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil) From 5c5951bc4274f3b4ff1ea3b41507e394faaeb22f Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Sun, 20 May 2018 19:01:10 -0400 Subject: [PATCH 0811/3000] Bug fix for repeated alerting even on OK state and add notification_journal cleanup when alert resolves --- pkg/services/alerting/engine.go | 14 ++++++++++++++ pkg/services/alerting/notifiers/base.go | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 0f8e24bcef5..43f6db66771 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -10,7 +10,9 @@ import ( tlog "github.com/opentracing/opentracing-go/log" "github.com/benbjohnson/clock" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -205,6 +207,18 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel } evalContext.Rule.State = evalContext.GetNewState() + if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { + for _, notifierId := range evalContext.Rule.Notifications { + cmd := &m.CleanNotificationJournalCommand{ + AlertId: evalContext.Rule.Id, + NotifierId: notifierId, + OrgId: evalContext.Rule.OrgId, + } + if err := bus.Dispatch(cmd); err != nil { + e.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) + } + } + } e.resultHandler.Handle(evalContext) span.Finish() e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 734b5e56b28..7672d397491 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -45,6 +45,10 @@ func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequen if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } + // Do not notify if alert state if OK or pending even on repeated notify + if !notifyOnce && (context.Rule.State == m.AlertStateOK || context.Rule.State == m.AlertStatePending) { + return false + } // Do not notify when we become OK for the first time. if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) { return false From bdf433594add113b05b2bbd4f0381c1090fd2d6b Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Fri, 25 May 2018 14:14:33 -0400 Subject: [PATCH 0812/3000] Implement code review changes --- pkg/models/alert_notifications.go | 5 +++++ pkg/services/alerting/engine.go | 14 -------------- pkg/services/alerting/result_handler.go | 12 ++++++++++++ pkg/services/sqlstore/alert_notification.go | 7 ++++--- .../features/alerting/notification_edit_ctrl.ts | 1 + .../alerting/partials/notification_edit.html | 15 +++++++++++---- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 6715eb21395..ed6b8f372d1 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -1,11 +1,16 @@ package models import ( + "errors" "time" "github.com/grafana/grafana/pkg/components/simplejson" ) +var ( + ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") +) + type AlertNotification struct { Id int64 `json:"id"` OrgId int64 `json:"-"` diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 43f6db66771..0f8e24bcef5 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -10,9 +10,7 @@ import ( tlog "github.com/opentracing/opentracing-go/log" "github.com/benbjohnson/clock" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -207,18 +205,6 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel } evalContext.Rule.State = evalContext.GetNewState() - if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { - for _, notifierId := range evalContext.Rule.Notifications { - cmd := &m.CleanNotificationJournalCommand{ - AlertId: evalContext.Rule.Id, - NotifierId: notifierId, - OrgId: evalContext.Rule.OrgId, - } - if err := bus.Dispatch(cmd); err != nil { - e.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) - } - } - } e.resultHandler.Handle(evalContext) span.Finish() e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index c57b28c7c3e..c4c20bd8beb 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,6 +88,18 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } + if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { + for _, notifierId := range evalContext.Rule.Notifications { + cmd := &m.CleanNotificationJournalCommand{ + AlertId: evalContext.Rule.Id, + NotifierId: notifierId, + OrgId: evalContext.Rule.OrgId, + } + if err := bus.Dispatch(cmd); err != nil { + handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) + } + } + } handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 0ecd6a18818..6913009a163 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -148,8 +148,9 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification frequency required") } - frequency, err_convert := time.ParseDuration(cmd.Frequency) - if err_convert != nil { + var frequency time.Duration + frequency, err = time.ParseDuration(cmd.Frequency) + if err != nil { return err } @@ -200,7 +201,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.NotifyOnce = cmd.NotifyOnce if cmd.Frequency == "" { - return fmt.Errorf("Alert notification frequency required") + return m.ErrNotificationFrequencyNotFound } frequency, err_convert := time.ParseDuration(cmd.Frequency) diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 2fd185bee29..9d20e871c7c 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl { defaults: any = { type: 'email', notifyOnce: true, + frequency: '15m', settings: { httpMethod: 'POST', autoResolve: true, diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index dd56564cb95..48d44b74581 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -18,10 +18,6 @@
    - + + +
    + Notify every + +
    From 8419cc05531a8db0bd3d3ce0a809096189ab3f33 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 4 Jun 2018 13:32:19 +0200 Subject: [PATCH 0813/3000] made folder text smaller --- public/sass/components/_search.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 3b6c1fbcce6..e2e3336db05 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -211,6 +211,7 @@ .search-item__body-folder-title { color: $text-color-weak; padding-left: 0.25rem; + font-size: $font-size-xs; } .search-item__icon { From 0d5579b4c04fa7c04c3ae59950f962775a3f0777 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Jun 2018 14:31:43 +0200 Subject: [PATCH 0814/3000] docs: what's new in v5.2 --- docs/sources/guides/whats-new-in-v5-2.md | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/sources/guides/whats-new-in-v5-2.md diff --git a/docs/sources/guides/whats-new-in-v5-2.md b/docs/sources/guides/whats-new-in-v5-2.md new file mode 100644 index 00000000000..8cff353ff45 --- /dev/null +++ b/docs/sources/guides/whats-new-in-v5-2.md @@ -0,0 +1,70 @@ ++++ +title = "What's New in Grafana v5.2" +description = "Feature & improvement highlights for Grafana v5.2" +keywords = ["grafana", "new", "documentation", "5.2"] +type = "docs" +[menu.docs] +name = "Version 5.2" +identifier = "v5.2" +parent = "whatsnew" +weight = -8 ++++ + +# What's New in Grafana v5.2 + +Grafana v5.2 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements. + +* [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here! +* [Cross platform build support]({{< relref "#cross-platform-build-support" >}}) enables native builds of Grafana for many more platforms! +* [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets +* [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements +* [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord +* [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) + +## Elasticsearch alerting + +{{< docs-imagebox img="/img/docs/v52/elasticsearch_alerting.png" max-width="800px" class="docs-image--right" >}} + +Grafana v5.2 ships with an updated Elasticsearch datasource with support for alerting. Alerting support for Elasticsearch has been one of +the most requested features by our community and now it's finally here. Please try it out and let us know what you think. + +
    + +## Cross platform build support + +Grafana v5.2 brings an improved build pipeline with cross platform support. This enables native builds of Grafana for ARMv7 (x32), ARM64 (x64), +MacOS/Darwin (x64) and Windows (x64) in both stable and nightly builds. + +We've been longing for native ARM build support for a long time. With the help from our amazing community this is now finally available. + +## Improved Docker image + +The Grafana docker image now includes support for Docker secrets which enables you to supply Grafana with configuration through files. More +information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets). + +## Prometheus + +The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate* +functions get consistent results, and thus avoid graphs jumping around on reload. + +## Alerting + +By popular demand Grafana now includes support for an alert notification channel type for [Discord](https://discordapp.com/). + +## Dashboards & Panels + +### Modified time range and variables are no longer saved by default + +{{< docs-imagebox img="/img/docs/v52/dashboard_save_modal.png" max-width="800px" class="docs-image--right" >}} + +Starting from Grafana v5.2 a modified time range or variable are no longer saved by default. To save a modified +time range or variable you'll need to actively select that when saving a dashboard, see screenshot. +This should hopefully make it easier to have sane defaults of time and variables in dashboards and make it more explicit +when you actually want to overwrite those settings. + +
    + +## Changelog + +Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list +of new features, changes, and bug fixes. From 38906acda98a43302f3f688042dc40e757284495 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Jun 2018 15:15:47 +0200 Subject: [PATCH 0815/3000] elasticsearch: sort bucket keys to fix issue wth response parser tests --- pkg/tsdb/elasticsearch/response_parser.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 4a45d6271b9..7bdab60389c 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -113,15 +113,22 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu } } - for k, v := range esAgg.Get("buckets").MustMap() { - bucket := simplejson.NewFromAny(v) + buckets := esAgg.Get("buckets").MustMap() + bucketKeys := make([]string, 0) + for k := range buckets { + bucketKeys = append(bucketKeys, k) + } + sort.Strings(bucketKeys) + + for _, bucketKey := range bucketKeys { + bucket := simplejson.NewFromAny(buckets[bucketKey]) newProps := make(map[string]string, 0) for k, v := range props { newProps[k] = v } - newProps["filter"] = k + newProps["filter"] = bucketKey err = rp.processBuckets(bucket.MustMap(), target, series, table, newProps, depth+1) if err != nil { From c138ff2c903c4cb7b5844529dae70037e651a15e Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 15:16:05 +0200 Subject: [PATCH 0816/3000] changelog: adds note about closing #11670 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eda912e86a..f11d06e990a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ * **Dashboard**: Validate uid when importing dashboards [#11515](https://github.com/grafana/grafana/issues/11515) * **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) * **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167) +* **Provisioning**: Verify checksum of dashboards before updating to reduce load on database [#11670](https://github.com/grafana/grafana/issues/11670) # 5.1.3 (2018-05-16) From d089b5e05dccfd60d49b802be3a28ec3530fb0e8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 15:20:26 +0200 Subject: [PATCH 0817/3000] provisioning: turn relative symlinked path into absolut paths --- pkg/services/provisioning/dashboards/file_reader.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 8af23980531..3196c3a35af 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -48,16 +48,25 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade } copy := path + + // get absolut path of config file path, err := filepath.Abs(path) if err != nil { log.Error("Could not create absolute path ", "path", path) } + // follow the symlink to get the real path path, err = filepath.EvalSymlinks(path) if err != nil { log.Error("Failed to read content of symlinked path: %s", path) } + // get the absolut path in case the symlink is relative + path, err = filepath.Abs(path) + if err != nil { + log.Error("Could not create absolute path ", "path", path) + } + if path == "" { path = copy log.Info("falling back to original path due to EvalSymlink/Abs failure") From cd4026da6b60967dee2c51d626715913d1fa9914 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 15:38:37 +0200 Subject: [PATCH 0818/3000] Revert "provisioning: turn relative symlinked path into absolut paths" This reverts commit d089b5e05dccfd60d49b802be3a28ec3530fb0e8. --- pkg/services/provisioning/dashboards/file_reader.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 3196c3a35af..8af23980531 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -48,25 +48,16 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade } copy := path - - // get absolut path of config file path, err := filepath.Abs(path) if err != nil { log.Error("Could not create absolute path ", "path", path) } - // follow the symlink to get the real path path, err = filepath.EvalSymlinks(path) if err != nil { log.Error("Failed to read content of symlinked path: %s", path) } - // get the absolut path in case the symlink is relative - path, err = filepath.Abs(path) - if err != nil { - log.Error("Could not create absolute path ", "path", path) - } - if path == "" { path = copy log.Info("falling back to original path due to EvalSymlink/Abs failure") From 829af9425f4e1f6d0d3cea9f8d5fa78e46bc4a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Jun 2018 15:45:29 +0200 Subject: [PATCH 0819/3000] revert: reverted singlestat panel position change PR #12004 --- public/sass/components/_panel_singlestat.scss | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/public/sass/components/_panel_singlestat.scss b/public/sass/components/_panel_singlestat.scss index af11de3b835..d680941bfb1 100644 --- a/public/sass/components/_panel_singlestat.scss +++ b/public/sass/components/_panel_singlestat.scss @@ -7,14 +7,13 @@ .singlestat-panel-value-container { line-height: 1; - position: absolute; + display: table-cell; + vertical-align: middle; + text-align: center; + position: relative; z-index: 1; font-size: 3em; - font-weight: bold; - margin: 0; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + font-weight: $font-weight-semi-bold; } .singlestat-panel-prefix { From 574e92e1d8497f2be17d781b2eeb3e98867d2b39 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 15:23:17 +0200 Subject: [PATCH 0820/3000] changelog: adds note about closing #11958 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f11d06e990a..22e2c29c91b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ * **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon) * **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167) * **Provisioning**: Verify checksum of dashboards before updating to reduce load on database [#11670](https://github.com/grafana/grafana/issues/11670) +* **Provisioning**: Support symlinked files in dashboard provisioning config files [#11958](https://github.com/grafana/grafana/issues/11958) # 5.1.3 (2018-05-16) From cb6c6c817234b59cce137f071ea31ccc58f1896d Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 23 May 2018 11:34:22 +0200 Subject: [PATCH 0821/3000] change admin password after first login --- public/app/core/controllers/login_ctrl.ts | 66 +++++++++-- public/app/partials/login.html | 135 +++++++++++++--------- public/sass/components/_gf-form.scss | 4 + public/sass/pages/_login.scss | 38 ++++++ 4 files changed, 184 insertions(+), 59 deletions(-) diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts index 313fc2efa1a..0a66f83d08a 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -11,10 +11,15 @@ export class LoginCtrl { password: '', }; + $scope.command = {}; + $scope.result = ''; + contextSrv.sidemenu = false; $scope.oauth = config.oauth; $scope.oauthEnabled = _.keys(config.oauth).length > 0; + $scope.ldapEnabled = config.ldapEnabled; + $scope.authProxyEnabled = config.authProxyEnabled; $scope.disableLoginForm = config.disableLoginForm; $scope.disableUserSignUp = config.disableUserSignUp; @@ -39,6 +44,43 @@ export class LoginCtrl { } }; + $scope.changeView = function() { + let loginView = document.querySelector('#login-view'); + let changePasswordView = document.querySelector('#change-password-view'); + + loginView.className += ' add'; + setTimeout(() => { + loginView.className += ' hidden'; + }, 250); + setTimeout(() => { + changePasswordView.classList.remove('hidden'); + }, 251); + setTimeout(() => { + changePasswordView.classList.remove('remove'); + }, 301); + + setTimeout(() => { + document.getElementById('newPassword').focus(); + }, 400); + }; + + $scope.changePassword = function() { + $scope.command.oldPassword = 'admin'; + + if ($scope.command.newPassword !== $scope.command.confirmNew) { + $scope.appEvent('alert-warning', ['New passwords do not match', '']); + return; + } + + backendSrv.put('/api/user/password', $scope.command).then(function() { + $scope.toGrafana(); + }); + }; + + $scope.skip = function() { + $scope.toGrafana(); + }; + $scope.loginModeChanged = function(newValue) { $scope.submitBtnText = newValue ? 'Log in' : 'Sign up'; }; @@ -65,18 +107,28 @@ export class LoginCtrl { } backendSrv.post('/login', $scope.formModel).then(function(result) { - var params = $location.search(); + $scope.result = result; - if (params.redirect && params.redirect[0] === '/') { - window.location.href = config.appSubUrl + params.redirect; - } else if (result.redirectUrl) { - window.location.href = result.redirectUrl; - } else { - window.location.href = config.appSubUrl + '/'; + if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) { + $scope.toGrafana(); + return; } + $scope.changeView(); }); }; + $scope.toGrafana = function() { + var params = $location.search(); + + if (params.redirect && params.redirect[0] === '/') { + window.location.href = config.appSubUrl + params.redirect; + } else if ($scope.result.redirectUrl) { + window.location.href = $scope.result.redirectUrl; + } else { + window.location.href = config.appSubUrl + '/'; + } + }; + $scope.init(); } } diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 8680924977f..8be9e777b9f 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -4,70 +4,101 @@ Grafana
    -