From ca6dd7392312fb05c15a18d71aba4df09133ab1c Mon Sep 17 00:00:00 2001 From: Athurg Feng Date: Thu, 25 Oct 2018 18:17:05 +0800 Subject: [PATCH 001/770] Add match values into Dingding notification message --- pkg/services/alerting/notifiers/dingding.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 1ef085c82f1..9ad85d55004 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -1,6 +1,8 @@ package notifiers import ( + "fmt" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" @@ -61,6 +63,10 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { message = title } + for i, match := range evalContext.EvalMatches { + message += fmt.Sprintf("\\n%2d. %s value %s", i+1, match.Metric, match.Value) + } + bodyJSON, err := simplejson.NewJson([]byte(`{ "msgtype": "link", "link": { From 7f45afac63b93bacd73d6ada811b5db0178b1723 Mon Sep 17 00:00:00 2001 From: Athurg Feng Date: Thu, 25 Oct 2018 18:24:04 +0800 Subject: [PATCH 002/770] Split text template into variable --- pkg/services/alerting/notifiers/dingding.go | 23 ++++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 9ad85d55004..bf1b721f753 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -10,19 +10,21 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" ) -func init() { - alerting.RegisterNotifier(&alerting.NotifierPlugin{ - Type: "dingding", - Name: "DingDing", - Description: "Sends HTTP POST request to DingDing", - Factory: NewDingDingNotifier, - OptionsTemplate: ` +const DingdingOptionsTemplate = `

DingDing settings

Url
- `, +` + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "dingding", + Name: "DingDing", + Description: "Sends HTTP POST request to DingDing", + Factory: NewDingDingNotifier, + OptionsTemplate: DingdingOptionsTemplate, }) } @@ -67,7 +69,7 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { message += fmt.Sprintf("\\n%2d. %s value %s", i+1, match.Metric, match.Value) } - bodyJSON, err := simplejson.NewJson([]byte(`{ + bodyStr := `{ "msgtype": "link", "link": { "text": "` + message + `", @@ -75,7 +77,8 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { "picUrl": "` + picUrl + `", "messageUrl": "` + messageUrl + `" } - }`)) + }` + bodyJSON, err := simplejson.NewJson([]byte(bodyStr)) if err != nil { this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name) From cb86e386289a02f1a8638b2e84030b36c697efa9 Mon Sep 17 00:00:00 2001 From: Athurg Feng Date: Thu, 25 Oct 2018 18:29:47 +0800 Subject: [PATCH 003/770] Add Dingding message type to support mass text notification --- pkg/services/alerting/notifiers/dingding.go | 45 ++++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index bf1b721f753..ce932b7a799 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -10,12 +10,17 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" ) +const DefaultDingdingMsgType = "link" const DingdingOptionsTemplate = `

DingDing settings

Url
+
+ MessageType + +
` func init() { @@ -35,8 +40,11 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} } + msgType := model.Settings.Get("msgType").MustString(DefaultDingdingMsgType) + return &DingDingNotifier{ NotifierBase: NewNotifierBase(model), + MsgType: msgType, Url: url, log: log.New("alerting.notifier.dingding"), }, nil @@ -44,8 +52,9 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) type DingDingNotifier struct { NotifierBase - Url string - log log.Logger + MsgType string + Url string + log log.Logger } func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { @@ -69,15 +78,29 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { message += fmt.Sprintf("\\n%2d. %s value %s", i+1, match.Metric, match.Value) } - bodyStr := `{ - "msgtype": "link", - "link": { - "text": "` + message + `", - "title": "` + title + `", - "picUrl": "` + picUrl + `", - "messageUrl": "` + messageUrl + `" - } - }` + var bodyStr string + if this.MsgType == "actionCard" { + bodyStr = `{ + "msgtype": "actionCard", + "actionCard": { + "text": "` + message + `", + "title": "` + title + `", + "singleTitle": "More", + "singleURL": "` + messageUrl + `" + } + }` + } else { + bodyStr = `{ + "msgtype": "link", + "link": { + "text": "` + message + `", + "title": "` + title + `", + "picUrl": "` + picUrl + `", + "messageUrl": "` + messageUrl + `" + } + }` + } + bodyJSON, err := simplejson.NewJson([]byte(bodyStr)) if err != nil { From 201dd6bf658501782180ce90111390d4970b16c8 Mon Sep 17 00:00:00 2001 From: Athurg Feng Date: Thu, 25 Oct 2018 18:53:45 +0800 Subject: [PATCH 004/770] Optimize the Dingding match values format --- pkg/services/alerting/notifiers/dingding.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index ce932b7a799..94961e82025 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -75,7 +75,7 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { } for i, match := range evalContext.EvalMatches { - message += fmt.Sprintf("\\n%2d. %s value %s", i+1, match.Metric, match.Value) + message += fmt.Sprintf("\\n%2d. %s: %s", i+1, match.Metric, match.Value) } var bodyStr string From b7787db34e2b71cdc59aef829ea1a3d69b0a1e3c Mon Sep 17 00:00:00 2001 From: Athurg Feng Date: Thu, 8 Nov 2018 18:44:00 +0800 Subject: [PATCH 005/770] Add new option to set where to open the message url --- pkg/services/alerting/notifiers/dingding.go | 37 ++++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 94961e82025..af1063a4c70 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -2,6 +2,7 @@ package notifiers import ( "fmt" + "net/url" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -15,12 +16,17 @@ const DingdingOptionsTemplate = `

DingDing settings

Url - +
MessageType
+
+ OpenInBrowser + + Open the message url in browser instead of inside of Dingding +
` func init() { @@ -41,20 +47,23 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) } msgType := model.Settings.Get("msgType").MustString(DefaultDingdingMsgType) + openInBrowser := model.Settings.Get("openInBrowser").MustBool(true) return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model), - MsgType: msgType, - Url: url, - log: log.New("alerting.notifier.dingding"), + NotifierBase: NewNotifierBase(model), + OpenInBrowser: openInBrowser, + MsgType: msgType, + Url: url, + log: log.New("alerting.notifier.dingding"), }, nil } type DingDingNotifier struct { NotifierBase - MsgType string - Url string - log log.Logger + MsgType string + OpenInBrowser bool //Set whether the message url will open outside of Dingding + Url string + log log.Logger } func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { @@ -65,6 +74,18 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name) messageUrl = "" } + + if this.OpenInBrowser { + q := url.Values{ + "pc_slide": {"false"}, + "url": {messageUrl}, + } + + // Use special link to auto open the message url outside of Dingding + // Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9 + messageUrl = "dingtalk://dingtalkclient/page/link?" + q.Encode() + } + this.log.Info("messageUrl:" + messageUrl) message := evalContext.Rule.Message From 919d00437e21944d5feea3c6ac175a2d85736784 Mon Sep 17 00:00:00 2001 From: Athurg Feng Date: Mon, 12 Nov 2018 11:18:53 +0800 Subject: [PATCH 006/770] Add pic into actionCard message --- pkg/services/alerting/notifiers/dingding.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index af1063a4c70..3514554a1db 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -101,6 +101,11 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { var bodyStr string if this.MsgType == "actionCard" { + // Embed the pic into the markdown directly because actionCard doesn't have a picUrl field + if picUrl != "" { + message = "![](" + picUrl + ")\\n\\n" + message + } + bodyStr = `{ "msgtype": "actionCard", "actionCard": { From 7db848f153f083b5efafc4c3bae177eeafb99f8b Mon Sep 17 00:00:00 2001 From: bugficks Date: Tue, 15 Jan 2019 13:29:56 +0100 Subject: [PATCH 007/770] [Feature request] MySQL SSL CA in datasource connector https://github.com/grafana/grafana/issues/8570 --- pkg/tsdb/mysql/mysql.go | 44 ++++++++++++ .../datasource/mysql/partials/config.html | 68 ++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 35b03e489a0..e713b87e265 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -6,6 +6,10 @@ import ( "reflect" "strconv" "strings" + "errors" + + "crypto/x509" + "crypto/tls" "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" @@ -32,6 +36,46 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin datasource.Url, datasource.Database, ) + + var tlsSkipVerify, tlsAuth, tlsAuthWithCACert bool + if datasource.JsonData != nil { + tlsAuth = datasource.JsonData.Get("tlsAuth").MustBool(false) + tlsAuthWithCACert = datasource.JsonData.Get("tlsAuthWithCACert").MustBool(false) + tlsSkipVerify = datasource.JsonData.Get("tlsSkipVerify").MustBool(false) + } + + if tlsAuth || tlsAuthWithCACert { + + secureJsonData := datasource.SecureJsonData.Decrypt() + tlsConfig := tls.Config { + InsecureSkipVerify: tlsSkipVerify, + } + + if tlsAuthWithCACert && len(secureJsonData["tlsCACert"]) > 0 { + + caPool := x509.NewCertPool() + if ok := caPool.AppendCertsFromPEM([]byte(secureJsonData["tlsCACert"])); !ok { + return nil, errors.New("Failed to parse TLS CA PEM certificate") + } + + tlsConfig.RootCAs = caPool + } + + if tlsAuth { + certs, err := tls.X509KeyPair([]byte(secureJsonData["tlsClientCert"]), []byte(secureJsonData["tlsClientKey"])) + if err != nil { + return nil, err + } + clientCert := make([]tls.Certificate, 0, 1) + clientCert = append(clientCert, certs) + + tlsConfig.Certificates = clientCert + } + + mysql.RegisterTLSConfig(datasource.Name, &tlsConfig) + cnnstr += "&tls=" + datasource.Name + } + logger.Debug("getEngine", "connection", cnnstr) config := tsdb.SqlQueryEndpointConfiguration{ diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html index a35633c626a..5f3ba5c1286 100644 --- a/public/app/plugins/datasource/mysql/partials/config.html +++ b/public/app/plugins/datasource/mysql/partials/config.html @@ -1,4 +1,3 @@ -

MySQL Connection

@@ -22,6 +21,72 @@
+ +
+
+ + +
+
+ +
+
+ +
+
+
TLS Auth Details
+ TLS Certs are encrypted and stored in the Grafana database. +
+
+
+
+ +
+
+ +
+ +
+ + reset +
+
+
+ +
+
+
+ +
+
+ +
+
+ + reset +
+
+ +
+
+ +
+
+ +
+
+ + reset +
+
+
+
Connection limits @@ -84,4 +149,3 @@

- From f31fe495e977cd9fe1c585e25221a423ff9a7c71 Mon Sep 17 00:00:00 2001 From: bugficks Date: Tue, 15 Jan 2019 13:54:25 +0100 Subject: [PATCH 008/770] fix go fmt --- pkg/tsdb/mysql/mysql.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index e713b87e265..82e7cac27f0 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -2,14 +2,14 @@ package mysql import ( "database/sql" + "errors" "fmt" "reflect" "strconv" "strings" - "errors" - "crypto/x509" "crypto/tls" + "crypto/x509" "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" @@ -47,7 +47,7 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin if tlsAuth || tlsAuthWithCACert { secureJsonData := datasource.SecureJsonData.Decrypt() - tlsConfig := tls.Config { + tlsConfig := tls.Config{ InsecureSkipVerify: tlsSkipVerify, } From 7df5e3cebf06b39c0007bca76c9e86254fc7bc5a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 28 Jan 2019 19:37:19 +0100 Subject: [PATCH 009/770] extract tls auth settings directive from datasource http settings directive --- public/app/features/all.ts | 1 + .../datasources/partials/http_settings.html | 52 +------------- .../partials/tls_auth_settings.html | 62 +++++++++++++++++ .../settings/TlsAuthSettingsCtrl.ts | 10 +++ .../datasource/mysql/partials/config.html | 68 +++---------------- 5 files changed, 84 insertions(+), 109 deletions(-) create mode 100644 public/app/features/datasources/partials/tls_auth_settings.html create mode 100644 public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts diff --git a/public/app/features/all.ts b/public/app/features/all.ts index 83146596ea0..d5e684e4a4e 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -12,3 +12,4 @@ import './manage-dashboards'; import './teams/CreateTeamCtrl'; import './profile/all'; import './datasources/settings/HttpSettingsCtrl'; +import './datasources/settings/TlsAuthSettingsCtrl'; diff --git a/public/app/features/datasources/partials/http_settings.html b/public/app/features/datasources/partials/http_settings.html index 521e2d3cdc6..b6f2c4fc0dd 100644 --- a/public/app/features/datasources/partials/http_settings.html +++ b/public/app/features/datasources/partials/http_settings.html @@ -101,53 +101,5 @@ -
-
-
TLS Auth Details
- TLS Certs are encrypted and stored in the Grafana database. -
-
-
-
- -
-
- -
- -
- - reset -
-
-
- -
-
-
- -
-
- -
-
- - reset -
-
- -
-
- -
-
- -
-
- - reset -
-
-
-
- + + \ No newline at end of file diff --git a/public/app/features/datasources/partials/tls_auth_settings.html b/public/app/features/datasources/partials/tls_auth_settings.html new file mode 100644 index 00000000000..c852e8ec70c --- /dev/null +++ b/public/app/features/datasources/partials/tls_auth_settings.html @@ -0,0 +1,62 @@ +
+
+
TLS Auth Details
+ TLS Certs are encrypted and stored in the Grafana database. +
+
+
+
+
+ +
+ +
+ + reset +
+
+
+ +
+
+
+
+ +
+
+ + reset +
+
+ +
+
+
+ +
+
+ + reset +
+
+
+
diff --git a/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts b/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts new file mode 100644 index 00000000000..7c21fab404c --- /dev/null +++ b/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts @@ -0,0 +1,10 @@ +import { coreModule } from 'app/core/core'; + +coreModule.directive('datasourceTlsAuthSettings', () => { + return { + scope: { + current: '=', + }, + templateUrl: 'public/app/features/datasources/partials/tls_auth_settings.html', + }; +}); diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html index 5f3ba5c1286..8221a06e1ee 100644 --- a/public/app/plugins/datasource/mysql/partials/config.html +++ b/public/app/plugins/datasource/mysql/partials/config.html @@ -24,70 +24,20 @@
- - + +
- +
-
-
-
TLS Auth Details
- TLS Certs are encrypted and stored in the Grafana database. -
-
-
-
- -
-
- -
- -
- - reset -
-
-
- -
-
-
- -
-
- -
-
- - reset -
-
- -
-
- -
-
- -
-
- - reset -
-
-
-
- + + Connection limits From f157c19e16cdc970542867ec77eb5a61fe5f11ad Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 28 Jan 2019 19:38:56 +0100 Subject: [PATCH 010/770] extract parsing of datasource tls config to method --- pkg/models/datasource_cache.go | 48 +++++++++++++++++++++------------- pkg/tsdb/mysql/mysql.go | 43 ++++-------------------------- 2 files changed, 35 insertions(+), 56 deletions(-) diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go index 66ba66e4d39..1c895514ace 100644 --- a/pkg/models/datasource_cache.go +++ b/pkg/models/datasource_cache.go @@ -46,19 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { return t.Transport, nil } - var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool - if ds.JsonData != nil { - tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false) - tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) - tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false) + tlsConfig, err := ds.GetTLSConfig() + if err != nil { + return nil, err } + tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient + transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: tlsSkipVerify, - Renegotiation: tls.RenegotiateFreelyAsClient, - }, - Proxy: http.ProxyFromEnvironment, + TLSClientConfig: tlsConfig, + Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, @@ -70,6 +67,26 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { IdleConnTimeout: 90 * time.Second, } + ptc.cache[ds.Id] = cachedTransport{ + Transport: transport, + updated: ds.Updated, + } + + return transport, nil +} + +func (ds *DataSource) GetTLSConfig() (*tls.Config, error) { + var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool + if ds.JsonData != nil { + tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false) + tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) + tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false) + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: tlsSkipVerify, + } + if tlsClientAuth || tlsAuthWithCACert { decrypted := ds.SecureJsonData.Decrypt() if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 { @@ -78,7 +95,7 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { if !ok { return nil, errors.New("Failed to parse TLS CA PEM certificate") } - transport.TLSClientConfig.RootCAs = caPool + tlsConfig.RootCAs = caPool } if tlsClientAuth { @@ -86,14 +103,9 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { if err != nil { return nil, err } - transport.TLSClientConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tls.Certificate{cert} } } - ptc.cache[ds.Id] = cachedTransport{ - Transport: transport, - updated: ds.Updated, - } - - return transport, nil + return tlsConfig, nil } diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 82e7cac27f0..d451150f1de 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -2,15 +2,11 @@ package mysql import ( "database/sql" - "errors" "fmt" "reflect" "strconv" "strings" - "crypto/tls" - "crypto/x509" - "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" "github.com/grafana/grafana/pkg/log" @@ -37,42 +33,13 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin datasource.Database, ) - var tlsSkipVerify, tlsAuth, tlsAuthWithCACert bool - if datasource.JsonData != nil { - tlsAuth = datasource.JsonData.Get("tlsAuth").MustBool(false) - tlsAuthWithCACert = datasource.JsonData.Get("tlsAuthWithCACert").MustBool(false) - tlsSkipVerify = datasource.JsonData.Get("tlsSkipVerify").MustBool(false) + tlsConfig, err := datasource.GetTLSConfig() + if err != nil { + return nil, err } - if tlsAuth || tlsAuthWithCACert { - - secureJsonData := datasource.SecureJsonData.Decrypt() - tlsConfig := tls.Config{ - InsecureSkipVerify: tlsSkipVerify, - } - - if tlsAuthWithCACert && len(secureJsonData["tlsCACert"]) > 0 { - - caPool := x509.NewCertPool() - if ok := caPool.AppendCertsFromPEM([]byte(secureJsonData["tlsCACert"])); !ok { - return nil, errors.New("Failed to parse TLS CA PEM certificate") - } - - tlsConfig.RootCAs = caPool - } - - if tlsAuth { - certs, err := tls.X509KeyPair([]byte(secureJsonData["tlsClientCert"]), []byte(secureJsonData["tlsClientKey"])) - if err != nil { - return nil, err - } - clientCert := make([]tls.Certificate, 0, 1) - clientCert = append(clientCert, certs) - - tlsConfig.Certificates = clientCert - } - - mysql.RegisterTLSConfig(datasource.Name, &tlsConfig) + if tlsConfig.RootCAs != nil || len(tlsConfig.Certificates) > 0 { + mysql.RegisterTLSConfig(datasource.Name, tlsConfig) cnnstr += "&tls=" + datasource.Name } From 43ac79685ad9fa16593c9b0ce103058292660a17 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 Jan 2019 15:45:11 +0100 Subject: [PATCH 011/770] delete auth token on signout --- pkg/api/common_test.go | 2 +- pkg/middleware/middleware_test.go | 2 +- pkg/services/auth/auth_token.go | 26 +++++++++++++++++++++++--- pkg/services/auth/auth_token_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index eb1f89e3f22..f6c6e53e91d 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -149,4 +149,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC return nil } -func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {} +func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) error { return nil } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 11740574d0b..9bb45062e00 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -602,4 +602,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC return nil } -func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {} +func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) error { return nil } diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 7e9433c2d70..d9c5e897f70 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -3,6 +3,7 @@ package auth import ( "crypto/sha256" "encoding/hex" + "errors" "net/http" "net/url" "time" @@ -31,7 +32,7 @@ var ( type UserAuthTokenService interface { InitContextWithToken(ctx *models.ReqContext, orgID int64) bool UserAuthenticatedHook(user *models.User, c *models.ReqContext) error - UserSignedOutHook(c *models.ReqContext) + UserSignedOutHook(c *models.ReqContext) error } type UserAuthTokenServiceImpl struct { @@ -111,8 +112,27 @@ func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *m return nil } -func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) { - s.writeSessionCookie(c, "", -1) +func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) error { + unhashedToken := c.GetCookie(s.Cfg.LoginCookieName) + if unhashedToken == "" { + return errors.New("cannot logout without session token") + } + + hashedToken := hashToken(unhashedToken) + + sql := `DELETE FROM user_auth_token WHERE auth_token = ?` + res, err := s.SQLStore.NewSession().Exec(sql, hashedToken) + if err != nil { + return err + } + + affected, _ := res.RowsAffected() + if affected > 0 { + s.writeSessionCookie(c, "", -1) + return nil + } + + return errors.New("failed to delete session") } func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 2f75c660d9d..0114939ea48 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -1,10 +1,13 @@ package auth import ( + "net/http" "testing" "time" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -46,6 +49,28 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldEqual, ErrAuthTokenNotFound) So(LookupToken, ShouldBeNil) }) + + Convey("signing out should delete token and cookie if present", func() { + token, err := userAuthTokenService.CreateToken(userID, "192.168.1.1:1234", "some user agent2") + So(err, ShouldBeNil) + So(token, ShouldNotBeNil) + + httpreq := &http.Request{Header: make(http.Header)} + httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.AuthToken}) + + ctx := &models.ReqContext{Context: &macaron.Context{Req: macaron.Request{Request: httpreq}}} + + err = userAuthTokenService.UserSignedOutHook(ctx) + So(err, ShouldBeNil) + + // makes sure we tell the browser to overwrite the cookie + So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "") + + // lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken) + // So(err, ShouldBeNil) + // So(lookedUp, ShouldNotBeNil) + + }) }) Convey("expires correctly", func() { From 88ca54eba96195d1fc0e0138c17d8c6991deb938 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 Jan 2019 16:22:40 +0100 Subject: [PATCH 012/770] renames signout function --- pkg/api/common_test.go | 2 +- pkg/api/login.go | 2 +- pkg/middleware/middleware_test.go | 2 +- pkg/services/auth/auth_token.go | 4 ++-- pkg/services/auth/auth_token_test.go | 11 +++-------- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index f6c6e53e91d..fe02c94e277 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -149,4 +149,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC return nil } -func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) error { return nil } +func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } diff --git a/pkg/api/login.go b/pkg/api/login.go index 50c62e0835a..49da147724e 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -136,7 +136,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { } func (hs *HTTPServer) Logout(c *m.ReqContext) { - hs.AuthTokenService.UserSignedOutHook(c) + hs.AuthTokenService.SignOutUser(c) if setting.SignoutRedirectUrl != "" { c.Redirect(setting.SignoutRedirectUrl) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 9bb45062e00..4679c449853 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -602,4 +602,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC return nil } -func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) error { return nil } +func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index d9c5e897f70..5f8f36fc373 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -32,7 +32,7 @@ var ( type UserAuthTokenService interface { InitContextWithToken(ctx *models.ReqContext, orgID int64) bool UserAuthenticatedHook(user *models.User, c *models.ReqContext) error - UserSignedOutHook(c *models.ReqContext) error + SignOutUser(c *models.ReqContext) error } type UserAuthTokenServiceImpl struct { @@ -112,7 +112,7 @@ func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *m return nil } -func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) error { +func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error { unhashedToken := c.GetCookie(s.Cfg.LoginCookieName) if unhashedToken == "" { return errors.New("cannot logout without session token") diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 0114939ea48..47afe627479 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -51,7 +51,7 @@ func TestUserAuthToken(t *testing.T) { }) Convey("signing out should delete token and cookie if present", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.1.1:1234", "some user agent2") + token, err := userAuthTokenService.CreateToken(userID, "192.168.1.1:1234", "user agent") So(err, ShouldBeNil) So(token, ShouldNotBeNil) @@ -60,16 +60,11 @@ func TestUserAuthToken(t *testing.T) { ctx := &models.ReqContext{Context: &macaron.Context{Req: macaron.Request{Request: httpreq}}} - err = userAuthTokenService.UserSignedOutHook(ctx) + err = userAuthTokenService.SignOutUser(ctx) So(err, ShouldBeNil) // makes sure we tell the browser to overwrite the cookie - So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "") - - // lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken) - // So(err, ShouldBeNil) - // So(lookedUp, ShouldNotBeNil) - + //So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "") }) }) From 0442a86400552f461f7c8a807a1f2fe609fd5e98 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 31 Jan 2019 19:22:25 +0100 Subject: [PATCH 013/770] tailing grafana logs and temporaily using an older build --- devenv/docker/blocks/loki/config.yaml | 27 +++++++++++++++++++ devenv/docker/blocks/loki/docker-compose.yaml | 6 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 devenv/docker/blocks/loki/config.yaml diff --git a/devenv/docker/blocks/loki/config.yaml b/devenv/docker/blocks/loki/config.yaml new file mode 100644 index 00000000000..9451b6ba79b --- /dev/null +++ b/devenv/docker/blocks/loki/config.yaml @@ -0,0 +1,27 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +client: + url: http://loki:3100/api/prom/push + +scrape_configs: +- job_name: system + entry_parser: raw + static_configs: + - targets: + - localhost + labels: + job: varlogs + __path__: /var/log/*log +- job_name: grafana + entry_parser: raw + static_configs: + - targets: + - localhost + labels: + job: grafana + __path__: /var/log/grafana/*log diff --git a/devenv/docker/blocks/loki/docker-compose.yaml b/devenv/docker/blocks/loki/docker-compose.yaml index d6cf21f7856..c2fee15b0bb 100644 --- a/devenv/docker/blocks/loki/docker-compose.yaml +++ b/devenv/docker/blocks/loki/docker-compose.yaml @@ -5,7 +5,7 @@ networks: services: loki: - image: grafana/loki:master + image: grafana/loki:master-3e6a75e ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml @@ -13,9 +13,11 @@ services: - loki promtail: - image: grafana/promtail:master + image: grafana/promtail:master-3e6a75e volumes: + - ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml - /var/log:/var/log + - ../data/log:/var/log/grafana command: -config.file=/etc/promtail/docker-config.yaml networks: From f9bab9585a68bb65201a9f7e5c15052601546e22 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 31 Jan 2019 19:38:49 +0100 Subject: [PATCH 014/770] wip --- public/app/core/utils/explore.test.ts | 9 ++++++++- public/app/core/utils/explore.ts | 19 ++++++++++++++++--- public/app/types/explore.ts | 7 +++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 32135eab90a..d818b2ef090 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -13,6 +13,11 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: null, queries: [], range: DEFAULT_RANGE, + ui: { + showingGraph: true, + showingTable: true, + showingLogs: true, + } }; describe('state functions', () => { @@ -69,9 +74,11 @@ describe('state functions', () => { to: 'now', }, }; + expect(serializeStateToUrlParam(state)).toBe( '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + - '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}' + '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' + + '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}' ); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 7a9f54a0cae..07c8cf1d24b 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -20,6 +20,7 @@ import { ResultType, QueryIntervals, QueryOptions, + ExploreUrlUIState, } from 'app/types/explore'; export const DEFAULT_RANGE = { @@ -27,6 +28,12 @@ export const DEFAULT_RANGE = { to: 'now', }; +export const DEFAULT_UI_STATE = { + showingTable: true, + showingGraph: true, + showingLogs: true, +}; + const MAX_HISTORY_ITEMS = 100; export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; @@ -151,6 +158,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { try { const parsed = JSON.parse(decodeURI(initial)); + // debugger if (Array.isArray(parsed)) { if (parsed.length <= 3) { throw new Error('Error parsing compact URL state for Explore.'); @@ -161,19 +169,24 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { }; const datasource = parsed[2]; const queries = parsed.slice(3); - return { datasource, queries, range }; + return { datasource, queries, range, ui: DEFAULT_UI_STATE }; } return parsed; } catch (e) { console.error(e); } } - return { datasource: null, queries: [], range: DEFAULT_RANGE }; + return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE }; } +const serializeUIState = (state: ExploreUrlUIState) => { + return Object.keys(state).map((key) => ({ [key]: state[key] })); +}; + export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { + if (compact) { - return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]); + return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries, ...serializeUIState(urlState.ui)]); } return JSON.stringify(urlState); } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 34b7ff08c99..d035b60d86a 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -231,10 +231,17 @@ export interface ExploreItemState { tableResult?: TableModel; } +export interface ExploreUrlUIState { + showingTable: boolean; + showingGraph: boolean; + showingLogs: boolean; +} + export interface ExploreUrlState { datasource: string; queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense range: RawTimeRange; + ui: ExploreUrlUIState; } export interface HistoryItem { From 91bd908e03ecdfbc691c13e66cb9512535ca78fb Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 Jan 2019 22:24:04 +0100 Subject: [PATCH 015/770] adds more tests signing out session --- pkg/services/auth/auth_token.go | 2 +- pkg/services/auth/auth_token_test.go | 35 +++++++++++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 5f8f36fc373..deb3c1a5bba 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -86,7 +86,7 @@ func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) { if setting.Env == setting.DEV { - ctx.Logger.Info("new token", "unhashed token", value) + ctx.Logger.Debug("new token", "unhashed token", value) } ctx.Resp.Header().Del("Set-Cookie") diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 47afe627479..e58fe795b4a 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -1,13 +1,15 @@ package auth import ( + "fmt" "net/http" + "net/http/httptest" "testing" "time" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" - "gopkg.in/macaron.v1" + macaron "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -51,20 +53,37 @@ func TestUserAuthToken(t *testing.T) { }) Convey("signing out should delete token and cookie if present", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.1.1:1234", "user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - httpreq := &http.Request{Header: make(http.Header)} - httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.AuthToken}) + httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.UnhashedToken}) - ctx := &models.ReqContext{Context: &macaron.Context{Req: macaron.Request{Request: httpreq}}} + ctx := &models.ReqContext{Context: &macaron.Context{ + Req: macaron.Request{Request: httpreq}, + Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()), + }, + Logger: log.New("fakelogger"), + } err = userAuthTokenService.SignOutUser(ctx) So(err, ShouldBeNil) // makes sure we tell the browser to overwrite the cookie - //So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "") + cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName) + So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader) + }) + + Convey("signing out an none existing session should return an error", func() { + httpreq := &http.Request{Header: make(http.Header)} + httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: "missing-session-cookie"}) + + ctx := &models.ReqContext{Context: &macaron.Context{ + Req: macaron.Request{Request: httpreq}, + Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()), + }, + Logger: log.New("fakelogger"), + } + + err = userAuthTokenService.SignOutUser(ctx) + So(err, ShouldNotBeNil) }) }) From dd5a8275f107dbc54160fc271c3230096ba3707b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 1 Feb 2019 01:21:23 +0100 Subject: [PATCH 016/770] must return json response from /api/login/ping Even though http error 401 was returned, the result was still a http 200 --- pkg/api/login.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/api/login.go b/pkg/api/login.go index 50c62e0835a..3f2d82a6c0f 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -78,12 +78,13 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool { return false } -func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) Response { - if c.IsSignedIn || c.IsAnonymous { - return JSON(200, "Logged in") +func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) { + if c.IsSignedIn || (c.AllowAnonymous && c.IsAnonymous) { + c.JsonOK("Logged in") + return } - return Error(401, "Unauthorized", nil) + c.JsonApiErr(401, "Unauthorized", nil) } func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { From bd830780250880f3c9920a5c1f5c467b3e5bdb2a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 1 Feb 2019 01:22:56 +0100 Subject: [PATCH 017/770] signout user if /api/login/ping returns 401 unauthorized --- public/app/core/services/backend_srv.ts | 37 +++++++++++++++++-------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 38d7f2b76cb..c73cc7661f5 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; +import config from 'app/core/config'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; export class BackendSrv { @@ -103,10 +104,17 @@ export class BackendSrv { err => { // handle unauthorized if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) { - return this.loginPing().then(() => { - options.retry = 1; - return this.request(options); - }); + return this.loginPing() + .then(() => { + options.retry = 1; + return this.request(options); + }) + .catch(err => { + if (err.status === 401) { + window.location.href = config.appSubUrl + '/logout'; + throw err; + } + }); } this.$timeout(this.requestErrorHandler.bind(this, err), 50); @@ -184,13 +192,20 @@ export class BackendSrv { // handle unauthorized for backend requests if (requestIsLocal && firstAttempt && err.status === 401) { - return this.loginPing().then(() => { - options.retry = 1; - if (canceler) { - canceler.resolve(); - } - return this.datasourceRequest(options); - }); + return this.loginPing() + .then(() => { + options.retry = 1; + if (canceler) { + canceler.resolve(); + } + return this.datasourceRequest(options); + }) + .catch(err => { + if (err.status === 401) { + window.location.href = config.appSubUrl + '/logout'; + throw err; + } + }); } // populate error obj on Internal Error From a1b3986532dbbb51145471304fccc6f254899bfe Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 1 Feb 2019 09:59:53 +0100 Subject: [PATCH 018/770] always delete session cookie even if db delete fails --- devenv/docker/blocks/loki/docker-compose.yaml | 2 ++ pkg/services/auth/auth_token.go | 14 +++----------- pkg/services/auth/auth_token_test.go | 2 +- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/devenv/docker/blocks/loki/docker-compose.yaml b/devenv/docker/blocks/loki/docker-compose.yaml index d6cf21f7856..bd4f8d3c728 100644 --- a/devenv/docker/blocks/loki/docker-compose.yaml +++ b/devenv/docker/blocks/loki/docker-compose.yaml @@ -20,3 +20,5 @@ services: -config.file=/etc/promtail/docker-config.yaml networks: - loki + depends_on: + - loki diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index deb3c1a5bba..5cb43974d34 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -121,18 +121,10 @@ func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error { hashedToken := hashToken(unhashedToken) sql := `DELETE FROM user_auth_token WHERE auth_token = ?` - res, err := s.SQLStore.NewSession().Exec(sql, hashedToken) - if err != nil { - return err - } + _, err := s.SQLStore.NewSession().Exec(sql, hashedToken) - affected, _ := res.RowsAffected() - if affected > 0 { - s.writeSessionCookie(c, "", -1) - return nil - } - - return errors.New("failed to delete session") + s.writeSessionCookie(c, "", -1) + return err } func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index e58fe795b4a..312e53a3970 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -73,7 +73,7 @@ func TestUserAuthToken(t *testing.T) { Convey("signing out an none existing session should return an error", func() { httpreq := &http.Request{Header: make(http.Header)} - httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: "missing-session-cookie"}) + httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""}) ctx := &models.ReqContext{Context: &macaron.Context{ Req: macaron.Request{Request: httpreq}, From ce2209585c1e1e885e1f02c29d004e8abde61c84 Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:32:40 +0300 Subject: [PATCH 019/770] Remove version.ts --- .../config_ctrl.ts | 2 +- .../version.test.ts | 53 ------------------- .../version.ts | 34 ------------ 3 files changed, 1 insertion(+), 88 deletions(-) delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts index 98fe5a87a56..4ee5c94fad6 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts @@ -1,6 +1,6 @@ import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource'; import config from 'app/core/config'; -import { isVersionGtOrEq } from './version'; +import { isVersionGtOrEq } from 'app/core/utils/version'; export class AzureMonitorConfigCtrl { static templateUrl = 'public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html'; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts deleted file mode 100644 index 17a6ce9bb0b..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SemVersion, isVersionGtOrEq } from './version'; - -describe('SemVersion', () => { - let version = '1.0.0-alpha.1'; - - describe('parsing', () => { - it('should parse version properly', () => { - const semver = new SemVersion(version); - expect(semver.major).toBe(1); - expect(semver.minor).toBe(0); - expect(semver.patch).toBe(0); - expect(semver.meta).toBe('alpha.1'); - }); - }); - - describe('comparing', () => { - beforeEach(() => { - version = '3.4.5'; - }); - - it('should detect greater version properly', () => { - const semver = new SemVersion(version); - const cases = [ - { value: '3.4.5', expected: true }, - { value: '3.4.4', expected: true }, - { value: '3.4.6', expected: false }, - { value: '4', expected: false }, - { value: '3.5', expected: false }, - ]; - cases.forEach(testCase => { - expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected); - }); - }); - }); - - describe('isVersionGtOrEq', () => { - it('should compare versions properly (a >= b)', () => { - const cases = [ - { values: ['3.4.5', '3.4.5'], expected: true }, - { values: ['3.4.5', '3.4.4'], expected: true }, - { values: ['3.4.5', '3.4.6'], expected: false }, - { values: ['3.4', '3.4.0'], expected: true }, - { values: ['3', '3.0.0'], expected: true }, - { values: ['3.1.1-beta1', '3.1'], expected: true }, - { values: ['3.4.5', '4'], expected: false }, - { values: ['3.4.5', '3.5'], expected: false }, - ]; - cases.forEach(testCase => { - expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected); - }); - }); - }); -}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts deleted file mode 100644 index 1131e1d2ab8..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts +++ /dev/null @@ -1,34 +0,0 @@ -import _ from 'lodash'; - -const versionPattern = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z\.]+))?/; - -export class SemVersion { - major: number; - minor: number; - patch: number; - meta: string; - - constructor(version: string) { - const match = versionPattern.exec(version); - if (match) { - this.major = Number(match[1]); - this.minor = Number(match[2] || 0); - this.patch = Number(match[3] || 0); - this.meta = match[4]; - } - } - - isGtOrEq(version: string): boolean { - const compared = new SemVersion(version); - return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch); - } - - isValid(): boolean { - return _.isNumber(this.major); - } -} - -export function isVersionGtOrEq(a: string, b: string): boolean { - const aSemver = new SemVersion(a); - return aSemver.isGtOrEq(b); -} From 6ab9355146193941c4e719e8cfa43af7f382179e Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 1 Feb 2019 12:33:15 +0100 Subject: [PATCH 020/770] Restoring explore panels state from URL --- public/app/core/utils/explore.test.ts | 23 +++++- public/app/core/utils/explore.ts | 38 +++++++--- public/app/features/explore/Explore.tsx | 10 ++- .../app/features/explore/state/actionTypes.ts | 2 + public/app/features/explore/state/actions.ts | 73 ++++++++++++------- public/app/features/explore/state/reducers.ts | 3 +- public/app/types/explore.ts | 4 +- 7 files changed, 109 insertions(+), 44 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index d818b2ef090..1c00142c3b8 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -100,7 +100,7 @@ describe('state functions', () => { }, }; expect(serializeStateToUrlParam(state, true)).toBe( - '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]' + '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true]}]' ); }); }); @@ -125,7 +125,28 @@ describe('state functions', () => { }; const serialized = serializeStateToUrlParam(state); const parsed = parseUrlState(serialized); + expect(state).toMatchObject(parsed); + }); + it('can parse the compact serialized state into the original state', () => { + const state = { + ...DEFAULT_EXPLORE_STATE, + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], + range: { + from: 'now - 5h', + to: 'now', + }, + }; + const serialized = serializeStateToUrlParam(state, true); + const parsed = parseUrlState(serialized); expect(state).toMatchObject(parsed); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 07c8cf1d24b..7128019b1fb 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -20,7 +20,6 @@ import { ResultType, QueryIntervals, QueryOptions, - ExploreUrlUIState, } from 'app/types/explore'; export const DEFAULT_RANGE = { @@ -154,11 +153,13 @@ export function buildQueryTransaction( export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; +const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr'); +const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui'); + export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { try { const parsed = JSON.parse(decodeURI(initial)); - // debugger if (Array.isArray(parsed)) { if (parsed.length <= 3) { throw new Error('Error parsing compact URL state for Explore.'); @@ -168,8 +169,24 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { to: parsed[1], }; const datasource = parsed[2]; - const queries = parsed.slice(3); - return { datasource, queries, range, ui: DEFAULT_UI_STATE }; + let queries = [], + ui; + + parsed.slice(3).forEach(segment => { + if (isMetricSegment(segment)) { + queries = [...queries, segment]; + } + + if (isUISegment(segment)) { + ui = { + showingGraph: segment.ui[0], + showingLogs: segment.ui[1], + showingTable: segment.ui[2], + }; + } + }); + + return { datasource, queries, range, ui }; } return parsed; } catch (e) { @@ -179,14 +196,15 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE }; } -const serializeUIState = (state: ExploreUrlUIState) => { - return Object.keys(state).map((key) => ({ [key]: state[key] })); -}; - export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { - if (compact) { - return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries, ...serializeUIState(urlState.ui)]); + return JSON.stringify([ + urlState.range.from, + urlState.range.to, + urlState.datasource, + ...urlState.queries, + { ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable] }, + ]); } return JSON.stringify(urlState); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 909c4e81b8b..d08243c7118 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -32,7 +32,7 @@ import { import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui'; import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { StoreState } from 'app/types'; -import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; +import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore'; import { Emitter } from 'app/core/utils/emitter'; import { ExploreToolbar } from './ExploreToolbar'; @@ -61,7 +61,7 @@ interface ExploreProps { supportsGraph: boolean | null; supportsLogs: boolean | null; supportsTable: boolean | null; - urlState: ExploreUrlState; + urlState?: ExploreUrlState; } /** @@ -107,18 +107,20 @@ export class Explore extends React.PureComponent { // Don't initialize on split, but need to initialize urlparameters when present if (!initialized) { // Load URL state and parse range - const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState; + const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState; const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); const initialQueries: DataQuery[] = ensureQueries(queries); const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; const width = this.el ? this.el.offsetWidth : 0; + this.props.initializeExplore( exploreId, initialDatasource, initialQueries, initialRange, width, - this.exploreEvents + this.exploreEvents, + ui ); } } diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index be7d5754bbe..3a0a564b651 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -8,6 +8,7 @@ import { RangeScanner, ResultType, QueryTransaction, + ExploreUIState, } from 'app/types/explore'; export enum ActionTypes { @@ -106,6 +107,7 @@ export interface InitializeExploreAction { exploreDatasources: DataSourceSelectItem[]; queries: DataQuery[]; range: RawTimeRange; + ui: ExploreUIState; }; } diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 1a11b7fcac9..02502a1d94c 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -38,6 +38,7 @@ import { ResultType, QueryOptions, QueryTransaction, + ExploreUIState, } from 'app/types/explore'; import { @@ -154,7 +155,8 @@ export function initializeExplore( queries: DataQuery[], range: RawTimeRange, containerWidth: number, - eventBridge: Emitter + eventBridge: Emitter, + ui: ExploreUIState ): ThunkResult { return async dispatch => { const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv() @@ -175,6 +177,7 @@ export function initializeExplore( exploreDatasources, queries, range, + ui, }, }); @@ -258,10 +261,7 @@ export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): Que * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, * e.g., Prometheus -> Loki queries. */ -export const loadDatasourceSuccess = ( - exploreId: ExploreId, - instance: any, -): LoadDatasourceSuccessAction => { +export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): LoadDatasourceSuccessAction => { // Capabilities const supportsGraph = instance.meta.metrics; const supportsLogs = instance.meta.logs; @@ -766,6 +766,11 @@ export function stateSave() { datasource: left.datasourceInstance.name, queries: left.modifiedQueries.map(clearQueryKeys), range: left.range, + ui: { + showingGraph: left.showingGraph, + showingLogs: left.showingLogs, + showingTable: left.showingTable, + }, }; urlStates.left = serializeStateToUrlParam(leftUrlState, true); if (split) { @@ -773,48 +778,64 @@ export function stateSave() { datasource: right.datasourceInstance.name, queries: right.modifiedQueries.map(clearQueryKeys), range: right.range, + ui: { + showingGraph: right.showingGraph, + showingLogs: right.showingLogs, + showingTable: right.showingTable, + }, }; + urlStates.right = serializeStateToUrlParam(rightUrlState, true); } + dispatch(updateLocation({ query: urlStates })); }; } /** - * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + * Creates action to collapse graph/logs/table panel. When panel is collapsed, + * queries won't be run */ -export function toggleGraph(exploreId: ExploreId): ThunkResult { +const togglePanelActionCreator = (type: ActionTypes.ToggleGraph | ActionTypes.ToggleTable | ActionTypes.ToggleLogs) => ( + exploreId: ExploreId +) => { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } }); - if (getState().explore[exploreId].showingGraph) { + let shouldRunQueries; + dispatch({ type, payload: { exploreId } }); + dispatch(stateSave()); + + switch (type) { + case ActionTypes.ToggleGraph: + shouldRunQueries = getState().explore[exploreId].showingGraph; + break; + case ActionTypes.ToggleLogs: + shouldRunQueries = getState().explore[exploreId].showingLogs; + break; + case ActionTypes.ToggleTable: + shouldRunQueries = getState().explore[exploreId].showingTable; + break; + } + + if (shouldRunQueries) { dispatch(runQueries(exploreId)); } }; -} +}; + +/** + * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + */ +export const toggleGraph = togglePanelActionCreator(ActionTypes.ToggleGraph); /** * Expand/collapse the logs result viewer. When collapsed, log queries won't be run. */ -export function toggleLogs(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } }); - if (getState().explore[exploreId].showingLogs) { - dispatch(runQueries(exploreId)); - } - }; -} +export const toggleLogs = togglePanelActionCreator(ActionTypes.ToggleLogs); /** * Expand/collapse the table result viewer. When collapsed, table queries won't be run. */ -export function toggleTable(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } }); - if (getState().explore[exploreId].showingTable) { - dispatch(runQueries(exploreId)); - } - }; -} +export const toggleTable = togglePanelActionCreator(ActionTypes.ToggleTable); /** * Resets state for explore. diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index eb67beee3b3..4ad07ddfc88 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -163,7 +163,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.InitializeExplore: { - const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload; + const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload; return { ...state, containerWidth, @@ -173,6 +173,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { initialQueries: queries, initialized: true, modifiedQueries: queries.slice(), + ...ui, }; } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index d035b60d86a..3abbc652c0d 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -231,7 +231,7 @@ export interface ExploreItemState { tableResult?: TableModel; } -export interface ExploreUrlUIState { +export interface ExploreUIState { showingTable: boolean; showingGraph: boolean; showingLogs: boolean; @@ -241,7 +241,7 @@ export interface ExploreUrlState { datasource: string; queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense range: RawTimeRange; - ui: ExploreUrlUIState; + ui: ExploreUIState; } export interface HistoryItem { From cf60ae79c31d6c0745b47e77e62e2d0605a19b73 Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:47:17 +0300 Subject: [PATCH 021/770] Move prism to app/features/explore --- .../editor => features/explore}/slate-plugins/prism/index.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename public/app/{plugins/datasource/grafana-azure-monitor-datasource/editor => features/explore}/slate-plugins/prism/index.tsx (100%) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx b/public/app/features/explore/slate-plugins/prism/index.tsx similarity index 100% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx rename to public/app/features/explore/slate-plugins/prism/index.tsx From bdd59de877f677d8831f64d438a146408097faed Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:47:33 +0300 Subject: [PATCH 022/770] Remove newline && runner plugins --- .../editor/slate-plugins/newline.ts | 35 ------------------- .../editor/slate-plugins/runner.ts | 14 -------- 2 files changed, 49 deletions(-) delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts deleted file mode 100644 index d484d93a542..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts +++ /dev/null @@ -1,35 +0,0 @@ -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/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts deleted file mode 100644 index 068bd9f0ad1..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts +++ /dev/null @@ -1,14 +0,0 @@ -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; - }, - }; -} From 9a3f4def98fcb89855ec278c924f1897d1b7357e Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:49:04 +0300 Subject: [PATCH 023/770] Use slate-plugins from app/features/explore --- .../editor/query_field.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 1c883a40c31..f93912f069e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -1,12 +1,9 @@ -import PluginPrism from './slate-plugins/prism'; -// import PluginPrism from 'slate-prism'; -// import Prism from 'prismjs'; +import PluginPrism from 'app/features/explore/slate-plugins/prism'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import ClearPlugin from 'app/features/explore/slate-plugins/clear'; -// Custom plugins (new line on Enter and run on Shift+Enter) -import NewlinePlugin from './slate-plugins/newline'; -import RunnerPlugin from './slate-plugins/runner'; +import NewlinePlugin from 'app/features/explore/slate-plugins/newline'; +import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; import Typeahead from './typeahead'; From 6d03766acecab8914d344a929708a22470838a43 Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:54:38 +0300 Subject: [PATCH 024/770] Remove extra newline --- .../grafana-azure-monitor-datasource/editor/query_field.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index f93912f069e..400126f7e55 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -1,5 +1,4 @@ import PluginPrism from 'app/features/explore/slate-plugins/prism'; - import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import ClearPlugin from 'app/features/explore/slate-plugins/clear'; import NewlinePlugin from 'app/features/explore/slate-plugins/newline'; From 2ddccb4a214a1828bde0ffb3c0d0191773d8146a Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 1 Feb 2019 12:57:09 +0100 Subject: [PATCH 025/770] Temporarily run queries independently from UI state of explore panels --- public/app/features/explore/state/actions.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 02502a1d94c..b24532c23f4 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -577,9 +577,9 @@ export function runQueries(exploreId: ExploreId) { const { datasourceInstance, modifiedQueries, - showingLogs, - showingGraph, - showingTable, + // showingLogs, + // showingGraph, + // showingTable, supportsGraph, supportsLogs, supportsTable, @@ -596,7 +596,7 @@ export function runQueries(exploreId: ExploreId) { const interval = datasourceInstance.interval; // Keep table queries first since they need to return quickly - if (showingTable && supportsTable) { + if (/*showingTable &&*/ supportsTable) { dispatch( runQueriesForType( exploreId, @@ -611,7 +611,7 @@ export function runQueries(exploreId: ExploreId) { ) ); } - if (showingGraph && supportsGraph) { + if (/*showingGraph &&*/ supportsGraph) { dispatch( runQueriesForType( exploreId, @@ -625,7 +625,7 @@ export function runQueries(exploreId: ExploreId) { ) ); } - if (showingLogs && supportsLogs) { + if (/*showingLogs &&*/ supportsLogs) { dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } dispatch(stateSave()); From 3c358e406e20f3a47b9d49fecadd53a6a2259843 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 1 Feb 2019 14:56:54 +0100 Subject: [PATCH 026/770] Make runQueries action independent from datasource loading --- public/app/features/explore/state/actions.ts | 40 ++++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index b24532c23f4..c7b47d1c3c7 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -79,7 +79,15 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance)); dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance)); - dispatch(loadDatasource(exploreId, newDataSourceInstance)); + + try { + await dispatch(loadDatasource(exploreId, newDataSourceInstance)); + } catch (error) { + console.error(error); + return; + } + + dispatch(runQueries(exploreId)); }; } @@ -197,7 +205,14 @@ export function initializeExplore( } dispatch(updateDatasourceInstance(exploreId, instance)); - dispatch(loadDatasource(exploreId, instance)); + + try { + await dispatch(loadDatasource(exploreId, instance)); + } catch (error) { + console.error(error); + return; + } + dispatch(runQueries(exploreId, true)); } else { dispatch(loadDatasourceMissing(exploreId)); } @@ -343,8 +358,8 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T // Keep ID to track selection dispatch(loadDatasourcePending(exploreId, datasourceName)); - let datasourceError = null; + try { const testResult = await instance.testDatasource(); datasourceError = testResult.status === 'success' ? null : testResult.message; @@ -354,7 +369,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T if (datasourceError) { dispatch(loadDatasourceFailure(exploreId, datasourceError)); - return; + return Promise.reject(`${datasourceName} loading failed`); } if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) { @@ -372,7 +387,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T } dispatch(loadDatasourceSuccess(exploreId, instance)); - dispatch(runQueries(exploreId)); + return Promise.resolve(); }; } @@ -572,14 +587,14 @@ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult /** * Main action to run queries and dispatches sub-actions based on which result viewers are active */ -export function runQueries(exploreId: ExploreId) { +export function runQueries(exploreId: ExploreId, ignoreUIState = false) { return (dispatch, getState) => { const { datasourceInstance, modifiedQueries, - // showingLogs, - // showingGraph, - // showingTable, + showingLogs, + showingGraph, + showingTable, supportsGraph, supportsLogs, supportsTable, @@ -596,7 +611,7 @@ export function runQueries(exploreId: ExploreId) { const interval = datasourceInstance.interval; // Keep table queries first since they need to return quickly - if (/*showingTable &&*/ supportsTable) { + if ((ignoreUIState || showingTable) && supportsTable) { dispatch( runQueriesForType( exploreId, @@ -611,7 +626,7 @@ export function runQueries(exploreId: ExploreId) { ) ); } - if (/*showingGraph &&*/ supportsGraph) { + if ((ignoreUIState || showingGraph) && supportsGraph) { dispatch( runQueriesForType( exploreId, @@ -625,9 +640,10 @@ export function runQueries(exploreId: ExploreId) { ) ); } - if (/*showingLogs &&*/ supportsLogs) { + if ((ignoreUIState || showingLogs) && supportsLogs) { dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } + dispatch(stateSave()); }; } From 1a0b21b8d1e2dc13037a908e7bbb2deba327acfb Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 1 Feb 2019 15:27:02 +0100 Subject: [PATCH 027/770] Minor post review changes --- public/app/core/utils/explore.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 7128019b1fb..faf46118718 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -157,6 +157,8 @@ const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnPr const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui'); export function parseUrlState(initial: string | undefined): ExploreUrlState { + let uiState = DEFAULT_UI_STATE; + if (initial) { try { const parsed = JSON.parse(decodeURI(initial)); @@ -169,8 +171,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { to: parsed[1], }; const datasource = parsed[2]; - let queries = [], - ui; + let queries = []; parsed.slice(3).forEach(segment => { if (isMetricSegment(segment)) { @@ -178,7 +179,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { } if (isUISegment(segment)) { - ui = { + uiState = { showingGraph: segment.ui[0], showingLogs: segment.ui[1], showingTable: segment.ui[2], @@ -186,14 +187,14 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { } }); - return { datasource, queries, range, ui }; + return { datasource, queries, range, ui: uiState }; } return parsed; } catch (e) { console.error(e); } } - return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE }; + return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: uiState }; } export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { From bd6fed54de73d195df50a7e02666a454e3120726 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 1 Feb 2019 15:45:47 +0100 Subject: [PATCH 028/770] first stuff --- .../ValueMappingsEditor.story.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx new file mode 100644 index 00000000000..31ba6454753 --- /dev/null +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { ValueMappingsEditor } from './ValueMappingsEditor'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; + +const ValueMappingsEditorStories = storiesOf('UI/ValueMappingsEditor', module); + +ValueMappingsEditorStories.addDecorator(withCenteredStory); + +ValueMappingsEditorStories.add('default', () => { + return ( + { + action('Mapping changed'); + }} + /> + ); +}); From 9ac960a80380b6b7612c443c041c0055bbd95759 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Sat, 2 Feb 2019 00:48:13 +0100 Subject: [PATCH 029/770] did not add file, removing centerered --- .../ValueMappingsEditor/ValueMappingsEditor.story.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx index 31ba6454753..d6c8cec8c1e 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx @@ -2,12 +2,9 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { ValueMappingsEditor } from './ValueMappingsEditor'; -import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; const ValueMappingsEditorStories = storiesOf('UI/ValueMappingsEditor', module); -ValueMappingsEditorStories.addDecorator(withCenteredStory); - ValueMappingsEditorStories.add('default', () => { return ( Date: Sat, 2 Feb 2019 13:35:17 +0800 Subject: [PATCH 030/770] Remove option used to control within browser --- pkg/services/alerting/notifiers/dingding.go | 38 ++++++++------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 3514554a1db..a3934903bd6 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -22,11 +22,6 @@ const DingdingOptionsTemplate = ` MessageType -
- OpenInBrowser - - Open the message url in browser instead of inside of Dingding -
` func init() { @@ -47,23 +42,20 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) } msgType := model.Settings.Get("msgType").MustString(DefaultDingdingMsgType) - openInBrowser := model.Settings.Get("openInBrowser").MustBool(true) return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model), - OpenInBrowser: openInBrowser, - MsgType: msgType, - Url: url, - log: log.New("alerting.notifier.dingding"), + NotifierBase: NewNotifierBase(model), + MsgType: msgType, + Url: url, + log: log.New("alerting.notifier.dingding"), }, nil } type DingDingNotifier struct { NotifierBase - MsgType string - OpenInBrowser bool //Set whether the message url will open outside of Dingding - Url string - log log.Logger + MsgType string + Url string + log log.Logger } func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { @@ -75,17 +67,15 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { messageUrl = "" } - if this.OpenInBrowser { - q := url.Values{ - "pc_slide": {"false"}, - "url": {messageUrl}, - } - - // Use special link to auto open the message url outside of Dingding - // Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9 - messageUrl = "dingtalk://dingtalkclient/page/link?" + q.Encode() + q := url.Values{ + "pc_slide": {"false"}, + "url": {messageUrl}, } + // Use special link to auto open the message url outside of Dingding + // Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9 + messageUrl = "dingtalk://dingtalkclient/page/link?" + q.Encode() + this.log.Info("messageUrl:" + messageUrl) message := evalContext.Rule.Message From 70b23ab73bfbf364c97da23fe9a829ae9099731c Mon Sep 17 00:00:00 2001 From: Athurg Feng Date: Sat, 2 Feb 2019 13:36:10 +0800 Subject: [PATCH 031/770] Add string quote func --- pkg/services/alerting/notifiers/dingding.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index a3934903bd6..3e3496622b7 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -3,6 +3,7 @@ package notifiers import ( "fmt" "net/url" + "strings" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -99,8 +100,8 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { bodyStr = `{ "msgtype": "actionCard", "actionCard": { - "text": "` + message + `", - "title": "` + title + `", + "text": "` + strings.Replace(message, `"`, "'", -1) + `", + "title": "` + strings.Replace(title, `"`, "'", -1) + `", "singleTitle": "More", "singleURL": "` + messageUrl + `" } From 60f700a1d217593ee1bca29d0be12328b21e0fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 Feb 2019 19:23:19 +0100 Subject: [PATCH 032/770] wip: dashboard react --- .../dashboard/containers/DashboardCtrl.ts | 6 - .../dashboard/containers/DashboardPage.tsx | 138 ++++++++++++++++++ .../features/dashboard/state/initDashboard.ts | 5 + public/app/routes/routes.ts | 9 +- 4 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 public/app/features/dashboard/containers/DashboardPage.tsx create mode 100644 public/app/features/dashboard/state/initDashboard.ts diff --git a/public/app/features/dashboard/containers/DashboardCtrl.ts b/public/app/features/dashboard/containers/DashboardCtrl.ts index 74795315504..0151f8f7331 100644 --- a/public/app/features/dashboard/containers/DashboardCtrl.ts +++ b/public/app/features/dashboard/containers/DashboardCtrl.ts @@ -31,12 +31,6 @@ export class DashboardCtrl { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet $scope.ctrl = this; - - // TODO: break out settings view to separate view & controller - this.editTab = 0; - - // funcs called from React component bindings and needs this binding - this.getPanelContainer = this.getPanelContainer.bind(this); } setupDashboard(data) { diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx new file mode 100644 index 00000000000..54eed34fc29 --- /dev/null +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -0,0 +1,138 @@ +// Libraries +import React, { Component } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; + +// Utils & Services +import locationUtil from 'app/core/utils/location_util'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { createErrorNotification } from 'app/core/copy/appNotification'; + +// Components +import { LoadingPlaceholder } from '@grafana/ui'; + +// Redux +import { updateLocation } from 'app/core/actions'; +import { notifyApp } from 'app/core/actions'; + +// Types +import { StoreState } from 'app/types'; +import { DashboardModel } from 'app/features/dashboard/state'; + +interface Props { + panelId: string; + urlUid?: string; + urlSlug?: string; + urlType?: string; + $scope: any; + $injector: any; + updateLocation: typeof updateLocation; + notifyApp: typeof notifyApp; +} + +interface State { + dashboard: DashboardModel | null; + notFound: boolean; +} + +export class DashboardPage extends Component { + state: State = { + dashboard: null, + notFound: false, + }; + + async componentDidMount() { + const { $injector, urlUid, urlType, urlSlug } = this.props; + + // handle old urls with no uid + if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { + this.redirectToNewUrl(); + return; + } + + const loaderSrv = $injector.get('dashboardLoaderSrv'); + const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + + try { + this.initDashboard(dashDTO); + } catch (err) { + this.props.notifyApp(createErrorNotification('Failed to init dashboard', err.toString())); + console.log(err); + } + } + + redirectToNewUrl() { + getBackendSrv() + .getDashboardBySlug(this.props.urlSlug) + .then(res => { + if (res) { + const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); + this.props.updateLocation(url); + } + }); + } + + initDashboard(dashDTO: any) { + const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); + + // init services + this.timeSrv.init(dashboard); + this.annotationsSrv.init(dashboard); + + // template values service needs to initialize completely before + // the rest of the dashboard can load + this.variableSrv + .init(dashboard) + // template values failes are non fatal + .catch(this.onInitFailed.bind(this, 'Templating init failed', false)) + // continue + .finally(() => { + this.dashboard = dashboard; + this.dashboard.processRepeats(); + this.dashboard.updateSubmenuVisibility(); + this.dashboard.autoFitPanels(window.innerHeight); + + this.unsavedChangesSrv.init(dashboard, this.$scope); + + // TODO refactor ViewStateSrv + this.$scope.dashboard = dashboard; + this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope); + + this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); + this.setWindowTitleAndTheme(); + + appEvents.emit('dashboard-initialized', dashboard); + }) + .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); + + this.setState({ dashboard }); + } + + render() { + const { notFound, dashboard } = this.state; + + if (notFound) { + return
Dashboard not found
; + } + + if (!dashboard) { + return ; + } + + return
title: {dashboard.title}
; + } +} + +const mapStateToProps = (state: StoreState) => ({ + urlUid: state.location.routeParams.uid, + urlSlug: state.location.routeParams.slug, + urlType: state.location.routeParams.type, + panelId: state.location.query.panelId, +}); + +const mapDispatchToProps = { + updateLocation, + notifyApp, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts new file mode 100644 index 00000000000..3b2307b3ccc --- /dev/null +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -0,0 +1,5 @@ + + +export function initDashboard(dashboard: DashboardModel, $injector: any, $scope: any) { + +} diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 0f4c09a9c77..cdd9ed89a08 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -20,6 +20,7 @@ import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards' import DataSourceSettingsPage from '../features/datasources/settings/DataSourceSettingsPage'; import OrgDetailsPage from '../features/org/OrgDetailsPage'; import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage'; +import DashboardPage from '../features/dashboard/containers/DashboardPage'; import config from 'app/core/config'; /** @ngInject */ @@ -34,10 +35,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { pageClass: 'page-dashboard', }) .when('/d/:uid/:slug', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'LoadDashboardCtrl', - reloadOnSearch: false, + template: '', pageClass: 'page-dashboard', + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, }) .when('/d/:uid', { templateUrl: 'public/app/partials/dashboard.html', From d86e773c756806bb826af50c347709bce265a65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 Feb 2019 22:43:19 +0100 Subject: [PATCH 033/770] wip: minor progress --- .../dashboard/containers/DashboardPage.tsx | 128 +++++++----------- .../app/features/dashboard/state/actions.ts | 41 +++--- .../features/dashboard/state/initDashboard.ts | 67 ++++++++- .../features/dashboard/state/reducers.test.ts | 4 +- .../app/features/dashboard/state/reducers.ts | 33 +++-- public/app/types/dashboard.ts | 17 ++- 6 files changed, 171 insertions(+), 119 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 54eed34fc29..0e3e1058660 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -3,21 +3,16 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -// Utils & Services -import locationUtil from 'app/core/utils/location_util'; -import { getBackendSrv } from 'app/core/services/backend_srv'; -import { createErrorNotification } from 'app/core/copy/appNotification'; - // Components import { LoadingPlaceholder } from '@grafana/ui'; // Redux -import { updateLocation } from 'app/core/actions'; -import { notifyApp } from 'app/core/actions'; +import { initDashboard } from '../state/initDashboard'; // Types import { StoreState } from 'app/types'; import { DashboardModel } from 'app/features/dashboard/state'; +import { DashboardLoadingState } from 'app/types/dashboard'; interface Props { panelId: string; @@ -26,8 +21,9 @@ interface Props { urlType?: string; $scope: any; $injector: any; - updateLocation: typeof updateLocation; - notifyApp: typeof notifyApp; + initDashboard: typeof initDashboard; + loadingState: DashboardLoadingState; + dashboard: DashboardModel; } interface State { @@ -42,81 +38,54 @@ export class DashboardPage extends Component { }; async componentDidMount() { - const { $injector, urlUid, urlType, urlSlug } = this.props; + this.props.initDashboard({ + injector: this.props.$injector, + scope: this.props.$scope, + urlSlug: this.props.urlSlug, + urlUid: this.props.urlUid, + urlType: this.props.urlType, + }) - // handle old urls with no uid - if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { - this.redirectToNewUrl(); - return; - } - - const loaderSrv = $injector.get('dashboardLoaderSrv'); - const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - - try { - this.initDashboard(dashDTO); - } catch (err) { - this.props.notifyApp(createErrorNotification('Failed to init dashboard', err.toString())); - console.log(err); - } + // const { $injector, urlUid, urlType, urlSlug } = this.props; + // + // // handle old urls with no uid + // if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { + // this.redirectToNewUrl(); + // return; + // } + // + // const loaderSrv = $injector.get('dashboardLoaderSrv'); + // const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + // + // try { + // this.initDashboard(dashDTO); + // } catch (err) { + // this.props.notifyApp(createErrorNotification('Failed to init dashboard', err.toString())); + // console.log(err); + // } } - redirectToNewUrl() { - getBackendSrv() - .getDashboardBySlug(this.props.urlSlug) - .then(res => { - if (res) { - const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - this.props.updateLocation(url); - } - }); - } - - initDashboard(dashDTO: any) { - const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); - - // init services - this.timeSrv.init(dashboard); - this.annotationsSrv.init(dashboard); - - // template values service needs to initialize completely before - // the rest of the dashboard can load - this.variableSrv - .init(dashboard) - // template values failes are non fatal - .catch(this.onInitFailed.bind(this, 'Templating init failed', false)) - // continue - .finally(() => { - this.dashboard = dashboard; - this.dashboard.processRepeats(); - this.dashboard.updateSubmenuVisibility(); - this.dashboard.autoFitPanels(window.innerHeight); - - this.unsavedChangesSrv.init(dashboard, this.$scope); - - // TODO refactor ViewStateSrv - this.$scope.dashboard = dashboard; - this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope); - - this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); - this.setWindowTitleAndTheme(); - - appEvents.emit('dashboard-initialized', dashboard); - }) - .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); - - this.setState({ dashboard }); - } + // redirectToNewUrl() { + // getBackendSrv() + // .getDashboardBySlug(this.props.urlSlug) + // .then(res => { + // if (res) { + // const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); + // this.props.updateLocation(url); + // } + // }); + // } + // + // initDashboard(dashDTO: any) { + // const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); + // this.setState({ dashboard }); + // } render() { - const { notFound, dashboard } = this.state; - - if (notFound) { - return
Dashboard not found
; - } + const { loadingState, dashboard } = this.props; if (!dashboard) { - return ; + return ; } return
title: {dashboard.title}
; @@ -128,11 +97,12 @@ const mapStateToProps = (state: StoreState) => ({ urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, panelId: state.location.query.panelId, + loadingState: state.dashboard.loadingState, + dashboard: state.dashboard as DashboardModel, }); const mapDispatchToProps = { - updateLocation, - notifyApp, + initDashboard }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index 4dcf0a925b7..1bb29dc3ad5 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -1,8 +1,18 @@ +// Libaries import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; + +// Services & Utils import { getBackendSrv } from 'app/core/services/backend_srv'; -import appEvents from 'app/core/app_events'; +import { actionCreatorFactory } from 'app/core/redux'; +import { ActionOf } from 'app/core/redux/actionCreatorFactory'; +import { createSuccessNotification } from 'app/core/copy/appNotification'; + +// Actions import { loadPluginDashboards } from '../../plugins/state/actions'; +import { notifyApp } from 'app/core/actions'; + +// Types import { DashboardAcl, DashboardAclDTO, @@ -10,30 +20,14 @@ import { DashboardAclUpdateDTO, NewDashboardAclItem, } from 'app/types/acl'; +import { DashboardLoadingState } from 'app/types/dashboard'; -export enum ActionTypes { - LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS', - LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS', -} +export const loadDashboardPermissions = actionCreatorFactory('LOAD_DASHBOARD_PERMISSIONS').create(); +export const setDashboardLoadingState = actionCreatorFactory('SET_DASHBOARD_LOADING_STATE').create(); -export interface LoadDashboardPermissionsAction { - type: ActionTypes.LoadDashboardPermissions; - payload: DashboardAcl[]; -} +export type Action = ActionOf; -export interface LoadStarredDashboardsAction { - type: ActionTypes.LoadStarredDashboards; - payload: DashboardAcl[]; -} - -export type Action = LoadDashboardPermissionsAction | LoadStarredDashboardsAction; - -type ThunkResult = ThunkAction; - -export const loadDashboardPermissions = (items: DashboardAclDTO[]): LoadDashboardPermissionsAction => ({ - type: ActionTypes.LoadDashboardPermissions, - payload: items, -}); +export type ThunkResult = ThunkAction; export function getDashboardPermissions(id: number): ThunkResult { return async dispatch => { @@ -124,7 +118,7 @@ export function addDashboardPermission(dashboardId: number, newItem: NewDashboar export function importDashboard(data, dashboardTitle: string): ThunkResult { return async dispatch => { await getBackendSrv().post('/api/dashboards/import', data); - appEvents.emit('alert-success', ['Dashboard Imported', dashboardTitle]); + dispatch(notifyApp(createSuccessNotification('Dashboard Imported', dashboardTitle))); dispatch(loadPluginDashboards()); }; } @@ -135,3 +129,4 @@ export function removeDashboard(uri: string): ThunkResult { dispatch(loadPluginDashboards()); }; } + diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 3b2307b3ccc..124d03eee4a 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,5 +1,70 @@ +// Libaries +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +// Services & Utils +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { createErrorNotification } from 'app/core/copy/appNotification'; -export function initDashboard(dashboard: DashboardModel, $injector: any, $scope: any) { +// Actions +import { updateLocation } from 'app/core/actions'; +import { notifyApp } from 'app/core/actions'; +import locationUtil from 'app/core/utils/location_util'; +import { setDashboardLoadingState, ThunkResult } from './actions'; +// Types +import { DashboardLoadingState } from 'app/types/dashboard'; +import { DashboardModel } from './DashboardModel'; + +export interface InitDashboardArgs { + injector: any; + scope: any; + urlUid?: string; + urlSlug?: string; + urlType?: string; +} + +export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { + return async dispatch => { + const loaderSrv = injector.get('dashboardLoaderSrv'); + + dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + + try { + // fetch dashboard from api + const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + // set initializing state + dispatch(setDashboardLoadingState(DashboardLoadingState.Initializing)); + // create model + const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); + // init services + + injector.get('timeSrv').init(dashboard); + injector.get('annotationsSrv').init(dashboard); + + // template values service needs to initialize completely before + // the rest of the dashboard can load + injector.get('variableSrv').init(dashboard) + .catch(err => { + dispatch(notifyApp(createErrorNotification('Templating init failed'))); + }) + .finally(() => { + + dashboard.processRepeats(); + dashboard.updateSubmenuVisibility(); + dashboard.autoFitPanels(window.innerHeight); + + injector.get('unsavedChangesSrv').init(dashboard, scope); + + scope.dashboard = dashboard; + injector.get('dashboardViewStateSrv').create(scope); + injector.get('keybindingSrv').setupDashboardBindings(scope, dashboard); + }) + .catch(err => { + dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + }); + } catch (err) { + dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + } + }; } diff --git a/public/app/features/dashboard/state/reducers.test.ts b/public/app/features/dashboard/state/reducers.test.ts index ced8866aad8..ea3353ce741 100644 --- a/public/app/features/dashboard/state/reducers.test.ts +++ b/public/app/features/dashboard/state/reducers.test.ts @@ -1,4 +1,4 @@ -import { Action, ActionTypes } from './actions'; +import { Action } from './actions'; import { OrgRole, PermissionLevel, DashboardState } from 'app/types'; import { initialState, dashboardReducer } from './reducers'; @@ -8,7 +8,7 @@ describe('dashboard reducer', () => { beforeEach(() => { const action: Action = { - type: ActionTypes.LoadDashboardPermissions, + type: 'LOAD_DASHBOARD_PERMISSIONS', payload: [ { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View }, { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit }, diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index 8a79a6c9f77..bd13446b090 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -1,21 +1,30 @@ -import { DashboardState } from 'app/types'; -import { Action, ActionTypes } from './actions'; +import { DashboardState, DashboardLoadingState } from 'app/types/dashboard'; +import { loadDashboardPermissions, setDashboardLoadingState } from './actions'; +import { reducerFactory } from 'app/core/redux'; import { processAclItems } from 'app/core/utils/acl'; export const initialState: DashboardState = { + loadingState: DashboardLoadingState.NotStarted, + dashboard: null, permissions: [], }; -export const dashboardReducer = (state = initialState, action: Action): DashboardState => { - switch (action.type) { - case ActionTypes.LoadDashboardPermissions: - return { - ...state, - permissions: processAclItems(action.payload), - }; - } - return state; -}; +export const dashboardReducer = reducerFactory(initialState) + .addMapper({ + filter: loadDashboardPermissions, + mapper: (state, action) => ({ + ...state, + permissions: processAclItems(action.payload), + }), + }) + .addMapper({ + filter: setDashboardLoadingState, + mapper: (state, action) => ({ + ...state, + loadingState: action.payload + }), + }) + .create() export default { dashboard: dashboardReducer, diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index d33405c985e..df9a2e53548 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,5 +1,18 @@ import { DashboardAcl } from './acl'; -export interface DashboardState { - permissions: DashboardAcl[]; +export interface Dashboard { +} + +export enum DashboardLoadingState { + NotStarted = 'Not started', + Fetching = 'Fetching', + Initializing = 'Initializing', + Error = 'Error', + Done = 'Done', +} + +export interface DashboardState { + dashboard: Dashboard | null; + loadingState: DashboardLoadingState; + permissions: DashboardAcl[] | null; } From 83937f59c008343e7e1d000a088807bccb476e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 Feb 2019 23:01:48 +0100 Subject: [PATCH 034/770] wip: dashboard in react starting to work --- .../dashboard/containers/DashboardPage.tsx | 40 +-------- .../app/features/dashboard/state/actions.ts | 3 +- .../features/dashboard/state/initDashboard.ts | 86 +++++++++++-------- .../app/features/dashboard/state/reducers.ts | 11 ++- public/app/types/dashboard.ts | 4 +- 5 files changed, 66 insertions(+), 78 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 0e3e1058660..c0d5c4d4730 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; // Components import { LoadingPlaceholder } from '@grafana/ui'; +import { DashboardGrid } from '../dashgrid/DashboardGrid'; // Redux import { initDashboard } from '../state/initDashboard'; @@ -45,42 +46,8 @@ export class DashboardPage extends Component { urlUid: this.props.urlUid, urlType: this.props.urlType, }) - - // const { $injector, urlUid, urlType, urlSlug } = this.props; - // - // // handle old urls with no uid - // if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { - // this.redirectToNewUrl(); - // return; - // } - // - // const loaderSrv = $injector.get('dashboardLoaderSrv'); - // const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - // - // try { - // this.initDashboard(dashDTO); - // } catch (err) { - // this.props.notifyApp(createErrorNotification('Failed to init dashboard', err.toString())); - // console.log(err); - // } } - // redirectToNewUrl() { - // getBackendSrv() - // .getDashboardBySlug(this.props.urlSlug) - // .then(res => { - // if (res) { - // const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - // this.props.updateLocation(url); - // } - // }); - // } - // - // initDashboard(dashDTO: any) { - // const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); - // this.setState({ dashboard }); - // } - render() { const { loadingState, dashboard } = this.props; @@ -88,7 +55,8 @@ export class DashboardPage extends Component { return ; } - return
title: {dashboard.title}
; + console.log(dashboard); + return } } @@ -98,7 +66,7 @@ const mapStateToProps = (state: StoreState) => ({ urlType: state.location.routeParams.type, panelId: state.location.query.panelId, loadingState: state.dashboard.loadingState, - dashboard: state.dashboard as DashboardModel, + dashboard: state.dashboard.model as DashboardModel, }); const mapDispatchToProps = { diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index 1bb29dc3ad5..14721cdbe96 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -20,10 +20,11 @@ import { DashboardAclUpdateDTO, NewDashboardAclItem, } from 'app/types/acl'; -import { DashboardLoadingState } from 'app/types/dashboard'; +import { DashboardLoadingState, MutableDashboard } from 'app/types/dashboard'; export const loadDashboardPermissions = actionCreatorFactory('LOAD_DASHBOARD_PERMISSIONS').create(); export const setDashboardLoadingState = actionCreatorFactory('SET_DASHBOARD_LOADING_STATE').create(); +export const setDashboardModel = actionCreatorFactory('SET_DASHBOARD_MODEL').create(); export type Action = ActionOf; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 124d03eee4a..d20f9ae1cf8 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,16 +1,11 @@ -// Libaries -import { StoreState } from 'app/types'; -import { ThunkAction } from 'redux-thunk'; - // Services & Utils -import { getBackendSrv } from 'app/core/services/backend_srv'; import { createErrorNotification } from 'app/core/copy/appNotification'; // Actions import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; import locationUtil from 'app/core/utils/location_util'; -import { setDashboardLoadingState, ThunkResult } from './actions'; +import { setDashboardLoadingState, ThunkResult, setDashboardModel } from './actions'; // Types import { DashboardLoadingState } from 'app/types/dashboard'; @@ -30,41 +25,58 @@ export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: Ini dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + let dashDTO = null; + try { // fetch dashboard from api - const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - // set initializing state - dispatch(setDashboardLoadingState(DashboardLoadingState.Initializing)); - // create model - const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); - // init services - - injector.get('timeSrv').init(dashboard); - injector.get('annotationsSrv').init(dashboard); - - // template values service needs to initialize completely before - // the rest of the dashboard can load - injector.get('variableSrv').init(dashboard) - .catch(err => { - dispatch(notifyApp(createErrorNotification('Templating init failed'))); - }) - .finally(() => { - - dashboard.processRepeats(); - dashboard.updateSubmenuVisibility(); - dashboard.autoFitPanels(window.innerHeight); - - injector.get('unsavedChangesSrv').init(dashboard, scope); - - scope.dashboard = dashboard; - injector.get('dashboardViewStateSrv').create(scope); - injector.get('keybindingSrv').setupDashboardBindings(scope, dashboard); - }) - .catch(err => { - dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); - }); + dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + console.log(err); + return; } + + // set initializing state + dispatch(setDashboardLoadingState(DashboardLoadingState.Initializing)); + + // create model + let dashboard: DashboardModel; + try { + dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); + } catch (err) { + dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + console.log(err); + return; + } + + // init services + injector.get('timeSrv').init(dashboard); + injector.get('annotationsSrv').init(dashboard); + + // template values service needs to initialize completely before + // the rest of the dashboard can load + try { + await injector.get('variableSrv').init(dashboard); + } catch (err) { + dispatch(notifyApp(createErrorNotification('Templating init failed', err.toString()))); + console.log(err); + } + + try { + dashboard.processRepeats(); + dashboard.updateSubmenuVisibility(); + dashboard.autoFitPanels(window.innerHeight); + + injector.get('unsavedChangesSrv').init(dashboard, scope); + + scope.dashboard = dashboard; + injector.get('dashboardViewStateSrv').create(scope); + injector.get('keybindingSrv').setupDashboardBindings(scope, dashboard); + } catch (err) { + dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); + console.log(err); + } + + dispatch(setDashboardModel(dashboard)); }; } diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index bd13446b090..5cfc879a1a4 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -1,11 +1,11 @@ import { DashboardState, DashboardLoadingState } from 'app/types/dashboard'; -import { loadDashboardPermissions, setDashboardLoadingState } from './actions'; +import { loadDashboardPermissions, setDashboardLoadingState, setDashboardModel } from './actions'; import { reducerFactory } from 'app/core/redux'; import { processAclItems } from 'app/core/utils/acl'; export const initialState: DashboardState = { loadingState: DashboardLoadingState.NotStarted, - dashboard: null, + model: null, permissions: [], }; @@ -24,6 +24,13 @@ export const dashboardReducer = reducerFactory(initialState) loadingState: action.payload }), }) + .addMapper({ + filter: setDashboardModel, + mapper: (state, action) => ({ + ...state, + model: action.payload + }), + }) .create() export default { diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index df9a2e53548..bdea5b04bac 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,6 +1,6 @@ import { DashboardAcl } from './acl'; -export interface Dashboard { +export interface MutableDashboard { } export enum DashboardLoadingState { @@ -12,7 +12,7 @@ export enum DashboardLoadingState { } export interface DashboardState { - dashboard: Dashboard | null; + model: MutableDashboard | null; loadingState: DashboardLoadingState; permissions: DashboardAcl[] | null; } From 8dec74689d2361febda5647d29f3ff5f46e0007c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 10:55:58 +0100 Subject: [PATCH 035/770] Dashboard settings starting to work --- public/app/core/app_events.ts | 3 +- .../dashboard/components/DashNav/DashNav.tsx | 144 ++++++++++++++++++ .../dashboard/components/DashNav/index.ts | 2 + .../DashboardSettings/DashboardSettings.tsx | 36 +++++ .../components/DashboardSettings/index.ts | 1 + .../dashboard/containers/DashboardPage.tsx | 100 ++++++++++-- .../features/dashboard/state/initDashboard.ts | 1 + .../app/features/dashboard/state/reducers.ts | 2 +- .../sass/components/_dashboard_settings.scss | 3 + 9 files changed, 276 insertions(+), 16 deletions(-) create mode 100644 public/app/features/dashboard/components/DashNav/DashNav.tsx create mode 100644 public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx diff --git a/public/app/core/app_events.ts b/public/app/core/app_events.ts index 6af7913167b..1951fd87001 100644 --- a/public/app/core/app_events.ts +++ b/public/app/core/app_events.ts @@ -1,4 +1,5 @@ import { Emitter } from './utils/emitter'; -const appEvents = new Emitter(); +export const appEvents = new Emitter(); + export default appEvents; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx new file mode 100644 index 00000000000..e1fb70e5d68 --- /dev/null +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -0,0 +1,144 @@ +// Libaries +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; + +// Utils & Services +import { appEvents } from 'app/core/app_events'; + +// State +import { updateLocation } from 'app/core/actions'; + +// Types +import { DashboardModel } from '../../state/DashboardModel'; + +export interface Props { + dashboard: DashboardModel | null; + updateLocation: typeof updateLocation; +} + +export class DashNav extends PureComponent { + onOpenSearch = () => { + appEvents.emit('show-dash-search'); + }; + + onAddPanel = () => {}; + onOpenSettings = () => { + this.props.updateLocation({ + query: { + editview: 'settings', + }, + partial: true, + }) + }; + + renderLoadingState() { + return ( + + ); + } + + render() { + let { dashboard } = this.props; + + if (!dashboard) { + return this.renderLoadingState(); + } + + const haveFolder = dashboard.meta.folderId > 0; + const { canEdit, canSave, folderTitle, showSettings } = dashboard.meta; + + return ( +
+ + +
+ {/* + + */} + +
+ {canEdit && ( + + )} + + {showSettings && ( + + )} + + { + // + // + // + // + // + // + // + // + // + // + // + //
+ // + // + // + // + // + // + } +
+
+ ); + } +} + +const mapStateToProps = () => ({ +}); + +const mapDispatchToProps = { + updateLocation +}; + +export default connect(mapStateToProps, mapDispatchToProps)(DashNav); diff --git a/public/app/features/dashboard/components/DashNav/index.ts b/public/app/features/dashboard/components/DashNav/index.ts index 854e32b24d2..cfa9003cd8a 100644 --- a/public/app/features/dashboard/components/DashNav/index.ts +++ b/public/app/features/dashboard/components/DashNav/index.ts @@ -1 +1,3 @@ export { DashNavCtrl } from './DashNavCtrl'; +import DashNav from './DashNav'; +export { DashNav }; diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx new file mode 100644 index 00000000000..8a92c0d69eb --- /dev/null +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -0,0 +1,36 @@ +// Libaries +import React, { PureComponent } from 'react'; + +// Utils & Services +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; + +// Types +import { DashboardModel } from '../../state/DashboardModel'; + +export interface Props { + dashboard: DashboardModel | null; +} + +export class DashboardSettings extends PureComponent { + element: HTMLElement; + angularCmp: AngularComponent; + + componentDidMount() { + const loader = getAngularLoader(); + + const template = ''; + const scopeProps = { dashboard: this.props.dashboard }; + + this.angularCmp = loader.load(this.element, scopeProps, template); + } + + componentWillUnmount() { + if (this.angularCmp) { + this.angularCmp.destroy(); + } + } + + render() { + return
this.element = element} />; + } +} diff --git a/public/app/features/dashboard/components/DashboardSettings/index.ts b/public/app/features/dashboard/components/DashboardSettings/index.ts index f81b8cdbc67..0a89feada33 100644 --- a/public/app/features/dashboard/components/DashboardSettings/index.ts +++ b/public/app/features/dashboard/components/DashboardSettings/index.ts @@ -1 +1,2 @@ export { SettingsCtrl } from './SettingsCtrl'; +export { DashboardSettings } from './DashboardSettings'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index c0d5c4d4730..9b088b4735f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -1,14 +1,19 @@ // Libraries -import React, { Component } from 'react'; +import $ from 'jquery'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; +import classNames from 'classnames'; // Components import { LoadingPlaceholder } from '@grafana/ui'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; +import { DashNav } from '../components/DashNav'; +import { DashboardSettings } from '../components/DashboardSettings'; // Redux import { initDashboard } from '../state/initDashboard'; +import { setDashboardModel } from '../state/actions'; // Types import { StoreState } from 'app/types'; @@ -20,22 +25,23 @@ interface Props { urlUid?: string; urlSlug?: string; urlType?: string; + editview: string; $scope: any; $injector: any; initDashboard: typeof initDashboard; + setDashboardModel: typeof setDashboardModel; loadingState: DashboardLoadingState; dashboard: DashboardModel; } interface State { - dashboard: DashboardModel | null; - notFound: boolean; + isSettingsOpening: boolean; } -export class DashboardPage extends Component { +export class DashboardPage extends PureComponent { state: State = { - dashboard: null, - notFound: false, + isSettingsOpening: false, + isSettingsOpen: false, }; async componentDidMount() { @@ -45,18 +51,82 @@ export class DashboardPage extends Component { urlSlug: this.props.urlSlug, urlUid: this.props.urlUid, urlType: this.props.urlType, - }) + }); + } + + componentDidUpdate(prevProps: Props) { + const { dashboard, editview } = this.props; + + // when dashboard has loaded subscribe to somme events + if (prevProps.dashboard === null && dashboard) { + dashboard.events.on('view-mode-changed', this.onViewModeChanged); + + // set initial fullscreen class state + this.setPanelFullscreenClass(); + } + + if (!prevProps.editview && editview) { + this.setState({ isSettingsOpening: true }); + setTimeout(() => { + this.setState({ isSettingsOpening: false}); + }, 10); + } + } + + onViewModeChanged = () => { + this.setPanelFullscreenClass(); + }; + + setPanelFullscreenClass() { + $('body').toggleClass('panel-in-fullscreen', this.props.dashboard.meta.fullscreen === true); + } + + componentWillUnmount() { + if (this.props.dashboard) { + this.props.dashboard.destroy(); + this.props.setDashboardModel(null); + } + } + + renderLoadingState() { + return ; + } + + renderDashboard() { + const { dashboard, editview } = this.props; + + const classes = classNames({ + 'dashboard-container': true, + 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled + }); + + return ( +
+ {dashboard && editview && } + +
+ +
+
+ ); } render() { - const { loadingState, dashboard } = this.props; + const { dashboard, editview } = this.props; + const { isSettingsOpening } = this.state; - if (!dashboard) { - return ; - } + const classes = classNames({ + 'dashboard-page--settings-opening': isSettingsOpening, + 'dashboard-page--settings-open': !isSettingsOpening && editview, + }); - console.log(dashboard); - return + return ( +
+ + {!dashboard && this.renderLoadingState()} + {dashboard && this.renderDashboard()} +
+ ); } } @@ -65,12 +135,14 @@ const mapStateToProps = (state: StoreState) => ({ urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, panelId: state.location.query.panelId, + editview: state.location.query.editview, loadingState: state.dashboard.loadingState, dashboard: state.dashboard.model as DashboardModel, }); const mapDispatchToProps = { - initDashboard + initDashboard, + setDashboardModel }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index d20f9ae1cf8..10d7164fbff 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -67,6 +67,7 @@ export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: Ini dashboard.updateSubmenuVisibility(); dashboard.autoFitPanels(window.innerHeight); + // init unsaved changes tracking injector.get('unsavedChangesSrv').init(dashboard, scope); scope.dashboard = dashboard; diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index 5cfc879a1a4..2f4e5df5c14 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -31,7 +31,7 @@ export const dashboardReducer = reducerFactory(initialState) model: action.payload }), }) - .create() + .create(); export default { dashboard: dashboardReducer, diff --git a/public/sass/components/_dashboard_settings.scss b/public/sass/components/_dashboard_settings.scss index 5e17e025196..38883b7c80e 100644 --- a/public/sass/components/_dashboard_settings.scss +++ b/public/sass/components/_dashboard_settings.scss @@ -16,6 +16,9 @@ opacity: 1; transition: opacity 300ms ease-in-out; } + .dashboard-container { + display: none; + } } .dashboard-settings__content { From cba2ca55319cfb7a3bb009548b8f87c839a7e4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 12:29:47 +0100 Subject: [PATCH 036/770] Url state -> dashboard model state sync starting to work --- .../dashboard/components/DashNav/DashNav.tsx | 3 + .../components/DashNav/template.html | 2 - .../dashboard/containers/DashboardPage.tsx | 124 +++++++++++++----- .../dashboard/dashgrid/DashboardGrid.tsx | 41 ++++-- .../services/DashboardViewStateSrv.ts | 30 ----- public/app/routes/GrafanaCtrl.ts | 2 + public/app/types/dashboard.ts | 4 + public/views/index-template.html | 2 +- 8 files changed, 128 insertions(+), 80 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index e1fb70e5d68..f9df483bf5f 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -13,6 +13,9 @@ import { DashboardModel } from '../../state/DashboardModel'; export interface Props { dashboard: DashboardModel | null; + editview: string; + isEditing: boolean; + isFullscreen: boolean; updateLocation: typeof updateLocation; } diff --git a/public/app/features/dashboard/components/DashNav/template.html b/public/app/features/dashboard/components/DashNav/template.html index e50a8cd0bff..7e53267cbfd 100644 --- a/public/app/features/dashboard/components/DashNav/template.html +++ b/public/app/features/dashboard/components/DashNav/template.html @@ -55,7 +55,5 @@
- - diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 9b088b4735f..281916acb14 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -5,6 +5,9 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import classNames from 'classnames'; +// Services & Utils +import { createErrorNotification } from 'app/core/copy/appNotification'; + // Components import { LoadingPlaceholder } from '@grafana/ui'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; @@ -14,34 +17,45 @@ import { DashboardSettings } from '../components/DashboardSettings'; // Redux import { initDashboard } from '../state/initDashboard'; import { setDashboardModel } from '../state/actions'; +import { updateLocation } from 'app/core/actions'; +import { notifyApp } from 'app/core/actions'; // Types import { StoreState } from 'app/types'; -import { DashboardModel } from 'app/features/dashboard/state'; +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardLoadingState } from 'app/types/dashboard'; interface Props { - panelId: string; urlUid?: string; urlSlug?: string; urlType?: string; - editview: string; + editview?: string; + urlPanelId?: string; $scope: any; $injector: any; - initDashboard: typeof initDashboard; - setDashboardModel: typeof setDashboardModel; + urlEdit: boolean; + urlFullscreen: boolean; loadingState: DashboardLoadingState; dashboard: DashboardModel; + initDashboard: typeof initDashboard; + setDashboardModel: typeof setDashboardModel; + notifyApp: typeof notifyApp; + updateLocation: typeof updateLocation; } interface State { isSettingsOpening: boolean; + isEditing: boolean; + isFullscreen: boolean; + fullscreenPanel: PanelModel | null; } export class DashboardPage extends PureComponent { state: State = { isSettingsOpening: false, - isSettingsOpen: false, + isEditing: false, + isFullscreen: false, + fullscreenPanel: null, }; async componentDidMount() { @@ -55,30 +69,66 @@ export class DashboardPage extends PureComponent { } componentDidUpdate(prevProps: Props) { - const { dashboard, editview } = this.props; + const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId } = this.props; - // when dashboard has loaded subscribe to somme events - if (prevProps.dashboard === null && dashboard) { - dashboard.events.on('view-mode-changed', this.onViewModeChanged); - - // set initial fullscreen class state - this.setPanelFullscreenClass(); + if (!dashboard) { + return; } + // handle animation states when opening dashboard settings if (!prevProps.editview && editview) { this.setState({ isSettingsOpening: true }); setTimeout(() => { - this.setState({ isSettingsOpening: false}); + this.setState({ isSettingsOpening: false }); }, 10); } + + // // when dashboard has loaded subscribe to somme events + // if (prevProps.dashboard === null) { + // // set initial fullscreen class state + // this.setPanelFullscreenClass(); + // } + + // Sync url state with model + if (urlFullscreen !== dashboard.meta.isFullscreen || urlEdit !== dashboard.meta.isEditing) { + // entering fullscreen/edit mode + if (urlPanelId) { + const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + + if (panel) { + dashboard.setViewMode(panel, urlFullscreen, urlEdit); + this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel }); + } else { + this.handleFullscreenPanelNotFound(urlPanelId); + } + } else { + // handle leaving fullscreen mode + if (this.state.fullscreenPanel) { + dashboard.setViewMode(this.state.fullscreenPanel, urlFullscreen, urlEdit); + } + this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: null }); + } + + this.setPanelFullscreenClass(urlFullscreen); + } } - onViewModeChanged = () => { - this.setPanelFullscreenClass(); - }; + handleFullscreenPanelNotFound(urlPanelId: string) { + // Panel not found + this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`)); + // Clear url state + this.props.updateLocation({ + query: { + edit: null, + fullscreen: null, + panelId: null, + }, + partial: true + }); + } - setPanelFullscreenClass() { - $('body').toggleClass('panel-in-fullscreen', this.props.dashboard.meta.fullscreen === true); + setPanelFullscreenClass(isFullscreen: boolean) { + $('body').toggleClass('panel-in-fullscreen', isFullscreen); } componentWillUnmount() { @@ -94,10 +144,11 @@ export class DashboardPage extends PureComponent { renderDashboard() { const { dashboard, editview } = this.props; + const { isEditing, isFullscreen } = this.state; const classes = classNames({ 'dashboard-container': true, - 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled + 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, }); return ( @@ -105,7 +156,7 @@ export class DashboardPage extends PureComponent { {dashboard && editview && }
- +
); @@ -113,7 +164,7 @@ export class DashboardPage extends PureComponent { render() { const { dashboard, editview } = this.props; - const { isSettingsOpening } = this.state; + const { isSettingsOpening, isEditing, isFullscreen } = this.state; const classes = classNames({ 'dashboard-page--settings-opening': isSettingsOpening, @@ -122,7 +173,7 @@ export class DashboardPage extends PureComponent { return (
- + {!dashboard && this.renderLoadingState()} {dashboard && this.renderDashboard()}
@@ -130,19 +181,26 @@ export class DashboardPage extends PureComponent { } } -const mapStateToProps = (state: StoreState) => ({ - urlUid: state.location.routeParams.uid, - urlSlug: state.location.routeParams.slug, - urlType: state.location.routeParams.type, - panelId: state.location.query.panelId, - editview: state.location.query.editview, - loadingState: state.dashboard.loadingState, - dashboard: state.dashboard.model as DashboardModel, -}); +const mapStateToProps = (state: StoreState) => { + console.log('state location', state.location.query); + return { + urlUid: state.location.routeParams.uid, + urlSlug: state.location.routeParams.slug, + urlType: state.location.routeParams.type, + editview: state.location.query.editview, + urlPanelId: state.location.query.panelId, + urlFullscreen: state.location.query.fullscreen === true, + urlEdit: state.location.query.edit === true, + loadingState: state.dashboard.loadingState, + dashboard: state.dashboard.model as DashboardModel, + }; +}; const mapDispatchToProps = { initDashboard, - setDashboardModel + setDashboardModel, + notifyApp, + updateLocation, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 658bfad3816..27f699ff3e6 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -1,11 +1,14 @@ -import React from 'react'; +// Libaries +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import ReactGridLayout, { ItemCallback } from 'react-grid-layout'; +import classNames from 'classnames'; +import sizeMe from 'react-sizeme'; + +// Types import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { DashboardPanel } from './DashboardPanel'; import { DashboardModel, PanelModel } from '../state'; -import classNames from 'classnames'; -import sizeMe from 'react-sizeme'; let lastGridWidth = 1200; let ignoreNextWidthChange = false; @@ -76,19 +79,18 @@ function GridWrapper({ const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper); -export interface DashboardGridProps { +export interface Props { dashboard: DashboardModel; + isEditing: boolean; + isFullscreen: boolean; } -export class DashboardGrid extends React.Component { +export class DashboardGrid extends PureComponent { gridToPanelMap: any; panelMap: { [id: string]: PanelModel }; - constructor(props: DashboardGridProps) { - super(props); - - // subscribe to dashboard events - const dashboard = this.props.dashboard; + componentDidMount() { + const { dashboard } = this.props; dashboard.on('panel-added', this.triggerForceUpdate); dashboard.on('panel-removed', this.triggerForceUpdate); dashboard.on('repeats-processed', this.triggerForceUpdate); @@ -97,6 +99,16 @@ export class DashboardGrid extends React.Component { dashboard.on('row-expanded', this.triggerForceUpdate); } + componentWillUnmount() { + const { dashboard } = this.props; + dashboard.off('panel-added', this.triggerForceUpdate); + dashboard.off('panel-removed', this.triggerForceUpdate); + dashboard.off('repeats-processed', this.triggerForceUpdate); + dashboard.off('view-mode-changed', this.onViewModeChanged); + dashboard.off('row-collapsed', this.triggerForceUpdate); + dashboard.off('row-expanded', this.triggerForceUpdate); + } + buildLayout() { const layout = []; this.panelMap = {}; @@ -151,7 +163,6 @@ export class DashboardGrid extends React.Component { onViewModeChanged = () => { ignoreNextWidthChange = true; - this.forceUpdate(); } updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => { @@ -197,18 +208,20 @@ export class DashboardGrid extends React.Component { } render() { + const { dashboard, isFullscreen } = this.props; + return ( {this.renderPanels()} diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts index fc38c3b241f..f5a68d6f647 100644 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.ts +++ b/public/app/features/dashboard/services/DashboardViewStateSrv.ts @@ -98,8 +98,6 @@ export class DashboardViewStateSrv { if (fromRouteUpdated !== true) { this.$location.search(this.serializeToUrl()); } - - this.syncState(); } toggleCollapsedPanelRow(panelId) { @@ -115,34 +113,6 @@ export class DashboardViewStateSrv { } } - syncState() { - if (this.state.fullscreen) { - const panel = this.dashboard.getPanelById(this.state.panelId); - - if (!panel) { - this.state.fullscreen = null; - this.state.panelId = null; - this.state.edit = null; - - this.update(this.state); - - setTimeout(() => { - appEvents.emit('alert-error', ['Error', 'Panel not found']); - }, 100); - - return; - } - - if (!panel.fullscreen) { - this.enterFullscreen(panel); - } else if (this.dashboard.meta.isEditing !== this.state.edit) { - this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit); - } - } else if (this.fullscreenPanel) { - this.leaveFullscreen(); - } - } - leaveFullscreen() { const panel = this.fullscreenPanel; diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 70bdf49e5e4..817e6452f44 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -165,6 +165,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop for (const drop of Drop.drops) { drop.destroy(); } + + appEvents.emit('hide-dash-search'); }); // handle kiosk mode diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index bdea5b04bac..713cd28efb1 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,6 +1,10 @@ import { DashboardAcl } from './acl'; export interface MutableDashboard { + meta: { + fullscreen: boolean; + isEditing: boolean; + } } export enum DashboardLoadingState { diff --git a/public/views/index-template.html b/public/views/index-template.html index a1c955d45d6..770ab74eccc 100644 --- a/public/views/index-template.html +++ b/public/views/index-template.html @@ -189,7 +189,7 @@ - +
From 2cb1733c59af3643bda1b03ca187c230120f9c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 14:53:42 +0100 Subject: [PATCH 037/770] wip: progress --- .../dashboard/components/DashNav/DashNav.tsx | 99 ++++++++++++------- .../dashboard/containers/DashboardPage.tsx | 16 ++- .../app/features/dashboard/state/actions.ts | 3 +- .../features/dashboard/state/initDashboard.ts | 60 ++++++++--- public/app/routes/routes.ts | 24 +++-- public/app/types/dashboard.ts | 2 +- 6 files changed, 138 insertions(+), 66 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index f9df483bf5f..79d4da94aca 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -16,6 +16,7 @@ export interface Props { editview: string; isEditing: boolean; isFullscreen: boolean; + $injector: any; updateLocation: typeof updateLocation; } @@ -25,13 +26,29 @@ export class DashNav extends PureComponent { }; onAddPanel = () => {}; + + onClose = () => { + this.props.updateLocation({ + query: { editview: null, panelId: null, edit: null, fullscreen: null }, + partial: true, + }); + }; + onOpenSettings = () => { this.props.updateLocation({ - query: { - editview: 'settings', - }, + query: { editview: 'settings' }, partial: true, - }) + }); + }; + + onStarDashboard = () => { + const { $injector, dashboard } = this.props; + const dashboardSrv = $injector.get('dashboardSrv'); + + dashboardSrv.starDashboard(dashboard.id, dashboard.meta.isStarred).then(newState => { + dashboard.meta.isStarred = newState; + this.forceUpdate(); + }); }; renderLoadingState() { @@ -48,15 +65,16 @@ export class DashNav extends PureComponent { ); } + render() { - let { dashboard } = this.props; + const { dashboard, isFullscreen, editview } = this.props; if (!dashboard) { return this.renderLoadingState(); } const haveFolder = dashboard.meta.folderId > 0; - const { canEdit, canSave, folderTitle, showSettings } = dashboard.meta; + const { canEdit, canStar, canSave, folderTitle, showSettings, isStarred } = dashboard.meta; return (
@@ -95,53 +113,66 @@ export class DashNav extends PureComponent { )} + {canStar && ( + + )} + { - // // // + // + // - // - // // // - // - // + // + // // // - //
- // - // - // - // - // - // + // + // + // + // + // } + {(isFullscreen || editview) && ( +
+ +
+ )}
); } } -const mapStateToProps = () => ({ -}); +const mapStateToProps = () => ({}); const mapDispatchToProps = { - updateLocation + updateLocation, }; export default connect(mapStateToProps, mapDispatchToProps)(DashNav); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 281916acb14..9f0f1cdff5f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -60,8 +60,8 @@ export class DashboardPage extends PureComponent { async componentDidMount() { this.props.initDashboard({ - injector: this.props.$injector, - scope: this.props.$scope, + $injector: this.props.$injector, + $scope: this.props.$scope, urlSlug: this.props.urlSlug, urlUid: this.props.urlUid, urlType: this.props.urlType, @@ -123,7 +123,7 @@ export class DashboardPage extends PureComponent { fullscreen: null, panelId: null, }, - partial: true + partial: true, }); } @@ -163,7 +163,7 @@ export class DashboardPage extends PureComponent { } render() { - const { dashboard, editview } = this.props; + const { dashboard, editview, $injector } = this.props; const { isSettingsOpening, isEditing, isFullscreen } = this.state; const classes = classNames({ @@ -173,7 +173,13 @@ export class DashboardPage extends PureComponent { return (
- + {!dashboard && this.renderLoadingState()} {dashboard && this.renderDashboard()}
diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index 14721cdbe96..bc57b8e5f10 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -1,5 +1,4 @@ // Libaries -import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; // Services & Utils @@ -13,6 +12,7 @@ import { loadPluginDashboards } from '../../plugins/state/actions'; import { notifyApp } from 'app/core/actions'; // Types +import { StoreState } from 'app/types'; import { DashboardAcl, DashboardAclDTO, @@ -27,7 +27,6 @@ export const setDashboardLoadingState = actionCreatorFactory('SET_DASHBOARD_MODEL').create(); export type Action = ActionOf; - export type ThunkResult = ThunkAction; export function getDashboardPermissions(id: number): ThunkResult { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 10d7164fbff..f7e23238b7c 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,5 +1,6 @@ // Services & Utils import { createErrorNotification } from 'app/core/copy/appNotification'; +import { getBackendSrv } from 'app/core/services/backend_srv'; // Actions import { updateLocation } from 'app/core/actions'; @@ -12,24 +13,53 @@ import { DashboardLoadingState } from 'app/types/dashboard'; import { DashboardModel } from './DashboardModel'; export interface InitDashboardArgs { - injector: any; - scope: any; + $injector: any; + $scope: any; urlUid?: string; urlSlug?: string; urlType?: string; } -export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { - return async dispatch => { - const loaderSrv = injector.get('dashboardLoaderSrv'); +async function redirectToNewUrl(slug: string, dispatch: any) { + const res = await getBackendSrv().getDashboardBySlug(slug); - dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + if (res) { + const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); + dispatch(updateLocation(url)); + } +} + +export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { + return async dispatch => { + // handle old urls with no uid + if (!urlUid && urlSlug) { + redirectToNewUrl(urlSlug, dispatch); + return; + } let dashDTO = null; + // set fetching state + dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + try { - // fetch dashboard from api - dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + // if no uid or slug, load home dashboard + if (!urlUid && !urlSlug) { + dashDTO = await getBackendSrv().get('/api/dashboards/home'); + + if (dashDTO.redirectUri) { + const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri); + dispatch(updateLocation({ path: newUrl })); + return; + } else { + dashDTO.meta.canSave = false; + dashDTO.meta.canShare = false; + dashDTO.meta.canStar = false; + } + } else { + const loaderSrv = $injector.get('dashboardLoaderSrv'); + dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + } } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); console.log(err); @@ -50,13 +80,13 @@ export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: Ini } // init services - injector.get('timeSrv').init(dashboard); - injector.get('annotationsSrv').init(dashboard); + $injector.get('timeSrv').init(dashboard); + $injector.get('annotationsSrv').init(dashboard); // template values service needs to initialize completely before // the rest of the dashboard can load try { - await injector.get('variableSrv').init(dashboard); + await $injector.get('variableSrv').init(dashboard); } catch (err) { dispatch(notifyApp(createErrorNotification('Templating init failed', err.toString()))); console.log(err); @@ -68,11 +98,11 @@ export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: Ini dashboard.autoFitPanels(window.innerHeight); // init unsaved changes tracking - injector.get('unsavedChangesSrv').init(dashboard, scope); + $injector.get('unsavedChangesSrv').init(dashboard, $scope); - scope.dashboard = dashboard; - injector.get('dashboardViewStateSrv').create(scope); - injector.get('keybindingSrv').setupDashboardBindings(scope, dashboard); + $scope.dashboard = dashboard; + $injector.get('dashboardViewStateSrv').create($scope); + $injector.get('keybindingSrv').setupDashboardBindings($scope, dashboard); } catch (err) { dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); console.log(err); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index cdd9ed89a08..abe347d689a 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -29,10 +29,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { $routeProvider .when('/', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'LoadDashboardCtrl', - reloadOnSearch: false, + template: '', pageClass: 'page-dashboard', + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, }) .when('/d/:uid/:slug', { template: '', @@ -43,16 +45,20 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }, }) .when('/d/:uid', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'LoadDashboardCtrl', - reloadOnSearch: false, + template: '', pageClass: 'page-dashboard', + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, }) .when('/dashboard/:type/:slug', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'LoadDashboardCtrl', - reloadOnSearch: false, + template: '', pageClass: 'page-dashboard', + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, }) .when('/d-solo/:uid/:slug', { template: '', diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 713cd28efb1..9b1e750e859 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -4,7 +4,7 @@ export interface MutableDashboard { meta: { fullscreen: boolean; isEditing: boolean; - } + }; } export enum DashboardLoadingState { From 09efa24f281c69998865efc627fa278a50187e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 15:29:14 +0100 Subject: [PATCH 038/770] Added more buttons in dashboard nav --- .../dashboard/components/DashNav/DashNav.tsx | 124 +++++++++++------- .../dashboard/containers/DashboardPage.tsx | 17 ++- .../features/dashboard/state/initDashboard.ts | 2 +- 3 files changed, 95 insertions(+), 48 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 79d4da94aca..e82fa0ba75e 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -12,7 +12,7 @@ import { updateLocation } from 'app/core/actions'; import { DashboardModel } from '../../state/DashboardModel'; export interface Props { - dashboard: DashboardModel | null; + dashboard: DashboardModel; editview: string; isEditing: boolean; isFullscreen: boolean; @@ -25,7 +25,20 @@ export class DashNav extends PureComponent { appEvents.emit('show-dash-search'); }; - onAddPanel = () => {}; + onAddPanel = () => { + const { dashboard } = this.props; + + // Return if the "Add panel" exists already + if (dashboard.panels.length > 0 && dashboard.panels[0].type === 'add-panel') { + return; + } + + dashboard.addPanel({ + type: 'add-panel', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + title: 'Panel Title', + }); + }; onClose = () => { this.props.updateLocation({ @@ -34,6 +47,16 @@ export class DashNav extends PureComponent { }); }; + onToggleTVMode = () => { + appEvents.emit('toggle-kiosk-mode'); + }; + + onSave = () => { + const { $injector } = this.props; + const dashboardSrv = $injector.get('dashboardSrv'); + dashboardSrv.saveDashboard(); + }; + onOpenSettings = () => { this.props.updateLocation({ query: { editview: 'settings' }, @@ -51,30 +74,25 @@ export class DashNav extends PureComponent { }); }; - renderLoadingState() { - return ( - - ); - } + onOpenShare = () => { + const $rootScope = this.props.$injector.get('$rootScope'); + const modalScope = $rootScope.$new(); + modalScope.tabIndex = 0; + modalScope.dashboard = this.props.dashboard; + appEvents.emit('show-modal', { + src: 'public/app/features/dashboard/components/ShareModal/template.html', + scope: modalScope, + }); + }; render() { const { dashboard, isFullscreen, editview } = this.props; - - if (!dashboard) { - return this.renderLoadingState(); - } + const { canEdit, canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; + const { snapshot } = dashboard; const haveFolder = dashboard.meta.folderId > 0; - const { canEdit, canStar, canSave, folderTitle, showSettings, isStarred } = dashboard.meta; + const snapshotUrl = snapshot && snapshot.originalUrl; return (
@@ -124,34 +142,50 @@ export class DashNav extends PureComponent { )} + {canShare && ( + + )} + + {canSave && ( + + )} + + {snapshotUrl && ( + + + + )} + +
+ +
+ { - // - // - // - // - // - // - // - // - // - // - //
- // - // - // // - // } + {(isFullscreen || editview) && (
-
+
+ +
+ + { + // + } + + {(isFullscreen || editview) && ( +
- - { - // - } - - {(isFullscreen || editview) && ( -
- -
- )} -
+ )} ); } diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 929b984a93f..8d96a2eec73 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -1,20 +1,25 @@ +// Libaries import moment from 'moment'; import _ from 'lodash'; -import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; +// Constants +import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; + +// Utils & Services import { Emitter } from 'app/core/utils/emitter'; import { contextSrv } from 'app/core/services/context_srv'; import sortByKeys from 'app/core/utils/sort_by_keys'; +// Types import { PanelModel } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; import { TimeRange } from '@grafana/ui/src'; export class DashboardModel { id: any; - uid: any; - title: any; + uid: string; + title: string; autoUpdate: any; description: any; tags: any; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 19727cd8ab0..01092617e2f 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -7,6 +7,7 @@ import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; import locationUtil from 'app/core/utils/location_util'; import { setDashboardLoadingState, ThunkResult, setDashboardModel } from './actions'; +import { removePanel } from '../utils/panel'; // Types import { DashboardLoadingState } from 'app/types/dashboard'; @@ -102,7 +103,15 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I $scope.dashboard = dashboard; $injector.get('dashboardViewStateSrv').create($scope); - $injector.get('keybindingSrv').setupDashboardBindings($scope, dashboard); + + // dashboard keybindings should not live in core, this needs a bigger refactoring + // So declaring this here so it can depend on the removePanel util function + // Long term onRemovePanel should be handled via react prop callback + const onRemovePanel = (panelId: number) => { + removePanel(dashboard, dashboard.getPanelById(panelId), true); + }; + + $injector.get('keybindingSrv').setupDashboardBindings($scope, dashboard, onRemovePanel); } catch (err) { dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); console.log(err); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 6799b209147..41810ab21c0 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -7,6 +7,7 @@ import { Emitter } from 'app/core/core'; import getFactors from 'app/core/utils/factors'; import { duplicatePanel, + removePanel, copyPanel as copyPanelUtil, editPanelJson as editPanelJsonUtil, sharePanel as sharePanelUtil, @@ -213,9 +214,7 @@ export class PanelCtrl { } removePanel() { - this.publishAppEvent('panel-remove', { - panelId: this.panel.id, - }); + removePanel(this.dashboard, this.panel, true); } editPanelJson() { From d7151e5c887082746dbd42cfcdcb4550a50cee30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 18:25:13 +0100 Subject: [PATCH 040/770] improving dash nav react comp --- .../dashboard/components/DashNav/DashNav.tsx | 47 ++++++++++++------- .../components/DashNav/DashNavButton.tsx | 22 +++++++++ .../features/dashboard/state/initDashboard.ts | 2 +- 3 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 public/app/features/dashboard/components/DashNav/DashNavButton.tsx diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 4513d15ebb7..d6ee9ae1f68 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -5,6 +5,9 @@ import { connect } from 'react-redux'; // Utils & Services import { appEvents } from 'app/core/app_events'; +// Components +import { DashNavButton } from './DashNavButton'; + // State import { updateLocation } from 'app/core/actions'; @@ -41,10 +44,17 @@ export class DashNav extends PureComponent { }; onClose = () => { - this.props.updateLocation({ - query: { editview: null, panelId: null, edit: null, fullscreen: null }, - partial: true, - }); + if (this.props.editview) { + this.props.updateLocation({ + query: { editview: null }, + partial: true, + }); + } else { + this.props.updateLocation({ + query: { panelId: null, edit: null, fullscreen: null }, + partial: true, + }); + } }; onToggleTVMode = () => { @@ -116,19 +126,12 @@ export class DashNav extends PureComponent {
{canEdit && ( - - )} - - {showSettings && ( - + )} {canStar && ( @@ -171,6 +174,16 @@ export class DashNav extends PureComponent { )} + + {showSettings && ( + + )}
diff --git a/public/app/features/dashboard/components/DashNav/DashNavButton.tsx b/public/app/features/dashboard/components/DashNav/DashNavButton.tsx new file mode 100644 index 00000000000..1a98bf961dc --- /dev/null +++ b/public/app/features/dashboard/components/DashNav/DashNavButton.tsx @@ -0,0 +1,22 @@ +// Libraries +import React, { FunctionComponent } from 'react'; + +// Components +import { Tooltip } from '@grafana/ui'; + +interface Props { + icon: string; + tooltip: string; + classSuffix: string; + onClick: () => void; +} + +export const DashNavButton: FunctionComponent = ({ icon, tooltip, classSuffix, onClick }) => { + return ( + + + + ); +}; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 01092617e2f..612ce46b422 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -33,7 +33,7 @@ async function redirectToNewUrl(slug: string, dispatch: any) { export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { return async dispatch => { // handle old urls with no uid - if (!urlUid && urlSlug) { + if (!urlUid && urlSlug && !urlType) { redirectToNewUrl(urlSlug, dispatch); return; } From 1f3fafb198fc47e143e81afaa5bf497e1059f401 Mon Sep 17 00:00:00 2001 From: Paresh Date: Sun, 3 Feb 2019 13:07:33 -0600 Subject: [PATCH 041/770] mssql: pass timerange for template variable queries --- .../app/plugins/datasource/mssql/datasource.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 23aa5504d3e..1ede9cc3d1e 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -107,13 +107,24 @@ export class MssqlDatasource { format: 'table', }; + const data = { + queries: [interpolatedQuery], + }; + + if (optionalOptions && optionalOptions.range) { + if (optionalOptions.range.from) { + data['from'] = optionalOptions.range.from.valueOf().toString(); + } + if (optionalOptions.range.to) { + data['to'] = optionalOptions.range.to.valueOf().toString(); + } + } + return this.backendSrv .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', - data: { - queries: [interpolatedQuery], - }, + data: data, }) .then(data => this.responseParser.parseMetricFindQueryResult(refId, data)); } From 0324de37d2caa63fac56746ad0dbcea53bea83a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 20:38:13 +0100 Subject: [PATCH 042/770] refactorings and cleanup --- .../dashboard/components/DashNav/DashNav.tsx | 71 ++++++++----------- .../components/DashNav/DashNavButton.tsx | 21 ++++-- .../features/dashboard/state/initDashboard.ts | 28 ++++++-- 3 files changed, 67 insertions(+), 53 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index d6ee9ae1f68..559e3e1d66f 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -135,61 +135,49 @@ export class DashNav extends PureComponent { )} {canStar && ( - + /> )} {canShare && ( - + /> )} {canSave && ( - + )} {snapshotUrl && ( - - - + /> )} {showSettings && ( - + )}
- +
{ @@ -198,13 +186,12 @@ export class DashNav extends PureComponent { {(isFullscreen || editview) && (
- + />
)} diff --git a/public/app/features/dashboard/components/DashNav/DashNavButton.tsx b/public/app/features/dashboard/components/DashNav/DashNavButton.tsx index 1a98bf961dc..505baaf1f5d 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavButton.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavButton.tsx @@ -8,15 +8,26 @@ interface Props { icon: string; tooltip: string; classSuffix: string; - onClick: () => void; + onClick?: () => void; + href?: string; } -export const DashNavButton: FunctionComponent = ({ icon, tooltip, classSuffix, onClick }) => { +export const DashNavButton: FunctionComponent = ({ icon, tooltip, classSuffix, onClick, href }) => { + if (onClick) { + return ( + + + + ); + } + return ( - - + ); }; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 612ce46b422..a39a7fce285 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,6 +1,11 @@ // Services & Utils import { createErrorNotification } from 'app/core/copy/appNotification'; import { getBackendSrv } from 'app/core/services/backend_srv'; +import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { AnnotationsSrv } from 'app/features/annotations/annotations_srv'; +import { VariableSrv } from 'app/features/templating/variable_srv'; +import { KeybindingSrv } from 'app/core/services/keybindingSrv'; // Actions import { updateLocation } from 'app/core/actions'; @@ -81,13 +86,21 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I } // init services - $injector.get('timeSrv').init(dashboard); - $injector.get('annotationsSrv').init(dashboard); + const timeSrv: TimeSrv = $injector.get('timeSrv'); + const annotationsSrv: AnnotationsSrv = $injector.get('annotationsSrv'); + const variableSrv: VariableSrv = $injector.get('variableSrv'); + const keybindingSrv: KeybindingSrv = $injector.get('keybindingSrv'); + const unsavedChangesSrv = $injector.get('unsavedChangesSrv'); + const viewStateSrv = $injector.get('dashboardViewStateSrv'); + const dashboardSrv: DashboardSrv = $injector.get('dashboardSrv'); + + timeSrv.init(dashboard); + annotationsSrv.init(dashboard); // template values service needs to initialize completely before // the rest of the dashboard can load try { - await $injector.get('variableSrv').init(dashboard); + await variableSrv.init(dashboard); } catch (err) { dispatch(notifyApp(createErrorNotification('Templating init failed'))); console.log(err); @@ -99,10 +112,10 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I dashboard.autoFitPanels(window.innerHeight); // init unsaved changes tracking - $injector.get('unsavedChangesSrv').init(dashboard, $scope); + unsavedChangesSrv.init(dashboard, $scope); $scope.dashboard = dashboard; - $injector.get('dashboardViewStateSrv').create($scope); + viewStateSrv.create($scope); // dashboard keybindings should not live in core, this needs a bigger refactoring // So declaring this here so it can depend on the removePanel util function @@ -111,12 +124,15 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I removePanel(dashboard, dashboard.getPanelById(panelId), true); }; - $injector.get('keybindingSrv').setupDashboardBindings($scope, dashboard, onRemovePanel); + keybindingSrv.setupDashboardBindings($scope, dashboard, onRemovePanel); } catch (err) { dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); console.log(err); } + // legacy srv state + dashboardSrv.setCurrent(dashboard); + // set model in redux (even though it's mutable) dispatch(setDashboardModel(dashboard)); }; } From 883f7a164b455d3f5f9b7ff23252d37e6a1a9da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 21:06:07 +0100 Subject: [PATCH 043/770] added time picker --- .../dashboard/components/DashNav/DashNav.tsx | 35 +++++++++++++++---- .../dashboard/containers/DashboardPage.tsx | 25 ++++++------- public/sass/components/_navbar.scss | 2 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 559e3e1d66f..66edb149433 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; // Utils & Services +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import { appEvents } from 'app/core/app_events'; // Components @@ -24,6 +25,24 @@ export interface Props { } export class DashNav extends PureComponent { + timePickerEl: HTMLElement; + timepickerCmp: AngularComponent; + + componentDidMount() { + const loader = getAngularLoader(); + + const template = ''; + const scopeProps = { dashboard: this.props.dashboard }; + + this.timepickerCmp = loader.load(this.timePickerEl, scopeProps, template); + } + + componentWillUnmount() { + if (this.timepickerCmp) { + this.timepickerCmp.destroy(); + } + } + onOpenSearch = () => { appEvents.emit('show-dash-search'); }; @@ -98,7 +117,7 @@ export class DashNav extends PureComponent { render() { const { dashboard, isFullscreen, editview } = this.props; - const { canEdit, canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; + const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; const haveFolder = dashboard.meta.folderId > 0; @@ -125,7 +144,7 @@ export class DashNav extends PureComponent { */}
- {canEdit && ( + {canSave && ( { )} {showSettings && ( - + )}
@@ -180,9 +203,7 @@ export class DashNav extends PureComponent { /> - { - // - } +
(this.timePickerEl = element)} /> {(isFullscreen || editview) && (
diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index d86d5aea221..be12657a829 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -200,20 +200,17 @@ export class DashboardPage extends PureComponent { } } -const mapStateToProps = (state: StoreState) => { - console.log('state location', state.location.query); - return { - urlUid: state.location.routeParams.uid, - urlSlug: state.location.routeParams.slug, - urlType: state.location.routeParams.type, - editview: state.location.query.editview, - urlPanelId: state.location.query.panelId, - urlFullscreen: state.location.query.fullscreen === true, - urlEdit: state.location.query.edit === true, - loadingState: state.dashboard.loadingState, - dashboard: state.dashboard.model as DashboardModel, - }; -}; +const mapStateToProps = (state: StoreState) => ({ + urlUid: state.location.routeParams.uid, + urlSlug: state.location.routeParams.slug, + urlType: state.location.routeParams.type, + editview: state.location.query.editview, + urlPanelId: state.location.query.panelId, + urlFullscreen: state.location.query.fullscreen === true, + urlEdit: state.location.query.edit === true, + loadingState: state.dashboard.loadingState, + dashboard: state.dashboard.model as DashboardModel, +}); const mapDispatchToProps = { initDashboard, diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index b3733b694fc..0744ed0dfc7 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -102,7 +102,7 @@ display: flex; align-items: center; justify-content: flex-end; - margin-right: $spacer; + margin-left: 10px; &--close { display: none; From 48c8ff8899b4307c2f15779fd180bb66034c2a75 Mon Sep 17 00:00:00 2001 From: Connor Patterson Date: Sun, 3 Feb 2019 16:29:35 -0500 Subject: [PATCH 044/770] Add AWS/Neptune to metricsMap and dimensionsMap --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index dfa03d2dfa9..f898a65f911 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -95,6 +95,7 @@ func init() { "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"}, + "AWS/Neptune": {"CPUUtilization", "ClusterReplicaLag", "ClusterReplicaLagMaximum", "ClusterReplicaLagMinimum", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "GremlinHttp1xx", "GremlinHttp2xx", "GremlinHttp4xx", "GremlinHttp5xx", "GremlinErrors", "GremlinRequests", "GremlinRequestsPerSec", "GremlinWebSocketSuccess", "GremlinWebSocketClientErrors", "GremlinWebSocketServerErrors", "GremlinWebSocketAvailableConnections", "Http1xx", "Http2xx", "Http4xx", "Http5xx", "Http100", "Http101", "Http200", "Http400", "Http403", "Http405", "Http413", "Http429", "Http500", "Http501", "LoaderErrors", "LoaderRequests", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "SparqlHttp1xx", "SparqlHttp2xx", "SparqlHttp4xx", "SparqlHttp5xx", "SparqlErrors", "SparqlRequests", "SparqlRequestsPerSec", "StatusErrors", "StatusRequests", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs"}, "AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"}, "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"}, @@ -149,6 +150,7 @@ func init() { "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/NATGateway": {"NatGatewayId"}, + "AWS/Neptune": {"DBClusterIdentifier", "Role", "DatabaseClass", "EngineName"}, "AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"}, From 43f8098981e0328d6d06e75cd746cf5f4b1e9912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 28 Jan 2019 17:41:33 +0100 Subject: [PATCH 045/770] Removed the on every key change event --- public/app/features/explore/QueryField.tsx | 68 ++++++++++++---------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 85315d2bdef..db6efb88f52 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -132,11 +132,11 @@ export class QueryField extends React.PureComponent { + onChange = ({ value }, invokeParentOnValueChanged?: boolean) => { const documentChanged = value.document !== this.state.value.document; const prevValue = this.state.value; @@ -144,7 +144,7 @@ export class QueryField extends React.PureComponent { if (documentChanged) { const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value); - if (textChanged) { + if (textChanged && invokeParentOnValueChanged) { this.handleChangeValue(); } } @@ -288,8 +288,37 @@ export class QueryField extends React.PureComponent { + handleEnterAndTabKey = change => { const { typeaheadIndex, suggestions } = this.state; + if (this.menuEl) { + // Dont blur input + event.preventDefault(); + if (!suggestions || suggestions.length === 0) { + return undefined; + } + + const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex); + const nextChange = this.applyTypeahead(change, suggestion); + + const insertTextOperation = nextChange.operations.find(operation => operation.type === 'insert_text'); + if (insertTextOperation) { + const suggestionText = insertTextOperation.text; + this.placeholdersBuffer.setNextPlaceholderValue(suggestionText); + if (this.placeholdersBuffer.hasPlaceholders()) { + nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus(); + } + } + + return true; + } else { + this.handleChangeValue(); + + return undefined; + } + }; + + onKeyDown = (event, change) => { + const { typeaheadIndex } = this.state; switch (event.key) { case 'Escape': { @@ -312,27 +341,7 @@ export class QueryField extends React.PureComponent operation.type === 'insert_text'); - if (insertTextOperation) { - const suggestionText = insertTextOperation.text; - this.placeholdersBuffer.setNextPlaceholderValue(suggestionText); - if (this.placeholdersBuffer.hasPlaceholders()) { - nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus(); - } - } - - return true; - } + return this.handleEnterAndTabKey(change); break; } @@ -364,12 +373,7 @@ export class QueryField extends React.PureComponent { if (this.mounted) { - this.setState({ - suggestions: [], - typeaheadIndex: 0, - typeaheadPrefix: '', - typeaheadContext: null, - }); + this.setState({ suggestions: [], typeaheadIndex: 0, typeaheadPrefix: '', typeaheadContext: null }); this.resetTimer = null; } }; @@ -396,7 +400,7 @@ export class QueryField extends React.PureComponent { // Manually triggering change const change = this.applyTypeahead(this.state.value.change(), item); - this.onChange(change); + this.onChange(change, true); }; updateMenu = () => { From acea1d7f0015d0014364d84d190f5e5b0eafae71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 1 Feb 2019 11:55:01 +0100 Subject: [PATCH 046/770] Alignment of interfaces and components --- packages/grafana-ui/src/types/plugin.ts | 18 ++++++++- public/app/features/explore/QueryField.tsx | 34 ++++++++--------- public/app/features/explore/QueryRow.tsx | 14 ++++--- .../loki/components/LokiQueryField.tsx | 38 +++++++++---------- .../prometheus/components/PromQueryField.tsx | 35 +++++++++-------- public/app/store/configureStore.ts | 4 +- 6 files changed, 77 insertions(+), 66 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index 00735827825..1be862e17f3 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -41,6 +41,12 @@ export interface DataSourceApi { pluginExports?: PluginExports; } +export interface ExploreDataSourceApi extends DataSourceApi { + modifyQuery?(query: TQuery, action: any): TQuery; + getHighlighterExpression?(query: TQuery): string; + languageProvider?: any; +} + export interface QueryEditorProps { datasource: DSType; query: TQuery; @@ -48,6 +54,16 @@ export interface QueryEditorProps void; } +export interface ExploreQueryFieldProps { + datasource: DSType; + initialQuery: TQuery; + error?: string | JSX.Element; + hint?: QueryHint; + history: any[]; + onExecuteQuery?: () => void; + onQueryChange?: (value: TQuery) => void; +} + export interface PluginExports { Datasource?: DataSourceApi; QueryCtrl?: any; @@ -55,7 +71,7 @@ export interface PluginExports { ConfigCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; - ExploreQueryField?: any; + ExploreQueryField?: ComponentClass>; ExploreStartPage?: any; // Panel plugin diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index db6efb88f52..880bedd7905 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -33,10 +33,9 @@ export interface QueryFieldProps { cleanText?: (text: string) => string; disabled?: boolean; initialQuery: string | null; - onBlur?: () => void; - onFocus?: () => void; + onExecuteQuery?: () => void; + onQueryChange?: (value: string) => void; onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput; - onValueChanged?: (value: string) => void; onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string; placeholder?: string; portalOrigin?: string; @@ -145,7 +144,7 @@ export class QueryField extends React.PureComponent { + executeOnQueryChangeAndExecuteQueries = () => { // Send text change to parent - const { onValueChanged } = this.props; - if (onValueChanged) { - onValueChanged(Plain.serialize(this.state.value)); + const { onQueryChange, onExecuteQuery } = this.props; + if (onQueryChange) { + onQueryChange(Plain.serialize(this.state.value)); + } + + if (onExecuteQuery) { + onExecuteQuery(); } }; @@ -311,7 +314,7 @@ export class QueryField extends React.PureComponent { - 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); // Disrupting placeholder entry wipes all remaining placeholders needing input this.placeholdersBuffer.clearPlaceholders(); - if (onBlur) { - onBlur(); - } + + this.executeOnQueryChangeAndExecuteQueries(); }; - handleFocus = () => { - const { onFocus } = this.props; - if (onFocus) { - onFocus(); - } - }; + handleFocus = () => {}; onClickMenu = (item: CompletionItem) => { // Manually triggering change diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index f6181161d56..7de728edb99 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -20,7 +20,7 @@ import { // Types import { StoreState } from 'app/types'; -import { RawTimeRange, DataQuery, QueryHint } from '@grafana/ui'; +import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint } from '@grafana/ui'; import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; @@ -37,7 +37,7 @@ interface QueryRowProps { changeQuery: typeof changeQuery; className?: string; exploreId: ExploreId; - datasourceInstance: any; + datasourceInstance: ExploreDataSourceApi; highlightLogsExpression: typeof highlightLogsExpression; history: HistoryItem[]; index: number; @@ -115,13 +115,15 @@ export class QueryRow extends PureComponent { {QueryField ? ( ) : ( void; - onPressEnter?: () => void; - onQueryChange?: (value: LokiQuery, override?: boolean) => void; +interface LokiQueryFieldProps extends ExploreQueryFieldProps { + history: HistoryItem[]; } interface LokiQueryFieldState { @@ -98,14 +91,14 @@ export class LokiQueryField extends React.PureComponent node.type === 'code_block', getSyntax: node => 'promql', }), ]; - this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })]; + this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })]; this.state = { logLabelOptions: [], @@ -169,21 +162,25 @@ export class LokiQueryField extends React.PureComponent { // Send text change to parent - const { initialQuery, onQueryChange } = this.props; + const { initialQuery, onQueryChange, onExecuteQuery } = this.props; if (onQueryChange) { const query = { ...initialQuery, expr: value, }; - onQueryChange(query, override); + onQueryChange(query); + + if (override && onExecuteQuery) { + onExecuteQuery(); + } } }; onClickHintFix = () => { - const { hint, onClickHintFix } = this.props; - if (onClickHintFix && hint && hint.fix) { - onClickHintFix(hint.fix.action); - } + // const { hint, onClickHintFix } = this.props; + // if (onClickHintFix && hint && hint.fix) { + // onClickHintFix(hint.fix.action); + // } }; onUpdateLanguage = () => { @@ -243,7 +240,8 @@ export class LokiQueryField extends React.PureComponent void; - onPressEnter?: () => void; - onQueryChange?: (value: PromQuery, override?: boolean) => void; +interface PromQueryFieldProps extends ExploreQueryFieldProps { + history: HistoryItem[]; } interface PromQueryFieldState { @@ -116,7 +110,7 @@ class PromQueryField extends React.PureComponent node.type === 'code_block', getSyntax: node => 'promql', @@ -174,21 +168,25 @@ class PromQueryField extends React.PureComponent { // Send text change to parent - const { initialQuery, onQueryChange } = this.props; + const { initialQuery, onQueryChange, onExecuteQuery } = this.props; if (onQueryChange) { const query: PromQuery = { ...initialQuery, expr: value, }; - onQueryChange(query, override); + onQueryChange(query); + + if (override && onExecuteQuery) { + onExecuteQuery(); + } } }; onClickHintFix = () => { - const { hint, onClickHintFix } = this.props; - if (onClickHintFix && hint && hint.fix) { - onClickHintFix(hint.fix.action); - } + // const { hint, onClickHintFix } = this.props; + // if (onClickHintFix && hint && hint.fix) { + // onClickHintFix(hint.fix.action); + // } }; onUpdateLanguage = () => { @@ -264,7 +262,8 @@ class PromQueryField extends React.PureComponent Date: Fri, 1 Feb 2019 12:54:16 +0100 Subject: [PATCH 047/770] More types and some refactoring --- packages/grafana-ui/src/types/plugin.ts | 5 +++-- public/app/features/explore/QueryField.tsx | 2 -- public/app/features/explore/QueryRow.tsx | 14 ++++++-------- public/app/features/explore/state/actionTypes.ts | 6 +++--- public/app/features/explore/state/actions.ts | 10 ++++++++-- public/app/features/explore/state/reducers.ts | 2 +- .../datasource/loki/components/LokiQueryField.tsx | 8 ++++---- .../prometheus/components/PromQueryField.tsx | 8 ++++---- 8 files changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index 1be862e17f3..e951e91a223 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -1,6 +1,6 @@ import { ComponentClass } from 'react'; import { PanelProps, PanelOptionsProps } from './panel'; -import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource'; +import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource'; export interface DataSourceApi { /** @@ -42,7 +42,7 @@ export interface DataSourceApi { } export interface ExploreDataSourceApi extends DataSourceApi { - modifyQuery?(query: TQuery, action: any): TQuery; + modifyQuery?(query: TQuery, action: QueryFixAction): TQuery; getHighlighterExpression?(query: TQuery): string; languageProvider?: any; } @@ -62,6 +62,7 @@ export interface ExploreQueryFieldProps void; onQueryChange?: (value: TQuery) => void; + onExecuteHint?: (action: QueryFixAction) => void; } export interface PluginExports { diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 880bedd7905..a0e70e8066c 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -387,8 +387,6 @@ export class QueryField extends React.PureComponent {}; diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index 7de728edb99..bbc0bf0d101 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -20,7 +20,7 @@ import { // Types import { StoreState } from 'app/types'; -import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint } from '@grafana/ui'; +import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui'; import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; @@ -78,10 +78,10 @@ export class QueryRow extends PureComponent { this.onChangeQuery(null, true); }; - onClickHintFix = action => { + onClickHintFix = (action: QueryFixAction) => { const { datasourceInstance, exploreId, index } = this.props; if (datasourceInstance && datasourceInstance.modifyQuery) { - const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + const modifier = (queries: DataQuery, action: QueryFixAction) => datasourceInstance.modifyQuery(queries, action); this.props.modifyQueries(exploreId, action, index, modifier); } }; @@ -116,14 +116,12 @@ export class QueryRow extends PureComponent { ) : ( DataQuery[]; + modifier: (queries: DataQuery[], modification: QueryFixAction) => DataQuery[]; }; } diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 1a11b7fcac9..63432e9c516 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -30,6 +30,7 @@ import { DataQuery, DataSourceSelectItem, QueryHint, + QueryFixAction, } from '@grafana/ui/src/types'; import { ExploreId, @@ -54,6 +55,7 @@ import { ScanStopAction, UpdateDatasourceInstanceAction, QueriesImported, + ModifyQueriesAction, } from './actionTypes'; type ThunkResult = ThunkAction; @@ -385,12 +387,16 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T */ export function modifyQueries( exploreId: ExploreId, - modification: any, + modification: QueryFixAction, index: number, modifier: any ): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } }); + const modifyQueryAction: ModifyQueriesAction = { + type: ActionTypes.ModifyQueries, + payload: { exploreId, modification, index, modifier }, + }; + dispatch(modifyQueryAction); if (!modification.preventSubmit) { dispatch(runQueries(exploreId)); } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index eb67beee3b3..14c8d87bbd2 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -230,7 +230,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ModifyQueries: { const { initialQueries, modifiedQueries, queryTransactions } = state; - const { modification, index, modifier } = action.payload as any; + const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 76d2facc5b6..5046c353f17 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -177,10 +177,10 @@ export class LokiQueryField extends React.PureComponent { - // const { hint, onClickHintFix } = this.props; - // if (onClickHintFix && hint && hint.fix) { - // onClickHintFix(hint.fix.action); - // } + const { hint, onExecuteHint } = this.props; + if (onExecuteHint && hint && hint.fix) { + onExecuteHint(hint.fix.action); + } }; onUpdateLanguage = () => { diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 4bdd9f17392..c86ea5c4072 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -183,10 +183,10 @@ class PromQueryField extends React.PureComponent { - // const { hint, onClickHintFix } = this.props; - // if (onClickHintFix && hint && hint.fix) { - // onClickHintFix(hint.fix.action); - // } + const { hint, onExecuteHint } = this.props; + if (onExecuteHint && hint && hint.fix) { + onExecuteHint(hint.fix.action); + } }; onUpdateLanguage = () => { From 1f5bb767186b8b0c36594771d1602bffef2af68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 07:47:10 +0100 Subject: [PATCH 048/770] Refactor of action, actionTypes and reducer --- public/app/features/explore/Explore.tsx | 17 +- public/app/features/explore/QueryRow.tsx | 23 +- public/app/features/explore/Wrapper.tsx | 14 +- .../app/features/explore/state/actionTypes.ts | 664 ++++++++++-------- public/app/features/explore/state/actions.ts | 296 +++----- .../features/explore/state/reducers.test.ts | 65 +- public/app/features/explore/state/reducers.ts | 313 +++++---- 7 files changed, 698 insertions(+), 694 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 06a6ae24cac..31ffdf4ab24 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -18,15 +18,7 @@ import TableContainer from './TableContainer'; import TimePicker, { parseTime } from './TimePicker'; // Actions -import { - changeSize, - changeTime, - initializeExplore, - modifyQueries, - scanStart, - scanStop, - setQueries, -} from './state/actions'; +import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions'; // Types import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui'; @@ -35,6 +27,7 @@ import { StoreState } from 'app/types'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { Emitter } from 'app/core/utils/emitter'; import { ExploreToolbar } from './ExploreToolbar'; +import { scanStopAction } from './state/actionTypes'; interface ExploreProps { StartPage?: any; @@ -54,7 +47,7 @@ interface ExploreProps { scanning?: boolean; scanRange?: RawTimeRange; scanStart: typeof scanStart; - scanStop: typeof scanStop; + scanStopAction: typeof scanStopAction; setQueries: typeof setQueries; split: boolean; showingStartPage?: boolean; @@ -171,7 +164,7 @@ export class Explore extends React.PureComponent { }; onStopScanning = () => { - this.props.scanStop(this.props.exploreId); + this.props.scanStopAction({ exploreId: this.props.exploreId }); }; render() { @@ -281,7 +274,7 @@ const mapDispatchToProps = { initializeExplore, modifyQueries, scanStart, - scanStop, + scanStopAction, setQueries, }; diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index bbc0bf0d101..5e2e8442e54 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -9,20 +9,14 @@ import QueryEditor from './QueryEditor'; import QueryTransactionStatus from './QueryTransactionStatus'; // Actions -import { - addQueryRow, - changeQuery, - highlightLogsExpression, - modifyQueries, - removeQueryRow, - runQueries, -} from './state/actions'; +import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/actions'; // Types import { StoreState } from 'app/types'; import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui'; import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; +import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes'; function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); @@ -38,7 +32,7 @@ interface QueryRowProps { className?: string; exploreId: ExploreId; datasourceInstance: ExploreDataSourceApi; - highlightLogsExpression: typeof highlightLogsExpression; + highlightLogsExpressionAction: typeof highlightLogsExpressionAction; history: HistoryItem[]; index: number; initialQuery: DataQuery; @@ -46,7 +40,7 @@ interface QueryRowProps { queryTransactions: QueryTransaction[]; exploreEvents: Emitter; range: RawTimeRange; - removeQueryRow: typeof removeQueryRow; + removeQueryRowAction: typeof removeQueryRowAction; runQueries: typeof runQueries; } @@ -88,14 +82,15 @@ export class QueryRow extends PureComponent { onClickRemoveButton = () => { const { exploreId, index } = this.props; - this.props.removeQueryRow(exploreId, index); + this.props.removeQueryRowAction({ exploreId, index }); }; updateLogsHighlights = _.debounce((value: DataQuery) => { const { datasourceInstance } = this.props; if (datasourceInstance.getHighlighterExpression) { + const { exploreId } = this.props; const expressions = [datasourceInstance.getHighlighterExpression(value)]; - this.props.highlightLogsExpression(this.props.exploreId, expressions); + this.props.highlightLogsExpressionAction({ exploreId, expressions }); } }, 500); @@ -168,9 +163,9 @@ function mapStateToProps(state: StoreState, { exploreId, index }) { const mapDispatchToProps = { addQueryRow, changeQuery, - highlightLogsExpression, + highlightLogsExpressionAction, modifyQueries, - removeQueryRow, + removeQueryRowAction, runQueries, }; diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index aca2e6d8cbd..f64b2704b71 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -7,16 +7,16 @@ import { StoreState } from 'app/types'; import { ExploreId, ExploreUrlState } from 'app/types/explore'; import { parseUrlState } from 'app/core/utils/explore'; -import { initializeExploreSplit, resetExplore } from './state/actions'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; import { CustomScrollbar } from '@grafana/ui'; +import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes'; interface WrapperProps { - initializeExploreSplit: typeof initializeExploreSplit; + initializeExploreSplitAction: typeof initializeExploreSplitAction; split: boolean; updateLocation: typeof updateLocation; - resetExplore: typeof resetExplore; + resetExploreAction: typeof resetExploreAction; urlStates: { [key: string]: string }; } @@ -39,12 +39,12 @@ export class Wrapper extends Component { componentDidMount() { if (this.initialSplit) { - this.props.initializeExploreSplit(); + this.props.initializeExploreSplitAction(); } } componentWillUnmount() { - this.props.resetExplore(); + this.props.resetExploreAction(); } render() { @@ -77,9 +77,9 @@ const mapStateToProps = (state: StoreState) => { }; const mapDispatchToProps = { - initializeExploreSplit, + initializeExploreSplitAction, updateLocation, - resetExplore, + resetExploreAction, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper)); diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 53954f4dc2b..05ef661a8e5 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -1,6 +1,13 @@ // Types import { Emitter } from 'app/core/core'; -import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction } from '@grafana/ui/src/types'; +import { + RawTimeRange, + TimeRange, + DataQuery, + DataSourceSelectItem, + DataSourceApi, + QueryFixAction, +} from '@grafana/ui/src/types'; import { ExploreId, ExploreItemState, @@ -9,233 +16,26 @@ import { ResultType, QueryTransaction, } from 'app/types/explore'; +import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; +/** Higher order actions + * + */ export enum ActionTypes { - AddQueryRow = 'explore/ADD_QUERY_ROW', - ChangeDatasource = 'explore/CHANGE_DATASOURCE', - ChangeQuery = 'explore/CHANGE_QUERY', - ChangeSize = 'explore/CHANGE_SIZE', - ChangeTime = 'explore/CHANGE_TIME', - ClearQueries = 'explore/CLEAR_QUERIES', - HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION', - InitializeExplore = 'explore/INITIALIZE_EXPLORE', InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT', - LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE', - LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING', - LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING', - LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS', - ModifyQueries = 'explore/MODIFY_QUERIES', - QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE', - QueryTransactionStart = 'explore/QUERY_TRANSACTION_START', - QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS', - RemoveQueryRow = 'explore/REMOVE_QUERY_ROW', - RunQueries = 'explore/RUN_QUERIES', - RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY', - ScanRange = 'explore/SCAN_RANGE', - ScanStart = 'explore/SCAN_START', - ScanStop = 'explore/SCAN_STOP', - SetQueries = 'explore/SET_QUERIES', SplitClose = 'explore/SPLIT_CLOSE', SplitOpen = 'explore/SPLIT_OPEN', - StateSave = 'explore/STATE_SAVE', - ToggleGraph = 'explore/TOGGLE_GRAPH', - ToggleLogs = 'explore/TOGGLE_LOGS', - ToggleTable = 'explore/TOGGLE_TABLE', - UpdateDatasourceInstance = 'explore/UPDATE_DATASOURCE_INSTANCE', ResetExplore = 'explore/RESET_EXPLORE', - QueriesImported = 'explore/QueriesImported', -} - -export interface AddQueryRowAction { - type: ActionTypes.AddQueryRow; - payload: { - exploreId: ExploreId; - index: number; - query: DataQuery; - }; -} - -export interface ChangeQueryAction { - type: ActionTypes.ChangeQuery; - payload: { - exploreId: ExploreId; - query: DataQuery; - index: number; - override: boolean; - }; -} - -export interface ChangeSizeAction { - type: ActionTypes.ChangeSize; - payload: { - exploreId: ExploreId; - width: number; - height: number; - }; -} - -export interface ChangeTimeAction { - type: ActionTypes.ChangeTime; - payload: { - exploreId: ExploreId; - range: TimeRange; - }; -} - -export interface ClearQueriesAction { - type: ActionTypes.ClearQueries; - payload: { - exploreId: ExploreId; - }; -} - -export interface HighlightLogsExpressionAction { - type: ActionTypes.HighlightLogsExpression; - payload: { - exploreId: ExploreId; - expressions: string[]; - }; -} - -export interface InitializeExploreAction { - type: ActionTypes.InitializeExplore; - payload: { - exploreId: ExploreId; - containerWidth: number; - eventBridge: Emitter; - exploreDatasources: DataSourceSelectItem[]; - queries: DataQuery[]; - range: RawTimeRange; - }; } export interface InitializeExploreSplitAction { type: ActionTypes.InitializeExploreSplit; -} - -export interface LoadDatasourceFailureAction { - type: ActionTypes.LoadDatasourceFailure; - payload: { - exploreId: ExploreId; - error: string; - }; -} - -export interface LoadDatasourcePendingAction { - type: ActionTypes.LoadDatasourcePending; - payload: { - exploreId: ExploreId; - requestedDatasourceName: string; - }; -} - -export interface LoadDatasourceMissingAction { - type: ActionTypes.LoadDatasourceMissing; - payload: { - exploreId: ExploreId; - }; -} - -export interface LoadDatasourceSuccessAction { - type: ActionTypes.LoadDatasourceSuccess; - payload: { - exploreId: ExploreId; - StartPage?: any; - datasourceInstance: any; - history: HistoryItem[]; - logsHighlighterExpressions?: any[]; - showingStartPage: boolean; - supportsGraph: boolean; - supportsLogs: boolean; - supportsTable: boolean; - }; -} - -export interface ModifyQueriesAction { - type: ActionTypes.ModifyQueries; - payload: { - exploreId: ExploreId; - modification: QueryFixAction; - index: number; - modifier: (queries: DataQuery[], modification: QueryFixAction) => DataQuery[]; - }; -} - -export interface QueryTransactionFailureAction { - type: ActionTypes.QueryTransactionFailure; - payload: { - exploreId: ExploreId; - queryTransactions: QueryTransaction[]; - }; -} - -export interface QueryTransactionStartAction { - type: ActionTypes.QueryTransactionStart; - payload: { - exploreId: ExploreId; - resultType: ResultType; - rowIndex: number; - transaction: QueryTransaction; - }; -} - -export interface QueryTransactionSuccessAction { - type: ActionTypes.QueryTransactionSuccess; - payload: { - exploreId: ExploreId; - history: HistoryItem[]; - queryTransactions: QueryTransaction[]; - }; -} - -export interface RemoveQueryRowAction { - type: ActionTypes.RemoveQueryRow; - payload: { - exploreId: ExploreId; - index: number; - }; -} - -export interface RunQueriesEmptyAction { - type: ActionTypes.RunQueriesEmpty; - payload: { - exploreId: ExploreId; - }; -} - -export interface ScanStartAction { - type: ActionTypes.ScanStart; - payload: { - exploreId: ExploreId; - scanner: RangeScanner; - }; -} - -export interface ScanRangeAction { - type: ActionTypes.ScanRange; - payload: { - exploreId: ExploreId; - range: RawTimeRange; - }; -} - -export interface ScanStopAction { - type: ActionTypes.ScanStop; - payload: { - exploreId: ExploreId; - }; -} - -export interface SetQueriesAction { - type: ActionTypes.SetQueries; - payload: { - exploreId: ExploreId; - queries: DataQuery[]; - }; + payload: {}; } export interface SplitCloseAction { type: ActionTypes.SplitClose; + payload: {}; } export interface SplitOpenAction { @@ -245,80 +45,384 @@ export interface SplitOpenAction { }; } -export interface StateSaveAction { - type: ActionTypes.StateSave; -} - -export interface ToggleTableAction { - type: ActionTypes.ToggleTable; - payload: { - exploreId: ExploreId; - }; -} - -export interface ToggleGraphAction { - type: ActionTypes.ToggleGraph; - payload: { - exploreId: ExploreId; - }; -} - -export interface ToggleLogsAction { - type: ActionTypes.ToggleLogs; - payload: { - exploreId: ExploreId; - }; -} - -export interface UpdateDatasourceInstanceAction { - type: ActionTypes.UpdateDatasourceInstance; - payload: { - exploreId: ExploreId; - datasourceInstance: DataSourceApi; - }; -} - export interface ResetExploreAction { type: ActionTypes.ResetExplore; payload: {}; } -export interface QueriesImported { - type: ActionTypes.QueriesImported; - payload: { - exploreId: ExploreId; - queries: DataQuery[]; - }; +/** Lower order actions + * + */ +export interface AddQueryRowPayload { + exploreId: ExploreId; + index: number; + query: DataQuery; } -export type Action = - | AddQueryRowAction - | ChangeQueryAction - | ChangeSizeAction - | ChangeTimeAction - | ClearQueriesAction - | HighlightLogsExpressionAction - | InitializeExploreAction +export interface ChangeQueryPayload { + exploreId: ExploreId; + query: DataQuery; + index: number; + override: boolean; +} + +export interface ChangeSizePayload { + exploreId: ExploreId; + width: number; + height: number; +} + +export interface ChangeTimePayload { + exploreId: ExploreId; + range: TimeRange; +} + +export interface ClearQueriesPayload { + exploreId: ExploreId; +} + +export interface HighlightLogsExpressionPayload { + exploreId: ExploreId; + expressions: string[]; +} + +export interface InitializeExplorePayload { + exploreId: ExploreId; + containerWidth: number; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; +} + +export interface LoadDatasourceFailurePayload { + exploreId: ExploreId; + error: string; +} + +export interface LoadDatasourceMissingPayload { + exploreId: ExploreId; +} + +export interface LoadDatasourcePendingPayload { + exploreId: ExploreId; + requestedDatasourceName: string; +} + +export interface LoadDatasourceSuccessPayload { + exploreId: ExploreId; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; +} + +export interface ModifyQueriesPayload { + exploreId: ExploreId; + modification: QueryFixAction; + index: number; + modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery; +} + +export interface QueryTransactionFailurePayload { + exploreId: ExploreId; + queryTransactions: QueryTransaction[]; +} + +export interface QueryTransactionStartPayload { + exploreId: ExploreId; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; +} + +export interface QueryTransactionSuccessPayload { + exploreId: ExploreId; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; +} + +export interface RemoveQueryRowPayload { + exploreId: ExploreId; + index: number; +} + +export interface RunQueriesEmptyPayload { + exploreId: ExploreId; +} + +export interface ScanStartPayload { + exploreId: ExploreId; + scanner: RangeScanner; +} + +export interface ScanRangePayload { + exploreId: ExploreId; + range: RawTimeRange; +} + +export interface ScanStopPayload { + exploreId: ExploreId; +} + +export interface SetQueriesPayload { + exploreId: ExploreId; + queries: DataQuery[]; +} + +export interface SplitOpenPayload { + itemState: ExploreItemState; +} + +export interface ToggleTablePayload { + exploreId: ExploreId; +} + +export interface ToggleGraphPayload { + exploreId: ExploreId; +} + +export interface ToggleLogsPayload { + exploreId: ExploreId; +} + +export interface UpdateDatasourceInstancePayload { + exploreId: ExploreId; + datasourceInstance: DataSourceApi; +} + +export interface QueriesImportedPayload { + exploreId: ExploreId; + queries: DataQuery[]; +} + +/** + * Adds a query row after the row with the given index. + */ +export const addQueryRowAction = actionCreatorFactory('explore/ADD_QUERY_ROW').create(); + +/** + * Loads a new datasource identified by the given name. + */ +export const changeDatasourceAction = noPayloadActionCreatorFactory('explore/CHANGE_DATASOURCE').create(); + +/** + * Query change handler for the query row with the given index. + * If `override` is reset the query modifications and run the queries. Use this to set queries via a link. + */ +export const changeQueryAction = actionCreatorFactory('explore/CHANGE_QUERY').create(); + +/** + * Keep track of the Explore container size, in particular the width. + * The width will be used to calculate graph intervals (number of datapoints). + */ +export const changeSizeAction = actionCreatorFactory('explore/CHANGE_SIZE').create(); + +/** + * Change the time range of Explore. Usually called from the Timepicker or a graph interaction. + */ +export const changeTimeAction = actionCreatorFactory('explore/CHANGE_TIME').create(); + +/** + * Clear all queries and results. + */ +export const clearQueriesAction = actionCreatorFactory('explore/CLEAR_QUERIES').create(); + +/** + * Highlight expressions in the log results + */ +export const highlightLogsExpressionAction = actionCreatorFactory( + 'explore/HIGHLIGHT_LOGS_EXPRESSION' +).create(); + +/** + * Initialize Explore state with state from the URL and the React component. + * Call this only on components for with the Explore state has not been initialized. + */ +export const initializeExploreAction = actionCreatorFactory( + 'explore/INITIALIZE_EXPLORE' +).create(); + +/** + * Initialize the wrapper split state + */ +export const initializeExploreSplitAction = noPayloadActionCreatorFactory('explore/INITIALIZE_EXPLORE_SPLIT').create(); + +/** + * Display an error that happened during the selection of a datasource + */ +export const loadDatasourceFailureAction = actionCreatorFactory( + 'explore/LOAD_DATASOURCE_FAILURE' +).create(); + +/** + * Display an error when no datasources have been configured + */ +export const loadDatasourceMissingAction = actionCreatorFactory( + 'explore/LOAD_DATASOURCE_MISSING' +).create(); + +/** + * Start the async process of loading a datasource to display a loading indicator + */ +export const loadDatasourcePendingAction = actionCreatorFactory( + 'explore/LOAD_DATASOURCE_PENDING' +).create(); + +/** + * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to + * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, + * e.g., Prometheus -> Loki queries. + */ +export const loadDatasourceSuccessAction = actionCreatorFactory( + 'explore/LOAD_DATASOURCE_SUCCESS' +).create(); + +/** + * Action to modify a query given a datasource-specific modifier action. + * @param exploreId Explore area + * @param modification Action object with a type, e.g., ADD_FILTER + * @param index Optional query row index. If omitted, the modification is applied to all query rows. + * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`. + */ +export const modifyQueriesAction = actionCreatorFactory('explore/MODIFY_QUERIES').create(); + +/** + * Mark a query transaction as failed with an error extracted from the query response. + * The transaction will be marked as `done`. + */ +export const queryTransactionFailureAction = actionCreatorFactory( + 'explore/QUERY_TRANSACTION_FAILURE' +).create(); + +/** + * Start a query transaction for the given result type. + * @param exploreId Explore area + * @param transaction Query options and `done` status. + * @param resultType Associate the transaction with a result viewer, e.g., Graph + * @param rowIndex Index is used to associate latency for this transaction with a query row + */ +export const queryTransactionStartAction = actionCreatorFactory( + 'explore/QUERY_TRANSACTION_START' +).create(); + +/** + * Complete a query transaction, mark the transaction as `done` and store query state in URL. + * If the transaction was started by a scanner, it keeps on scanning for more results. + * Side-effect: the query is stored in localStorage. + * @param exploreId Explore area + * @param transactionId ID + * @param result Response from `datasourceInstance.query()` + * @param latency Duration between request and response + * @param queries Queries from all query rows + * @param datasourceId Origin datasource instance, used to discard results if current datasource is different + */ +export const queryTransactionSuccessAction = actionCreatorFactory( + 'explore/QUERY_TRANSACTION_SUCCESS' +).create(); + +/** + * Remove query row of the given index, as well as associated query results. + */ +export const removeQueryRowAction = actionCreatorFactory('explore/REMOVE_QUERY_ROW').create(); +export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').create(); +export const runQueriesEmptyAction = actionCreatorFactory('explore/RUN_QUERIES_EMPTY').create(); + +/** + * Start a scan for more results using the given scanner. + * @param exploreId Explore area + * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range + */ +export const scanStartAction = actionCreatorFactory('explore/SCAN_START').create(); +export const scanRangeAction = actionCreatorFactory('explore/SCAN_RANGE').create(); + +/** + * Stop any scanning for more results. + */ +export const scanStopAction = actionCreatorFactory('explore/SCAN_STOP').create(); + +/** + * Reset queries to the given queries. Any modifications will be discarded. + * Use this action for clicks on query examples. Triggers a query run. + */ +export const setQueriesAction = actionCreatorFactory('explore/SET_QUERIES').create(); + +/** + * Close the split view and save URL state. + */ +export const splitCloseAction = noPayloadActionCreatorFactory('explore/SPLIT_CLOSE').create(); + +/** + * Open the split view and copy the left state to be the right state. + * The right state is automatically initialized. + * The copy keeps all query modifications but wipes the query results. + */ +export const splitOpenAction = actionCreatorFactory('explore/SPLIT_OPEN').create(); +export const stateSaveAction = noPayloadActionCreatorFactory('explore/STATE_SAVE').create(); + +/** + * Expand/collapse the table result viewer. When collapsed, table queries won't be run. + */ +export const toggleTableAction = actionCreatorFactory('explore/TOGGLE_TABLE').create(); + +/** + * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + */ +export const toggleGraphAction = actionCreatorFactory('explore/TOGGLE_GRAPH').create(); + +/** + * Expand/collapse the logs result viewer. When collapsed, log queries won't be run. + */ +export const toggleLogsAction = actionCreatorFactory('explore/TOGGLE_LOGS').create(); + +/** + * Updates datasource instance before datasouce loading has started + */ +export const updateDatasourceInstanceAction = actionCreatorFactory( + 'explore/UPDATE_DATASOURCE_INSTANCE' +).create(); + +/** + * Resets state for explore. + */ +export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create(); +export const queriesImportedAction = actionCreatorFactory('explore/QueriesImported').create(); + +export type HigherOrderAction = | InitializeExploreSplitAction - | LoadDatasourceFailureAction - | LoadDatasourceMissingAction - | LoadDatasourcePendingAction - | LoadDatasourceSuccessAction - | ModifyQueriesAction - | QueryTransactionFailureAction - | QueryTransactionStartAction - | QueryTransactionSuccessAction - | RemoveQueryRowAction - | RunQueriesEmptyAction - | ScanRangeAction - | ScanStartAction - | ScanStopAction - | SetQueriesAction | SplitCloseAction | SplitOpenAction - | ToggleGraphAction - | ToggleLogsAction - | ToggleTableAction - | UpdateDatasourceInstanceAction | ResetExploreAction - | QueriesImported; + | ActionOf; + +export type Action = + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 63432e9c516..f32575edda5 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -32,40 +32,49 @@ import { QueryHint, QueryFixAction, } from '@grafana/ui/src/types'; +import { ExploreId, ExploreUrlState, RangeScanner, ResultType, QueryOptions } from 'app/types/explore'; import { - ExploreId, - ExploreUrlState, - RangeScanner, - ResultType, - QueryOptions, - QueryTransaction, -} from 'app/types/explore'; - -import { - Action as ThunkableAction, - ActionTypes, - AddQueryRowAction, - ChangeSizeAction, - HighlightLogsExpressionAction, - LoadDatasourceFailureAction, - LoadDatasourceMissingAction, - LoadDatasourcePendingAction, - LoadDatasourceSuccessAction, - QueryTransactionStartAction, - ScanStopAction, - UpdateDatasourceInstanceAction, - QueriesImported, - ModifyQueriesAction, + Action, + updateDatasourceInstanceAction, + changeQueryAction, + changeSizeAction, + ChangeSizePayload, + changeTimeAction, + scanStopAction, + clearQueriesAction, + initializeExploreAction, + loadDatasourceMissingAction, + loadDatasourceFailureAction, + loadDatasourcePendingAction, + queriesImportedAction, + LoadDatasourceSuccessPayload, + loadDatasourceSuccessAction, + modifyQueriesAction, + queryTransactionFailureAction, + queryTransactionStartAction, + queryTransactionSuccessAction, + scanRangeAction, + runQueriesEmptyAction, + scanStartAction, + setQueriesAction, + splitCloseAction, + splitOpenAction, + toggleGraphAction, + toggleLogsAction, + toggleTableAction, + addQueryRowAction, + AddQueryRowPayload, } from './actionTypes'; +import { ActionOf } from 'app/core/redux/actionCreatorFactory'; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; -/** - * Adds a query row after the row with the given index. - */ -export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { +// /** +// * Adds a query row after the row with the given index. +// */ +export function addQueryRow(exploreId: ExploreId, index: number): ActionOf { const query = generateEmptyQuery(index + 1); - return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } }; + return addQueryRowAction({ exploreId, index, query }); } /** @@ -79,7 +88,7 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance)); - dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance)); + dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance })); dispatch(loadDatasource(exploreId, newDataSourceInstance)); }; } @@ -100,7 +109,7 @@ export function changeQuery( query = { ...generateEmptyQuery(index) }; } - dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } }); + dispatch(changeQueryAction({ exploreId, query, index, override })); if (override) { dispatch(runQueries(exploreId)); } @@ -114,8 +123,8 @@ export function changeQuery( export function changeSize( exploreId: ExploreId, { height, width }: { height: number; width: number } -): ChangeSizeAction { - return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } }; +): ActionOf { + return changeSizeAction({ exploreId, height, width }); } /** @@ -123,7 +132,7 @@ export function changeSize( */ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } }); + dispatch(changeTimeAction({ exploreId, range })); dispatch(runQueries(exploreId)); }; } @@ -133,19 +142,12 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult< */ export function clearQueries(exploreId: ExploreId): ThunkResult { return dispatch => { - dispatch(scanStop(exploreId)); - dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } }); + dispatch(scanStopAction({ exploreId })); + dispatch(clearQueriesAction({ exploreId })); dispatch(stateSave()); }; } -/** - * Highlight expressions in the log results - */ -export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { - return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } }; -} - /** * Initialize Explore state with state from the URL and the React component. * Call this only on components for with the Explore state has not been initialized. @@ -167,18 +169,16 @@ export function initializeExplore( meta: ds.meta, })); - dispatch({ - type: ActionTypes.InitializeExplore, - payload: { + dispatch( + initializeExploreAction({ exploreId, containerWidth, - datasourceName, eventBridge, exploreDatasources, queries, range, - }, - }); + }) + ); if (exploreDatasources.length >= 1) { let instance; @@ -195,75 +195,20 @@ export function initializeExplore( instance = await getDatasourceSrv().get(); } - dispatch(updateDatasourceInstance(exploreId, instance)); + dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: instance })); dispatch(loadDatasource(exploreId, instance)); } else { - dispatch(loadDatasourceMissing(exploreId)); + dispatch(loadDatasourceMissingAction({ exploreId })); } }; } -/** - * Initialize the wrapper split state - */ -export function initializeExploreSplit() { - return async dispatch => { - dispatch({ type: ActionTypes.InitializeExploreSplit }); - }; -} - -/** - * Display an error that happened during the selection of a datasource - */ -export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ - type: ActionTypes.LoadDatasourceFailure, - payload: { - exploreId, - error, - }, -}); - -/** - * Display an error when no datasources have been configured - */ -export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ - type: ActionTypes.LoadDatasourceMissing, - payload: { exploreId }, -}); - -/** - * Start the async process of loading a datasource to display a loading indicator - */ -export const loadDatasourcePending = ( - exploreId: ExploreId, - requestedDatasourceName: string -): LoadDatasourcePendingAction => ({ - type: ActionTypes.LoadDatasourcePending, - payload: { - exploreId, - requestedDatasourceName, - }, -}); - -export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): QueriesImported => { - return { - type: ActionTypes.QueriesImported, - payload: { - exploreId, - queries, - }, - }; -}; - /** * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, * e.g., Prometheus -> Loki queries. */ -export const loadDatasourceSuccess = ( - exploreId: ExploreId, - instance: any, -): LoadDatasourceSuccessAction => { +export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): ActionOf => { // Capabilities const supportsGraph = instance.meta.metrics; const supportsLogs = instance.meta.logs; @@ -276,37 +221,18 @@ export const loadDatasourceSuccess = ( // Save last-used datasource store.set(LAST_USED_DATASOURCE_KEY, instance.name); - return { - type: ActionTypes.LoadDatasourceSuccess, - payload: { - exploreId, - StartPage, - datasourceInstance: instance, - history, - showingStartPage: Boolean(StartPage), - supportsGraph, - supportsLogs, - supportsTable, - }, - }; + return loadDatasourceSuccessAction({ + exploreId, + StartPage, + datasourceInstance: instance, + history, + showingStartPage: Boolean(StartPage), + supportsGraph, + supportsLogs, + supportsTable, + }); }; -/** - * Updates datasource instance before datasouce loading has started - */ -export function updateDatasourceInstance( - exploreId: ExploreId, - instance: DataSourceApi -): UpdateDatasourceInstanceAction { - return { - type: ActionTypes.UpdateDatasourceInstance, - payload: { - exploreId, - datasourceInstance: instance, - }, - }; -} - export function importQueries( exploreId: ExploreId, queries: DataQuery[], @@ -332,7 +258,7 @@ export function importQueries( ...generateEmptyQuery(i), })); - dispatch(queriesImported(exploreId, nextQueries)); + dispatch(queriesImportedAction({ exploreId, queries: nextQueries })); }; } @@ -344,7 +270,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T const datasourceName = instance.name; // Keep ID to track selection - dispatch(loadDatasourcePending(exploreId, datasourceName)); + dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName })); let datasourceError = null; try { @@ -355,7 +281,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T } if (datasourceError) { - dispatch(loadDatasourceFailure(exploreId, datasourceError)); + dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError })); return; } @@ -392,11 +318,7 @@ export function modifyQueries( modifier: any ): ThunkResult { return dispatch => { - const modifyQueryAction: ModifyQueriesAction = { - type: ActionTypes.ModifyQueries, - payload: { exploreId, modification, index, modifier }, - }; - dispatch(modifyQueryAction); + dispatch(modifyQueriesAction({ exploreId, modification, index, modifier })); if (!modification.preventSubmit) { dispatch(runQueries(exploreId)); } @@ -461,29 +383,10 @@ export function queryTransactionFailure( return qt; }); - dispatch({ - type: ActionTypes.QueryTransactionFailure, - payload: { exploreId, queryTransactions: nextQueryTransactions }, - }); + dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions })); }; } -/** - * Start a query transaction for the given result type. - * @param exploreId Explore area - * @param transaction Query options and `done` status. - * @param resultType Associate the transaction with a result viewer, e.g., Graph - * @param rowIndex Index is used to associate latency for this transaction with a query row - */ -export function queryTransactionStart( - exploreId: ExploreId, - transaction: QueryTransaction, - resultType: ResultType, - rowIndex: number -): QueryTransactionStartAction { - return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } }; -} - /** * Complete a query transaction, mark the transaction as `done` and store query state in URL. * If the transaction was started by a scanner, it keeps on scanning for more results. @@ -540,14 +443,13 @@ export function queryTransactionSuccess( // Side-effect: Saving history in localstorage const nextHistory = updateHistory(history, datasourceId, queries); - dispatch({ - type: ActionTypes.QueryTransactionSuccess, - payload: { + dispatch( + queryTransactionSuccessAction({ exploreId, history: nextHistory, queryTransactions: nextQueryTransactions, - }, - }); + }) + ); // Keep scanning for results if this was the last scanning transaction if (scanning) { @@ -555,26 +457,16 @@ export function queryTransactionSuccess( const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); if (!other) { const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); + dispatch(scanRangeAction({ exploreId, range })); } } else { // We can stop scanning if we have a result - dispatch(scanStop(exploreId)); + dispatch(scanStopAction({ exploreId })); } } }; } -/** - * Remove query row of the given index, as well as associated query results. - */ -export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { - return dispatch => { - dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } }); - dispatch(runQueries(exploreId)); - }; -} - /** * Main action to run queries and dispatches sub-actions based on which result viewers are active */ @@ -592,7 +484,7 @@ export function runQueries(exploreId: ExploreId) { } = getState().explore[exploreId]; if (!hasNonEmptyQuery(modifiedQueries)) { - dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } }); + dispatch(runQueriesEmptyAction({ exploreId })); dispatch(stateSave()); // Remember to saves to state and update location return; } @@ -673,7 +565,7 @@ function runQueriesForType( queryIntervals, scanning ); - dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex)); + dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction })); try { const now = Date.now(); const res = await datasourceInstance.query(transaction.options); @@ -697,21 +589,14 @@ function runQueriesForType( export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { return dispatch => { // Register the scanner - dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } }); + dispatch(scanStartAction({ exploreId, scanner })); // Scanning must trigger query run, and return the new range const range = scanner(); // Set the new range to be displayed - dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); + dispatch(scanRangeAction({ exploreId, range })); }; } -/** - * Stop any scanning for more results. - */ -export function scanStop(exploreId: ExploreId): ScanStopAction { - return { type: ActionTypes.ScanStop, payload: { exploreId } }; -} - /** * Reset queries to the given queries. Any modifications will be discarded. * Use this action for clicks on query examples. Triggers a query run. @@ -720,13 +605,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk return dispatch => { // Inject react keys into query objects const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() })); - dispatch({ - type: ActionTypes.SetQueries, - payload: { - exploreId, - queries, - }, - }); + dispatch(setQueriesAction({ exploreId, queries })); dispatch(runQueries(exploreId)); }; } @@ -736,7 +615,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk */ export function splitClose(): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.SplitClose }); + dispatch(splitCloseAction()); dispatch(stateSave()); }; } @@ -755,7 +634,7 @@ export function splitOpen(): ThunkResult { queryTransactions: [], initialQueries: leftState.modifiedQueries.slice(), }; - dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } }); + dispatch(splitOpenAction({ itemState })); dispatch(stateSave()); }; } @@ -791,7 +670,7 @@ export function stateSave() { */ export function toggleGraph(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } }); + dispatch(toggleGraphAction({ exploreId })); if (getState().explore[exploreId].showingGraph) { dispatch(runQueries(exploreId)); } @@ -803,7 +682,7 @@ export function toggleGraph(exploreId: ExploreId): ThunkResult { */ export function toggleLogs(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } }); + dispatch(toggleLogsAction({ exploreId })); if (getState().explore[exploreId].showingLogs) { dispatch(runQueries(exploreId)); } @@ -815,18 +694,9 @@ export function toggleLogs(exploreId: ExploreId): ThunkResult { */ export function toggleTable(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } }); + dispatch(toggleTableAction({ exploreId })); if (getState().explore[exploreId].showingTable) { dispatch(runQueries(exploreId)); } }; } - -/** - * Resets state for explore. - */ -export function resetExplore(): ThunkResult { - return dispatch => { - dispatch({ type: ActionTypes.ResetExplore, payload: {} }); - }; -} diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index 8227a947c5b..44079313c04 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/public/app/features/explore/state/reducers.test.ts @@ -1,42 +1,47 @@ -import { Action, ActionTypes } from './actionTypes'; import { itemReducer, makeExploreItemState } from './reducers'; -import { ExploreId } from 'app/types/explore'; +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { reducerTester } from 'test/core/redux/reducerTester'; +import { scanStartAction, scanStopAction } from './actionTypes'; +import { Reducer } from 'redux'; +import { ActionOf } from 'app/core/redux/actionCreatorFactory'; describe('Explore item reducer', () => { describe('scanning', () => { test('should start scanning', () => { - let state = makeExploreItemState(); - const action: Action = { - type: ActionTypes.ScanStart, - payload: { - exploreId: ExploreId.left, - scanner: jest.fn(), - }, + const scanner = jest.fn(); + const initalState = { + ...makeExploreItemState(), + scanning: false, + scanner: undefined, }; - state = itemReducer(state, action); - expect(state.scanning).toBeTruthy(); - expect(state.scanner).toBe(action.payload.scanner); + + reducerTester() + .givenReducer(itemReducer as Reducer>, initalState) + .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left, scanner })) + .thenStateShouldEqual({ + ...makeExploreItemState(), + scanning: true, + scanner, + }); }); test('should stop scanning', () => { - let state = makeExploreItemState(); - const start: Action = { - type: ActionTypes.ScanStart, - payload: { - exploreId: ExploreId.left, - scanner: jest.fn(), - }, + const scanner = jest.fn(); + const initalState = { + ...makeExploreItemState(), + scanning: true, + scanner, + scanRange: {}, }; - state = itemReducer(state, start); - expect(state.scanning).toBeTruthy(); - const action: Action = { - type: ActionTypes.ScanStop, - payload: { - exploreId: ExploreId.left, - }, - }; - state = itemReducer(state, action); - expect(state.scanning).toBeFalsy(); - expect(state.scanner).toBeUndefined(); + + reducerTester() + .givenReducer(itemReducer as Reducer>, initalState) + .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left })) + .thenStateShouldEqual({ + ...makeExploreItemState(), + scanning: false, + scanner: undefined, + scanRange: undefined, + }); }); }); }); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 14c8d87bbd2..fc9be0c28b8 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -7,7 +7,36 @@ import { import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { DataQuery } from '@grafana/ui/src/types'; -import { Action, ActionTypes } from './actionTypes'; +import { HigherOrderAction, ActionTypes } from './actionTypes'; +import { reducerFactory } from 'app/core/redux'; +import { + addQueryRowAction, + changeQueryAction, + changeSizeAction, + changeTimeAction, + clearQueriesAction, + highlightLogsExpressionAction, + initializeExploreAction, + updateDatasourceInstanceAction, + loadDatasourceFailureAction, + loadDatasourceMissingAction, + loadDatasourcePendingAction, + loadDatasourceSuccessAction, + modifyQueriesAction, + queryTransactionFailureAction, + queryTransactionStartAction, + queryTransactionSuccessAction, + removeQueryRowAction, + runQueriesEmptyAction, + scanRangeAction, + scanStartAction, + scanStopAction, + setQueriesAction, + toggleGraphAction, + toggleLogsAction, + toggleTableAction, + queriesImportedAction, +} from './actionTypes'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -58,9 +87,10 @@ export const initialExploreState: ExploreState = { /** * Reducer for an Explore area, to be used by the global Explore reducer. */ -export const itemReducer = (state, action: Action): ExploreItemState => { - switch (action.type) { - case ActionTypes.AddQueryRow: { +export const itemReducer = reducerFactory({} as ExploreItemState) + .addMapper({ + filter: addQueryRowAction, + mapper: (state, action): ExploreItemState => { const { initialQueries, modifiedQueries, queryTransactions } = state; const { index, query } = action.payload; @@ -77,10 +107,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { // Ongoing transactions need to update their row indices const nextQueryTransactions = queryTransactions.map(qt => { if (qt.rowIndex > index) { - return { - ...qt, - rowIndex: qt.rowIndex + 1, - }; + return { ...qt, rowIndex: qt.rowIndex + 1 }; } return qt; }); @@ -92,9 +119,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { modifiedQueries: nextModifiedQueries, queryTransactions: nextQueryTransactions, }; - } - - case ActionTypes.ChangeQuery: { + }, + }) + .addMapper({ + filter: changeQueryAction, + mapper: (state, action): ExploreItemState => { const { initialQueries, queryTransactions } = state; let { modifiedQueries } = state; const { query, index, override } = action.payload; @@ -102,17 +131,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { // Fast path: only change modifiedQueries to not trigger an update modifiedQueries[index] = query; if (!override) { - return { - ...state, - modifiedQueries, - }; + return { ...state, modifiedQueries }; } // Override path: queries are completely reset - const nextQuery: DataQuery = { - ...query, - ...generateEmptyQuery(index), - }; + const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) }; const nextQueries = [...initialQueries]; nextQueries[index] = nextQuery; modifiedQueries = [...nextQueries]; @@ -126,9 +149,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; - } - - case ActionTypes.ChangeSize: { + }, + }) + .addMapper({ + filter: changeSizeAction, + mapper: (state, action): ExploreItemState => { const { range, datasourceInstance } = state; let interval = '1s'; if (datasourceInstance && datasourceInstance.interval) { @@ -137,16 +162,17 @@ export const itemReducer = (state, action: Action): ExploreItemState => { const containerWidth = action.payload.width; const queryIntervals = getIntervals(range, interval, containerWidth); return { ...state, containerWidth, queryIntervals }; - } - - case ActionTypes.ChangeTime: { - return { - ...state, - range: action.payload.range, - }; - } - - case ActionTypes.ClearQueries: { + }, + }) + .addMapper({ + filter: changeTimeAction, + mapper: (state, action): ExploreItemState => { + return { ...state, range: action.payload.range }; + }, + }) + .addMapper({ + filter: clearQueriesAction, + mapper: (state): ExploreItemState => { const queries = ensureQueries(); return { ...state, @@ -155,14 +181,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => { queryTransactions: [], showingStartPage: Boolean(state.StartPage), }; - } - - case ActionTypes.HighlightLogsExpression: { + }, + }) + .addMapper({ + filter: highlightLogsExpressionAction, + mapper: (state, action): ExploreItemState => { const { expressions } = action.payload; return { ...state, logsHighlighterExpressions: expressions }; - } - - case ActionTypes.InitializeExplore: { + }, + }) + .addMapper({ + filter: initializeExploreAction, + mapper: (state, action): ExploreItemState => { const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload; return { ...state, @@ -174,30 +204,37 @@ export const itemReducer = (state, action: Action): ExploreItemState => { initialized: true, modifiedQueries: queries.slice(), }; - } - - case ActionTypes.UpdateDatasourceInstance: { + }, + }) + .addMapper({ + filter: updateDatasourceInstanceAction, + mapper: (state, action): ExploreItemState => { const { datasourceInstance } = action.payload; - return { - ...state, - datasourceInstance, - datasourceName: datasourceInstance.name, - }; - } - - case ActionTypes.LoadDatasourceFailure: { + return { ...state, datasourceInstance }; + /*datasourceName: datasourceInstance.name removed after refactor, datasourceName does not exists on ExploreItemState */ + }, + }) + .addMapper({ + filter: loadDatasourceFailureAction, + mapper: (state, action): ExploreItemState => { return { ...state, datasourceError: action.payload.error, datasourceLoading: false }; - } - - case ActionTypes.LoadDatasourceMissing: { + }, + }) + .addMapper({ + filter: loadDatasourceMissingAction, + mapper: (state): ExploreItemState => { return { ...state, datasourceMissing: true, datasourceLoading: false }; - } - - case ActionTypes.LoadDatasourcePending: { + }, + }) + .addMapper({ + filter: loadDatasourcePendingAction, + mapper: (state, action): ExploreItemState => { return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName }; - } - - case ActionTypes.LoadDatasourceSuccess: { + }, + }) + .addMapper({ + filter: loadDatasourceSuccessAction, + mapper: (state, action): ExploreItemState => { const { containerWidth, range } = state; const { StartPage, @@ -226,9 +263,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { logsHighlighterExpressions: undefined, queryTransactions: [], }; - } - - case ActionTypes.ModifyQueries: { + }, + }) + .addMapper({ + filter: modifyQueriesAction, + mapper: (state, action): ExploreItemState => { const { initialQueries, modifiedQueries, queryTransactions } = state; const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; @@ -246,12 +285,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { nextQueries = initialQueries.map((query, i) => { // Synchronize all queries with local query cache to ensure consistency // TODO still needed? - return i === index - ? { - ...modifier(modifiedQueries[i], modification), - ...generateEmptyQuery(i), - } - : query; + return i === index ? { ...modifier(modifiedQueries[i], modification), ...generateEmptyQuery(i) } : query; }); nextQueryTransactions = queryTransactions // Consume the hint corresponding to the action @@ -270,18 +304,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => { modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; - } - - case ActionTypes.QueryTransactionFailure: { + }, + }) + .addMapper({ + filter: queryTransactionFailureAction, + mapper: (state, action): ExploreItemState => { const { queryTransactions } = action.payload; - return { - ...state, - queryTransactions, - showingStartPage: false, - }; - } - - case ActionTypes.QueryTransactionStart: { + return { ...state, queryTransactions, showingStartPage: false }; + }, + }) + .addMapper({ + filter: queryTransactionStartAction, + mapper: (state, action): ExploreItemState => { const { queryTransactions } = state; const { resultType, rowIndex, transaction } = action.payload; // Discarding existing transactions of same type @@ -292,14 +326,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => { // Append new transaction const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction]; - return { - ...state, - queryTransactions: nextQueryTransactions, - showingStartPage: false, - }; - } - - case ActionTypes.QueryTransactionSuccess: { + return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false }; + }, + }) + .addMapper({ + filter: queryTransactionSuccessAction, + mapper: (state, action): ExploreItemState => { const { datasourceInstance, queryIntervals } = state; const { history, queryTransactions } = action.payload; const results = calculateResultsFromQueryTransactions( @@ -308,16 +340,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => { queryIntervals.intervalMs ); - return { - ...state, - ...results, - history, - queryTransactions, - showingStartPage: false, - }; - } - - case ActionTypes.RemoveQueryRow: { + return { ...state, ...results, history, queryTransactions, showingStartPage: false }; + }, + }) + .addMapper({ + filter: removeQueryRowAction, + mapper: (state, action): ExploreItemState => { const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; let { modifiedQueries } = state; const { index } = action.payload; @@ -346,21 +374,29 @@ export const itemReducer = (state, action: Action): ExploreItemState => { modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; - } - - case ActionTypes.RunQueriesEmpty: { + }, + }) + .addMapper({ + filter: runQueriesEmptyAction, + mapper: (state): ExploreItemState => { return { ...state, queryTransactions: [] }; - } - - case ActionTypes.ScanRange: { + }, + }) + .addMapper({ + filter: scanRangeAction, + mapper: (state, action): ExploreItemState => { return { ...state, scanRange: action.payload.range }; - } - - case ActionTypes.ScanStart: { + }, + }) + .addMapper({ + filter: scanStartAction, + mapper: (state, action): ExploreItemState => { return { ...state, scanning: true, scanner: action.payload.scanner }; - } - - case ActionTypes.ScanStop: { + }, + }) + .addMapper({ + filter: scanStopAction, + mapper: (state): ExploreItemState => { const { queryTransactions } = state; const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); return { @@ -370,14 +406,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => { scanRange: undefined, scanner: undefined, }; - } - - case ActionTypes.SetQueries: { + }, + }) + .addMapper({ + filter: setQueriesAction, + mapper: (state, action): ExploreItemState => { const { queries } = action.payload; return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; - } - - case ActionTypes.ToggleGraph: { + }, + }) + .addMapper({ + filter: toggleGraphAction, + mapper: (state): ExploreItemState => { const showingGraph = !state.showingGraph; let nextQueryTransactions = state.queryTransactions; if (!showingGraph) { @@ -385,9 +425,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); } return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; - } - - case ActionTypes.ToggleLogs: { + }, + }) + .addMapper({ + filter: toggleLogsAction, + mapper: (state): ExploreItemState => { const showingLogs = !state.showingLogs; let nextQueryTransactions = state.queryTransactions; if (!showingLogs) { @@ -395,9 +437,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); } return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; - } - - case ActionTypes.ToggleTable: { + }, + }) + .addMapper({ + filter: toggleTableAction, + mapper: (state): ExploreItemState => { const showingTable = !state.showingTable; if (showingTable) { return { ...state, showingTable, queryTransactions: state.queryTransactions }; @@ -412,25 +456,21 @@ export const itemReducer = (state, action: Action): ExploreItemState => { ); return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; - } - - case ActionTypes.QueriesImported: { - return { - ...state, - initialQueries: action.payload.queries, - modifiedQueries: action.payload.queries.slice(), - }; - } - } - - return state; -}; + }, + }) + .addMapper({ + filter: queriesImportedAction, + mapper: (state, action): ExploreItemState => { + return { ...state, initialQueries: action.payload.queries, modifiedQueries: action.payload.queries.slice() }; + }, + }) + .create(); /** * Global Explore reducer that handles multiple Explore areas (left and right). * Actions that have an `exploreId` get routed to the ExploreItemReducer. */ -export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { +export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => { switch (action.type) { case ActionTypes.SplitClose: { return { ...state, split: false }; @@ -453,10 +493,7 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp const { exploreId } = action.payload as any; if (exploreId !== undefined) { const exploreItemState = state[exploreId]; - return { - ...state, - [exploreId]: itemReducer(exploreItemState, action), - }; + return { ...state, [exploreId]: itemReducer(exploreItemState, action) }; } } From d9578bc48505c890a75c9e6c7ef996fe0886531d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 08:17:18 +0100 Subject: [PATCH 049/770] Merge with master --- .../datasource/loki/components/LokiQueryEditor.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index 634a642c65e..e9912522f16 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -33,7 +33,7 @@ export class LokiQueryEditor extends PureComponent { query: { ...this.state.query, expr: query.expr, - } + }, }); }; @@ -61,12 +61,18 @@ export class LokiQueryEditor extends PureComponent { datasource={datasource} initialQuery={query} onQueryChange={this.onFieldChange} - onPressEnter={this.onRunQuery} + onExecuteQuery={this.onRunQuery} + history={[]} />
Format as
-
From f5084045f216c79702ad7124ae72a7144d014881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 09:32:39 +0100 Subject: [PATCH 050/770] Fix save provisioned dashboard modal --- public/app/features/dashboard/components/SaveModals/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/components/SaveModals/index.ts b/public/app/features/dashboard/components/SaveModals/index.ts index afab0796d28..6f55cc2ce06 100644 --- a/public/app/features/dashboard/components/SaveModals/index.ts +++ b/public/app/features/dashboard/components/SaveModals/index.ts @@ -1,2 +1,3 @@ export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl'; export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl'; +export { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl'; From fdd5ac1895e4ee7af5c9a060ba2c8fc8b4ef830d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 09:55:23 +0100 Subject: [PATCH 051/770] devenv: switching back using loki master plus various fixes --- devenv/docker/blocks/loki/docker-compose.yaml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/devenv/docker/blocks/loki/docker-compose.yaml b/devenv/docker/blocks/loki/docker-compose.yaml index c2fee15b0bb..0ac5d439354 100644 --- a/devenv/docker/blocks/loki/docker-compose.yaml +++ b/devenv/docker/blocks/loki/docker-compose.yaml @@ -1,24 +1,14 @@ -version: "3" - -networks: loki: - -services: - loki: - image: grafana/loki:master-3e6a75e + image: grafana/loki:master ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml - networks: - - loki promtail: - image: grafana/promtail:master-3e6a75e + image: grafana/promtail:master volumes: - ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml - /var/log:/var/log - ../data/log:/var/log/grafana command: -config.file=/etc/promtail/docker-config.yaml - networks: - - loki From c61e90543411fee4971f9f67918ef09de32065ac Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 4 Feb 2019 10:22:45 +0100 Subject: [PATCH 052/770] fixing logging action --- .../ValueMappingsEditor/ValueMappingsEditor.story.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx index d6c8cec8c1e..85504f6cd09 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx @@ -6,12 +6,5 @@ import { ValueMappingsEditor } from './ValueMappingsEditor'; const ValueMappingsEditorStories = storiesOf('UI/ValueMappingsEditor', module); ValueMappingsEditorStories.add('default', () => { - return ( - { - action('Mapping changed'); - }} - /> - ); + return ; }); From 6b98b05976fb837433370ff45d214b6889e1bc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 11:07:32 +0100 Subject: [PATCH 053/770] Removed modifiedQueries from state --- public/app/features/explore/QueryEditor.tsx | 11 ++---- public/app/features/explore/QueryRows.tsx | 9 +++-- public/app/features/explore/state/actions.ts | 18 ++++----- public/app/features/explore/state/reducers.ts | 39 ++++--------------- public/app/types/explore.ts | 8 +--- 5 files changed, 26 insertions(+), 59 deletions(-) diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index 083cd8a2e17..1d329f1c56e 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -14,7 +14,7 @@ interface QueryEditorProps { datasource: any; error?: string | JSX.Element; onExecuteQuery?: () => void; - onQueryChange?: (value: DataQuery, override?: boolean) => void; + onQueryChange?: (value: DataQuery) => void; initialQuery: DataQuery; exploreEvents: Emitter; range: RawTimeRange; @@ -40,20 +40,17 @@ export default class QueryEditor extends PureComponent { datasource, target, refresh: () => { - this.props.onQueryChange(target, false); + this.props.onQueryChange(target); this.props.onExecuteQuery(); }, events: exploreEvents, - panel: { - datasource, - targets: [target], - }, + panel: { datasource, targets: [target] }, dashboard: {}, }, }; this.component = loader.load(this.element, scopeProps, template); - this.props.onQueryChange(target, false); + this.props.onQueryChange(target); } componentWillUnmount() { diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index f8bb6e5ce6b..d65c1283bd6 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -21,10 +21,11 @@ export default class QueryRows extends PureComponent { const { className = '', exploreEvents, exploreId, initialQueries } = this.props; return (
- {initialQueries.map((query, index) => ( - // TODO instead of relying on initialQueries, move to react key list in redux - - ))} + {initialQueries.map((query, index) => { + // using query.key will introduce infinite loop because QueryEditor#53 + const key = query.datasource ? `${query.datasource}-${index}` : query.key; + return ; + })}
); } diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index f32575edda5..8530e7678ad 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -84,9 +84,9 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun return async (dispatch, getState) => { const newDataSourceInstance = await getDatasourceSrv().get(datasource); const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance; - const modifiedQueries = getState().explore[exploreId].modifiedQueries; + const queries = getState().explore[exploreId].initialQueries; - await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance)); + await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance)); dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance })); dispatch(loadDatasource(exploreId, newDataSourceInstance)); @@ -254,7 +254,7 @@ export function importQueries( } const nextQueries = importedQueries.map((q, i) => ({ - ...importedQueries[i], + ...q, ...generateEmptyQuery(i), })); @@ -474,7 +474,7 @@ export function runQueries(exploreId: ExploreId) { return (dispatch, getState) => { const { datasourceInstance, - modifiedQueries, + initialQueries, showingLogs, showingGraph, showingTable, @@ -483,7 +483,7 @@ export function runQueries(exploreId: ExploreId) { supportsTable, } = getState().explore[exploreId]; - if (!hasNonEmptyQuery(modifiedQueries)) { + if (!hasNonEmptyQuery(initialQueries)) { dispatch(runQueriesEmptyAction({ exploreId })); dispatch(stateSave()); // Remember to saves to state and update location return; @@ -547,7 +547,7 @@ function runQueriesForType( const { datasourceInstance, eventBridge, - modifiedQueries: queries, + initialQueries: queries, queryIntervals, range, scanning, @@ -632,7 +632,7 @@ export function splitOpen(): ThunkResult { const itemState = { ...leftState, queryTransactions: [], - initialQueries: leftState.modifiedQueries.slice(), + initialQueries: leftState.initialQueries.slice(), }; dispatch(splitOpenAction({ itemState })); dispatch(stateSave()); @@ -649,14 +649,14 @@ export function stateSave() { const urlStates: { [index: string]: string } = {}; const leftUrlState: ExploreUrlState = { datasource: left.datasourceInstance.name, - queries: left.modifiedQueries.map(clearQueryKeys), + queries: left.initialQueries.map(clearQueryKeys), range: left.range, }; urlStates.left = serializeStateToUrlParam(leftUrlState, true); if (split) { const rightUrlState: ExploreUrlState = { datasource: right.datasourceInstance.name, - queries: right.modifiedQueries.map(clearQueryKeys), + queries: right.initialQueries.map(clearQueryKeys), range: right.range, }; urlStates.right = serializeStateToUrlParam(rightUrlState, true); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index fc9be0c28b8..9343cf0ec57 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -61,7 +61,6 @@ export const makeExploreItemState = (): ExploreItemState => ({ history: [], initialQueries: [], initialized: false, - modifiedQueries: [], queryTransactions: [], queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, range: DEFAULT_RANGE, @@ -91,16 +90,9 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: addQueryRowAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, modifiedQueries, queryTransactions } = state; + const { initialQueries, queryTransactions } = state; const { index, query } = action.payload; - // Add new query row after given index, keep modifications of existing rows - const nextModifiedQueries = [ - ...modifiedQueries.slice(0, index + 1), - { ...query }, - ...initialQueries.slice(index + 1), - ]; - // Add to initialQueries, which will cause a new row to be rendered const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)]; @@ -116,7 +108,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta ...state, initialQueries: nextQueries, logsHighlighterExpressions: undefined, - modifiedQueries: nextModifiedQueries, queryTransactions: nextQueryTransactions, }; }, @@ -125,20 +116,12 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: changeQueryAction, mapper: (state, action): ExploreItemState => { const { initialQueries, queryTransactions } = state; - let { modifiedQueries } = state; - const { query, index, override } = action.payload; - - // Fast path: only change modifiedQueries to not trigger an update - modifiedQueries[index] = query; - if (!override) { - return { ...state, modifiedQueries }; - } + const { query, index } = action.payload; // Override path: queries are completely reset const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) }; const nextQueries = [...initialQueries]; nextQueries[index] = nextQuery; - modifiedQueries = [...nextQueries]; // Discard ongoing transaction related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); @@ -146,7 +129,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, initialQueries: nextQueries, - modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; }, @@ -177,7 +159,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, initialQueries: queries.slice(), - modifiedQueries: queries.slice(), queryTransactions: [], showingStartPage: Boolean(state.StartPage), }; @@ -202,7 +183,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta range, initialQueries: queries, initialized: true, - modifiedQueries: queries.slice(), }; }, }) @@ -268,14 +248,14 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: modifyQueriesAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, modifiedQueries, queryTransactions } = state; + const { initialQueries, queryTransactions } = state; const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { // Modify all queries nextQueries = initialQueries.map((query, i) => ({ - ...modifier(modifiedQueries[i], modification), + ...modifier({ ...query }, modification), ...generateEmptyQuery(i), })); // Discard all ongoing transactions @@ -285,7 +265,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta nextQueries = initialQueries.map((query, i) => { // Synchronize all queries with local query cache to ensure consistency // TODO still needed? - return i === index ? { ...modifier(modifiedQueries[i], modification), ...generateEmptyQuery(i) } : query; + return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query; }); nextQueryTransactions = queryTransactions // Consume the hint corresponding to the action @@ -301,7 +281,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, initialQueries: nextQueries, - modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; }, @@ -347,11 +326,8 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: removeQueryRowAction, mapper: (state, action): ExploreItemState => { const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; - let { modifiedQueries } = state; const { index } = action.payload; - modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; - if (initialQueries.length <= 1) { return state; } @@ -371,7 +347,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta ...results, initialQueries: nextQueries, logsHighlighterExpressions: undefined, - modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; }, @@ -412,7 +387,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: setQueriesAction, mapper: (state, action): ExploreItemState => { const { queries } = action.payload; - return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; + return { ...state, initialQueries: queries.slice() }; }, }) .addMapper({ @@ -461,7 +436,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: queriesImportedAction, mapper: (state, action): ExploreItemState => { - return { ...state, initialQueries: action.payload.queries, modifiedQueries: action.payload.queries.slice() }; + return { ...state, initialQueries: action.payload.queries }; }, }) .create(); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 34b7ff08c99..92145dc2324 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -145,7 +145,7 @@ export interface ExploreItemState { history: HistoryItem[]; /** * Initial queries for this Explore, e.g., set via URL. Each query will be - * converted to a query row. Query edits should be tracked in `modifiedQueries` though. + * converted to a query row. */ initialQueries: DataQuery[]; /** @@ -162,12 +162,6 @@ export interface ExploreItemState { * Log query result to be displayed in the logs result viewer. */ logsResult?: LogsModel; - /** - * Copy of `initialQueries` that tracks user edits. - * Don't connect this property to a react component as it is updated on every query change. - * Used when running queries. Needs to be reset to `initialQueries` when those are reset as well. - */ - modifiedQueries: DataQuery[]; /** * Query intervals for graph queries to determine how many datapoints to return. * Needs to be updated when `datasourceInstance` or `containerWidth` is changed. From eb8dfefb231559acce4291a9d288d8d44fc02b50 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 11:16:11 +0100 Subject: [PATCH 054/770] changelog: add notes about closing #14231 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7164f5d99a9..1a19957b3ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Minor * **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae) +* **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson) # 6.0.0-beta1 (2019-01-30) From 217468074f6f8f420554cbed203db97921324e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 11:19:45 +0100 Subject: [PATCH 055/770] added submenu, made sure submenu visibility is always up to date --- .../app/features/annotations/editor_ctrl.ts | 7 +++- .../DashLinks/DashLinksEditorCtrl.ts | 4 ++- .../components/DashNav/DashNavCtrl.ts | 2 -- .../dashboard/components/SubMenu/SubMenu.tsx | 36 +++++++++++++++++++ .../dashboard/components/SubMenu/index.ts | 1 + .../dashboard/containers/DashboardPage.tsx | 2 ++ .../dashboard/services/DashboardSrv.ts | 7 ++-- .../app/features/explore/ExploreToolbar.tsx | 4 +-- public/sass/components/_navbar.scss | 3 +- 9 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 public/app/features/dashboard/components/SubMenu/SubMenu.tsx diff --git a/public/app/features/annotations/editor_ctrl.ts b/public/app/features/annotations/editor_ctrl.ts index 18b00793ff8..c12e442f6d3 100644 --- a/public/app/features/annotations/editor_ctrl.ts +++ b/public/app/features/annotations/editor_ctrl.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; +import { DashboardModel } from 'app/features/dashboard/state'; export class AnnotationsEditorCtrl { mode: any; @@ -10,6 +11,7 @@ export class AnnotationsEditorCtrl { currentAnnotation: any; currentDatasource: any; currentIsNew: any; + dashboard: DashboardModel; annotationDefaults: any = { name: '', @@ -26,9 +28,10 @@ export class AnnotationsEditorCtrl { constructor($scope, private datasourceSrv) { $scope.ctrl = this; + this.dashboard = $scope.dashboard; this.mode = 'list'; this.datasources = datasourceSrv.getAnnotationSources(); - this.annotations = $scope.dashboard.annotations.list; + this.annotations = this.dashboard.annotations.list; this.reset(); this.onColorChange = this.onColorChange.bind(this); @@ -78,11 +81,13 @@ export class AnnotationsEditorCtrl { this.annotations.push(this.currentAnnotation); this.reset(); this.mode = 'list'; + this.dashboard.updateSubmenuVisibility(); } removeAnnotation(annotation) { const index = _.indexOf(this.annotations, annotation); this.annotations.splice(index, 1); + this.dashboard.updateSubmenuVisibility(); } onColorChange(newColor) { diff --git a/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts b/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts index 398ad757bf3..339c8e7de4c 100644 --- a/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts +++ b/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts @@ -1,5 +1,6 @@ import angular from 'angular'; import _ from 'lodash'; +import { DashboardModel } from 'app/features/dashboard/state'; export let iconMap = { 'external link': 'fa-external-link', @@ -12,7 +13,7 @@ export let iconMap = { }; export class DashLinksEditorCtrl { - dashboard: any; + dashboard: DashboardModel; iconMap: any; mode: any; link: any; @@ -40,6 +41,7 @@ export class DashLinksEditorCtrl { addLink() { this.dashboard.links.push(this.link); this.mode = 'list'; + this.dashboard.updateSubmenuVisibility(); } editLink(link) { diff --git a/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts b/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts index e75c1468a1f..fbf84d354e3 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts +++ b/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts @@ -10,8 +10,6 @@ export class DashNavCtrl { /** @ngInject */ constructor(private $scope, private dashboardSrv, private $location, public playlistSrv) { - appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope); - if (this.dashboard.meta.isSnapshot) { const meta = this.dashboard.meta; this.titleTooltip = 'Created:  ' + moment(meta.created).calendar(); diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx new file mode 100644 index 00000000000..caef8f2de38 --- /dev/null +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -0,0 +1,36 @@ +// Libaries +import React, { PureComponent } from 'react'; + +// Utils & Services +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; + +// Types +import { DashboardModel } from '../../state/DashboardModel'; + +export interface Props { + dashboard: DashboardModel | null; +} + +export class SubMenu extends PureComponent { + element: HTMLElement; + angularCmp: AngularComponent; + + componentDidMount() { + const loader = getAngularLoader(); + + const template = ''; + const scopeProps = { dashboard: this.props.dashboard }; + + this.angularCmp = loader.load(this.element, scopeProps, template); + } + + componentWillUnmount() { + if (this.angularCmp) { + this.angularCmp.destroy(); + } + } + + render() { + return
this.element = element} />; + } +} diff --git a/public/app/features/dashboard/components/SubMenu/index.ts b/public/app/features/dashboard/components/SubMenu/index.ts index 1790aa66782..ca113ab75d6 100644 --- a/public/app/features/dashboard/components/SubMenu/index.ts +++ b/public/app/features/dashboard/components/SubMenu/index.ts @@ -1 +1,2 @@ export { SubMenuCtrl } from './SubMenuCtrl'; +export { SubMenu } from './SubMenu'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index be12657a829..e01998f65a9 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -12,6 +12,7 @@ import { createErrorNotification } from 'app/core/copy/appNotification'; import { LoadingPlaceholder } from '@grafana/ui'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashNav } from '../components/DashNav'; +import { SubMenu } from '../components/SubMenu'; import { DashboardSettings } from '../components/DashboardSettings'; // Redux @@ -192,6 +193,7 @@ export class DashboardPage extends PureComponent { {dashboard && editview && }
+ {dashboard.meta.submenuEnabled && }
diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 03aeb34ed36..7d3dfb68cd8 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -1,12 +1,15 @@ import coreModule from 'app/core/core_module'; -import { DashboardModel } from '../state/DashboardModel'; +import { appEvents } from 'app/core/app_events'; import locationUtil from 'app/core/utils/location_util'; +import { DashboardModel } from '../state/DashboardModel'; export class DashboardSrv { dash: any; /** @ngInject */ - constructor(private backendSrv, private $rootScope, private $location) {} + constructor(private backendSrv, private $rootScope, private $location) { + appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); + } create(dashboard, meta) { return new DashboardModel(dashboard, meta); diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 35f06d11c81..228cfb147e8 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -97,10 +97,10 @@ export class UnConnectedExploreToolbar extends PureComponent {
{exploreId === 'left' && ( - + Explore - + )}
diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 0744ed0dfc7..0cfa314a985 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -83,8 +83,7 @@ font-size: 19px; line-height: 8px; opacity: 0.75; - margin-right: 8px; - // icon hidden on smaller screens + margin-right: 13px; display: none; } From 5e2b9e40a2f6a858bef3ba9ccabc7fff6d96c47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 11:25:07 +0100 Subject: [PATCH 056/770] Added more typings --- packages/grafana-ui/src/types/plugin.ts | 10 ++++++---- public/app/features/explore/Explore.tsx | 6 +++--- .../datasource/loki/components/LokiStartPage.tsx | 7 ++----- .../datasource/prometheus/components/PromStart.tsx | 7 ++----- public/app/types/explore.ts | 13 +++++++++++-- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index e951e91a223..e674c9fbc32 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -65,15 +65,19 @@ export interface ExploreQueryFieldProps void; } +export interface ExploreStartPageProps { + onClickExample: (query: DataQuery) => void; +} + export interface PluginExports { Datasource?: DataSourceApi; QueryCtrl?: any; - QueryEditor?: ComponentClass>; + QueryEditor?: ComponentClass>; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; ExploreQueryField?: ComponentClass>; - ExploreStartPage?: any; + ExploreStartPage?: ComponentClass; // Panel plugin PanelCtrl?: any; @@ -131,5 +135,3 @@ export interface PluginMetaInfo { updated: string; version: string; } - - diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 31ffdf4ab24..36c1f7f5ad7 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -1,5 +1,5 @@ // Libraries -import React from 'react'; +import React, { ComponentClass } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import _ from 'lodash'; @@ -21,7 +21,7 @@ import TimePicker, { parseTime } from './TimePicker'; import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions'; // Types -import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui'; +import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps } from '@grafana/ui'; import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { StoreState } from 'app/types'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; @@ -30,7 +30,7 @@ import { ExploreToolbar } from './ExploreToolbar'; import { scanStopAction } from './state/actionTypes'; interface ExploreProps { - StartPage?: any; + StartPage?: ComponentClass; changeSize: typeof changeSize; changeTime: typeof changeTime; datasourceError: string; diff --git a/public/app/plugins/datasource/loki/components/LokiStartPage.tsx b/public/app/plugins/datasource/loki/components/LokiStartPage.tsx index da20661fe1b..62063a790ec 100644 --- a/public/app/plugins/datasource/loki/components/LokiStartPage.tsx +++ b/public/app/plugins/datasource/loki/components/LokiStartPage.tsx @@ -1,11 +1,8 @@ import React, { PureComponent } from 'react'; import LokiCheatSheet from './LokiCheatSheet'; +import { ExploreStartPageProps } from '@grafana/ui'; -interface Props { - onClickExample: () => void; -} - -export default class LokiStartPage extends PureComponent { +export default class LokiStartPage extends PureComponent { render() { return (
diff --git a/public/app/plugins/datasource/prometheus/components/PromStart.tsx b/public/app/plugins/datasource/prometheus/components/PromStart.tsx index 9acfc534853..de545e826e3 100644 --- a/public/app/plugins/datasource/prometheus/components/PromStart.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromStart.tsx @@ -1,11 +1,8 @@ import React, { PureComponent } from 'react'; import PromCheatSheet from './PromCheatSheet'; +import { ExploreStartPageProps } from '@grafana/ui'; -interface Props { - onClickExample: () => void; -} - -export default class PromStart extends PureComponent { +export default class PromStart extends PureComponent { render() { return (
diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 92145dc2324..4e099480cf0 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,5 +1,14 @@ +import { ComponentClass } from 'react'; import { Value } from 'slate'; -import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi, QueryHint } from '@grafana/ui'; +import { + RawTimeRange, + TimeRange, + DataQuery, + DataSourceSelectItem, + DataSourceApi, + QueryHint, + ExploreStartPageProps, +} from '@grafana/ui'; import { Emitter } from 'app/core/core'; import { LogsModel } from 'app/core/logs_model'; @@ -102,7 +111,7 @@ export interface ExploreItemState { /** * React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet. */ - StartPage?: any; + StartPage?: ComponentClass; /** * Width used for calculating the graph interval (can't have more datapoints than pixels) */ From efa48390b71d6d6397bc518cdda9ffb270ea8544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 12:09:06 +0100 Subject: [PATCH 057/770] Reverted redux-logger --- public/app/store/configureStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 570a387cd74..dc9a478adf3 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,6 +1,6 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; -import { createLogger } from 'redux-logger'; +// import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; @@ -39,7 +39,7 @@ export function configureStore() { if (process.env.NODE_ENV !== 'production') { // DEV builds we had the logger middleware - setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())))); + setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); } else { setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); } From f2d2712a9547ac8bcd6ef8b69f85d41d2d19dd51 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 30 Jan 2019 14:21:51 +0300 Subject: [PATCH 058/770] azuremonitor: add more builtin functions and operators --- .../editor/KustoQueryField.tsx | 26 +- .../editor/editor_component.tsx | 2 +- .../editor/kusto.ts | 114 ------ .../editor/kusto/kusto.ts | 355 ++++++++++++++++++ 4 files changed, 379 insertions(+), 118 deletions(-) delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto/kusto.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 849cf62efe0..fa79d4bdb99 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -6,7 +6,7 @@ import QueryField from './query_field'; import debounce from 'app/features/explore/utils/debounce'; import { getNextCharacter } from 'app/features/explore/utils/dom'; -import { FUNCTIONS, KEYWORDS } from './kusto'; +import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto/kusto'; // import '../sass/editor.base.scss'; @@ -260,10 +260,20 @@ export default class KustoQueryField extends QueryField { label: 'Keywords', items: KEYWORDS.map(wrapText) }, + { + prefixMatch: true, + label: 'Operators', + items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) + }, { prefixMatch: true, label: 'Functions', - items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; }) + items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Macros', + items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) } ]; } @@ -276,10 +286,20 @@ export default class KustoQueryField extends QueryField { label: 'Keywords', items: KEYWORDS.map(wrapText) }, + { + prefixMatch: true, + label: 'Operators', + items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) + }, { prefixMatch: true, label: 'Functions', - items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; }) + items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Macros', + items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) } ]; } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx index da7db58567f..59e4ab12c81 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx @@ -1,5 +1,5 @@ import KustoQueryField from './KustoQueryField'; -import Kusto from './kusto'; +import Kusto from './kusto/kusto'; import React, { Component } from 'react'; import coreModule from 'app/core/core_module'; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts deleted file mode 100644 index 647ebb8024a..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts +++ /dev/null @@ -1,114 +0,0 @@ -export const FUNCTIONS = [ - { text: 'countof', display: 'countof()', hint: '' }, - { text: 'bin', display: 'bin()', hint: '' }, - { text: 'extentid', display: 'extentid()', hint: '' }, - { text: 'extract', display: 'extract()', hint: '' }, - { text: 'extractjson', display: 'extractjson()', hint: '' }, - { text: 'floor', display: 'floor()', hint: '' }, - { text: 'iif', display: 'iif()', hint: '' }, - { text: 'isnull', display: 'isnull()', hint: '' }, - { text: 'isnotnull', display: 'isnotnull()', hint: '' }, - { text: 'notnull', display: 'notnull()', hint: '' }, - { text: 'isempty', display: 'isempty()', hint: '' }, - { text: 'isnotempty', display: 'isnotempty()', hint: '' }, - { text: 'notempty', display: 'notempty()', hint: '' }, - { text: 'now', display: 'now()', hint: '' }, - { text: 're2', display: 're2()', hint: '' }, - { text: 'strcat', display: 'strcat()', hint: '' }, - { text: 'strlen', display: 'strlen()', hint: '' }, - { text: 'toupper', display: 'toupper()', hint: '' }, - { text: 'tostring', display: 'tostring()', hint: '' }, - { text: 'count', display: 'count()', hint: '' }, - { text: 'cnt', display: 'cnt()', hint: '' }, - { text: 'sum', display: 'sum()', hint: '' }, - { text: 'min', display: 'min()', hint: '' }, - { text: 'max', display: 'max()', hint: '' }, - { text: 'avg', display: 'avg()', hint: '' }, - { - text: '$__timeFilter', - display: '$__timeFilter()', - hint: 'Macro that uses the selected timerange in Grafana to filter the query.', - }, - { - text: '$__escapeMulti', - display: '$__escapeMulti()', - hint: 'Macro to escape multi-value template variables that contain illegal characters.', - }, - { text: '$__contains', display: '$__contains()', hint: 'Macro for multi-value template variables.' }, -]; - -export const KEYWORDS = [ - 'by', - 'on', - 'contains', - 'notcontains', - 'containscs', - 'notcontainscs', - 'startswith', - 'has', - 'matches', - 'regex', - 'true', - 'false', - 'and', - 'or', - 'typeof', - 'int', - 'string', - 'date', - 'datetime', - 'time', - 'long', - 'real', - '​boolean', - 'bool', - // add some more keywords - 'where', - 'order', -]; - -// Kusto operators -// export const OPERATORS = ['+', '-', '*', '/', '>', '<', '==', '<>', '<=', '>=', '~', '!~']; - -export const DURATION = ['SECONDS', 'MINUTES', 'HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'YEARS']; - -const tokenizer = { - comment: { - pattern: /(^|[^\\:])\/\/.*/, - lookbehind: true, - greedy: true, - }, - 'function-context': { - pattern: /[a-z0-9_]+\([^)]*\)?/i, - inside: {}, - }, - duration: { - pattern: new RegExp(`${DURATION.join('?|')}?`, 'i'), - alias: 'number', - }, - builtin: new RegExp(`\\b(?:${FUNCTIONS.map(f => f.text).join('|')})(?=\\s*\\()`, 'i'), - string: { - pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, - greedy: true, - }, - keyword: new RegExp(`\\b(?:${KEYWORDS.join('|')}|\\*)\\b`, 'i'), - boolean: /\b(?:true|false)\b/, - number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i, - operator: /-|\+|\*|\/|>|<|==|<=?|>=?|<>|!~|~|=|\|/, - punctuation: /[{};(),.:]/, - variable: /(\[\[(.+?)\]\])|(\$(.+?))\b/, -}; - -tokenizer['function-context'].inside = { - argument: { - pattern: /[a-z0-9_]+(?=:)/i, - alias: 'symbol', - }, - duration: tokenizer.duration, - number: tokenizer.number, - builtin: tokenizer.builtin, - string: tokenizer.string, - variable: tokenizer.variable, -}; - -export default tokenizer; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto/kusto.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto/kusto.ts new file mode 100644 index 00000000000..e2a1142597b --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto/kusto.ts @@ -0,0 +1,355 @@ +/* tslint:disable:max-line-length */ +export const operatorTokens = [ + { text: "!between", hint: "Matches the input that is outside the inclusive range." }, + { text: "as", hint: "Binds a name to the operator's input tabular expression." }, + { text: "between", hint: "Matches the input that is inside the inclusive range." }, + { text: "consume", hint: "The `consume` operator consumes the tabular data stream handed to it. It is\r\nmostly used for triggering the query side-effect without actually returning\r\nthe results back to the caller." }, + { text: "count", hint: "Returns the number of records in the input record set." }, + { text: "datatable", hint: "Returns a table whose schema and values are defined in the query itself." }, + { text: "distinct", hint: "Produces a table with the distinct combination of the provided columns of the input table." }, + { text: "evaluate", hint: "Invokes a service-side query extension (plugin)." }, + { text: "extend", hint: "Create calculated columns and append them to the result set." }, + { text: "externaldata", hint: "Returns a table whose schema is defined in the query itself, and whose data is read from an external raw file." }, + { text: "facet", hint: "Returns a set of tables, one for each specified column.\r\nEach table specifies the list of values taken by its column.\r\nAn additional table can be created by using the `with` clause." }, + { text: "find", hint: "Finds rows that match a predicate across a set of tables." }, + { text: "fork", hint: "Runs multiple consumer operators in parallel." }, + { text: "getschema", hint: "Produce a table that represents a tabular schema of the input." }, + { text: "in", hint: "Filters a recordset based on the provided set of values." }, + { text: "invoke", hint: "Invokes lambda that receives the source of `invoke` as tabular parameter argument." }, + { text: "join", hint: "Merge the rows of two tables to form a new table by matching values of the specified column(s) from each table." }, + { text: "limit", hint: "Return up to the specified number of rows." }, + { text: "make-series", hint: "Create series of specified aggregated values along specified axis." }, + { text: "mvexpand", hint: "Expands multi-value array or property bag." }, + { text: "order", hint: "Sort the rows of the input table into order by one or more columns." }, + { text: "parse", hint: "Evaluates a string expression and parses its value into one or more calculated columns." }, + { text: "print", hint: "Evaluates one or more scalar expressions and inserts the results (as a single-row table with as many columns as there are expressions) into the output." }, + { text: "project", hint: "Select the columns to include, rename or drop, and insert new computed columns." }, + { text: "project-away", hint: "Select what columns to exclude from the input." }, + { text: "project-rename", hint: "Renames columns in the result output." }, + { text: "range", hint: "Generates a single-column table of values." }, + { text: "reduce", hint: "Groups a set of strings together based on values similarity." }, + { text: "render", hint: "Instructs the user agent to render the results of the query in a particular way." }, + { text: "sample", hint: "Returns up to the specified number of random rows from the input table." }, + { text: "sample-distinct", hint: "Returns a single column that contains up to the specified number of distinct values of the requested column." }, + { text: "search", hint: "The search operator provides a multi-table/multi-column search experience." }, + { text: "serialize", hint: "Marks that order of the input row set is safe for window functions usage." }, + { text: "sort", hint: "Sort the rows of the input table into order by one or more columns." }, + { text: "summarize", hint: "Produces a table that aggregates the content of the input table." }, + { text: "take", hint: "Return up to the specified number of rows." }, + { text: "top", hint: "Returns the first *N* records sorted by the specified columns." }, + { text: "top-hitters", hint: "Returns an approximation of the first *N* results (assuming skewed distribution of the input)." }, + { text: "top-nested", hint: "Produces hierarchical top results, where each level is a drill-down based on previous level values." }, + { text: "union", hint: "Takes two or more tables and returns the rows of all of them." }, + { text: "where", hint: "Filters a table to the subset of rows that satisfy a predicate." }, +]; + +export const functionTokens = [ + { text: "abs", hint: "Calculates the absolute value of the input." }, + { text: "acos", hint: "Returns the angle whose cosine is the specified number (the inverse operation of [`cos()`](cosfunction.md)) ." }, + { text: "ago", hint: "Subtracts the given timespan from the current UTC clock time." }, + { text: "any", hint: "Returns random non-empty value from the specified expression values." }, + { text: "arg_max", hint: "Finds a row in the group that maximizes *ExprToMaximize*, and returns the value of *ExprToReturn* (or `*` to return the entire row)." }, + { text: "arg_min", hint: "Finds a row in the group that minimizes *ExprToMinimize*, and returns the value of *ExprToReturn* (or `*` to return the entire row)." }, + { text: "argmax", hint: "Finds a row in the group that maximizes *ExprToMaximize*, and returns the value of *ExprToReturn* (or `*` to return the entire row)." }, + { text: "argmin", hint: "Finds a row in the group that minimizes *ExprToMinimize*, and returns the value of *ExprToReturn* (or `*` to return the entire row)." }, + { text: "array_concat", hint: "Concatenates a number of dynamic arrays to a single array." }, + { text: "array_length", hint: "Calculates the number of elements in a dynamic array." }, + { text: "array_slice", hint: "Extracts a slice of a dynamic array." }, + { text: "array_split", hint: "Splits an array to multiple arrays according to the split indices and packs the generated array in a dynamic array." }, + { text: "asin", hint: "Returns the angle whose sine is the specified number (the inverse operation of [`sin()`](sinfunction.md)) ." }, + { text: "assert", hint: "Checks for a condition; if the condition is false, outputs error messages and fails the query." }, + { text: "atan", hint: "Returns the angle whose tangent is the specified number (the inverse operation of [`tan()`](tanfunction.md)) ." }, + { text: "atan2", hint: "Calculates the angle, in radians, between the positive x-axis and the ray from the origin to the point (y, x)." }, + { text: "avg", hint: "Calculates the average of *Expr* across the group." }, + { text: "avgif", hint: "Calculates the [average](avg-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`." }, + { text: "bag_keys", hint: "Enumerates all the root keys in a dynamic property-bag object." }, + { text: "base64_decodestring", hint: "Decodes a base64 string to a UTF-8 string" }, + { text: "base64_encodestring", hint: "Encodes a string as base64 string" }, + { text: "beta_cdf", hint: "Returns the standard cumulative beta distribution function." }, + { text: "beta_inv", hint: "Returns the inverse of the beta cumulative probability beta density function." }, + { text: "beta_pdf", hint: "Returns the probability density beta function." }, + { text: "bin", hint: "Rounds values down to an integer multiple of a given bin size." }, + { text: "bin_at", hint: "Rounds values down to a fixed-size \'bin\', with control over the bin's starting point.\r\n(See also [`bin function`](./binfunction.md).)" }, + { text: "bin_auto", hint: "Rounds values down to a fixed-size \'bin\', with control over the bin size and starting point provided by a query property." }, + { text: "binary_and", hint: "Returns a result of the bitwise `and` operation between two values." }, + { text: "binary_not", hint: "Returns a bitwise negation of the input value." }, + { text: "binary_or", hint: "Returns a result of the bitwise `or` operation of the two values." }, + { text: "binary_shift_left", hint: "Returns binary shift left operation on a pair of numbers." }, + { text: "binary_shift_right", hint: "Returns binary shift right operation on a pair of numbers." }, + { text: "binary_xor", hint: "Returns a result of the bitwise `xor` operation of the two values." }, + { text: "buildschema", hint: "Returns the minimal schema that admits all values of *DynamicExpr*." }, + { text: "case", hint: "Evaluates a list of predicates and returns the first result expression whose predicate is satisfied." }, + { text: "ceiling", hint: "Calculates the smallest integer greater than, or equal to, the specified numeric expression." }, + { text: "cluster", hint: "Changes the reference of the query to a remote cluster." }, + { text: "coalesce", hint: "Evaluates a list of expressions and returns the first non-null (or non-empty for string) expression." }, + { text: "cos", hint: "Returns the cosine function." }, + { text: "cot", hint: "Calculates the trigonometric cotangent of the specified angle, in radians." }, + { text: "count", hint: "Returns a count of the records per summarization group (or in total if summarization is done without grouping)." }, + { text: "countif", hint: "Returns a count of rows for which *Predicate* evaluates to `true`." }, + { text: "countof", hint: "Counts occurrences of a substring in a string. Plain string matches may overlap; regex matches do not." }, + { text: "current_principal", hint: "Returns the current principal running this query." }, + { text: "cursor_after", hint: "A predicate over the records of a table to compare their ingestion time\r\nagainst a database cursor." }, + { text: "cursor_before_or_at", hint: "A predicate over the records of a table to compare their ingestion time\r\nagainst a database cursor." }, + { text: "database", hint: "Changes the reference of the query to a specific database within the cluster scope." }, + { text: "datetime_add", hint: "Calculates a new [datetime](./scalar-data-types/datetime.md) from a specified datepart multiplied by a specified amount, added to a specified [datetime](./scalar-data-types/datetime.md)." }, + { text: "datetime_diff", hint: "Calculates calendarian difference between two [datetime](./scalar-data-types/datetime.md) values." }, + { text: "datetime_part", hint: "Extracts the requested date part as an integer value." }, + { text: "dayofmonth", hint: "Returns the integer number representing the day number of the given month" }, + { text: "dayofweek", hint: "Returns the integer number of days since the preceding Sunday, as a `timespan`." }, + { text: "dayofyear", hint: "Returns the integer number represents the day number of the given year." }, + { text: "dcount", hint: "Returns an estimate of the number of distinct values of *Expr* in the group." }, + { text: "dcount_hll", hint: "Calculates the dcount from hll results (which was generated by [hll](hll-aggfunction.md) or [hll_merge](hll-merge-aggfunction.md))." }, + { text: "dcountif", hint: "Returns an estimate of the number of distinct values of *Expr* of rows for which *Predicate* evaluates to `true`." }, + { text: "degrees", hint: "Converts angle value in radians into value in degrees, using formula `degrees = (180 / PI ) * angle_in_radians`" }, + { text: "distance", hint: "Returns the distance between two points in meters." }, + { text: "endofday", hint: "Returns the end of the day containing the date, shifted by an offset, if provided." }, + { text: "endofmonth", hint: "Returns the end of the month containing the date, shifted by an offset, if provided." }, + { text: "endofweek", hint: "Returns the end of the week containing the date, shifted by an offset, if provided." }, + { text: "endofyear", hint: "Returns the end of the year containing the date, shifted by an offset, if provided." }, + { text: "estimate_data_size", hint: "Returns an estimated data size of the selected columns of the tabular expression." }, + { text: "exp", hint: "The base-e exponential function of x, which is e raised to the power x: e^x." }, + { text: "exp10", hint: "The base-10 exponential function of x, which is 10 raised to the power x: 10^x. \r\n**Syntax**" }, + { text: "exp2", hint: "The base-2 exponential function of x, which is 2 raised to the power x: 2^x." }, + { text: "extent_id", hint: "Returns a unique identifier that identifies the data shard (\"extent\") that the current record resides in." }, + { text: "extent_tags", hint: "Returns a dynamic array with the [tags](../management/extents-overview.md#extent-tagging) of the data shard (\"extent\") that the current record resides in." }, + { text: "extract", hint: "Get a match for a [regular expression](./re2.md) from a text string." }, + { text: "extract_all", hint: "Get all matches for a [regular expression](./re2.md) from a text string." }, + { text: "extractjson", hint: "Get a specified element out of a JSON text using a path expression." }, + { text: "floor", hint: "An alias for [`bin()`](binfunction.md)." }, + { text: "format_datetime", hint: "Formats a datetime parameter based on the format pattern parameter." }, + { text: "format_timespan", hint: "Formats a timespan parameter based on the format pattern parameter." }, + { text: "gamma", hint: "Computes [gamma function](https://en.wikipedia.org/wiki/Gamma_function)" }, + { text: "getmonth", hint: "Get the month number (1-12) from a datetime." }, + { text: "gettype", hint: "Returns the runtime type of its single argument." }, + { text: "getyear", hint: "Returns the year part of the `datetime` argument." }, + { text: "hash", hint: "Returns a hash value for the input value." }, + { text: "hash_sha256", hint: "Returns a sha256 hash value for the input value." }, + { text: "hll", hint: "Calculates the Intermediate results of [dcount](dcount-aggfunction.md) across the group." }, + { text: "hll_merge", hint: "Merges hll results (scalar version of the aggregate version [`hll_merge()`](hll-merge-aggfunction.md))." }, + { text: "hourofday", hint: "Returns the integer number representing the hour number of the given date" }, + { text: "iff", hint: "Evaluates the first argument (the predicate), and returns the value of either the second or third arguments, depending on whether the predicate evaluated to `true` (second) or `false` (third)." }, + { text: "iif", hint: "Evaluates the first argument (the predicate), and returns the value of either the second or third arguments, depending on whether the predicate evaluated to `true` (second) or `false` (third)." }, + { text: "indexof", hint: "Function reports the zero-based index of the first occurrence of a specified string within input string." }, + { text: "ingestion_time", hint: "Retrieves the record's `$IngestionTime` hidden `datetime` column, or null." }, + { text: "iscolumnexists", hint: "Returns a boolean value indicating if the given string argument exists in the schema produced by the preceding tabular operator." }, + { text: "isempty", hint: "Returns `true` if the argument is an empty string or is null." }, + { text: "isfinite", hint: "Returns whether input is a finite value (is neither infinite nor NaN)." }, + { text: "isinf", hint: "Returns whether input is an infinite (positive or negative) value." }, + { text: "isnan", hint: "Returns whether input is Not-a-Number (NaN) value." }, + { text: "isnotempty", hint: "Returns `true` if the argument is not an empty string nor it is a null." }, + { text: "isnotnull", hint: "Returns `true` if the argument is not null." }, + { text: "isnull", hint: "Evaluates its sole argument and returns a `bool` value indicating if the argument evaluates to a null value." }, + { text: "log", hint: "Returns the natural logarithm function." }, + { text: "log10", hint: "Returns the common (base-10) logarithm function." }, + { text: "log2", hint: "Returns the base-2 logarithm function." }, + { text: "loggamma", hint: "Computes log of absolute value of the [gamma function](https://en.wikipedia.org/wiki/Gamma_function)" }, + { text: "make_datetime", hint: "Creates a [datetime](./scalar-data-types/datetime.md) scalar value from the specified date and time." }, + { text: "make_dictionary", hint: "Returns a `dynamic` (JSON) property-bag (dictionary) of all the values of *Expr* in the group." }, + { text: "make_string", hint: "Returns the string generated by the Unicode characters." }, + { text: "make_timespan", hint: "Creates a [timespan](./scalar-data-types/timespan.md) scalar value from the specified time period." }, + { text: "makelist", hint: "Returns a `dynamic` (JSON) array of all the values of *Expr* in the group." }, + { text: "makeset", hint: "Returns a `dynamic` (JSON) array of the set of distinct values that *Expr* takes in the group." }, + { text: "materialize", hint: "Allows caching a sub-query result during the time of query execution in a way that other subqueries can reference the partial result." }, + { text: "max", hint: "Returns the maximum value across the group." }, + { text: "max_of", hint: "Returns the maximum value of several evaluated numeric expressions." }, + { text: "merge_tdigests", hint: "Merges tdigest results (scalar version of the aggregate version [`merge_tdigests()`](merge-tdigests-aggfunction.md))." }, + { text: "min", hint: "Returns the minimum value agross the group." }, + { text: "min_of", hint: "Returns the minimum value of several evaluated numeric expressions." }, + { text: "monthofyear", hint: "Returns the integer number represents the month number of the given year." }, + { text: "next", hint: "Returns the value of a column in a row that it at some offset following the\r\ncurrent row in a [serialized row set](./windowsfunctions.md#serialized-row-set)." }, + { text: "not", hint: "Reverses the value of its `bool` argument." }, + { text: "now", hint: "Returns the current UTC clock time, optionally offset by a given timespan.\r\nThis function can be used multiple times in a statement and the clock time being referenced will be the same for all instances." }, + { text: "pack", hint: "Creates a `dynamic` object (property bag) from a list of names and values." }, + { text: "pack_all", hint: "Creates a `dynamic` object (property bag) from all the columns of the tabular expression." }, + { text: "pack_array", hint: "Packs all input values into a dynamic array." }, + { text: "parse_ipv4", hint: "Converts input to integer (signed 64-bit) number representation." }, + { text: "parse_json", hint: "Interprets a `string` as a [JSON value](https://json.org/)) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md). \r\nIt is superior to using [extractjson() function](./extractjsonfunction.md)\r\nwhen you need to extract more than one element of a JSON compound object." }, + { text: "parse_path", hint: "Parses a file path `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object that contains the following parts of the path: \r\nScheme, RootPath, DirectoryPath, DirectoryName, FileName, Extension, AlternateDataStreamName.\r\nIn addition to the simple paths with both types of slashes, supports paths with schemas (e.g. \"file://...\"), shared paths (e.g. \"\\\\shareddrive\\users...\"), long paths (e.g \"\\\\?\\C:...\"\"), alternate data streams (e.g. \"file1.exe:file2.exe\")" }, + { text: "parse_url", hint: "Parses an absolute URL `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object contains all parts of the URL (Scheme, Host, Port, Path, Username, Password, Query Parameters, Fragment)." }, + { text: "parse_urlquery", hint: "Parses a url query `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object contains the Query parameters." }, + { text: "parse_user_agent", hint: "Interprets a user-agent string, which identifies the user's browser and provides certain system details to servers hosting the websites the user visits. The result is returned as [`dynamic`](./scalar-data-types/dynamic.md)." }, + { text: "parse_version", hint: "Converts input string representation of version to a comparable decimal number." }, + { text: "parse_xml", hint: "Interprets a `string` as a XML value, converts the value to a [JSON value](https://json.org/) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md)." }, + { text: "percentile", hint: "Returns an estimate for the specified [nearest-rank percentile](#nearest-rank-percentile) of the population defined by *Expr*. \r\nThe accuracy depends on the density of population in the region of the percentile." }, + { text: "percentile_tdigest", hint: "Calculates the percentile result from tdigest results (which was generated by [tdigest](tdigest-aggfunction.md) or [merge-tdigests](merge-tdigests-aggfunction.md))" }, + { text: "percentrank_tdigest", hint: "Calculates the approximate rank of the value in a set where rank is expressed as percentage of set's size. \r\nThis function can be viewed as the inverse of the percentile." }, + { text: "pi", hint: "Returns the constant value of Pi (π)." }, + { text: "point", hint: "Returns a dynamic array representation of a point." }, + { text: "pow", hint: "Returns a result of raising to power" }, + { text: "prev", hint: "Returns the value of a column in a row that it at some offset prior to the\r\ncurrent row in a [serialized row set](./windowsfunctions.md#serialized-row-set)." }, + { text: "radians", hint: "Converts angle value in degrees into value in radians, using formula `radians = (PI / 180 ) * angle_in_degrees`" }, + { text: "rand", hint: "Returns a random number." }, + { text: "range", hint: "Generates a dynamic array holding a series of equally-spaced values." }, + { text: "repeat", hint: "Generates a dynamic array holding a series of equal values." }, + { text: "replace", hint: "Replace all regex matches with another string." }, + { text: "reverse", hint: "Function makes reverse of input string." }, + { text: "round", hint: "Returns the rounded source to the specified precision." }, + { text: "row_cumsum", hint: "Calculates the cumulative sum of a column in a [serialized row set](./windowsfunctions.md#serialized-row-set)." }, + { text: "row_number", hint: "Returns the current row's index in a [serialized row set](./windowsfunctions.md#serialized-row-set).\r\nThe row index starts by default at `1` for the first row, and is incremented by `1` for each additional row.\r\nOptionally, the row index can start at a different value than `1`.\r\nAdditionally, the row index may be reset according to some provided predicate." }, + { text: "series_add", hint: "Calculates the element-wise addition of two numeric series inputs." }, + { text: "series_decompose", hint: "Applies a decomposition transformation on a series." }, + { text: "series_decompose_anomalies", hint: "Anomaly Detection based on series decomposition (refer to [series_decompose()](series-decomposefunction.md))" }, + { text: "series_decompose_forecast", hint: "Forecast based on series decomposition." }, + { text: "series_divide", hint: "Calculates the element-wise division of two numeric series inputs." }, + { text: "series_equals", hint: "Calculates the element-wise equals (`==`) logic operation of two numeric series inputs." }, + { text: "series_fill_backward", hint: "Performs backward fill interpolation of missing values in a series." }, + { text: "series_fill_const", hint: "Replaces missing values in a series with a specified constant value." }, + { text: "series_fill_forward", hint: "Performs forward fill interpolation of missing values in a series." }, + { text: "series_fill_linear", hint: "Performs linear interpolation of missing values in a series." }, + { text: "series_fir", hint: "Applies a Finite Impulse Response filter on a series." }, + { text: "series_fit_2lines", hint: "Applies two segments linear regression on a series, returning multiple columns." }, + { text: "series_fit_2lines_dynamic", hint: "Applies two segments linear regression on a series, returning dynamic object." }, + { text: "series_fit_line", hint: "Applies linear regression on a series, returning multiple columns." }, + { text: "series_fit_line_dynamic", hint: "Applies linear regression on a series, returning dynamic object." }, + { text: "series_greater", hint: "Calculates the element-wise greater (`>`) logic operation of two numeric series inputs." }, + { text: "series_greater_equals", hint: "Calculates the element-wise greater or equals (`>=`) logic operation of two numeric series inputs." }, + { text: "series_iir", hint: "Applies a Infinite Impulse Response filter on a series." }, + { text: "series_less", hint: "Calculates the element-wise less (`<`) logic operation of two numeric series inputs." }, + { text: "series_less_equals", hint: "Calculates the element-wise less or equal (`<=`) logic operation of two numeric series inputs." }, + { text: "series_multiply", hint: "Calculates the element-wise multiplication of two numeric series inputs." }, + { text: "series_not_equals", hint: "Calculates the element-wise not equals (`!=`) logic operation of two numeric series inputs." }, + { text: "series_outliers", hint: "Scores anomaly points in a series." }, + { text: "series_periods_detect", hint: "Finds the most significant periods that exist in a time series." }, + { text: "series_periods_validate", hint: "Checks whether a time series contains periodic patterns of given lengths." }, + { text: "series_seasonal", hint: "Calculates the seasonal component of a series according to the detected or given seasonal period." }, + { text: "series_stats", hint: "Returns statistics for a series in multiple columns." }, + { text: "series_stats_dynamic", hint: "Returns statistics for a series in dynamic object." }, + { text: "series_subtract", hint: "Calculates the element-wise subtraction of two numeric series inputs." }, + { text: "sign", hint: "Sign of a numeric expression" }, + { text: "sin", hint: "Returns the sine function." }, + { text: "split", hint: "Splits a given string according to a given delimiter and returns a string array with the contained substrings." }, + { text: "sqrt", hint: "Returns the square root function." }, + { text: "startofday", hint: "Returns the start of the day containing the date, shifted by an offset, if provided." }, + { text: "startofmonth", hint: "Returns the start of the month containing the date, shifted by an offset, if provided." }, + { text: "startofweek", hint: "Returns the start of the week containing the date, shifted by an offset, if provided." }, + { text: "startofyear", hint: "Returns the start of the year containing the date, shifted by an offset, if provided." }, + { text: "stdev", hint: "Calculates the standard deviation of *Expr* across the group, considering the group as a [sample](https://en.wikipedia.org/wiki/Sample_%28statistics%29)." }, + { text: "stdevif", hint: "Calculates the [stdev](stdev-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`." }, + { text: "stdevp", hint: "Calculates the standard deviation of *Expr* across the group, considering the group as a [population](https://en.wikipedia.org/wiki/Statistical_population)." }, + { text: "strcat", hint: "Concatenates between 1 and 64 arguments." }, + { text: "strcat_array", hint: "Creates a concatenated string of array values using specified delimiter." }, + { text: "strcat_delim", hint: "Concatenates between 2 and 64 arguments, with delimiter, provided as first argument." }, + { text: "strcmp", hint: "Compares two strings." }, + { text: "string_size", hint: "Returns the size, in bytes, of the input string." }, + { text: "strlen", hint: "Returns the length, in characters, of the input string." }, + { text: "strrep", hint: "Repeats given [string](./scalar-data-types/string.md) provided amount of times." }, + { text: "substring", hint: "Extracts a substring from a source string starting from some index to the end of the string." }, + { text: "sum", hint: "Calculates the sum of *Expr* across the group." }, + { text: "sumif", hint: "Returns a sum of *Expr* for which *Predicate* evaluates to `true`." }, + { text: "table", hint: "References specific table using an query-time evaluated string-expression." }, + { text: "tan", hint: "Returns the tangent function." }, + { text: "tdigest", hint: "Calculates the Intermediate results of [`percentiles()`](percentiles-aggfunction.md) across the group." }, + { text: "tdigest_merge", hint: "Merges tdigest results (scalar version of the aggregate version [`tdigest_merge()`](tdigest-merge-aggfunction.md))." }, + { text: "tobool", hint: "Converts input to boolean (signed 8-bit) representation." }, + { text: "todatetime", hint: "Converts input to [datetime](./scalar-data-types/datetime.md) scalar." }, + { text: "todecimal", hint: "Converts input to decimal number representation." }, + { text: "todouble", hint: "Converts the input to a value of type `real`. (`todouble()` and `toreal()` are synonyms.)" }, + { text: "todynamic", hint: "Interprets a `string` as a [JSON value](https://json.org/) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md)." }, + { text: "toguid", hint: "Converts input to [`guid`](./scalar-data-types/guid.md) representation." }, + { text: "tohex", hint: "Converts input to a hexadecimal string." }, + { text: "toint", hint: "Converts input to integer (signed 32-bit) number representation." }, + { text: "tolong", hint: "Converts input to long (signed 64-bit) number representation." }, + { text: "tolower", hint: "Converts input string to lower case." }, + { text: "toscalar", hint: "Returns a scalar constant value of the evaluated expression." }, + { text: "tostring", hint: "Converts input to a string representation." }, + { text: "totimespan", hint: "Converts input to [timespan](./scalar-data-types/timespan.md) scalar." }, + { text: "toupper", hint: "Converts a string to upper case." }, + { text: "translate", hint: "Replaces a set of characters ('searchList') with another set of characters ('replacementList') in a given a string.\r\nThe function searches for characters in the 'searchList' and replaces them with the corresponding characters in 'replacementList'" }, + { text: "treepath", hint: "Enumerates all the path expressions that identify leaves in a dynamic object." }, + { text: "trim", hint: "Removes all leading and trailing matches of the specified regular expression." }, + { text: "trim_end", hint: "Removes trailing match of the specified regular expression." }, + { text: "trim_start", hint: "Removes leading match of the specified regular expression." }, + { text: "url_decode", hint: "The function converts encoded URL into a to regular URL representation." }, + { text: "url_encode", hint: "The function converts characters of the input URL into a format that can be transmitted over the Internet." }, + { text: "variance", hint: "Calculates the variance of *Expr* across the group, considering the group as a [sample](https://en.wikipedia.org/wiki/Sample_%28statistics%29)." }, + { text: "varianceif", hint: "Calculates the [variance](variance-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`." }, + { text: "variancep", hint: "Calculates the variance of *Expr* across the group, considering the group as a [population](https://en.wikipedia.org/wiki/Statistical_population)." }, + { text: "weekofyear", hint: "Returns the integer number represents the week number." }, + { text: "welch_test", hint: "Computes the p_value of the [Welch-test function](https://en.wikipedia.org/wiki/Welch%27s_t-test)" }, + { text: "zip", hint: "The `zip` function accepts any number of `dynamic` arrays, and returns an\r\narray whose elements are each an array holding the elements of the input\r\narrays of the same index." }, +]; + +export const KEYWORDS = [ + 'by', + 'on', + 'contains', + 'notcontains', + 'containscs', + 'notcontainscs', + 'startswith', + 'has', + 'matches', + 'regex', + 'true', + 'false', + 'and', + 'or', + 'typeof', + 'int', + 'string', + 'date', + 'datetime', + 'time', + 'long', + 'real', + '​boolean', + 'bool', +]; + +export const grafanaMacros = [ + { text: '$__timeFilter', display: '$__timeFilter()', hint: 'Macro that uses the selected timerange in Grafana to filter the query.', }, + { text: '$__escapeMulti', display: '$__escapeMulti()', hint: 'Macro to escape multi-value template variables that contain illegal characters.', }, + { text: '$__contains', display: '$__contains()', hint: 'Macro for multi-value template variables.' }, +]; + +// Kusto operators +// export const OPERATORS = ['+', '-', '*', '/', '>', '<', '==', '<>', '<=', '>=', '~', '!~']; + +export const DURATION = ['SECONDS', 'MINUTES', 'HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'YEARS']; + +const tokenizer = { + comment: { + pattern: /(^|[^\\:])\/\/.*/, + lookbehind: true, + greedy: true, + }, + 'function-context': { + pattern: /[a-z0-9_]+\([^)]*\)?/i, + inside: {}, + }, + duration: { + pattern: new RegExp(`${DURATION.join('?|')}?`, 'i'), + alias: 'number', + }, + builtin: new RegExp(`\\b(?:${functionTokens.map(f => f.text).join('|')})(?=\\s*\\()`, 'i'), + string: { + pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, + greedy: true, + }, + keyword: new RegExp(`\\b(?:${KEYWORDS.join('|')}|${operatorTokens.map(f => f.text).join('|')}|\\*)\\b`, 'i'), + boolean: /\b(?:true|false)\b/, + number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i, + operator: /-|\+|\*|\/|>|<|==|<=?|>=?|<>|!~|~|=|\|/, + punctuation: /[{};(),.:]/, + variable: /(\[\[(.+?)\]\])|(\$(.+?))\b/, +}; + +tokenizer['function-context'].inside = { + argument: { + pattern: /[a-z0-9_]+(?=:)/i, + alias: 'symbol', + }, + duration: tokenizer.duration, + number: tokenizer.number, + builtin: tokenizer.builtin, + string: tokenizer.string, + variable: tokenizer.variable, +}; + +// console.log(tokenizer.builtin); + +export default tokenizer; + +// function escapeRegExp(str: string): string { +// return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +// } From 0c3657da7e41f4d895cbc7f32eda87695bbb25f9 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 30 Jan 2019 15:24:37 +0300 Subject: [PATCH 059/770] azuremonitor: suggest tables initially --- .../editor/KustoQueryField.tsx | 90 ++++++++++++++----- .../editor/editor_component.tsx | 8 +- .../partials/query.editor.html | 1 + .../query_ctrl.ts | 4 +- 4 files changed, 79 insertions(+), 24 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index fa79d4bdb99..c8f96fba211 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -1,3 +1,4 @@ +import _ from 'lodash'; import Plain from 'slate-plain-serializer'; import QueryField from './query_field'; @@ -25,21 +26,43 @@ interface SuggestionGroup { skipFilter?: boolean; } +interface KustoSchema { + Databases: { + Default?: KustoDBSchema; + }; + Plugins?: any[]; +} + +interface KustoDBSchema { + Name?: string; + Functions?: any; + Tables?: any; +} + +const defaultSchema = () => ({ + Databases: { + Default: {} + } +}); + const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); const wrapText = text => ({ text }); export default class KustoQueryField extends QueryField { fields: any; events: any; + schema: KustoSchema; constructor(props, context) { super(props, context); + this.schema = defaultSchema(); this.onTypeahead = debounce(this.onTypeahead, TYPEAHEAD_DELAY); } componentDidMount() { this.updateMenu(); + this.fetchSchema(); } onTypeahead = () => { @@ -128,7 +151,13 @@ export default class KustoQueryField extends QueryField { suggestionGroups = this._getKeywordSuggestions(); } else if (Plain.serialize(this.state.value) === '') { typeaheadContext = 'context-new'; - suggestionGroups = this._getInitialSuggestions(); + if (this.schema) { + suggestionGroups = this._getInitialSuggestions(); + } else { + this.fetchSchema(); + setTimeout(this.onTypeahead, 0); + return; + } } let results = 0; @@ -263,7 +292,7 @@ export default class KustoQueryField extends QueryField { { prefixMatch: true, label: 'Operators', - items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) + items: operatorTokens }, { prefixMatch: true, @@ -274,34 +303,46 @@ export default class KustoQueryField extends QueryField { prefixMatch: true, label: 'Macros', items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Tables', + items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name })) } ]; } private _getInitialSuggestions(): SuggestionGroup[] { - // TODO: return datbase tables as an initial suggestion return [ { prefixMatch: true, - label: 'Keywords', - items: KEYWORDS.map(wrapText) - }, - { - prefixMatch: true, - label: 'Operators', - items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) - }, - { - prefixMatch: true, - label: 'Functions', - items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) - }, - { - prefixMatch: true, - label: 'Macros', - items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) + label: 'Tables', + items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name })) } ]; + + // return [ + // { + // prefixMatch: true, + // label: 'Keywords', + // items: KEYWORDS.map(wrapText) + // }, + // { + // prefixMatch: true, + // label: 'Operators', + // items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) + // }, + // { + // prefixMatch: true, + // label: 'Functions', + // items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) + // }, + // { + // prefixMatch: true, + // label: 'Macros', + // items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) + // } + // ]; } private async _fetchEvents() { @@ -329,4 +370,13 @@ export default class KustoQueryField extends QueryField { // Stub this.fields = []; } + + private async fetchSchema() { + const schema = await this.props.getSchema(); + if (schema) { + this.schema = schema; + } else { + this.schema = defaultSchema(); + } + } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx index 59e4ab12c81..7787f029ee7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx @@ -31,7 +31,7 @@ class Editor extends Component { }; render() { - const { request, variables } = this.props; + const { request, variables, getSchema } = this.props; const { edited, query } = this.state; return ( @@ -45,6 +45,7 @@ class Editor extends Component { placeholder="Enter a query" request={request} templateVariables={variables} + getSchema={getSchema} />
); @@ -54,6 +55,9 @@ class Editor extends Component { coreModule.directive('kustoEditor', [ 'reactDirective', reactDirective => { - return reactDirective(Editor, ['change', 'database', 'execute', 'query', 'request', 'variables']); + return reactDirective(Editor, [ + 'change', 'database', 'execute', 'query', 'request', 'variables', + ['getSchema', { watchDepth: 'reference' }] + ]); }, ]); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html index 49f02ec8355..592fccdcda9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html @@ -130,6 +130,7 @@ change="ctrl.onLogAnalyticsQueryChange" execute="ctrl.onLogAnalyticsQueryExecute" variables="ctrl.templateVariables" + getSchema="ctrl.getAzureLogAnalyticsSchema" />
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts index fd42c172f11..b3aa5f9f6e9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts @@ -304,7 +304,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { /* Azure Log Analytics */ - getWorkspaces() { + getWorkspaces = () => { return this.datasource.azureLogAnalyticsDatasource .getWorkspaces() .then(list => { @@ -316,7 +316,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { .catch(this.handleQueryCtrlError.bind(this)); } - getAzureLogAnalyticsSchema() { + getAzureLogAnalyticsSchema = () => { return this.getWorkspaces() .then(() => { return this.datasource.azureLogAnalyticsDatasource.getSchema(this.target.azureLogAnalytics.workspace); From df9ecc68162ae678a8f22acda868b06fb0433f0d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 31 Jan 2019 17:44:00 +0300 Subject: [PATCH 060/770] azuremonitor: don't go back to dashboard if escape pressed in the editor --- public/app/core/services/keybindingSrv.ts | 18 ++++++++++++++++++ .../editor/KustoQueryField.tsx | 2 +- .../editor/query_field.tsx | 14 ++++++++++++++ public/app/routes/GrafanaCtrl.ts | 3 +++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 989746fd067..6d790baa336 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -139,6 +139,10 @@ export class KeybindingSrv { ); } + unbind(keyArg: string, keyType?: string) { + Mousetrap.unbind(keyArg, keyType); + } + showDashEditView() { const search = _.extend(this.$location.search(), { editview: 'settings' }); this.$location.search(search); @@ -293,3 +297,17 @@ export class KeybindingSrv { } coreModule.service('keybindingSrv', KeybindingSrv); + +/** + * Code below exports the service to react components + */ + +let singletonInstance: KeybindingSrv; + +export function setKeybindingSrv(instance: KeybindingSrv) { + singletonInstance = instance; +} + +export function getKeybindingSrv(): KeybindingSrv { + return singletonInstance; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index c8f96fba211..719d57b9b6a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -61,7 +61,7 @@ export default class KustoQueryField extends QueryField { } componentDidMount() { - this.updateMenu(); + super.componentDidMount(); this.fetchSchema(); } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 1c883a40c31..0acd53cabff 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -9,6 +9,7 @@ import NewlinePlugin from './slate-plugins/newline'; import RunnerPlugin from './slate-plugins/runner'; import Typeahead from './typeahead'; +import { getKeybindingSrv, KeybindingSrv } from 'app/core/services/keybindingSrv'; import { Block, Document, Text, Value } from 'slate'; import { Editor } from 'slate-react'; @@ -61,6 +62,7 @@ class QueryField extends React.Component { menuEl: any; plugins: any; resetTimer: any; + keybindingSrv: KeybindingSrv = getKeybindingSrv(); constructor(props, context) { super(props, context); @@ -90,6 +92,7 @@ class QueryField extends React.Component { } componentWillUnmount() { + this.restoreEscapeKeyBinding(); clearTimeout(this.resetTimer); } @@ -218,6 +221,7 @@ class QueryField extends React.Component { if (onBlur) { onBlur(); } + this.restoreEscapeKeyBinding(); }; handleFocus = () => { @@ -225,8 +229,18 @@ class QueryField extends React.Component { if (onFocus) { onFocus(); } + // Don't go back to dashboard if Escape pressed inside the editor. + this.removeEscapeKeyBinding(); }; + removeEscapeKeyBinding() { + this.keybindingSrv.unbind('esc', 'keydown'); + } + + restoreEscapeKeyBinding() { + this.keybindingSrv.setupGlobal(); + } + onClickItem = item => { const { suggestions } = this.state; if (!suggestions || suggestions.length === 0) { diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 70bdf49e5e4..e50abdc0710 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -10,6 +10,7 @@ import appEvents from 'app/core/app_events'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { TimeSrv, setTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { KeybindingSrv, setKeybindingSrv } from 'app/core/services/keybindingSrv'; import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader'; import { configureStore } from 'app/store/configureStore'; @@ -25,6 +26,7 @@ export class GrafanaCtrl { backendSrv: BackendSrv, timeSrv: TimeSrv, datasourceSrv: DatasourceSrv, + keybindingSrv: KeybindingSrv, angularLoader: AngularLoader ) { // make angular loader service available to react components @@ -32,6 +34,7 @@ export class GrafanaCtrl { setBackendSrv(backendSrv); setDatasourceSrv(datasourceSrv); setTimeSrv(timeSrv); + setKeybindingSrv(keybindingSrv); configureStore(); $scope.init = () => { From ad821cf6296b224d3995e1473e0db6533e1fed0c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 31 Jan 2019 20:23:40 +0300 Subject: [PATCH 061/770] azuremonitor: where clause autocomplete --- .../editor/KustoQueryField.tsx | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 719d57b9b6a..33be370ada3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -11,7 +11,7 @@ import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto // import '../sass/editor.base.scss'; -const TYPEAHEAD_DELAY = 500; +const TYPEAHEAD_DELAY = 100; interface Suggestion { text: string; @@ -104,12 +104,13 @@ export default class KustoQueryField extends QueryField { this._fetchFields(); return; } - } else if (modelPrefix.match(/(facet\s$)/i)) { - typeaheadContext = 'context-facet'; - if (this.fields) { - suggestionGroups = this._getKeywordSuggestions(); + } else if (modelPrefix.match(/(where\s$)/i)) { + typeaheadContext = 'context-where'; + const fullQuery = Plain.serialize(this.state.value); + const table = this.getTableFromContext(fullQuery); + if (table) { + suggestionGroups = this.getWhereSuggestions(table); } else { - this._fetchFields(); return; } } else if (modelPrefix.match(/(,\s*$)/)) { @@ -345,6 +346,35 @@ export default class KustoQueryField extends QueryField { // ]; } + private getWhereSuggestions(table: string): SuggestionGroup[] { + const tableSchema = this.schema.Databases.Default.Tables[table]; + if (tableSchema) { + return [ + { + prefixMatch: true, + label: 'Fields', + items: _.map(tableSchema.OrderedColumns, (f: any) => ({ + text: f.Name, + hint: f.Type + })) + } + ]; + } else { + return []; + } + } + + private getTableFromContext(query: string) { + const tablePattern = /^\s*(\w+)\s*|/g; + const normalizedQuery = normalizeQuery(query); + const match = tablePattern.exec(normalizedQuery); + if (match && match.length > 1 && match[0] && match[1]) { + return match[1]; + } else { + return null; + } + } + private async _fetchEvents() { // const query = 'events'; // const result = await this.request(query); @@ -380,3 +410,10 @@ export default class KustoQueryField extends QueryField { } } } + +function normalizeQuery(query: string): string { + const commentPattern = /\/\/.*$/gm; + let normalizedQuery = query.replace(commentPattern, ''); + normalizedQuery = normalizedQuery.replace('\n', ' '); + return normalizedQuery; +} From d433ca7d40d0dfc4a154163334efbe6efeea7cc6 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 13:10:32 +0100 Subject: [PATCH 062/770] fix util for splitting host and port Now you can provide both a default host and a default port --- pkg/services/sqlstore/sqlstore.go | 5 +- pkg/tsdb/mssql/mssql.go | 5 +- pkg/util/ip.go | 25 --------- pkg/util/ip_address.go | 47 +++++++++++++---- pkg/util/ip_address_test.go | 84 ++++++++++++++++++++++++++++++- pkg/util/ip_test.go | 43 ---------------- 6 files changed, 121 insertions(+), 88 deletions(-) delete mode 100644 pkg/util/ip.go delete mode 100644 pkg/util/ip_test.go diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index fb0f0938573..6debaca89a1 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -242,10 +242,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) { cnnstr += ss.buildExtraConnectionString('&') case migrator.POSTGRES: - host, port, err := util.SplitIPPort(ss.dbCfg.Host, "5432") - if err != nil { - return "", err - } + host, port := util.SplitHostPortDefault(ss.dbCfg.Host, "127.0.0.1", "5432") if ss.dbCfg.Pwd == "" { ss.dbCfg.Pwd = "''" } diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index bd4510f6cf3..12f2b6c03c9 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -49,10 +49,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) { } } - server, port, err := util.SplitIPPort(datasource.Url, "1433") - if err != nil { - return "", err - } + server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433") encrypt := datasource.JsonData.Get("encrypt").MustString("false") connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;", diff --git a/pkg/util/ip.go b/pkg/util/ip.go deleted file mode 100644 index d3809318191..00000000000 --- a/pkg/util/ip.go +++ /dev/null @@ -1,25 +0,0 @@ -package util - -import ( - "net" -) - -// SplitIPPort splits the ip string and port. -func SplitIPPort(ipStr string, portDefault string) (ip string, port string, err error) { - ipAddr := net.ParseIP(ipStr) - - if ipAddr == nil { - // Port was included - ip, port, err = net.SplitHostPort(ipStr) - - if err != nil { - return "", "", err - } - } else { - // No port was included - ip = ipAddr.String() - port = portDefault - } - - return ip, port, nil -} diff --git a/pkg/util/ip_address.go b/pkg/util/ip_address.go index d8d95ef3acd..b5ffb361e0b 100644 --- a/pkg/util/ip_address.go +++ b/pkg/util/ip_address.go @@ -7,23 +7,48 @@ import ( // ParseIPAddress parses an IP address and removes port and/or IPV6 format func ParseIPAddress(input string) string { - s := input - lastIndex := strings.LastIndex(input, ":") + host, _ := SplitHostPort(input) - if lastIndex != -1 { - if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" { - s = input[:lastIndex] - } + ip := net.ParseIP(host) + + if ip == nil { + return host } - s = strings.Replace(s, "[", "", -1) - s = strings.Replace(s, "]", "", -1) - - ip := net.ParseIP(s) - if ip.IsLoopback() { return "127.0.0.1" } return ip.String() } + +// SplitHostPortDefault splits ip address/hostname string by host and port. Defaults used if no match found +func SplitHostPortDefault(input, defaultHost, defaultPort string) (host string, port string) { + port = defaultPort + s := input + lastIndex := strings.LastIndex(input, ":") + + if lastIndex != -1 { + if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" { + s = input[:lastIndex] + port = input[lastIndex+1:] + } else if lastIndex == 0 { + s = defaultHost + port = input[lastIndex+1:] + } + } else { + port = defaultPort + } + + s = strings.Replace(s, "[", "", -1) + s = strings.Replace(s, "]", "", -1) + port = strings.Replace(port, "[", "", -1) + port = strings.Replace(port, "]", "", -1) + + return s, port +} + +// SplitHostPort splits ip address/hostname string by host and port +func SplitHostPort(input string) (host string, port string) { + return SplitHostPortDefault(input, "", "") +} diff --git a/pkg/util/ip_address_test.go b/pkg/util/ip_address_test.go index fd3e3ea8587..b926de1a36b 100644 --- a/pkg/util/ip_address_test.go +++ b/pkg/util/ip_address_test.go @@ -9,8 +9,90 @@ import ( func TestParseIPAddress(t *testing.T) { Convey("Test parse ip address", t, func() { So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140") + So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140") So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1") So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1") - So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140") + So(ParseIPAddress("::1"), ShouldEqual, "127.0.0.1") + So(ParseIPAddress("::1:123"), ShouldEqual, "127.0.0.1") + }) +} + +func TestSplitHostPortDefault(t *testing.T) { + Convey("Test split ip address to host and port", t, func() { + host, port := SplitHostPortDefault("192.168.0.140:456", "", "") + So(host, ShouldEqual, "192.168.0.140") + So(port, ShouldEqual, "456") + + host, port = SplitHostPortDefault("192.168.0.140", "", "123") + So(host, ShouldEqual, "192.168.0.140") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault("[::1:456]", "", "") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "456") + + host, port = SplitHostPortDefault("[::1]", "", "123") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault("::1:123", "", "") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault("::1", "", "123") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault(":456", "1.2.3.4", "") + So(host, ShouldEqual, "1.2.3.4") + So(port, ShouldEqual, "456") + + host, port = SplitHostPortDefault("xyz.rds.amazonaws.com", "", "123") + So(host, ShouldEqual, "xyz.rds.amazonaws.com") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault("xyz.rds.amazonaws.com:123", "", "") + So(host, ShouldEqual, "xyz.rds.amazonaws.com") + So(port, ShouldEqual, "123") + }) +} + +func TestSplitHostPort(t *testing.T) { + Convey("Test split ip address to host and port", t, func() { + host, port := SplitHostPort("192.168.0.140:456") + So(host, ShouldEqual, "192.168.0.140") + So(port, ShouldEqual, "456") + + host, port = SplitHostPort("192.168.0.140") + So(host, ShouldEqual, "192.168.0.140") + So(port, ShouldEqual, "") + + host, port = SplitHostPort("[::1:456]") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "456") + + host, port = SplitHostPort("[::1]") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "") + + host, port = SplitHostPort("::1:123") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "123") + + host, port = SplitHostPort("::1") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "") + + host, port = SplitHostPort(":456") + So(host, ShouldEqual, "") + So(port, ShouldEqual, "456") + + host, port = SplitHostPort("xyz.rds.amazonaws.com") + So(host, ShouldEqual, "xyz.rds.amazonaws.com") + So(port, ShouldEqual, "") + + host, port = SplitHostPort("xyz.rds.amazonaws.com:123") + So(host, ShouldEqual, "xyz.rds.amazonaws.com") + So(port, ShouldEqual, "123") }) } diff --git a/pkg/util/ip_test.go b/pkg/util/ip_test.go deleted file mode 100644 index 3a62a080e26..00000000000 --- a/pkg/util/ip_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package util - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestSplitIPPort(t *testing.T) { - - Convey("When parsing an IPv4 without explicit port", t, func() { - ip, port, err := SplitIPPort("1.2.3.4", "5678") - - So(err, ShouldEqual, nil) - So(ip, ShouldEqual, "1.2.3.4") - So(port, ShouldEqual, "5678") - }) - - Convey("When parsing an IPv6 without explicit port", t, func() { - ip, port, err := SplitIPPort("::1", "5678") - - So(err, ShouldEqual, nil) - So(ip, ShouldEqual, "::1") - So(port, ShouldEqual, "5678") - }) - - Convey("When parsing an IPv4 with explicit port", t, func() { - ip, port, err := SplitIPPort("1.2.3.4:56", "78") - - So(err, ShouldEqual, nil) - So(ip, ShouldEqual, "1.2.3.4") - So(port, ShouldEqual, "56") - }) - - Convey("When parsing an IPv6 with explicit port", t, func() { - ip, port, err := SplitIPPort("[::1]:56", "78") - - So(err, ShouldEqual, nil) - So(ip, ShouldEqual, "::1") - So(port, ShouldEqual, "56") - }) - -} From 96aef3bab878644a16091d4522302361ebea99f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 13:41:29 +0100 Subject: [PATCH 063/770] Replaced intialQueris with queryKeys --- public/app/core/utils/explore.ts | 11 ++++++++- public/app/features/explore/Explore.tsx | 10 ++++---- public/app/features/explore/QueryRows.tsx | 9 +++---- public/app/features/explore/state/reducers.ts | 24 +++++++++++++++---- public/app/types/explore.ts | 5 ++++ 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 7a9f54a0cae..efa54b7bc23 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -11,7 +11,7 @@ import { colors } from '@grafana/ui'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; // Types -import { RawTimeRange, IntervalValues, DataQuery } from '@grafana/ui/src/types'; +import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui/src/types'; import TimeSeries from 'app/core/time_series2'; import { ExploreUrlState, @@ -304,3 +304,12 @@ export function clearHistory(datasourceId: string) { const historyKey = `grafana.explore.history.${datasourceId}`; store.delete(historyKey); } + +export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => { + const queryKeys = queries.reduce((newQueryKeys, query, index) => { + const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key; + return newQueryKeys.concat(`${primaryKey}-${index}`); + }, []); + + return queryKeys; +}; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 36c1f7f5ad7..2012a52c338 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -38,7 +38,6 @@ interface ExploreProps { datasourceLoading: boolean | null; datasourceMissing: boolean; exploreId: ExploreId; - initialQueries: DataQuery[]; initializeExplore: typeof initializeExplore; initialized: boolean; modifyQueries: typeof modifyQueries; @@ -55,6 +54,7 @@ interface ExploreProps { supportsLogs: boolean | null; supportsTable: boolean | null; urlState: ExploreUrlState; + queryKeys: string[]; } /** @@ -175,12 +175,12 @@ export class Explore extends React.PureComponent { datasourceLoading, datasourceMissing, exploreId, - initialQueries, showingStartPage, split, supportsGraph, supportsLogs, supportsTable, + queryKeys, } = this.props; const exploreClass = split ? 'explore explore-split' : 'explore'; @@ -201,7 +201,7 @@ export class Explore extends React.PureComponent { {datasourceInstance && !datasourceError && (
- + {({ width }) => (
@@ -243,13 +243,13 @@ function mapStateToProps(state: StoreState, { exploreId }) { datasourceInstance, datasourceLoading, datasourceMissing, - initialQueries, initialized, range, showingStartPage, supportsGraph, supportsLogs, supportsTable, + queryKeys, } = item; return { StartPage, @@ -257,7 +257,6 @@ function mapStateToProps(state: StoreState, { exploreId }) { datasourceInstance, datasourceLoading, datasourceMissing, - initialQueries, initialized, range, showingStartPage, @@ -265,6 +264,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { supportsGraph, supportsLogs, supportsTable, + queryKeys, }; } diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index d65c1283bd6..4b5a16ef781 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -6,24 +6,21 @@ import QueryRow from './QueryRow'; // Types import { Emitter } from 'app/core/utils/emitter'; -import { DataQuery } from '@grafana/ui/src/types'; import { ExploreId } from 'app/types/explore'; interface QueryRowsProps { className?: string; exploreEvents: Emitter; exploreId: ExploreId; - initialQueries: DataQuery[]; + queryKeys: string[]; } export default class QueryRows extends PureComponent { render() { - const { className = '', exploreEvents, exploreId, initialQueries } = this.props; + const { className = '', exploreEvents, exploreId, queryKeys } = this.props; return (
- {initialQueries.map((query, index) => { - // using query.key will introduce infinite loop because QueryEditor#53 - const key = query.datasource ? `${query.datasource}-${index}` : query.key; + {queryKeys.map((key, index) => { return ; })}
diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 9343cf0ec57..f7eca489b6e 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -3,6 +3,7 @@ import { generateEmptyQuery, getIntervals, ensureQueries, + getQueryKeys, } from 'app/core/utils/explore'; import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { DataQuery } from '@grafana/ui/src/types'; @@ -72,6 +73,7 @@ export const makeExploreItemState = (): ExploreItemState => ({ supportsGraph: null, supportsLogs: null, supportsTable: null, + queryKeys: [], }); /** @@ -109,6 +111,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta initialQueries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, + queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; }, }) @@ -130,6 +133,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta ...state, initialQueries: nextQueries, queryTransactions: nextQueryTransactions, + queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; }, }) @@ -161,6 +165,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta initialQueries: queries.slice(), queryTransactions: [], showingStartPage: Boolean(state.StartPage), + queryKeys: getQueryKeys(queries, state.datasourceInstance), }; }, }) @@ -183,6 +188,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta range, initialQueries: queries, initialized: true, + queryKeys: getQueryKeys(queries, state.datasourceInstance), }; }, }) @@ -190,8 +196,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: updateDatasourceInstanceAction, mapper: (state, action): ExploreItemState => { const { datasourceInstance } = action.payload; - return { ...state, datasourceInstance }; - /*datasourceName: datasourceInstance.name removed after refactor, datasourceName does not exists on ExploreItemState */ + return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.initialQueries, datasourceInstance) }; }, }) .addMapper({ @@ -281,6 +286,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, initialQueries: nextQueries, + queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), queryTransactions: nextQueryTransactions, }; }, @@ -348,6 +354,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta initialQueries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, + queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; }, }) @@ -387,7 +394,11 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: setQueriesAction, mapper: (state, action): ExploreItemState => { const { queries } = action.payload; - return { ...state, initialQueries: queries.slice() }; + return { + ...state, + initialQueries: queries.slice(), + queryKeys: getQueryKeys(queries, state.datasourceInstance), + }; }, }) .addMapper({ @@ -436,7 +447,12 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: queriesImportedAction, mapper: (state, action): ExploreItemState => { - return { ...state, initialQueries: action.payload.queries }; + const { queries } = action.payload; + return { + ...state, + initialQueries: queries, + queryKeys: getQueryKeys(queries, state.datasourceInstance), + }; }, }) .create(); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 4e099480cf0..8faf0d2ed09 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -232,6 +232,11 @@ export interface ExploreItemState { * Table model that combines all query table results into a single table. */ tableResult?: TableModel; + + /** + * React keys for rendering of QueryRows + */ + queryKeys: string[]; } export interface ExploreUrlState { From ae768193e3b78532ff7b2c2dbdbac2e7ff1fff05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 13:49:14 +0100 Subject: [PATCH 064/770] Now handles all dashbord routes --- public/app/core/reducers/location.ts | 4 +- public/app/core/services/bridge_srv.ts | 4 + .../dashboard/containers/DashboardPage.tsx | 8 +- .../services/DashboardViewStateSrv.ts | 12 +-- .../features/dashboard/state/initDashboard.ts | 100 ++++++++++++++---- public/app/routes/ReactContainer.tsx | 1 + public/app/routes/routes.ts | 25 +++-- public/app/types/dashboard.ts | 9 +- public/app/types/location.ts | 5 + 9 files changed, 132 insertions(+), 36 deletions(-) diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 6b39710dcca..c038ab53c9f 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -8,12 +8,13 @@ export const initialState: LocationState = { path: '', query: {}, routeParams: {}, + replace: false, }; export const locationReducer = (state = initialState, action: Action): LocationState => { switch (action.type) { case CoreActionTypes.UpdateLocation: { - const { path, routeParams } = action.payload; + const { path, routeParams, replace } = action.payload; let query = action.payload.query || state.query; if (action.payload.partial) { @@ -26,6 +27,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS path: path || state.path, query: { ...query }, routeParams: routeParams || state.routeParams, + replace: replace === true, }; } } diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 37f71946364..8bb828310cf 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -46,6 +46,10 @@ export class BridgeSrv { if (angularUrl !== url) { this.$timeout(() => { this.$location.url(url); + // some state changes should not trigger new browser history + if (state.location.replace) { + this.$location.replace(); + } }); console.log('store updating angular $location.url', url); } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index e01998f65a9..3fbac681498 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -22,9 +22,8 @@ import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; // Types -import { StoreState } from 'app/types'; +import { StoreState, DashboardLoadingState, DashboardRouteInfo } from 'app/types'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { DashboardLoadingState } from 'app/types/dashboard'; interface Props { urlUid?: string; @@ -32,8 +31,10 @@ interface Props { urlType?: string; editview?: string; urlPanelId?: string; + urlFolderId?: string; $scope: any; $injector: any; + routeInfo: DashboardRouteInfo; urlEdit: boolean; urlFullscreen: boolean; loadingState: DashboardLoadingState; @@ -66,6 +67,8 @@ export class DashboardPage extends PureComponent { urlSlug: this.props.urlSlug, urlUid: this.props.urlUid, urlType: this.props.urlType, + urlFolderId: this.props.urlFolderId, + routeInfo: this.props.routeInfo, }); } @@ -208,6 +211,7 @@ const mapStateToProps = (state: StoreState) => ({ urlType: state.location.routeParams.type, editview: state.location.query.editview, urlPanelId: state.location.query.panelId, + urlFolderId: state.location.query.folderId, urlFullscreen: state.location.query.fullscreen === true, urlEdit: state.location.query.edit === true, loadingState: state.dashboard.loadingState, diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts index f5a68d6f647..aa64a2e93cf 100644 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.ts +++ b/public/app/features/dashboard/services/DashboardViewStateSrv.ts @@ -23,10 +23,10 @@ export class DashboardViewStateSrv { self.dashboard = $scope.dashboard; $scope.onAppEvent('$routeUpdate', () => { - const urlState = self.getQueryStringState(); - if (self.needsSync(urlState)) { - self.update(urlState, true); - } + // const urlState = self.getQueryStringState(); + // if (self.needsSync(urlState)) { + // self.update(urlState, true); + // } }); $scope.onAppEvent('panel-change-view', (evt, payload) => { @@ -35,8 +35,8 @@ export class DashboardViewStateSrv { // this marks changes to location during this digest cycle as not to add history item // don't want url changes like adding orgId to add browser history - $location.replace(); - this.update(this.getQueryStringState()); + // $location.replace(); + // this.update(this.getQueryStringState()); } needsSync(urlState) { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index a39a7fce285..d497ef92d1f 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -15,7 +15,7 @@ import { setDashboardLoadingState, ThunkResult, setDashboardModel } from './acti import { removePanel } from '../utils/panel'; // Types -import { DashboardLoadingState } from 'app/types/dashboard'; +import { DashboardLoadingState, DashboardRouteInfo } from 'app/types'; import { DashboardModel } from './DashboardModel'; export interface InitDashboardArgs { @@ -24,6 +24,8 @@ export interface InitDashboardArgs { urlUid?: string; urlSlug?: string; urlType?: string; + urlFolderId: string; + routeInfo: string; } async function redirectToNewUrl(slug: string, dispatch: any) { @@ -35,36 +37,67 @@ async function redirectToNewUrl(slug: string, dispatch: any) { } } -export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { - return async dispatch => { - // handle old urls with no uid - if (!urlUid && urlSlug && !urlType) { - redirectToNewUrl(urlSlug, dispatch); - return; - } - +export function initDashboard({ + $injector, + $scope, + urlUid, + urlSlug, + urlType, + urlFolderId, + routeInfo, +}: InitDashboardArgs): ThunkResult { + return async (dispatch, getState) => { let dashDTO = null; // set fetching state dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); try { - // if no uid or slug, load home dashboard - if (!urlUid && !urlSlug) { - dashDTO = await getBackendSrv().get('/api/dashboards/home'); - - if (dashDTO.redirectUri) { - const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri); - dispatch(updateLocation({ path: newUrl })); + switch (routeInfo) { + // handle old urls with no uid + case DashboardRouteInfo.Old: { + redirectToNewUrl(urlSlug, dispatch); return; - } else { + } + case DashboardRouteInfo.Home: { + // load home dash + dashDTO = await getBackendSrv().get('/api/dashboards/home'); + + // if user specified a custom home dashboard redirect to that + if (dashDTO.redirectUri) { + const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri); + dispatch(updateLocation({ path: newUrl, replace: true })); + return; + } + + // disable some actions on the default home dashboard dashDTO.meta.canSave = false; dashDTO.meta.canShare = false; dashDTO.meta.canStar = false; + break; + } + case DashboardRouteInfo.Normal: { + const loaderSrv = $injector.get('dashboardLoaderSrv'); + dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + + // check if the current url is correct (might be old slug) + const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); + const currentPath = getState().location.path; + console.log('loading dashboard: currentPath', currentPath); + console.log('loading dashboard: dashboardUrl', dashboardUrl); + + if (dashboardUrl !== currentPath) { + // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. + dispatch(updateLocation({path: dashboardUrl, partial: true, replace: true})); + return; + } + + break; + } + case DashboardRouteInfo.New: { + dashDTO = getNewDashboardModelData(urlFolderId); + break; } - } else { - const loaderSrv = $injector.get('dashboardLoaderSrv'); - dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); } } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); @@ -136,3 +169,30 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I dispatch(setDashboardModel(dashboard)); }; } + +function getNewDashboardModelData(urlFolderId?: string): any { + const data = { + meta: { + canStar: false, + canShare: false, + isNew: true, + folderId: 0, + }, + dashboard: { + title: 'New dashboard', + panels: [ + { + type: 'add-panel', + gridPos: { x: 0, y: 0, w: 12, h: 9 }, + title: 'Panel Title', + }, + ], + }, + }; + + if (urlFolderId) { + data.meta.folderId = parseInt(urlFolderId, 10); + } + + return data; +} diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index 2cad3d828bf..a56c8878fb1 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -44,6 +44,7 @@ export function reactContainer( $injector: $injector, $rootScope: $rootScope, $scope: scope, + routeInfo: $route.current.$$route.routeInfo, }; ReactDOM.render(WrapInProvider(store, component, props), elem[0]); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index abe347d689a..ecd934cdccf 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -2,6 +2,7 @@ import './dashboard_loaders'; import './ReactContainer'; import { applyRouteRegistrationHandlers } from './registry'; +// Pages import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import TeamPages from 'app/features/teams/TeamPages'; @@ -23,6 +24,9 @@ import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage'; import DashboardPage from '../features/dashboard/containers/DashboardPage'; import config from 'app/core/config'; +// Types +import { DashboardRouteInfo } from 'app/types'; + /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { $locationProvider.html5Mode(true); @@ -31,6 +35,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/', { template: '', pageClass: 'page-dashboard', + routeInfo: DashboardRouteInfo.Home, reloadOnSearch: false, resolve: { component: () => DashboardPage, @@ -39,6 +44,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/d/:uid/:slug', { template: '', pageClass: 'page-dashboard', + routeInfo: DashboardRouteInfo.Normal, reloadOnSearch: false, resolve: { component: () => DashboardPage, @@ -48,6 +54,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { template: '', pageClass: 'page-dashboard', reloadOnSearch: false, + routeInfo: DashboardRouteInfo.Normal, resolve: { component: () => DashboardPage, }, @@ -55,6 +62,16 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboard/:type/:slug', { template: '', pageClass: 'page-dashboard', + routeInfo: DashboardRouteInfo.Old, + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, + }) + .when('/dashboard/new', { + template: '', + pageClass: 'page-dashboard', + routeInfo: DashboardRouteInfo.New, reloadOnSearch: false, resolve: { component: () => DashboardPage, @@ -63,6 +80,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/d-solo/:uid/:slug', { template: '', pageClass: 'dashboard-solo', + routeInfo: DashboardRouteInfo.Normal, resolve: { component: () => SoloPanelPage, }, @@ -70,16 +88,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboard-solo/:type/:slug', { template: '', pageClass: 'dashboard-solo', + routeInfo: DashboardRouteInfo.Old, resolve: { component: () => SoloPanelPage, }, }) - .when('/dashboard/new', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'NewDashboardCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', - }) .when('/dashboard/import', { templateUrl: 'public/app/features/manage-dashboards/partials/dashboard_import.html', controller: DashboardImportCtrl, diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 9b1e750e859..9b8f539aeb2 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -7,9 +7,16 @@ export interface MutableDashboard { }; } +export enum DashboardRouteInfo { + Old = 'old-dashboard', + Home = 'home-dashboard', + New = 'new-dashboard', + Normal = 'normal-dashboard', +} + export enum DashboardLoadingState { NotStarted = 'Not started', - Fetching = 'Fetching', + Fetching = 'Fetching', Initializing = 'Initializing', Error = 'Error', Done = 'Done', diff --git a/public/app/types/location.ts b/public/app/types/location.ts index 7dcf57f7e02..a47ef05d2be 100644 --- a/public/app/types/location.ts +++ b/public/app/types/location.ts @@ -3,6 +3,10 @@ export interface LocationUpdate { query?: UrlQueryMap; routeParams?: UrlQueryMap; partial?: boolean; + /* + * If true this will replace url state (ie cause no new browser history) + */ + replace?: boolean; } export interface LocationState { @@ -10,6 +14,7 @@ export interface LocationState { path: string; query: UrlQueryMap; routeParams: UrlQueryMap; + replace: boolean; } export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; From 34dd1a22ab51e145211bef9a8414e55baee164c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 14:16:15 +0100 Subject: [PATCH 065/770] Fixed bug with removing a QueryRow thats not part of nextQueries --- public/app/features/explore/state/reducers.ts | 7 ++++--- public/app/store/configureStore.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index f7eca489b6e..86c263e39e9 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -331,7 +331,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: removeQueryRowAction, mapper: (state, action): ExploreItemState => { - const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; + const { datasourceInstance, initialQueries, queryIntervals, queryTransactions, queryKeys } = state; const { index } = action.payload; if (initialQueries.length <= 1) { @@ -339,9 +339,10 @@ export const itemReducer = reducerFactory({} as ExploreItemSta } const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)]; // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key)); const results = calculateResultsFromQueryTransactions( nextQueryTransactions, datasourceInstance, @@ -354,7 +355,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta initialQueries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, - queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), + queryKeys: nextQueryKeys, }; }, }) diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index dc9a478adf3..570a387cd74 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,6 +1,6 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; -// import { createLogger } from 'redux-logger'; +import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; @@ -39,7 +39,7 @@ export function configureStore() { if (process.env.NODE_ENV !== 'production') { // DEV builds we had the logger middleware - setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); + setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())))); } else { setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); } From 7634e0423159485366afe89d3ff67445f3837759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 14:45:13 +0100 Subject: [PATCH 066/770] Fixed template variable value changed handling --- .../components/AdHocFilters/AdHocFiltersCtrl.ts | 6 ++++-- .../dashboard/components/DashboardRow/DashboardRow.tsx | 4 ++-- .../dashboard/components/SubMenu/template.html | 2 +- public/app/features/dashboard/state/DashboardModel.ts | 5 +++++ public/app/features/dashboard/state/initDashboard.ts | 1 - .../app/features/templating/specs/variable_srv.test.ts | 1 - .../templating/specs/variable_srv_init.test.ts | 5 +---- public/app/features/templating/variable_srv.ts | 10 +++++----- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts b/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts index 0ceac9ddbba..a7616e0e513 100644 --- a/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts +++ b/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts @@ -1,10 +1,12 @@ import _ from 'lodash'; import angular from 'angular'; import coreModule from 'app/core/core_module'; +import { DashboardModel } from 'app/features/dashboard/state'; export class AdHocFiltersCtrl { segments: any; variable: any; + dashboard: DashboardModel; removeTagFilterSegment: any; /** @ngInject */ @@ -14,14 +16,13 @@ export class AdHocFiltersCtrl { private $q, private variableSrv, $scope, - private $rootScope ) { this.removeTagFilterSegment = uiSegmentSrv.newSegment({ fake: true, value: '-- remove filter --', }); this.buildSegmentModel(); - this.$rootScope.onAppEvent('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope); + this.dashboard.events.on('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope); } buildSegmentModel() { @@ -171,6 +172,7 @@ export function adHocFiltersComponent() { controllerAs: 'ctrl', scope: { variable: '=', + dashboard: '=', }, }; } diff --git a/public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx b/public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx index e7778a31fdb..bb63cea90ea 100644 --- a/public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx +++ b/public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx @@ -18,11 +18,11 @@ export class DashboardRow extends React.Component { collapsed: this.props.panel.collapsed, }; - appEvents.on('template-variable-value-updated', this.onVariableUpdated); + this.props.dashboard.on('template-variable-value-updated', this.onVariableUpdated); } componentWillUnmount() { - appEvents.off('template-variable-value-updated', this.onVariableUpdated); + this.props.dashboard.off('template-variable-value-updated', this.onVariableUpdated); } onVariableUpdated = () => { diff --git a/public/app/features/dashboard/components/SubMenu/template.html b/public/app/features/dashboard/components/SubMenu/template.html index 5d0f200d862..1ccbfcc915c 100644 --- a/public/app/features/dashboard/components/SubMenu/template.html +++ b/public/app/features/dashboard/components/SubMenu/template.html @@ -7,7 +7,7 @@
- +
diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 8d96a2eec73..ab9d764358c 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -900,4 +900,9 @@ export class DashboardModel { panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; }); } + + templateVariableValueUpdated() { + this.processRepeats(); + this.events.emit('template-variable-value-updated'); + } } diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index d497ef92d1f..a697a8e77f9 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -91,7 +91,6 @@ export function initDashboard({ dispatch(updateLocation({path: dashboardUrl, partial: true, replace: true})); return; } - break; } case DashboardRouteInfo.New: { diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index db42df7f516..cf10235f6e8 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -48,7 +48,6 @@ describe('VariableSrv', function(this: any) { ds.metricFindQuery = () => Promise.resolve(scenario.queryResult); ctx.variableSrv = new VariableSrv( - ctx.$rootScope, $q, ctx.$location, ctx.$injector, diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index b8cabf711ac..d256ab28c2c 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -25,9 +25,6 @@ describe('VariableSrv init', function(this: any) { }; const $injector = {} as any; - const $rootscope = { - $on: () => {}, - }; let ctx = {} as any; @@ -54,7 +51,7 @@ describe('VariableSrv init', function(this: any) { }; // @ts-ignore - ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv, timeSrv); + ctx.variableSrv = new VariableSrv($q, {}, $injector, templateSrv, timeSrv); $injector.instantiate = (variable, model) => { return getVarMockConstructor(variable, model, ctx); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index b2f8b43fb08..81e4e0a4a0b 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -18,18 +18,18 @@ export class VariableSrv { variables: any[]; /** @ngInject */ - constructor(private $rootScope, - private $q, + constructor(private $q, private $location, private $injector, private templateSrv: TemplateSrv, private timeSrv: TimeSrv) { - $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope); + } init(dashboard: DashboardModel) { this.dashboard = dashboard; this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this)); + this.dashboard.events.on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this)); // create working class models representing variables this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this)); @@ -59,7 +59,7 @@ export class VariableSrv { return variable.updateOptions().then(() => { if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) { - this.$rootScope.$emit('template-variable-value-updated'); + this.dashboard.templateVariableValueUpdated(); } }); }); @@ -144,7 +144,7 @@ export class VariableSrv { return this.$q.all(promises).then(() => { if (emitChangeEvents) { - this.$rootScope.appEvent('template-variable-value-updated'); + this.dashboard.templateVariableValueUpdated(); this.dashboard.startRefresh(); } }); From f695975f651457454bc5f6ec94455487ca12da0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 15:02:35 +0100 Subject: [PATCH 067/770] Fixed handling of orgId --- public/app/core/config.ts | 2 +- public/app/features/dashboard/services/DashboardSrv.ts | 1 + public/app/features/dashboard/state/initDashboard.ts | 8 +++++++- .../features/templating/specs/variable_srv_init.test.ts | 1 - public/app/types/user.ts | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 395e40e914b..368b3798117 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -68,5 +68,5 @@ const bootData = (window as any).grafanaBootData || { const options = bootData.settings; options.bootData = bootData; -const config = new Settings(options); +export const config = new Settings(options); export default config; diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 7d3dfb68cd8..532e2e1c828 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -9,6 +9,7 @@ export class DashboardSrv { /** @ngInject */ constructor(private backendSrv, private $rootScope, private $location) { appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); + appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); } create(dashboard, meta) { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index a697a8e77f9..14d6196d69c 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -6,6 +6,7 @@ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { AnnotationsSrv } from 'app/features/annotations/annotations_srv'; import { VariableSrv } from 'app/features/templating/variable_srv'; import { KeybindingSrv } from 'app/core/services/keybindingSrv'; +import { config } from 'app/core/config'; // Actions import { updateLocation } from 'app/core/actions'; @@ -88,7 +89,7 @@ export function initDashboard({ if (dashboardUrl !== currentPath) { // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. - dispatch(updateLocation({path: dashboardUrl, partial: true, replace: true})); + dispatch(updateLocation({ path: dashboardUrl, partial: true, replace: true })); return; } break; @@ -117,6 +118,11 @@ export function initDashboard({ return; } + // add missing orgId query param + if (!getState().location.query.orgId) { + dispatch(updateLocation({ query: { orgId: config.bootData.user.orgId }, partial: true, replace: true })); + } + // init services const timeSrv: TimeSrv = $injector.get('timeSrv'); const annotationsSrv: AnnotationsSrv = $injector.get('annotationsSrv'); diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index d256ab28c2c..480b5207c17 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -25,7 +25,6 @@ describe('VariableSrv init', function(this: any) { }; const $injector = {} as any; - let ctx = {} as any; function describeInitScenario(desc, fn) { diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 37c80074dca..365411147bb 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,4 +1,4 @@ -import { DashboardSearchHit } from './search'; +import { DashboardSearchHit } from './search'; export interface OrgUser { avatarUrl: string; From bc21c9520fafee7c6a0380389d695526aa921fd8 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Feb 2019 15:07:11 +0100 Subject: [PATCH 068/770] fix: Data source picker in panel queries options should overlap content below, including ace scrollbar #15122 --- public/sass/components/_toolbar.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/components/_toolbar.scss b/public/sass/components/_toolbar.scss index 14db85f7e65..36be8a18739 100644 --- a/public/sass/components/_toolbar.scss +++ b/public/sass/components/_toolbar.scss @@ -4,7 +4,6 @@ align-items: center; padding: 3px 20px 3px 20px; position: relative; - z-index: 1; flex: 0 0 auto; background: $toolbar-bg; border-radius: 3px; From f74ebdade663d727d153fb0b15a76c9cb0de6693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 15:11:19 +0100 Subject: [PATCH 069/770] Missed to save --- public/app/features/explore/state/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index a41c0701994..a0357315484 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -710,7 +710,7 @@ const togglePanelActionCreator = ( ) => (exploreId: ExploreId) => { return (dispatch, getState) => { let shouldRunQueries; - dispatch(actionCreator); + dispatch(actionCreator({ exploreId })); dispatch(stateSave()); switch (actionCreator.type) { From 648bec1807b9685693b8664ed730d5f5b9975434 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Feb 2019 15:19:23 +0100 Subject: [PATCH 070/770] fix: Set ace editor min height to avoid problem with scrollbar overlapping ace content #15122 --- public/sass/components/_code_editor.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_code_editor.scss b/public/sass/components/_code_editor.scss index 4f50495789d..a9c7ebf2e75 100644 --- a/public/sass/components/_code_editor.scss +++ b/public/sass/components/_code_editor.scss @@ -7,7 +7,7 @@ &.ace_editor { @include font-family-monospace(); font-size: 1rem; - min-height: 2.6rem; + min-height: 3.6rem; // Include space for horizontal scrollbar @include border-radius($input-border-radius-sm); border: $input-btn-border-width solid $input-border-color; From 716258339625e8b5e36f2615537517a2b2e8f362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 15:41:57 +0100 Subject: [PATCH 071/770] Made dashboard view state srv panel view state obsolete --- public/app/core/services/keybindingSrv.ts | 6 +- .../dashboard/containers/DashboardPage.tsx | 8 +-- .../dashboard/services/DashboardSrv.ts | 39 +++++++++++ .../services/DashboardViewStateSrv.test.ts | 64 ------------------- .../services/DashboardViewStateSrv.ts | 2 +- .../dashboard/state/DashboardModel.ts | 2 + 6 files changed, 49 insertions(+), 72 deletions(-) delete mode 100644 public/app/features/dashboard/services/DashboardViewStateSrv.test.ts diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index aa39763841e..dfacc483b8e 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -104,7 +104,7 @@ export class KeybindingSrv { } if (search.fullscreen) { - this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false }); + appEvents.emit('panel-change-view', { fullscreen: false, edit: false }); return; } @@ -174,7 +174,7 @@ export class KeybindingSrv { // edit panel this.bind('e', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { - this.$rootScope.appEvent('panel-change-view', { + appEvents.emit('panel-change-view', { fullscreen: true, edit: true, panelId: dashboard.meta.focusPanelId, @@ -186,7 +186,7 @@ export class KeybindingSrv { // view panel this.bind('v', () => { if (dashboard.meta.focusPanelId) { - this.$rootScope.appEvent('panel-change-view', { + appEvents.emit('panel-change-view', { fullscreen: true, edit: null, panelId: dashboard.meta.focusPanelId, diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 3fbac681498..ec143d735ab 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -94,7 +94,7 @@ export class DashboardPage extends PureComponent { // } // Sync url state with model - if (urlFullscreen !== dashboard.meta.isFullscreen || urlEdit !== dashboard.meta.isEditing) { + if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) { // entering fullscreen/edit mode if (urlPanelId) { const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); @@ -102,6 +102,7 @@ export class DashboardPage extends PureComponent { if (panel) { dashboard.setViewMode(panel, urlFullscreen, urlEdit); this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel }); + this.setPanelFullscreenClass(urlFullscreen); } else { this.handleFullscreenPanelNotFound(urlPanelId); } @@ -110,10 +111,9 @@ export class DashboardPage extends PureComponent { if (this.state.fullscreenPanel) { dashboard.setViewMode(this.state.fullscreenPanel, urlFullscreen, urlEdit); } - this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: null }); + this.setState({ isEditing: false, isFullscreen: false, fullscreenPanel: null }); + this.setPanelFullscreenClass(false); } - - this.setPanelFullscreenClass(urlFullscreen); } } diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 532e2e1c828..e2e524941f8 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -10,6 +10,7 @@ export class DashboardSrv { constructor(private backendSrv, private $rootScope, private $location) { appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); + appEvents.on('panel-change-view', this.onPanelChangeView); } create(dashboard, meta) { @@ -24,6 +25,44 @@ export class DashboardSrv { return this.dash; } + onPanelChangeView = (options) => { + const urlParams = this.$location.search(); + + // handle toggle logic + if (options.fullscreen === urlParams.fullscreen) { + // I hate using these truthy converters (!!) but in this case + // I think it's appropriate. edit can be null/false/undefined and + // here i want all of those to compare the same + if (!!options.edit === !!urlParams.edit) { + delete urlParams.fullscreen; + delete urlParams.edit; + delete urlParams.panelId; + this.$location.search(urlParams); + return; + } + } + + if (options.fullscreen) { + urlParams.fullscreen = true; + } else { + delete urlParams.fullscreen; + } + + if (options.edit) { + urlParams.edit = true; + } else { + delete urlParams.edit; + } + + if (options.panelId) { + urlParams.panelId = options.panelId; + } else { + delete urlParams.panelId; + } + + this.$location.search(urlParams); + }; + handleSaveDashboardError(clone, options, err) { options = options || {}; options.overwrite = true; diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.test.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.test.ts deleted file mode 100644 index 12bb11b7a08..00000000000 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import config from 'app/core/config'; -import { DashboardViewStateSrv } from './DashboardViewStateSrv'; -import { DashboardModel } from '../state/DashboardModel'; - -describe('when updating view state', () => { - const location = { - replace: jest.fn(), - search: jest.fn(), - }; - - const $scope = { - appEvent: jest.fn(), - onAppEvent: jest.fn(() => {}), - dashboard: new DashboardModel({ - panels: [{ id: 1 }], - }), - }; - - let viewState; - - beforeEach(() => { - config.bootData = { - user: { - orgId: 1, - }, - }; - }); - - describe('to fullscreen true and edit true', () => { - beforeEach(() => { - location.search = jest.fn(() => { - return { fullscreen: true, edit: true, panelId: 1 }; - }); - viewState = new DashboardViewStateSrv($scope, location, {}); - }); - - it('should update querystring and view state', () => { - const updateState = { fullscreen: true, edit: true, panelId: 1 }; - - viewState.update(updateState); - - expect(location.search).toHaveBeenCalledWith({ - edit: true, - editview: null, - fullscreen: true, - orgId: 1, - panelId: 1, - }); - expect(viewState.dashboard.meta.fullscreen).toBe(true); - expect(viewState.state.fullscreen).toBe(true); - }); - }); - - describe('to fullscreen false', () => { - beforeEach(() => { - viewState = new DashboardViewStateSrv($scope, location, {}); - }); - it('should remove params from query string', () => { - viewState.update({ fullscreen: true, panelId: 1, edit: true }); - viewState.update({ fullscreen: false }); - expect(viewState.state.fullscreen).toBe(null); - }); - }); -}); diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts index aa64a2e93cf..7cb4c1de7ab 100644 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.ts +++ b/public/app/features/dashboard/services/DashboardViewStateSrv.ts @@ -30,7 +30,7 @@ export class DashboardViewStateSrv { }); $scope.onAppEvent('panel-change-view', (evt, payload) => { - self.update(payload); + // self.update(payload); }); // this marks changes to location during this digest cycle as not to add history item diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index ab9d764358c..a6d45bb3cc0 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -132,6 +132,8 @@ export class DashboardModel { meta.canEdit = meta.canEdit !== false; meta.showSettings = meta.canEdit; meta.canMakeEditable = meta.canSave && !this.editable; + meta.fullscreen = false; + meta.isEditing = false; if (!this.editable) { meta.canEdit = false; From fdeea9144ccc62c1be15663e215edb5dc7261ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 15:50:47 +0100 Subject: [PATCH 072/770] fixed unit test --- .../dashboard/components/DashboardRow/DashboardRow.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx b/public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx index 9ac6a6b74e1..96b673242e4 100644 --- a/public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx +++ b/public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx @@ -9,6 +9,7 @@ describe('DashboardRow', () => { beforeEach(() => { dashboardMock = { toggleRow: jest.fn(), + on: jest.fn(), meta: { canEdit: true, }, From b9c58d88dc381dc096a66bc9eadcc6489cf70588 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 4 Feb 2019 16:48:27 +0100 Subject: [PATCH 073/770] basic layout --- .../AddPanelWidget/AddPanelWidget.tsx | 29 ++++++++++--------- .../AddPanelWidget/_AddPanelWidget.scss | 22 +++++++++++++- public/sass/base/_icons.scss | 2 +- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 8c1ab93cec1..7a9767666c0 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -125,13 +125,20 @@ export class AddPanelWidget extends React.Component { dashboard.removePanel(this.props.panel); }; + renderOptionLink = (icon, text, onClick) => { + return ( + + ); + }; + render() { - let addCopyButton; - - if (this.state.copiedPanelPlugins.length === 1) { - addCopyButton = this.copyButton(this.state.copiedPanelPlugins[0]); - } - return (
@@ -142,13 +149,9 @@ export class AddPanelWidget extends React.Component {
- - {addCopyButton} - + {this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} + {this.renderOptionLink('visualization', 'Choose Panel type', this.onCreateNewPanel)} + {this.renderOptionLink('queries', 'Convert to row', this.onCreateNewRow)}
diff --git a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss index 5a1cbee4b44..587daa2703f 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss +++ b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss @@ -26,6 +26,26 @@ } } +.add-panel-widget__link { + display: block; + margin: 0 8px; + width: 130px; + text-align: center; + padding: 8px 0; +} + +.add-panel-widget__icon { + margin-bottom: 8px; + + .gicon { + color: white; + height: 44px; + width: 53px; + position: relative; + left: 5px; + } +} + .add-panel-widget__close { margin-left: auto; background-color: transparent; @@ -39,7 +59,7 @@ justify-content: center; align-items: center; height: 100%; - flex-direction: column; + flex-direction: row; .btn { margin-bottom: 10px; diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index a60259ac0f2..a2649b31fcd 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -212,7 +212,7 @@ padding-right: 5px; } -.panel-editor-tabs { +.panel-editor-tabs, .add-panel-widget__icon { .gicon-advanced-active { background-image: url('../img/icons_#{$theme-name}_theme/icon_advanced_active.svg'); } From dd8ca70151672293b48267a800b2e3682e8a79c5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Feb 2019 18:51:56 +0300 Subject: [PATCH 074/770] azuremonitor: use kusto editor for App Insights --- .../app_insights/app_insights_datasource.ts | 9 +++++++ .../app_insights/response_parser.ts | 26 +++++++++++++++++++ .../editor/KustoQueryField.tsx | 14 +++++++++- .../editor/editor_component.tsx | 22 ++++++++++++---- .../partials/query.editor.html | 15 ++++++++--- .../query_ctrl.ts | 14 ++++++++++ 6 files changed, 91 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts index 950fa73a16b..97f76d229fb 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts @@ -224,4 +224,13 @@ export default class AppInsightsDatasource { return new ResponseParser(result).parseGroupBys(); }); } + + getQuerySchema() { + const url = `${this.baseUrl}/query/schema`; + return this.doRequest(url).then(result => { + const schema = new ResponseParser(result).parseQuerySchema(); + // console.log(schema); + return schema; + }); + } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts index 848472cf101..fa96e4a2e3e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts @@ -199,6 +199,32 @@ export default class ResponseParser { return ResponseParser.toTextValueList(this.results.supportedGroupBy); } + parseQuerySchema() { + const result = { + Type: 'AppInsights', + Tables: {} + }; + if (this.results && this.results.data && this.results.data.Tables) { + for (let i = 0; i < this.results.data.Tables[0].Rows.length; i++) { + const column = this.results.data.Tables[0].Rows[i]; + const columnTable = column[0]; + const columnName = column[1]; + const columnType = column[2]; + if (result.Tables[columnTable]) { + result.Tables[columnTable].OrderedColumns.push({ Name: columnName, Type: columnType }); + } else { + result.Tables[columnTable] = { + Name: columnTable, + OrderedColumns: [ + { Name: columnName, Type: columnType } + ] + }; + } + } + } + return result; + } + static toTextValueList(values) { const list: any[] = []; for (let i = 0; i < values.length; i++) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 33be370ada3..09573f29047 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -402,8 +402,11 @@ export default class KustoQueryField extends QueryField { } private async fetchSchema() { - const schema = await this.props.getSchema(); + let schema = await this.props.getSchema(); if (schema) { + if (schema.Type === 'AppInsights') { + schema = castSchema(schema); + } this.schema = schema; } else { this.schema = defaultSchema(); @@ -411,6 +414,15 @@ export default class KustoQueryField extends QueryField { } } +/** + * Cast schema from App Insights to default Kusto schema + */ +function castSchema(schema) { + const defaultSchemaTemplate = defaultSchema(); + defaultSchemaTemplate.Databases.Default = schema; + return defaultSchemaTemplate; +} + function normalizeQuery(query: string): string { const commentPattern = /\/\/.*$/gm; let normalizedQuery = query.replace(commentPattern, ''); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx index 7787f029ee7..bdc85f1577d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx @@ -4,7 +4,20 @@ import Kusto from './kusto/kusto'; import React, { Component } from 'react'; import coreModule from 'app/core/core_module'; -class Editor extends Component { +interface EditorProps { + index: number; + placeholder?: string; + change: (value: string, index: number) => void; + variables: () => string[] | string[]; + getSchema?: () => Promise; + execute?: () => void; +} + +class Editor extends Component { + static defaultProps = { + placeholder: 'Enter a query' + }; + constructor(props) { super(props); this.state = { @@ -31,7 +44,7 @@ class Editor extends Component { }; render() { - const { request, variables, getSchema } = this.props; + const { variables, getSchema, placeholder } = this.props; const { edited, query } = this.state; return ( @@ -42,8 +55,7 @@ class Editor extends Component { onQueryChange={this.onChangeQuery} prismLanguage="kusto" prismDefinition={Kusto} - placeholder="Enter a query" - request={request} + placeholder={placeholder} templateVariables={variables} getSchema={getSchema} /> @@ -56,7 +68,7 @@ coreModule.directive('kustoEditor', [ 'reactDirective', reactDirective => { return reactDirective(Editor, [ - 'change', 'database', 'execute', 'query', 'request', 'variables', + 'change', 'database', 'execute', 'query', 'variables', 'placeholder', ['getSchema', { watchDepth: 'reference' }] ]); }, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html index 592fccdcda9..6299947b30a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html @@ -124,8 +124,6 @@
-
+ +
+
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts index b3aa5f9f6e9..cee67d11ab3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts @@ -345,6 +345,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { } return interval; } + getAppInsightsMetricNames() { if (!this.datasource.appInsightsDatasource.isConfigured()) { return; @@ -377,6 +378,19 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { .catch(this.handleQueryCtrlError.bind(this)); } + onAppInsightsQueryChange = (nextQuery: string) => { + this.target.appInsights.rawQueryString = nextQuery; + } + + onAppInsightsQueryExecute = () => { + return this.refresh(); + } + + getAppInsightsQuerySchema = () => { + return this.datasource.appInsightsDatasource.getQuerySchema() + .catch(this.handleQueryCtrlError.bind(this)); + } + getAppInsightsGroupBySegments(query) { return _.map(this.target.appInsights.groupByOptions, option => { return { text: option, value: option }; From f6b46f7a34c822508605ffca9da8c564c26fb209 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 4 Feb 2019 17:18:46 +0100 Subject: [PATCH 075/770] prepping go to visualization --- .../components/AddPanelWidget/AddPanelWidget.tsx | 9 +++++---- .../features/dashboard/panel_editor/VisualizationTab.tsx | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 7a9767666c0..b3d5e6167c4 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -62,20 +62,21 @@ export class AddPanelWidget extends React.Component { ); } - moveToEdit(panel) { + moveToEdit(panel, tab) { reduxStore.dispatch( updateLocation({ query: { panelId: panel.id, edit: true, fullscreen: true, + tab: tab, }, partial: true, }) ); } - onCreateNewPanel = () => { + onCreateNewPanel = (tab = 'queries') => { const dashboard = this.props.dashboard; const { gridPos } = this.props.panel; @@ -88,7 +89,7 @@ export class AddPanelWidget extends React.Component { dashboard.addPanel(newPanel); dashboard.removePanel(this.props.panel); - this.moveToEdit(newPanel); + this.moveToEdit(newPanel, tab); }; onPasteCopiedPanel = panelPluginInfo => { @@ -150,7 +151,7 @@ export class AddPanelWidget extends React.Component {
{this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} - {this.renderOptionLink('visualization', 'Choose Panel type', this.onCreateNewPanel)} + {this.renderOptionLink('visualization', 'Choose Panel type', () => this.onCreateNewPanel('visualization'))} {this.renderOptionLink('queries', 'Convert to row', this.onCreateNewRow)}
diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index 35b9b71112a..fdf978acdf9 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react'; // Utils & Services import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; +//TODO: See PanelEdit // Components import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; From 99ff8e68ffbd851163d539891b884fbf66da67ca Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Feb 2019 19:20:18 +0300 Subject: [PATCH 076/770] azuremonitor: fix where suggestions --- .../editor/KustoQueryField.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 09573f29047..9b2df96fcdd 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -99,12 +99,12 @@ export default class KustoQueryField extends QueryField { if (wrapperClasses.contains('function-context')) { typeaheadContext = 'context-function'; if (this.fields) { - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else { this._fetchFields(); return; } - } else if (modelPrefix.match(/(where\s$)/i)) { + } else if (modelPrefix.match(/(where\s(\w+\b)?$)/i)) { typeaheadContext = 'context-where'; const fullQuery = Plain.serialize(this.state.value); const table = this.getTableFromContext(fullQuery); @@ -116,7 +116,7 @@ export default class KustoQueryField extends QueryField { } else if (modelPrefix.match(/(,\s*$)/)) { typeaheadContext = 'context-multiple-fields'; if (this.fields) { - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else { this._fetchFields(); return; @@ -124,7 +124,7 @@ export default class KustoQueryField extends QueryField { } else if (modelPrefix.match(/(from\s$)/i)) { typeaheadContext = 'context-from'; if (this.events) { - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else { this._fetchEvents(); return; @@ -132,7 +132,7 @@ export default class KustoQueryField extends QueryField { } else if (modelPrefix.match(/(^select\s\w*$)/i)) { typeaheadContext = 'context-select'; if (this.fields) { - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else { this._fetchFields(); return; @@ -140,16 +140,19 @@ export default class KustoQueryField extends QueryField { } else if (modelPrefix.match(/from\s\S+\s\w*$/i)) { prefix = ''; typeaheadContext = 'context-since'; - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); // } else if (modelPrefix.match(/\d+\s\w*$/)) { // typeaheadContext = 'context-number'; // suggestionGroups = this._getAfterNumberSuggestions(); } else if (modelPrefix.match(/ago\b/i) || modelPrefix.match(/facet\b/i) || modelPrefix.match(/\$__timefilter\b/i)) { typeaheadContext = 'context-timeseries'; - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else if (prefix && !wrapperClasses.contains('argument')) { + if (modelPrefix.match(/\s$/i)) { + prefix = ''; + } typeaheadContext = 'context-builtin'; - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else if (Plain.serialize(this.state.value) === '') { typeaheadContext = 'context-new'; if (this.schema) { @@ -159,6 +162,12 @@ export default class KustoQueryField extends QueryField { setTimeout(this.onTypeahead, 0); return; } + } else { + typeaheadContext = 'context-builtin'; + if (modelPrefix.match(/\s$/i)) { + prefix = ''; + } + suggestionGroups = this.getKeywordSuggestions(); } let results = 0; @@ -178,6 +187,7 @@ export default class KustoQueryField extends QueryField { .filter(group => group.items.length > 0); // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); + // console.log('onTypeahead', modelPrefix, prefix, typeaheadContext); this.setState({ typeaheadPrefix: prefix, @@ -283,7 +293,7 @@ export default class KustoQueryField extends QueryField { // ]; // } - private _getKeywordSuggestions(): SuggestionGroup[] { + private getKeywordSuggestions(): SuggestionGroup[] { return [ { prefixMatch: true, From 9ab5eeb7f30cdbcd1b950ecbaeed8938679650b5 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Feb 2019 17:26:20 +0100 Subject: [PATCH 077/770] fix: Explore: Query wrapping on long queries #15222 --- .../src/components/Select/SelectOptionGroup.tsx | 2 +- .../app/features/dashboard/panel_editor/QueriesTab.tsx | 2 +- public/app/features/explore/QueryRow.tsx | 4 ++-- .../prometheus/components/PromQueryField.tsx | 6 +++--- public/sass/components/_gf-form.scss | 4 ++++ public/sass/utils/_utils.scss | 10 +++++++++- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx index efc5e4516fc..9a787a84819 100644 --- a/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx @@ -49,7 +49,7 @@ export default class SelectOptionGroup extends PureComponent
- {label} + {label} {' '}
{expanded && children} diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 491f255d761..d46ff020906 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -133,7 +133,7 @@ export class QueriesTab extends PureComponent { return ( <> -
+
{!isAddingMixed && (
-
+
Date: Mon, 4 Feb 2019 17:28:57 +0100 Subject: [PATCH 078/770] WIP Enable js defined theme to be used in SASS --- package.json | 1 + packages/grafana-ui/src/index.ts | 1 + packages/grafana-ui/src/theme.d.ts | 116 ++++++++++++++++++++++ packages/grafana-ui/src/theme.js | 15 +++ packages/grafana-ui/src/themes/dark.js | 64 ++++++++++++ packages/grafana-ui/src/themes/default.js | 47 +++++++++ packages/grafana-ui/src/themes/light.js | 65 ++++++++++++ public/sass/_variables.dark.scss | 101 +++++++++---------- public/sass/_variables.light.scss | 94 +++++++++--------- public/sass/_variables.scss | 44 ++++---- scripts/webpack/getThemeVariable.js | 53 ++++++++++ scripts/webpack/sass.rule.js | 13 ++- scripts/webpack/webpack.hot.js | 10 +- yarn.lock | 5 + 14 files changed, 506 insertions(+), 123 deletions(-) create mode 100644 packages/grafana-ui/src/theme.d.ts create mode 100644 packages/grafana-ui/src/theme.js create mode 100644 packages/grafana-ui/src/themes/dark.js create mode 100644 packages/grafana-ui/src/themes/default.js create mode 100644 packages/grafana-ui/src/themes/light.js create mode 100644 scripts/webpack/getThemeVariable.js diff --git a/package.json b/package.json index 77fd92baf57..60a8a20cde3 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "ng-annotate-webpack-plugin": "^0.3.0", "ngtemplate-loader": "^2.0.1", "node-sass": "^4.11.0", + "node-sass-utils": "^1.1.2", "npm": "^5.4.2", "optimize-css-assets-webpack-plugin": "^4.0.2", "phantomjs-prebuilt": "^2.1.15", diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index 974d976bbef..4ddc7c8485a 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -1,3 +1,4 @@ export * from './components'; export * from './types'; export * from './utils'; +export * from './theme'; diff --git a/packages/grafana-ui/src/theme.d.ts b/packages/grafana-ui/src/theme.d.ts new file mode 100644 index 00000000000..015bcd16136 --- /dev/null +++ b/packages/grafana-ui/src/theme.d.ts @@ -0,0 +1,116 @@ +export interface GrafanaThemeType { + name: string; + // TODO: not sure if should be a part of theme + brakpoints: { + xs: string; + s: string; + m: string; + l: string; + xl: string; + }; + typography: { + fontFamily: { + sansSerif: string; + serif: string; + monospace: string; + }; + size: { + base: string; + xs: string; + s: string; + m: string; + l: string; + }; + weight: { + light: number; + normal: number; + semibold: number; + }; + lineHeight: { + xs: number; //1 + s: number; //1.1 + m: number; // 4/3 + l: number; // 1.5 + }; + // TODO: Refactor to use size instead of custom defs + heading: { + h1: string; + h2: string; + h3: string; + h4: string; + h5: string; + h6: string; + }; + }; + spacing: { + xs: string; + s: string; + m: string; + l: string; + gutter: string; + }; + border: { + radius: { + xs: string; + s: string; + m: string; + }; + }; + colors: { + black: string; + white: string; + dark1: string; + dark2: string; + dark3: string; + dark4: string; + dark5: string; + gray1: string; + gray2: string; + gray3: string; + gray4: string; + gray5: string; + gray6: string; + gray7: string; + grayBlue: string; + inputBlack: string; + + // Accent colors + blue: string; + blueLight: string; + blueDark: string; + green: string; + red: string; + yellow: string; + pink: string; + purple: string; + variable: string; + orange: string; + queryRed: string; + queryGreen: string; + queryPurple: string; + queryKeyword: string; + queryOrange: string; + + // Status colors + online: string; + warn: string; + critical: string; + + // TODO: should this be a part of theme? + bodyBg: string; + pageBg: string; + bodyColor: string; + textColor: string; + textColorStrong: string; + textColorWeak: string; + textColorFaint: string; + textColorEmphasis: string; + linkColor: string; + linkColorDisabled: string; + linkColorHover: string; + linkColorExternal: string; + headingColor: string; + }; +} +export function getTheme(): GrafanaThemeType +export function mockTheme(themeMock: Partial): () => void diff --git a/packages/grafana-ui/src/theme.js b/packages/grafana-ui/src/theme.js new file mode 100644 index 00000000000..3d0695e2490 --- /dev/null +++ b/packages/grafana-ui/src/theme.js @@ -0,0 +1,15 @@ +const darkTheme = require('./themes/dark'); +const lightTheme = require('./themes/light'); + +const getTheme = name => (name === 'light' ? lightTheme : darkTheme); + +const mockTheme = mock => { + const originalGetTheme = getTheme; + getTheme = () => mock; + return () => (getTheme = originalGetTheme); +}; + +module.exports = { + getTheme, + mockTheme, +}; diff --git a/packages/grafana-ui/src/themes/dark.js b/packages/grafana-ui/src/themes/dark.js new file mode 100644 index 00000000000..c80a4593f53 --- /dev/null +++ b/packages/grafana-ui/src/themes/dark.js @@ -0,0 +1,64 @@ + + +const defaultTheme = require('./default'); +const tinycolor = require('tinycolor2'); + +const basicColors = { + black: '#00ff00', + white: '#ffffff', + dark1: '#141414', + dark2: '#1f1f20', + dark3: '#262628', + dark4: '#333333', + dark5: '#444444', + gray1: '#555555', + gray2: '#8e8e8e', + gray3: '#b3b3b3', + gray4: '#d8d9da', + gray5: '#ececec', + gray6: '#f4f5f8', + gray7: '#fbfbfb', + grayBlue: '#212327', + blue: '#33b5e5', + blueDark: '#005f81', + blueLight: '#00a8e6', // not used in dark theme + green: '#299c46', + red: '#d44a3a', + yellow: '#ecbb13', + pink: '#ff4444', + purple: '#9933cc', + variable: '#32d1df', + orange: '#eb7b18', +}; + +const darkTheme = { + ...defaultTheme, + name: 'Grafana Dark', + colors: { + ...basicColors, + inputBlack: '#09090b', + queryRed: '#e24d42', + queryGreen: '#74e680', + queryPurple: '#fe85fc', + queryKeyword: '#66d9ef', + queryOrange: 'eb7b18', + online: '#10a345', + warn: '#f79520', + critical: '#ed2e18', + bodyBg: '#171819', + pageBg: '#161719', + bodyColor: basicColors.gray4, + textColor: basicColors.gray4, + textColorStrong: basicColors.white, + textColorWeak: basicColors.gray2, + textColorEmphasis: basicColors.gray5, + textColorFaint: basicColors.dark5, + linkColor: new tinycolor(basicColors.white).darken(11).toString(), + linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(), + linkColorHover: basicColors.white, + linkColorExternal: basicColors.blue, + headingColor: new tinycolor(basicColors.white).darken(11).toString(), + } +} + +module.exports = darkTheme; diff --git a/packages/grafana-ui/src/themes/default.js b/packages/grafana-ui/src/themes/default.js new file mode 100644 index 00000000000..59ed050e360 --- /dev/null +++ b/packages/grafana-ui/src/themes/default.js @@ -0,0 +1,47 @@ + + +const theme = { + name: 'Grafana Default', + typography: { + fontFamily: { + sansSerif: "'Roboto', Helvetica, Arial, sans-serif;", + serif: "Georgia, 'Times New Roman', Times, serif;", + monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;" + }, + size: { + base: '13px', + xs: '10px', + s: '12px', + m: '14px', + l: '18px', + }, + heading: { + h1: '2rem', + h2: '1.75rem', + h3: '1.5rem', + h4: '1.3rem', + h5: '1.2rem', + h6: '1rem', + }, + weight: { + light: 300, + normal: 400, + semibold: 500, + }, + lineHeight: { + xs: 1, + s: 1.1, + m: 4/3, + l: 1.5 + } + }, + brakpoints: { + xs: '0', + s: '544px', + m: '768px', + l: '992px', + xl: '1200px' + } +}; + +module.exports = theme; diff --git a/packages/grafana-ui/src/themes/light.js b/packages/grafana-ui/src/themes/light.js new file mode 100644 index 00000000000..84d1e656baa --- /dev/null +++ b/packages/grafana-ui/src/themes/light.js @@ -0,0 +1,65 @@ +// import { GrafanaThemeType } from "../theme"; + +const defaultTheme = require('./default'); +const tinycolor = require('tinycolor2'); + +const basicColors = { + black: '#000000', + white: '#ffffff', + dark1: '#13161d', + dark2: '#1e2028', + dark3: '#303133', + dark4: '#35373f', + dark5: '#41444b', + gray1: '#52545c', + gray2: '#767980', + gray3: '#acb6bf', + gray4: '#c7d0d9', + gray5: '#dde4ed', + gray6: '#e9edf2', + gray7: '#f7f8fa', + grayBlue: '#212327', // not used in light theme + blue: '#0083b3', + blueDark: '#005f81', + blueLight: '#00a8e6', + green: '#3aa655', + red: '#d44939', + yellow: '#ff851b', + pink: '#e671b8', + purple: '#9954bb', + variable: '#0083b3', + orange: '#ff7941', +}; + +const lightTheme/*: GrafanaThemeType*/ = { + ...defaultTheme, + name: 'Grafana Light', + colors: { + ...basicColors, + variable: basicColors.blue, + inputBlack: '#09090b', + queryRed: basicColors.red, + queryGreen: basicColors.green, + queryPurple: basicColors.purple, + queryKeyword: basicColors.blue, + queryOrange: basicColors.orange, + online: '#01a64f', + warn: '#f79520', + critical: '#ec2128', + bodyBg: basicColors.gray7, + pageBg: basicColors.gray7, + bodyColor: basicColors.gray1, + textColor: basicColors.gray1, + textColorStrong: basicColors.dark2, + textColorWeak: basicColors.gray2, + textColorEmphasis: basicColors.gray5, + textColorFaint: basicColors.dark4, + linkColor: basicColors.gray1, + linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(), + linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(), + linkColorExternal: basicColors.blueLight, + headingColor: basicColors.gray1, + } +} + +module.exports = lightTheme; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 7b0ed869bdc..61e2c8bfc76 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -3,73 +3,69 @@ $theme-name: dark; -// Grays // ------------------------- -$black: #000; +$black: getThemeVariable('colors.black', $theme-name); +$dark-1: getThemeVariable('colors.dark1', $theme-name); +$dark-2: getThemeVariable('colors.dark2', $theme-name); +$dark-3: getThemeVariable('colors.dark3', $theme-name); +$dark-4: getThemeVariable('colors.dark4', $theme-name); +$dark-5: getThemeVariable('colors.dark5', $theme-name); +$gray-1: getThemeVariable('colors.gray1', $theme-name); +$gray-2: getThemeVariable('colors.gray2', $theme-name); +$gray-3: getThemeVariable('colors.gray3', $theme-name); +$gray-4: getThemeVariable('colors.gray4', $theme-name); +$gray-5: getThemeVariable('colors.gray5', $theme-name); +$gray-6: getThemeVariable('colors.gray6', $theme-name); +$gray-7: getThemeVariable('colors.gray7', $theme-name); -// ------------------------- -$black: #000; -$dark-1: #141414; -$dark-2: #1f1f20; -$dark-3: #262628; -$dark-4: #333333; -$dark-5: #444444; -$gray-1: #555555; -$gray-2: #8e8e8e; -$gray-3: #b3b3b3; -$gray-4: #d8d9da; -$gray-5: #ececec; -$gray-6: #f4f5f8; -$gray-7: #fbfbfb; +$gray-blue: getThemeVariable('colors.grayBlue', $theme-name); +$input-black: getThemeVariable('colors.inputBlack', $theme-name); -$gray-blue: #212327; -$input-black: #09090b; - -$white: #fff; +$white: getThemeVariable('colors.white', $theme-name); // Accent colors // ------------------------- -$blue: #33b5e5; -$blue-dark: #005f81; -$green: #299c46; -$red: #d44a3a; -$yellow: #ecbb13; -$pink: #ff4444; -$purple: #9933cc; -$variable: #32d1df; -$orange: #eb7b18; +$blue: getThemeVariable('colors.blue', $theme-name); +$blue-dark: getThemeVariable('colors.blueDark', $theme-name); +$green: getThemeVariable('colors.green', $theme-name); +$red: getThemeVariable('colors.red', $theme-name); +$yellow: getThemeVariable('colors.yellow', $theme-name); +$pink: getThemeVariable('colors.pink', $theme-name); +$purple: getThemeVariable('colors.purple', $theme-name); +$variable: getThemeVariable('colors.variable', $theme-name); +$orange: getThemeVariable('colors.orange', $theme-name); $brand-primary: $orange; $brand-success: $green; $brand-warning: $brand-primary; $brand-danger: $red; -$query-red: #e24d42; -$query-green: #74e680; -$query-purple: #fe85fc; -$query-keyword: #66d9ef; -$query-orange: $orange; +$query-red: getThemeVariable('colors.queryRed', $theme-name); +$query-green: getThemeVariable('colors.queryGreen', $theme-name); +$query-purple: getThemeVariable('colors.queryPurple', $theme-name); +$query-keyword: getThemeVariable('colors.queryKeyword', $theme-name); +$query-orange: getThemeVariable('colors.queryOrange', $theme-name); // Status colors // ------------------------- -$online: #10a345; -$warn: #f79520; -$critical: #ed2e18; +$online: getThemeVariable('colors.online', $theme-name); +$warn: getThemeVariable('colors.warn', $theme-name); +$critical: getThemeVariable('colors.critical', $theme-name); // Scaffolding // ------------------------- -$body-bg: rgb(23, 24, 25); -$page-bg: rgb(22, 23, 25); +$body-bg: getThemeVariable('colors.bodyBg', $theme-name); +$page-bg: getThemeVariable('colors.pageBg', $theme-name); -$body-color: $gray-4; -$text-color: $gray-4; -$text-color-strong: $white; -$text-color-weak: $gray-2; -$text-color-faint: $dark-5; -$text-color-emphasis: $gray-5; +$body-color: getThemeVariable('colors.bodyColor', $theme-name); +$text-color: getThemeVariable('colors.textColor', $theme-name); +$text-color-strong: getThemeVariable('colors.textColorStrong', $theme-name); +$text-color-weak: getThemeVariable('colors.textColorWeak', $theme-name); +$text-color-faint: getThemeVariable('colors.textColorFaint', $theme-name); +$text-color-emphasis: getThemeVariable('colors.textColorEmphasis', $theme-name); -$text-shadow-strong: 1px 1px 4px $black; -$text-shadow-faint: 1px 1px 4px rgb(45, 45, 45); +$text-shadow-strong: 1px 1px 4px getThemeVariable('colors.black', $theme-name); +$text-shadow-faint: 1px 1px 4px #2d2d2d; // gradients $brand-gradient: linear-gradient( @@ -84,10 +80,11 @@ $edit-gradient: linear-gradient(180deg, rgb(22, 23, 25) 50%, #090909); // Links // ------------------------- -$link-color: darken($white, 11%); -$link-color-disabled: darken($link-color, 30%); -$link-hover-color: $white; -$external-link-color: $blue; +$link-color: getThemeVariable('colors.linkColor', $theme-name); +$link-color-disabled: getThemeVariable('colors.linkColorDisabled', $theme-name); +$link-hover-color: getThemeVariable('colors.linkColorHover', $theme-name); + +$external-link-color: getThemeVariable('colors.linkColorExternal', $theme-name); // Typography // ------------------------- @@ -135,7 +132,7 @@ $list-item-shadow: $card-shadow; $empty-list-cta-bg: $gray-blue; // Scrollbars -$scrollbarBackground: #404357; +$scrollbarBackground: #aeb5df; $scrollbarBackground2: #3a3a3a; $scrollbarBorder: black; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 10c074e1481..ad2b22e201a 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -12,83 +12,85 @@ $theme-name: light; $black: #000; // ------------------------- -$black: #000; -$dark-1: #13161d; -$dark-2: #1e2028; -$dark-3: #303133; -$dark-4: #35373f; -$dark-5: #41444b; -$gray-1: #52545c; -$gray-2: #767980; -$gray-3: #acb6bf; -$gray-4: #c7d0d9; -$gray-5: #dde4ed; -$gray-6: #e9edf2; -$gray-7: #f7f8fa; +$black: getThemeVariable('colors.black', $theme-name); +$dark-1: getThemeVariable('colors.dark1', $theme-name); +$dark-2: getThemeVariable('colors.dark2', $theme-name); +$dark-3: getThemeVariable('colors.dark3', $theme-name); +$dark-4: getThemeVariable('colors.dark4', $theme-name); +$dark-5: getThemeVariable('colors.dark5', $theme-name); +$gray-1: getThemeVariable('colors.gray1', $theme-name); +$gray-2: getThemeVariable('colors.gray2', $theme-name); +$gray-3: getThemeVariable('colors.gray3', $theme-name); +$gray-4: getThemeVariable('colors.gray4', $theme-name); +$gray-5: getThemeVariable('colors.gray5', $theme-name); +$gray-6: getThemeVariable('colors.gray6', $theme-name); +$gray-7: getThemeVariable('colors.gray7', $theme-name); -$white: #fff; +$white: getThemeVariable('colors.white', $theme-name); // Accent colors // ------------------------- -$blue: #0083b3; -$blue-dark: #005f81; -$blue-light: #00a8e6; -$green: #3aa655; -$red: #d44939; -$yellow: #ff851b; -$orange: #ff7941; -$pink: #e671b8; -$purple: #9954bb; -$variable: $blue; +$blue: getThemeVariable('colors.blue', $theme-name); +$blue-dark: getThemeVariable('colors.blueDark', $theme-name); +$blue-light: getThemeVariable('colors.blueLight', $theme-name); +$green: getThemeVariable('colors.green', $theme-name); +$red: getThemeVariable('colors.red', $theme-name); +$yellow: getThemeVariable('colors.yellow', $theme-name); +$orange: getThemeVariable('colors.orange', $theme-name); +$pink: getThemeVariable('colors.pink', $theme-name); +$purple: getThemeVariable('colors.purple', $theme-name); +$variable: getThemeVariable('colors.variable', $theme-name); $brand-primary: $orange; $brand-success: $green; $brand-warning: $orange; $brand-danger: $red; -$query-red: $red; -$query-green: $green; -$query-purple: $purple; -$query-orange: $orange; -$query-keyword: $blue; +$query-red: getThemeVariable('colors.queryRed', $theme-name); +$query-green: getThemeVariable('colors.queryGreen', $theme-name); +$query-purple: getThemeVariable('colors.queryPurple', $theme-name); +$query-keyword: getThemeVariable('colors.queryKeyword', $theme-name); +$query-orange: getThemeVariable('colors.queryOrange', $theme-name); // Status colors // ------------------------- -$online: #01a64f; -$warn: #f79520; -$critical: #ec2128; +$online: getThemeVariable('colors.online', $theme-name); +$warn: getThemeVariable('colors.warn', $theme-name); +$critical: getThemeVariable('colors.critical', $theme-name); // Scaffolding // ------------------------- -$body-bg: $gray-7; -$page-bg: $gray-7; -$body-color: $gray-1; -$text-color: $gray-1; -$text-color-strong: $dark-2; -$text-color-weak: $gray-2; -$text-color-faint: $gray-4; -$text-color-emphasis: $dark-5; +$body-bg: getThemeVariable('colors.bodyBg', $theme-name); +$page-bg: getThemeVariable('colors.pageBg', $theme-name); + +$body-color: getThemeVariable('colors.bodyColor', $theme-name); +$text-color: getThemeVariable('colors.textColor', $theme-name); +$text-color-strong: getThemeVariable('colors.textColorStrong', $theme-name); +$text-color-weak: getThemeVariable('colors.textColorWeak', $theme-name); +$text-color-faint: getThemeVariable('colors.textColorFaint', $theme-name); +$text-color-emphasis: getThemeVariable('colors.textColorEmphasis', $theme-name); $text-shadow-strong: none; $text-shadow-faint: none; $textShadow: none; // gradients -$brand-gradient: linear-gradient(to right, rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); +$brand-gradient: linear-gradient(to right, hsl(50, 100%, 50%) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); $page-gradient: linear-gradient(180deg, $white 10px, $gray-7 100px); $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%); // Links // ------------------------- -$link-color: $gray-1; -$link-color-disabled: lighten($link-color, 30%); -$link-hover-color: darken($link-color, 20%); -$external-link-color: $blue-light; +$link-color: getThemeVariable('colors.linkColor', $theme-name); +$link-color-disabled: getThemeVariable('colors.linkColorDisabled', $theme-name); +$link-hover-color: getThemeVariable('colors.linkColorHover', $theme-name); + +$external-link-color: getThemeVariable('colors.linkColorExternal', $theme-name); // Typography // ------------------------- -$headings-color: $text-color; +$headings-color: getThemeVariable('colors.headingColor', $theme-name); $abbr-border-color: $gray-2 !default; $text-muted: $text-color-weak; diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 4e9e69c4d2f..eab0e8c7f5a 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -47,45 +47,45 @@ $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: getThemeVariable('typography.fontFamily.sansSerif'); +$font-family-serif: getThemeVariable('typography.fontFamily.serif'); +$font-family-monospace: getThemeVariable('typography.fontFamily.monospace'); $font-family-base: $font-family-sans-serif !default; -$font-size-root: 14px !default; -$font-size-base: 13px !default; +$font-size-root: getThemeVariable('typography.size.m') !default; +$font-size-base: getThemeVariable('typography.size.base') !default; -$font-size-lg: 18px !default; -$font-size-md: 14px !default; -$font-size-sm: 12px !default; -$font-size-xs: 10px !default; +$font-size-lg: getThemeVariable('typography.size.l') !default; +$font-size-md: getThemeVariable('typography.size.m') !default; +$font-size-sm: getThemeVariable('typography.size.s') !default; +$font-size-xs: getThemeVariable('typography.size.xs') !default; -$line-height-base: 1.5 !default; -$font-weight-semi-bold: 500; +$line-height-base: getThemeVariable('typography.lineHeight.l') !default; +$font-weight-semi-bold: getThemeVariable('typography.weight.semibold'); -$font-size-h1: 2rem !default; -$font-size-h2: 1.75rem !default; -$font-size-h3: 1.5rem !default; -$font-size-h4: 1.3rem !default; -$font-size-h5: 1.2rem !default; -$font-size-h6: 1rem !default; +$font-size-h1: getThemeVariable('typography.heading.h1') !default; +$font-size-h2: getThemeVariable('typography.heading.h2') !default; +$font-size-h3: getThemeVariable('typography.heading.h3') !default; +$font-size-h4: getThemeVariable('typography.heading.h4') !default; +$font-size-h5: getThemeVariable('typography.heading.h5') !default; +$font-size-h6: getThemeVariable('typography.heading.h6') !default; $display1-size: 6rem !default; $display2-size: 5.5rem !default; $display3-size: 4.5rem !default; $display4-size: 3.5rem !default; -$display1-weight: 400 !default; -$display2-weight: 400 !default; -$display3-weight: 400 !default; -$display4-weight: 400 !default; +$display1-weight: getThemeVariable('typography.weight.normal') !default; +$display2-weight: getThemeVariable('typography.weight.normal') !default; +$display3-weight: getThemeVariable('typography.weight.normal') !default; +$display4-weight: getThe1meVariable('typography.weight.normal') !default; $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-weight: 400 !default; +$headings-font-weight: getThemeVariable('typography.weight.normal') !default; $headings-line-height: 1.1 !default; $hr-border-width: $border-width !default; diff --git a/scripts/webpack/getThemeVariable.js b/scripts/webpack/getThemeVariable.js new file mode 100644 index 00000000000..c0b6bc4ed79 --- /dev/null +++ b/scripts/webpack/getThemeVariable.js @@ -0,0 +1,53 @@ +const sass = require('node-sass'); +const sassUtils = require('node-sass-utils')(sass); +const { getTheme } = require('../../packages/grafana-ui/src/theme'); +const { get } = require('lodash'); +const tinycolor = require('tinycolor2'); + +const units = ['rem', 'em', 'vh', 'vw', 'vmin', 'vmax', 'ex', '%', 'px', 'cm', 'mm', 'in', 'pt', 'pc', 'ch']; +const matchDimension = value => value.match(/[a-zA-Z]+|[0-9]+/g); + +const isHex = value => { + const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6})$/gi; + return hexRegex.test(value); +}; + +const isDimension = value => { + if( typeof value !== "string") { + return false; + } + + const [val, unit] = matchDimension(value); + return units.indexOf(unit) > -1 +}; + +/** + * @param {SassString} variablePath + * @param {"dark"|"light"} themeName + */ +function getThemeVariable(variablePath, themeName) { + const theme = getTheme(themeName.getValue()); + const variable = get(theme, variablePath.getValue()); + + if (!variable) { + throw new Error(`${variablePath} is not defined fo ${themeName}`); + } + + if (isHex(variable)) { + const rgb = new tinycolor(variable).toRgb(); + const color = sass.types.Color(rgb.r, rgb.g, rgb.b); + return color; + } + + if (isDimension(variable)) { + const [value, unit] = matchDimension(variable) + + const tmp = new sassUtils.SassDimension(parseInt(value,10), unit); + // debugger + return sassUtils.castToSass(tmp) + } + + return sassUtils.castToSass(variable); +} + +module.exports = getThemeVariable; diff --git a/scripts/webpack/sass.rule.js b/scripts/webpack/sass.rule.js index 75455fc4184..78f6b60d33f 100644 --- a/scripts/webpack/sass.rule.js +++ b/scripts/webpack/sass.rule.js @@ -1,6 +1,7 @@ 'use strict'; -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const getThemeVariable = require('./getThemeVariable'); module.exports = function(options) { return { @@ -23,7 +24,15 @@ module.exports = function(options) { config: { path: __dirname + '/postcss.config.js' }, }, }, - { loader: 'sass-loader', options: { sourceMap: options.sourceMap } }, + { + loader: 'sass-loader', + options: { + sourceMap: options.sourceMap, + functions: { + 'getThemeVariable($themeVar, $themeName: dark)': getThemeVariable, + }, + }, + }, ], }; }; diff --git a/scripts/webpack/webpack.hot.js b/scripts/webpack/webpack.hot.js index b37e4c08592..4519e292c6b 100644 --- a/scripts/webpack/webpack.hot.js +++ b/scripts/webpack/webpack.hot.js @@ -8,6 +8,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const IgnoreNotFoundExportPlugin = require("./IgnoreNotFoundExportPlugin.js"); +const getThemeVariable = require("./getThemeVariable"); module.exports = merge(common, { entry: { @@ -85,7 +86,14 @@ module.exports = merge(common, { config: { path: __dirname + '/postcss.config.js' }, }, }, - 'sass-loader', // compiles Sass to CSS + { + loader: 'sass-loader', + options: { + functions: { + "getThemeVariable($themeVar, $themeName: dark)": getThemeVariable + } + } + } ], }, { diff --git a/yarn.lock b/yarn.lock index 169abd40ee4..bc94a05d702 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11910,6 +11910,11 @@ node-releases@^1.0.0-alpha.11, node-releases@^1.1.3: dependencies: semver "^5.3.0" +node-sass-utils@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/node-sass-utils/-/node-sass-utils-1.1.2.tgz#d03639cfa4fc962398ba3648ab466f0db7cc2131" + integrity sha1-0DY5z6T8liOYujZIq0ZvDbfMITE= + node-sass@^4.11.0: version "4.11.0" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" From 3baaf2c3e4f13c8ee4e70984953f92f4839a6801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 17:36:04 +0100 Subject: [PATCH 079/770] Added handling of kiosk mode --- .../dashboard/state/DashboardModel.ts | 11 ++--- .../features/dashboard/state/initDashboard.ts | 40 ++++++++++--------- public/app/routes/routes.ts | 4 +- public/app/types/dashboard.ts | 2 +- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index a6d45bb3cc0..8756af2ceea 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -15,6 +15,7 @@ import sortByKeys from 'app/core/utils/sort_by_keys'; import { PanelModel } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; import { TimeRange } from '@grafana/ui/src'; +import { UrlQueryValue } from 'app/types'; export class DashboardModel { id: any; @@ -867,11 +868,7 @@ export class DashboardModel { return !_.isEqual(updated, this.originalTemplating); } - autoFitPanels(viewHeight: number) { - if (!this.meta.autofitpanels) { - return; - } - + autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) { const currentGridHeight = Math.max( ...this.panels.map(panel => { return panel.gridPos.h + panel.gridPos.y; @@ -885,12 +882,12 @@ export class DashboardModel { let visibleHeight = viewHeight - navbarHeight - margin; // Remove submenu height if visible - if (this.meta.submenuEnabled && !this.meta.kiosk) { + if (this.meta.submenuEnabled && !kioskMode) { visibleHeight -= submenuHeight; } // add back navbar height - if (this.meta.kiosk === 'b') { + if (kioskMode === 'tv') { visibleHeight += 55; } diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 14d6196d69c..89b8d470d3c 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -56,10 +56,6 @@ export function initDashboard({ try { switch (routeInfo) { // handle old urls with no uid - case DashboardRouteInfo.Old: { - redirectToNewUrl(urlSlug, dispatch); - return; - } case DashboardRouteInfo.Home: { // load home dash dashDTO = await getBackendSrv().get('/api/dashboards/home'); @@ -78,20 +74,27 @@ export function initDashboard({ break; } case DashboardRouteInfo.Normal: { + // for old db routes we redirect + if (urlType === 'db') { + redirectToNewUrl(urlSlug, dispatch); + return; + } + const loaderSrv = $injector.get('dashboardLoaderSrv'); dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - // check if the current url is correct (might be old slug) - const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); - const currentPath = getState().location.path; - console.log('loading dashboard: currentPath', currentPath); - console.log('loading dashboard: dashboardUrl', dashboardUrl); + if (dashDTO.meta.url) { + // check if the current url is correct (might be old slug) + const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); + const currentPath = getState().location.path; - if (dashboardUrl !== currentPath) { - // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. - dispatch(updateLocation({ path: dashboardUrl, partial: true, replace: true })); - return; + if (dashboardUrl !== currentPath) { + // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. + dispatch(updateLocation({ path: dashboardUrl, partial: true, replace: true })); + return; + } } + break; } case DashboardRouteInfo.New: { @@ -129,7 +132,6 @@ export function initDashboard({ const variableSrv: VariableSrv = $injector.get('variableSrv'); const keybindingSrv: KeybindingSrv = $injector.get('keybindingSrv'); const unsavedChangesSrv = $injector.get('unsavedChangesSrv'); - const viewStateSrv = $injector.get('dashboardViewStateSrv'); const dashboardSrv: DashboardSrv = $injector.get('dashboardSrv'); timeSrv.init(dashboard); @@ -147,14 +149,16 @@ export function initDashboard({ try { dashboard.processRepeats(); dashboard.updateSubmenuVisibility(); - dashboard.autoFitPanels(window.innerHeight); + + // handle auto fix experimental feature + const queryParams = getState().location.query; + if (queryParams.autofitpanels) { + dashboard.autoFitPanels(window.innerHeight, queryParams.kiosk); + } // init unsaved changes tracking unsavedChangesSrv.init(dashboard, $scope); - $scope.dashboard = dashboard; - viewStateSrv.create($scope); - // dashboard keybindings should not live in core, this needs a bigger refactoring // So declaring this here so it can depend on the removePanel util function // Long term onRemovePanel should be handled via react prop callback diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index ecd934cdccf..e0029cf2464 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -62,7 +62,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboard/:type/:slug', { template: '', pageClass: 'page-dashboard', - routeInfo: DashboardRouteInfo.Old, + routeInfo: DashboardRouteInfo.Normal, reloadOnSearch: false, resolve: { component: () => DashboardPage, @@ -88,7 +88,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboard-solo/:type/:slug', { template: '', pageClass: 'dashboard-solo', - routeInfo: DashboardRouteInfo.Old, + routeInfo: DashboardRouteInfo.Normal, resolve: { component: () => SoloPanelPage, }, diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 9b8f539aeb2..36c0a420f28 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -8,10 +8,10 @@ export interface MutableDashboard { } export enum DashboardRouteInfo { - Old = 'old-dashboard', Home = 'home-dashboard', New = 'new-dashboard', Normal = 'normal-dashboard', + Scripted = 'scripted-dashboard', } export enum DashboardLoadingState { From cfd8eb5167303afa7176d52c9d456a144bf537b5 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 17:37:07 +0100 Subject: [PATCH 080/770] now /api/login/ping returns Response --- pkg/api/api.go | 4 ++-- pkg/api/login.go | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 07cb712f794..980706d8355 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -108,8 +108,8 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey)) r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot)) - // api renew session based on remember cookie - r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing) + // api renew session based on cookie + r.Get("/api/login/ping", quota("session"), Wrap(hs.LoginAPIPing)) // authed api r.Group("/api", func(apiRoute routing.RouteRegister) { diff --git a/pkg/api/login.go b/pkg/api/login.go index 3f2d82a6c0f..50c62e0835a 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -78,13 +78,12 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool { return false } -func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) { - if c.IsSignedIn || (c.AllowAnonymous && c.IsAnonymous) { - c.JsonOK("Logged in") - return +func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) Response { + if c.IsSignedIn || c.IsAnonymous { + return JSON(200, "Logged in") } - c.JsonApiErr(401, "Unauthorized", nil) + return Error(401, "Unauthorized", nil) } func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { From 23ac9405c16b259887a9a5610e955537950de811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 17:39:29 +0100 Subject: [PATCH 081/770] Set page title on dashboard load --- .../dashboard/containers/DashboardPage.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index ec143d735ab..3b902e4d089 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -72,6 +72,13 @@ export class DashboardPage extends PureComponent { }); } + componentWillUnmount() { + if (this.props.dashboard) { + this.props.dashboard.destroy(); + this.props.setDashboardModel(null); + } + } + componentDidUpdate(prevProps: Props) { const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId } = this.props; @@ -79,6 +86,11 @@ export class DashboardPage extends PureComponent { return; } + // if we just got dashboard update title + if (!prevProps.dashboard) { + document.title = dashboard.title + ' - Grafana'; + } + // handle animation states when opening dashboard settings if (!prevProps.editview && editview) { this.setState({ isSettingsOpening: true }); @@ -135,13 +147,6 @@ export class DashboardPage extends PureComponent { $('body').toggleClass('panel-in-fullscreen', isFullscreen); } - componentWillUnmount() { - if (this.props.dashboard) { - this.props.dashboard.destroy(); - this.props.setDashboardModel(null); - } - } - renderLoadingState() { return ; } From d978a66ef6b1b1b496959e017d5d714d1f187ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 18:24:56 +0100 Subject: [PATCH 082/770] Fixed lots of loading flow issues and updated solo route page --- public/app/core/utils/location_util.ts | 2 +- .../dashboard/components/DashNav/DashNav.tsx | 148 ++++++++++-------- .../dashboard/containers/DashboardPage.tsx | 1 + .../dashboard/containers/SoloPanelPage.tsx | 92 +++++------ .../dashboard/services/DashboardSrv.ts | 1 - .../features/dashboard/state/initDashboard.ts | 21 ++- public/app/routes/GrafanaCtrl.ts | 6 - 7 files changed, 137 insertions(+), 134 deletions(-) diff --git a/public/app/core/utils/location_util.ts b/public/app/core/utils/location_util.ts index 76f2fc5881f..15e1c275550 100644 --- a/public/app/core/utils/location_util.ts +++ b/public/app/core/utils/location_util.ts @@ -1,6 +1,6 @@ import config from 'app/core/config'; -export const stripBaseFromUrl = url => { +export const stripBaseFromUrl = (url: string): string => { const appSubUrl = config.appSubUrl; const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; const urlWithoutBase = diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 66edb149433..00f89920727 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; // Utils & Services import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import { appEvents } from 'app/core/app_events'; +import { PlaylistSrv } from 'app/features/playlist/playlist_srv'; // Components import { DashNavButton } from './DashNavButton'; @@ -116,12 +117,13 @@ export class DashNav extends PureComponent { }; render() { - const { dashboard, isFullscreen, editview } = this.props; + const { dashboard, isFullscreen, editview, $injector } = this.props; const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; const haveFolder = dashboard.meta.folderId > 0; const snapshotUrl = snapshot && snapshot.originalUrl; + const playlistSrv: PlaylistSrv = $injector.get('playlistSrv'); return (
@@ -135,13 +137,29 @@ export class DashNav extends PureComponent {
- {/* - - */} + + {playlistSrv.isPlaying && ( +
+ playlistSrv.prev()} + /> + playlistSrv.stop()} + /> + playlistSrv.next()} + /> +
+ )}
{canSave && ( @@ -151,71 +169,71 @@ export class DashNav extends PureComponent { icon="gicon gicon-add-panel" onClick={this.onAddPanel} /> - )} + )} - {canStar && ( - - )} + {canStar && ( + + )} - {canShare && ( - - )} + {canShare && ( + + )} - {canSave && ( - - )} + {canSave && ( + + )} - {snapshotUrl && ( - - )} + {snapshotUrl && ( + + )} - {showSettings && ( - - )} -
+ {showSettings && ( + + )} +
-
- -
+
+ +
-
(this.timePickerEl = element)} /> +
(this.timePickerEl = element)} /> - {(isFullscreen || editview) && ( -
- -
- )} -
+ {(isFullscreen || editview) && ( +
+ +
+ )} +
); } } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 3b902e4d089..a7b3f51d92c 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -69,6 +69,7 @@ export class DashboardPage extends PureComponent { urlType: this.props.urlType, urlFolderId: this.props.urlFolderId, routeInfo: this.props.routeInfo, + fixUrl: true, }); } diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 097c8015929..beb45b6904d 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -3,98 +3,78 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -// Utils & Services -import appEvents from 'app/core/app_events'; -import locationUtil from 'app/core/utils/location_util'; -import { getBackendSrv } from 'app/core/services/backend_srv'; - // Components import { DashboardPanel } from '../dashgrid/DashboardPanel'; // Redux -import { updateLocation } from 'app/core/actions'; +import { initDashboard } from '../state/initDashboard'; // Types -import { StoreState } from 'app/types'; +import { StoreState, DashboardRouteInfo } from 'app/types'; import { PanelModel, DashboardModel } from 'app/features/dashboard/state'; interface Props { - panelId: string; + urlPanelId: string; urlUid?: string; urlSlug?: string; urlType?: string; $scope: any; $injector: any; - updateLocation: typeof updateLocation; + routeInfo: DashboardRouteInfo; + initDashboard: typeof initDashboard; + dashboard: DashboardModel | null; } interface State { panel: PanelModel | null; - dashboard: DashboardModel | null; notFound: boolean; } export class SoloPanelPage extends Component { - state: State = { panel: null, - dashboard: null, notFound: false, }; componentDidMount() { - const { $injector, $scope, urlUid, urlType, urlSlug } = this.props; + const { $injector, $scope, urlUid, urlType, urlSlug, routeInfo } = this.props; - // handle old urls with no uid - if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { - this.redirectToNewUrl(); - return; - } - - const dashboardLoaderSrv = $injector.get('dashboardLoaderSrv'); - - // subscribe to event to know when dashboard controller is done with inititalization - appEvents.on('dashboard-initialized', this.onDashoardInitialized); - - dashboardLoaderSrv.loadDashboard(urlType, urlSlug, urlUid).then(result => { - result.meta.soloMode = true; - $scope.initDashboard(result, $scope); + this.props.initDashboard({ + $injector: $injector, + $scope: $scope, + urlSlug: urlSlug, + urlUid: urlUid, + urlType: urlType, + routeInfo: routeInfo, + fixUrl: false, }); } - redirectToNewUrl() { - getBackendSrv().getDashboardBySlug(this.props.urlSlug).then(res => { - if (res) { - const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - this.props.updateLocation(url); + componentDidUpdate(prevProps: Props) { + const { urlPanelId, dashboard } = this.props; + + if (!dashboard) { + return; + } + + // we just got the dashboard! + if (!prevProps.dashboard) { + const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + if (!panel) { + this.setState({ notFound: true }); + return; } - }); - } - onDashoardInitialized = () => { - const { $scope, panelId } = this.props; - - const dashboard: DashboardModel = $scope.dashboard; - const panel = dashboard.getPanelById(parseInt(panelId, 10)); - - if (!panel) { - this.setState({ notFound: true }); - return; + this.setState({ panel }); } - - this.setState({ dashboard, panel }); - }; + } render() { - const { panelId } = this.props; - const { notFound, panel, dashboard } = this.state; + const { urlPanelId, dashboard } = this.props; + const { notFound, panel } = this.state; if (notFound) { - return ( -
- Panel with id { panelId } not found -
- ); + return
Panel with id {urlPanelId} not found
; } if (!panel) { @@ -113,11 +93,13 @@ const mapStateToProps = (state: StoreState) => ({ urlUid: state.location.routeParams.uid, urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, - panelId: state.location.query.panelId + urlPanelId: state.location.query.panelId, + loadingState: state.dashboard.loadingState, + dashboard: state.dashboard.model as DashboardModel, }); const mapDispatchToProps = { - updateLocation + initDashboard, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage)); diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index e2e524941f8..38fadfecdc1 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -8,7 +8,6 @@ export class DashboardSrv { /** @ngInject */ constructor(private backendSrv, private $rootScope, private $location) { - appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); appEvents.on('panel-change-view', this.onPanelChangeView); } diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 89b8d470d3c..5419fcb41d7 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -25,16 +25,24 @@ export interface InitDashboardArgs { urlUid?: string; urlSlug?: string; urlType?: string; - urlFolderId: string; + urlFolderId?: string; routeInfo: string; + fixUrl: boolean; } -async function redirectToNewUrl(slug: string, dispatch: any) { +async function redirectToNewUrl(slug: string, dispatch: any, currentPath: string) { const res = await getBackendSrv().getDashboardBySlug(slug); if (res) { - const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - dispatch(updateLocation(url)); + let newUrl = res.meta.url; + + // fix solo route urls + if (currentPath.indexOf('dashboard-solo') !== -1) { + newUrl = newUrl.replace('/d/', '/d-solo/'); + } + + const url = locationUtil.stripBaseFromUrl(newUrl); + dispatch(updateLocation({ path: url, partial: true, replace: true })); } } @@ -46,6 +54,7 @@ export function initDashboard({ urlType, urlFolderId, routeInfo, + fixUrl, }: InitDashboardArgs): ThunkResult { return async (dispatch, getState) => { let dashDTO = null; @@ -76,14 +85,14 @@ export function initDashboard({ case DashboardRouteInfo.Normal: { // for old db routes we redirect if (urlType === 'db') { - redirectToNewUrl(urlSlug, dispatch); + redirectToNewUrl(urlSlug, dispatch, getState().location.path); return; } const loaderSrv = $injector.get('dashboardLoaderSrv'); dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - if (dashDTO.meta.url) { + if (fixUrl && dashDTO.meta.url) { // check if the current url is correct (might be old slug) const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); const currentPath = getState().location.path; diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 817e6452f44..f87fe69a684 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -45,12 +45,6 @@ export class GrafanaCtrl { }; $rootScope.colors = colors; - - $scope.initDashboard = (dashboardData, viewScope) => { - $scope.appEvent('dashboard-fetch-end', dashboardData); - $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData); - }; - $rootScope.onAppEvent = function(name, callback, localScope) { const unbind = $rootScope.$on(name, callback); let callerScope = this; From 70974c01f2cdf487cc0e24800f93db2333ae26ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 21:08:30 +0100 Subject: [PATCH 083/770] Added playlist controls to new react DashNav --- pkg/api/dtos/playlist.go | 1 + pkg/api/playlist_play.go | 1 + .../dashboard/components/DashNav/DashNav.tsx | 37 ++++++++++++++----- .../dashboard/containers/DashboardPage.tsx | 6 --- public/app/features/playlist/playlist_srv.ts | 23 +++++++++--- public/app/routes/GrafanaCtrl.ts | 17 ++++----- 6 files changed, 55 insertions(+), 30 deletions(-) diff --git a/pkg/api/dtos/playlist.go b/pkg/api/dtos/playlist.go index 317ff83339a..7f43bb4df8a 100644 --- a/pkg/api/dtos/playlist.go +++ b/pkg/api/dtos/playlist.go @@ -5,6 +5,7 @@ type PlaylistDashboard struct { Slug string `json:"slug"` Title string `json:"title"` Uri string `json:"uri"` + Url string `json:"url"` Order int `json:"order"` } diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index e82c7b438b4..5ca136c32c4 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -26,6 +26,7 @@ func populateDashboardsByID(dashboardByIDs []int64, dashboardIDOrder map[int64]i Slug: item.Slug, Title: item.Title, Uri: "db/" + item.Slug, + Url: m.GetDashboardUrl(item.Uid, item.Slug), Order: dashboardIDOrder[item.Id], }) } diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 00f89920727..374fd6dcd36 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -28,6 +28,13 @@ export interface Props { export class DashNav extends PureComponent { timePickerEl: HTMLElement; timepickerCmp: AngularComponent; + playlistSrv: PlaylistSrv; + + constructor(props: Props) { + super(props); + + this.playlistSrv = this.props.$injector.get('playlistSrv'); + } componentDidMount() { const loader = getAngularLoader(); @@ -95,7 +102,7 @@ export class DashNav extends PureComponent { }; onStarDashboard = () => { - const { $injector, dashboard } = this.props; + const { dashboard, $injector } = this.props; const dashboardSrv = $injector.get('dashboardSrv'); dashboardSrv.starDashboard(dashboard.id, dashboard.meta.isStarred).then(newState => { @@ -104,6 +111,19 @@ export class DashNav extends PureComponent { }); }; + onPlaylistPrev = () => { + this.playlistSrv.prev(); + }; + + onPlaylistNext = () => { + this.playlistSrv.next(); + }; + + onPlaylistStop = () => { + this.playlistSrv.stop(); + this.forceUpdate(); + }; + onOpenShare = () => { const $rootScope = this.props.$injector.get('$rootScope'); const modalScope = $rootScope.$new(); @@ -117,13 +137,12 @@ export class DashNav extends PureComponent { }; render() { - const { dashboard, isFullscreen, editview, $injector } = this.props; + const { dashboard, isFullscreen, editview } = this.props; const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; const haveFolder = dashboard.meta.folderId > 0; const snapshotUrl = snapshot && snapshot.originalUrl; - const playlistSrv: PlaylistSrv = $injector.get('playlistSrv'); return (
@@ -138,25 +157,25 @@ export class DashNav extends PureComponent {
- {playlistSrv.isPlaying && ( + {this.playlistSrv.isPlaying && (
playlistSrv.prev()} + onClick={this.onPlaylistPrev} /> playlistSrv.stop()} + onClick={this.onPlaylistStop} /> playlistSrv.next()} + onClick={this.onPlaylistNext} />
)} diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index a7b3f51d92c..3705cf15dac 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -100,12 +100,6 @@ export class DashboardPage extends PureComponent { }, 10); } - // // when dashboard has loaded subscribe to somme events - // if (prevProps.dashboard === null) { - // // set initial fullscreen class state - // this.setPanelFullscreenClass(); - // } - // Sync url state with model if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) { // entering fullscreen/edit mode diff --git a/public/app/features/playlist/playlist_srv.ts b/public/app/features/playlist/playlist_srv.ts index 0a80ce0cdf0..6c1cf2b4256 100644 --- a/public/app/features/playlist/playlist_srv.ts +++ b/public/app/features/playlist/playlist_srv.ts @@ -1,12 +1,16 @@ -import coreModule from '../../core/core_module'; -import kbn from 'app/core/utils/kbn'; -import appEvents from 'app/core/app_events'; +// Libraries import _ from 'lodash'; + +// Utils import { toUrlParams } from 'app/core/utils/url'; +import coreModule from '../../core/core_module'; +import appEvents from 'app/core/app_events'; +import locationUtil from 'app/core/utils/location_util'; +import kbn from 'app/core/utils/kbn'; export class PlaylistSrv { private cancelPromise: any; - private dashboards: Array<{ uri: string }>; + private dashboards: Array<{ url: string }>; private index: number; private interval: number; private startUrl: string; @@ -36,7 +40,12 @@ export class PlaylistSrv { const queryParams = this.$location.search(); const filteredParams = _.pickBy(queryParams, value => value !== null); - this.$location.url('dashboard/' + dash.uri + '?' + toUrlParams(filteredParams)); + // this is done inside timeout to make sure digest happens after + // as this can be called from react + this.$timeout(() => { + const stripedUrl = locationUtil.stripBaseFromUrl(dash.url); + this.$location.url(stripedUrl + '?' + toUrlParams(filteredParams)); + }); this.index++; this.cancelPromise = this.$timeout(() => this.next(), this.interval); @@ -54,6 +63,8 @@ export class PlaylistSrv { this.index = 0; this.isPlaying = true; + appEvents.emit('playlist-started'); + return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => { return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => { this.dashboards = dashboards; @@ -77,6 +88,8 @@ export class PlaylistSrv { if (this.cancelPromise) { this.$timeout.cancel(this.cancelPromise); } + + appEvents.emit('playlist-stopped'); } } diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index f87fe69a684..07d99725113 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -120,12 +120,13 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop body.toggleClass('sidemenu-hidden'); }); - scope.$watch( - () => playlistSrv.isPlaying, - newValue => { - elem.toggleClass('view-mode--playlist', newValue === true); - } - ); + appEvents.on('playlist-started', () => { + elem.toggleClass('view-mode--playlist', true); + }); + + appEvents.on('playlist-stopped', () => { + elem.toggleClass('view-mode--playlist', false); + }); // check if we are in server side render if (document.cookie.indexOf('renderKey') !== -1) { @@ -258,10 +259,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop }, 100); } - if (target.parents('.navbar-buttons--playlist').length === 0) { - playlistSrv.stop(); - } - // hide search if (body.find('.search-container').length > 0) { if (target.parents('.search-results-container, .search-field-wrapper').length === 0) { From e4dad78045bc70a17e8274310c67d7b82511660c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 4 Feb 2019 21:26:49 +0100 Subject: [PATCH 084/770] added flags to vizpicker from query param --- .../AddPanelWidget/AddPanelWidget.tsx | 48 ++++++++++++++----- .../panel_editor/VisualizationTab.tsx | 4 +- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index b3d5e6167c4..21c4451d9b9 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -17,6 +17,17 @@ export interface State { copiedPanelPlugins: any[]; } +type Location = { + query: { + panelId: number; + edit: boolean; + fullscreen: boolean; + tab?: string; + isVizPickerOpen?: boolean; + }; + partial: boolean; +}; + export class AddPanelWidget extends React.Component { constructor(props) { super(props); @@ -62,18 +73,8 @@ export class AddPanelWidget extends React.Component { ); } - moveToEdit(panel, tab) { - reduxStore.dispatch( - updateLocation({ - query: { - panelId: panel.id, - edit: true, - fullscreen: true, - tab: tab, - }, - partial: true, - }) - ); + moveToEdit(location) { + reduxStore.dispatch(updateLocation(location)); } onCreateNewPanel = (tab = 'queries') => { @@ -89,7 +90,28 @@ export class AddPanelWidget extends React.Component { dashboard.addPanel(newPanel); dashboard.removePanel(this.props.panel); - this.moveToEdit(newPanel, tab); + let location: Location = { + query: { + panelId: newPanel.id, + edit: true, + fullscreen: true, + }, + partial: true, + }; + + if (tab === 'visualization') { + location = { + ...location, + query: { + ...location.query, + tab: 'visualization', + isVizPickerOpen: true, + }, + }; + this.moveToEdit(location); + } else { + this.moveToEdit(location); + } }; onPasteCopiedPanel = panelPluginInfo => { diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index fdf978acdf9..1ca290d4051 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; // Utils & Services import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; -//TODO: See PanelEdit +import { store } from 'app/store/store'; // Components import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; @@ -39,7 +39,7 @@ export class VisualizationTab extends PureComponent { super(props); this.state = { - isVizPickerOpen: false, + isVizPickerOpen: store.getState().location.query.isVizPickerOpen === true, searchQuery: '', scrollTop: 0, }; From d29e1278dca3c7b07cf79e9c9d161569ba6b6460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 21:39:48 +0100 Subject: [PATCH 085/770] render after leaving fullscreen --- .../dashboard/containers/DashboardPage.tsx | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 3705cf15dac..404c953eecb 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -102,28 +102,46 @@ export class DashboardPage extends PureComponent { // Sync url state with model if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) { - // entering fullscreen/edit mode if (urlPanelId) { - const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); - - if (panel) { - dashboard.setViewMode(panel, urlFullscreen, urlEdit); - this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel }); - this.setPanelFullscreenClass(urlFullscreen); - } else { - this.handleFullscreenPanelNotFound(urlPanelId); - } + this.onEnterFullscreen(); } else { - // handle leaving fullscreen mode - if (this.state.fullscreenPanel) { - dashboard.setViewMode(this.state.fullscreenPanel, urlFullscreen, urlEdit); - } - this.setState({ isEditing: false, isFullscreen: false, fullscreenPanel: null }); - this.setPanelFullscreenClass(false); + this.onLeaveFullscreen(); } } } + onEnterFullscreen() { + const { dashboard, urlEdit, urlFullscreen, urlPanelId } = this.props; + + const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + + if (panel) { + dashboard.setViewMode(panel, urlFullscreen, urlEdit); + this.setState({ + isEditing: urlEdit, + isFullscreen: urlFullscreen, + fullscreenPanel: panel, + }); + this.setPanelFullscreenClass(urlFullscreen); + } else { + this.handleFullscreenPanelNotFound(urlPanelId); + } + } + + onLeaveFullscreen() { + const { dashboard } = this.props; + + if (this.state.fullscreenPanel) { + dashboard.setViewMode(this.state.fullscreenPanel, false, false); + } + + this.setState({ isEditing: false, isFullscreen: false, fullscreenPanel: null }, () => { + dashboard.render(); + }); + + this.setPanelFullscreenClass(false); + } + handleFullscreenPanelNotFound(urlPanelId: string) { // Panel not found this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`)); From 7cd3cd6cd43cb0ef2597ce032935e4b2f38904db Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Sat, 2 Feb 2019 12:11:30 +0100 Subject: [PATCH 086/770] auth package refactoring moving middleware/hooks away from package exposing public struct UserToken accessible from other packages fix debug log lines so the same order and naming are used --- pkg/services/auth/auth.go | 6 + pkg/services/auth/auth_token.go | 279 ------------- pkg/services/auth/auth_token_test.go | 378 ----------------- pkg/services/auth/authtoken/auth_token.go | 225 ++++++++++ .../auth/authtoken/auth_token_test.go | 386 ++++++++++++++++++ pkg/services/auth/authtoken/model.go | 76 ++++ .../auth/{ => authtoken}/session_cleanup.go | 2 +- .../{ => authtoken}/session_cleanup_test.go | 2 +- pkg/services/auth/model.go | 25 -- 9 files changed, 695 insertions(+), 684 deletions(-) create mode 100644 pkg/services/auth/auth.go delete mode 100644 pkg/services/auth/auth_token.go delete mode 100644 pkg/services/auth/auth_token_test.go create mode 100644 pkg/services/auth/authtoken/auth_token.go create mode 100644 pkg/services/auth/authtoken/auth_token_test.go create mode 100644 pkg/services/auth/authtoken/model.go rename pkg/services/auth/{ => authtoken}/session_cleanup.go (98%) rename pkg/services/auth/{ => authtoken}/session_cleanup_test.go (98%) delete mode 100644 pkg/services/auth/model.go diff --git a/pkg/services/auth/auth.go b/pkg/services/auth/auth.go new file mode 100644 index 00000000000..31316f473f5 --- /dev/null +++ b/pkg/services/auth/auth.go @@ -0,0 +1,6 @@ +package auth + +type UserToken interface { + GetUserId() int64 + GetToken() string +} diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go deleted file mode 100644 index 13b9ef607f5..00000000000 --- a/pkg/services/auth/auth_token.go +++ /dev/null @@ -1,279 +0,0 @@ -package auth - -import ( - "crypto/sha256" - "encoding/hex" - "errors" - "net/http" - "net/url" - "time" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/serverlock" - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" -) - -func init() { - registry.RegisterService(&UserAuthTokenServiceImpl{}) -} - -var ( - getTime = time.Now - UrgentRotateTime = 1 * time.Minute - oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often. -) - -// UserAuthTokenService are used for generating and validating user auth tokens -type UserAuthTokenService interface { - InitContextWithToken(ctx *models.ReqContext, orgID int64) bool - UserAuthenticatedHook(user *models.User, c *models.ReqContext) error - SignOutUser(c *models.ReqContext) error -} - -type UserAuthTokenServiceImpl struct { - SQLStore *sqlstore.SqlStore `inject:""` - ServerLockService *serverlock.ServerLockService `inject:""` - Cfg *setting.Cfg `inject:""` - log log.Logger -} - -// Init this service -func (s *UserAuthTokenServiceImpl) Init() error { - s.log = log.New("auth") - return nil -} - -func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool { - //auth User - unhashedToken := ctx.GetCookie(s.Cfg.LoginCookieName) - if unhashedToken == "" { - return false - } - - userToken, err := s.LookupToken(unhashedToken) - if err != nil { - ctx.Logger.Info("failed to look up user based on cookie", "error", err) - return false - } - - query := models.GetSignedInUserQuery{UserId: userToken.UserId, OrgId: orgID} - if err := bus.Dispatch(&query); err != nil { - ctx.Logger.Error("Failed to get user with id", "userId", userToken.UserId, "error", err) - return false - } - - ctx.SignedInUser = query.Result - ctx.IsSignedIn = true - - //rotate session token if needed. - rotated, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) - if err != nil { - ctx.Logger.Error("failed to rotate token", "error", err, "userId", userToken.UserId, "tokenId", userToken.Id) - return true - } - - if rotated { - s.writeSessionCookie(ctx, userToken.UnhashedToken, oneYearInSeconds) - } - - return true -} - -func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) { - if setting.Env == setting.DEV { - ctx.Logger.Debug("new token", "unhashed token", value) - } - - ctx.Resp.Header().Del("Set-Cookie") - cookie := http.Cookie{ - Name: s.Cfg.LoginCookieName, - Value: url.QueryEscape(value), - HttpOnly: true, - Path: setting.AppSubUrl + "/", - Secure: s.Cfg.SecurityHTTPSCookies, - MaxAge: maxAge, - SameSite: s.Cfg.LoginCookieSameSite, - } - - http.SetCookie(ctx.Resp, &cookie) -} - -func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error { - userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) - if err != nil { - return err - } - - s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds) - return nil -} - -func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error { - unhashedToken := c.GetCookie(s.Cfg.LoginCookieName) - if unhashedToken == "" { - return errors.New("cannot logout without session token") - } - - hashedToken := hashToken(unhashedToken) - - sql := `DELETE FROM user_auth_token WHERE auth_token = ?` - _, err := s.SQLStore.NewSession().Exec(sql, hashedToken) - - s.writeSessionCookie(c, "", -1) - return err -} - -func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { - clientIP = util.ParseIPAddress(clientIP) - token, err := util.RandomHex(16) - if err != nil { - return nil, err - } - - hashedToken := hashToken(token) - - now := getTime().Unix() - - userToken := userAuthToken{ - UserId: userId, - AuthToken: hashedToken, - PrevAuthToken: hashedToken, - ClientIp: clientIP, - UserAgent: userAgent, - RotatedAt: now, - CreatedAt: now, - UpdatedAt: now, - SeenAt: 0, - AuthTokenSeen: false, - } - _, err = s.SQLStore.NewSession().Insert(&userToken) - if err != nil { - return nil, err - } - - userToken.UnhashedToken = token - - return &userToken, nil -} - -func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) { - hashedToken := hashToken(unhashedToken) - if setting.Env == setting.DEV { - s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) - } - - expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix() - - var userToken userAuthToken - exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken) - if err != nil { - return nil, err - } - - if !exists { - return nil, ErrAuthTokenNotFound - } - - if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen { - userTokenCopy := userToken - userTokenCopy.AuthTokenSeen = false - expireBefore := getTime().Add(-UrgentRotateTime).Unix() - affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userTokenCopy.Id, userTokenCopy.PrevAuthToken, expireBefore).AllCols().Update(&userTokenCopy) - if err != nil { - return nil, err - } - - if affectedRows == 0 { - s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) - } else { - s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) - } - } - - if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken { - userTokenCopy := userToken - userTokenCopy.AuthTokenSeen = true - userTokenCopy.SeenAt = getTime().Unix() - affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy) - if err != nil { - return nil, err - } - - if affectedRows == 1 { - userToken = userTokenCopy - } - - if affectedRows == 0 { - s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) - } else { - s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) - } - } - - userToken.UnhashedToken = unhashedToken - - return &userToken, nil -} - -func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) { - if token == nil { - return false, nil - } - - now := getTime() - - needsRotation := false - rotatedAt := time.Unix(token.RotatedAt, 0) - if token.AuthTokenSeen { - needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute)) - } else { - needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime)) - } - - if !needsRotation { - return false, nil - } - - s.log.Debug("refresh token needs rotation?", "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id) - - clientIP = util.ParseIPAddress(clientIP) - newToken, _ := util.RandomHex(16) - hashedToken := hashToken(newToken) - - // very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly - sql := ` - UPDATE user_auth_token - SET - seen_at = 0, - user_agent = ?, - client_ip = ?, - prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end, - auth_token = ?, - auth_token_seen = ?, - rotated_at = ? - WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)` - - res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), token.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix()) - if err != nil { - return false, err - } - - affected, _ := res.RowsAffected() - s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId) - if affected > 0 { - token.UnhashedToken = newToken - return true, nil - } - - return false, nil -} - -func hashToken(token string) string { - hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) - return hex.EncodeToString(hashBytes[:]) -} diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go deleted file mode 100644 index 312e53a3970..00000000000 --- a/pkg/services/auth/auth_token_test.go +++ /dev/null @@ -1,378 +0,0 @@ -package auth - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" - macaron "gopkg.in/macaron.v1" - - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/services/sqlstore" - . "github.com/smartystreets/goconvey/convey" -) - -func TestUserAuthToken(t *testing.T) { - Convey("Test user auth token", t, func() { - ctx := createTestContext(t) - userAuthTokenService := ctx.tokenService - userID := int64(10) - - t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC) - getTime = func() time.Time { - return t - } - - Convey("When creating token", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - So(token.AuthTokenSeen, ShouldBeFalse) - - Convey("When lookup unhashed token should return user auth token", func() { - LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(LookupToken, ShouldNotBeNil) - So(LookupToken.UserId, ShouldEqual, userID) - So(LookupToken.AuthTokenSeen, ShouldBeTrue) - - storedAuthToken, err := ctx.getAuthTokenByID(LookupToken.Id) - So(err, ShouldBeNil) - So(storedAuthToken, ShouldNotBeNil) - So(storedAuthToken.AuthTokenSeen, ShouldBeTrue) - }) - - Convey("When lookup hashed token should return user auth token not found error", func() { - LookupToken, err := userAuthTokenService.LookupToken(token.AuthToken) - So(err, ShouldEqual, ErrAuthTokenNotFound) - So(LookupToken, ShouldBeNil) - }) - - Convey("signing out should delete token and cookie if present", func() { - httpreq := &http.Request{Header: make(http.Header)} - httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.UnhashedToken}) - - ctx := &models.ReqContext{Context: &macaron.Context{ - Req: macaron.Request{Request: httpreq}, - Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()), - }, - Logger: log.New("fakelogger"), - } - - err = userAuthTokenService.SignOutUser(ctx) - So(err, ShouldBeNil) - - // makes sure we tell the browser to overwrite the cookie - cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName) - So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader) - }) - - Convey("signing out an none existing session should return an error", func() { - httpreq := &http.Request{Header: make(http.Header)} - httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""}) - - ctx := &models.ReqContext{Context: &macaron.Context{ - Req: macaron.Request{Request: httpreq}, - Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()), - }, - Logger: log.New("fakelogger"), - } - - err = userAuthTokenService.SignOutUser(ctx) - So(err, ShouldNotBeNil) - }) - }) - - Convey("expires correctly", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - _, err = userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - - token, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - - getTime = func() time.Time { - return t.Add(time.Hour) - } - - refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - _, err = userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - - stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(stillGood, ShouldNotBeNil) - - getTime = func() time.Time { - return t.Add(24 * 7 * time.Hour) - } - notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldEqual, ErrAuthTokenNotFound) - So(notGood, ShouldBeNil) - }) - - Convey("can properly rotate tokens", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - prevToken := token.AuthToken - unhashedPrev := token.UnhashedToken - - refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") - So(err, ShouldBeNil) - So(refreshed, ShouldBeFalse) - - updated, err := ctx.markAuthTokenAsSeen(token.Id) - So(err, ShouldBeNil) - So(updated, ShouldBeTrue) - - token, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - - getTime = func() time.Time { - return t.Add(time.Hour) - } - - refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - unhashedToken := token.UnhashedToken - - token, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - token.UnhashedToken = unhashedToken - - So(token.RotatedAt, ShouldEqual, getTime().Unix()) - So(token.ClientIp, ShouldEqual, "192.168.10.12") - So(token.UserAgent, ShouldEqual, "a new user agent") - So(token.AuthTokenSeen, ShouldBeFalse) - So(token.SeenAt, ShouldEqual, 0) - So(token.PrevAuthToken, ShouldEqual, prevToken) - - // ability to auth using an old token - - lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.AuthTokenSeen, ShouldBeTrue) - So(lookedUp.SeenAt, ShouldEqual, getTime().Unix()) - - lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.Id, ShouldEqual, token.Id) - So(lookedUp.AuthTokenSeen, ShouldBeTrue) - - getTime = func() time.Time { - return t.Add(time.Hour + (2 * time.Minute)) - } - - lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.AuthTokenSeen, ShouldBeTrue) - - lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.AuthTokenSeen, ShouldBeFalse) - - refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - token, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - So(token.SeenAt, ShouldEqual, 0) - }) - - Convey("keeps prev token valid for 1 minute after it is confirmed", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - - getTime = func() time.Time { - return t.Add(10 * time.Minute) - } - - prevToken := token.UnhashedToken - refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - getTime = func() time.Time { - return t.Add(20 * time.Minute) - } - - current, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(current, ShouldNotBeNil) - - prev, err := userAuthTokenService.LookupToken(prevToken) - So(err, ShouldBeNil) - So(prev, ShouldNotBeNil) - }) - - Convey("will not mark token unseen when prev and current are the same", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - - lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - - lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.AuthTokenSeen, ShouldBeTrue) - }) - - Convey("Rotate token", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - prevToken := token.AuthToken - - Convey("Should rotate current token and previous token when auth token seen", func() { - updated, err := ctx.markAuthTokenAsSeen(token.Id) - So(err, ShouldBeNil) - So(updated, ShouldBeTrue) - - getTime = func() time.Time { - return t.Add(10 * time.Minute) - } - - refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - storedToken, err := ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - So(storedToken, ShouldNotBeNil) - So(storedToken.AuthTokenSeen, ShouldBeFalse) - So(storedToken.PrevAuthToken, ShouldEqual, prevToken) - So(storedToken.AuthToken, ShouldNotEqual, prevToken) - - prevToken = storedToken.AuthToken - - updated, err = ctx.markAuthTokenAsSeen(token.Id) - So(err, ShouldBeNil) - So(updated, ShouldBeTrue) - - getTime = func() time.Time { - return t.Add(20 * time.Minute) - } - - refreshed, err = userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - storedToken, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - So(storedToken, ShouldNotBeNil) - So(storedToken.AuthTokenSeen, ShouldBeFalse) - So(storedToken.PrevAuthToken, ShouldEqual, prevToken) - So(storedToken.AuthToken, ShouldNotEqual, prevToken) - }) - - Convey("Should rotate current token, but keep previous token when auth token not seen", func() { - token.RotatedAt = getTime().Add(-2 * time.Minute).Unix() - - getTime = func() time.Time { - return t.Add(2 * time.Minute) - } - - refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - storedToken, err := ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - So(storedToken, ShouldNotBeNil) - So(storedToken.AuthTokenSeen, ShouldBeFalse) - So(storedToken.PrevAuthToken, ShouldEqual, prevToken) - So(storedToken.AuthToken, ShouldNotEqual, prevToken) - }) - }) - - Reset(func() { - getTime = time.Now - }) - }) -} - -func createTestContext(t *testing.T) *testContext { - t.Helper() - - sqlstore := sqlstore.InitTestDB(t) - tokenService := &UserAuthTokenServiceImpl{ - SQLStore: sqlstore, - Cfg: &setting.Cfg{ - LoginCookieName: "grafana_session", - LoginCookieMaxDays: 7, - LoginDeleteExpiredTokensAfterDays: 30, - LoginCookieRotation: 10, - }, - log: log.New("test-logger"), - } - - UrgentRotateTime = time.Minute - - return &testContext{ - sqlstore: sqlstore, - tokenService: tokenService, - } -} - -type testContext struct { - sqlstore *sqlstore.SqlStore - tokenService *UserAuthTokenServiceImpl -} - -func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) { - sess := c.sqlstore.NewSession() - var t userAuthToken - found, err := sess.ID(id).Get(&t) - if err != nil || !found { - return nil, err - } - - return &t, nil -} - -func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) { - sess := c.sqlstore.NewSession() - res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id) - if err != nil { - return false, err - } - - rowsAffected, err := res.RowsAffected() - if err != nil { - return false, err - } - return rowsAffected == 1, nil -} diff --git a/pkg/services/auth/authtoken/auth_token.go b/pkg/services/auth/authtoken/auth_token.go new file mode 100644 index 00000000000..4e4bd375501 --- /dev/null +++ b/pkg/services/auth/authtoken/auth_token.go @@ -0,0 +1,225 @@ +package authtoken + +import ( + "crypto/sha256" + "encoding/hex" + "time" + + "github.com/grafana/grafana/pkg/services/auth" + + "github.com/grafana/grafana/pkg/infra/serverlock" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" +) + +func init() { + registry.Register(®istry.Descriptor{ + Name: "AuthTokenService", + Instance: &UserAuthTokenServiceImpl{}, + InitPriority: registry.Low, + }) +} + +var getTime = time.Now + +const urgentRotateTime = 1 * time.Minute + +type UserAuthTokenServiceImpl struct { + SQLStore *sqlstore.SqlStore `inject:""` + ServerLockService *serverlock.ServerLockService `inject:""` + Cfg *setting.Cfg `inject:""` + log log.Logger +} + +func (s *UserAuthTokenServiceImpl) Init() error { + s.log = log.New("auth") + return nil +} + +func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + clientIP = util.ParseIPAddress(clientIP) + token, err := util.RandomHex(16) + if err != nil { + return nil, err + } + + hashedToken := hashToken(token) + + now := getTime().Unix() + + userAuthToken := userAuthToken{ + UserId: userId, + AuthToken: hashedToken, + PrevAuthToken: hashedToken, + ClientIp: clientIP, + UserAgent: userAgent, + RotatedAt: now, + CreatedAt: now, + UpdatedAt: now, + SeenAt: 0, + AuthTokenSeen: false, + } + _, err = s.SQLStore.NewSession().Insert(&userAuthToken) + if err != nil { + return nil, err + } + + userAuthToken.UnhashedToken = token + + s.log.Debug("user auth token created", "tokenId", userAuthToken.Id, "userId", userAuthToken.UserId, "clientIP", userAuthToken.ClientIp, "userAgent", userAuthToken.UserAgent, "authToken", userAuthToken.AuthToken) + + return userAuthToken.toUserToken() +} + +func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (auth.UserToken, error) { + hashedToken := hashToken(unhashedToken) + if setting.Env == setting.DEV { + s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) + } + + expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix() + + var model userAuthToken + exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&model) + if err != nil { + return nil, err + } + + if !exists { + return nil, ErrAuthTokenNotFound + } + + if model.AuthToken != hashedToken && model.PrevAuthToken == hashedToken && model.AuthTokenSeen { + modelCopy := model + modelCopy.AuthTokenSeen = false + expireBefore := getTime().Add(-urgentRotateTime).Unix() + affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", modelCopy.Id, modelCopy.PrevAuthToken, expireBefore).AllCols().Update(&modelCopy) + if err != nil { + return nil, err + } + + if affectedRows == 0 { + s.log.Debug("prev seen token unchanged", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken) + } else { + s.log.Debug("prev seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken) + } + } + + if !model.AuthTokenSeen && model.AuthToken == hashedToken { + modelCopy := model + modelCopy.AuthTokenSeen = true + modelCopy.SeenAt = getTime().Unix() + affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", modelCopy.Id, modelCopy.AuthToken).AllCols().Update(&modelCopy) + if err != nil { + return nil, err + } + + if affectedRows == 1 { + model = modelCopy + } + + if affectedRows == 0 { + s.log.Debug("seen wrong token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken) + } else { + s.log.Debug("seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken) + } + } + + model.UnhashedToken = unhashedToken + return model.toUserToken() +} + +func (s *UserAuthTokenServiceImpl) TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) { + if token == nil { + return false, nil + } + + model, err := extractModelFromToken(token) + if err != nil { + return false, err + } + + now := getTime() + + needsRotation := false + rotatedAt := time.Unix(model.RotatedAt, 0) + if model.AuthTokenSeen { + needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute)) + } else { + needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime)) + } + + if !needsRotation { + return false, nil + } + + s.log.Debug("token needs rotation", "tokenId", model.Id, "authTokenSeen", model.AuthTokenSeen, "rotatedAt", rotatedAt) + + clientIP = util.ParseIPAddress(clientIP) + newToken, err := util.RandomHex(16) + if err != nil { + return false, err + } + hashedToken := hashToken(newToken) + + // very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly + sql := ` + UPDATE user_auth_token + SET + seen_at = 0, + user_agent = ?, + client_ip = ?, + prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end, + auth_token = ?, + auth_token_seen = ?, + rotated_at = ? + WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)` + + res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), model.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix()) + if err != nil { + return false, err + } + + affected, _ := res.RowsAffected() + s.log.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId) + if affected > 0 { + model.UnhashedToken = newToken + return true, nil + } + + return false, nil +} + +func (s *UserAuthTokenServiceImpl) RevokeToken(token auth.UserToken) error { + if token == nil { + return ErrAuthTokenNotFound + } + + model, err := extractModelFromToken(token) + if err != nil { + return err + } + + rowsAffected, err := s.SQLStore.NewSession().Delete(model) + if err != nil { + return err + } + + if rowsAffected == 0 { + s.log.Debug("user auth token not found/revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent) + return ErrAuthTokenNotFound + } + + s.log.Debug("user auth token revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent) + + return nil +} + +func hashToken(token string) string { + hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) + return hex.EncodeToString(hashBytes[:]) +} diff --git a/pkg/services/auth/authtoken/auth_token_test.go b/pkg/services/auth/authtoken/auth_token_test.go new file mode 100644 index 00000000000..7809e235f5c --- /dev/null +++ b/pkg/services/auth/authtoken/auth_token_test.go @@ -0,0 +1,386 @@ +package authtoken + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/setting" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" + . "github.com/smartystreets/goconvey/convey" +) + +func TestUserAuthToken(t *testing.T) { + Convey("Test user auth token", t, func() { + ctx := createTestContext(t) + userAuthTokenService := ctx.tokenService + userID := int64(10) + + t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC) + getTime = func() time.Time { + return t + } + + Convey("When creating token", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + So(model.AuthTokenSeen, ShouldBeFalse) + + Convey("When lookup unhashed token should return user auth token", func() { + userToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + lookedUpModel, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.UserId, ShouldEqual, userID) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + + storedAuthToken, err := ctx.getAuthTokenByID(lookedUpModel.Id) + So(err, ShouldBeNil) + So(storedAuthToken, ShouldNotBeNil) + So(storedAuthToken.AuthTokenSeen, ShouldBeTrue) + }) + + Convey("When lookup hashed token should return user auth token not found error", func() { + userToken, err := userAuthTokenService.LookupToken(model.AuthToken) + So(err, ShouldEqual, ErrAuthTokenNotFound) + So(userToken, ShouldBeNil) + }) + + Convey("revoking existing token should delete token", func() { + err = userAuthTokenService.RevokeToken(userToken) + So(err, ShouldBeNil) + + model, err := ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(model, ShouldBeNil) + }) + + Convey("revoking nil token should return error", func() { + err = userAuthTokenService.RevokeToken(nil) + So(err, ShouldEqual, ErrAuthTokenNotFound) + }) + + Convey("revoking non-existing token should return error", func() { + model.Id = 1000 + nonExistingToken, err := model.toUserToken() + So(err, ShouldBeNil) + err = userAuthTokenService.RevokeToken(nonExistingToken) + So(err, ShouldEqual, ErrAuthTokenNotFound) + }) + }) + + Convey("expires correctly", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + _, err = userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + + userToken, err = model.toUserToken() + So(err, ShouldBeNil) + + getTime = func() time.Time { + return t.Add(time.Hour) + } + + rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + _, err = userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + + stillGood, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + So(stillGood, ShouldNotBeNil) + + getTime = func() time.Time { + return t.Add(24 * 7 * time.Hour) + } + notGood, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldEqual, ErrAuthTokenNotFound) + So(notGood, ShouldBeNil) + }) + + Convey("can properly rotate tokens", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + prevToken := model.AuthToken + unhashedPrev := model.UnhashedToken + + rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(rotated, ShouldBeFalse) + + updated, err := ctx.markAuthTokenAsSeen(model.Id) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + tok, err := model.toUserToken() + So(err, ShouldBeNil) + + getTime = func() time.Time { + return t.Add(time.Hour) + } + + rotated, err = userAuthTokenService.TryRotateToken(tok, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + unhashedToken := model.UnhashedToken + + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + model.UnhashedToken = unhashedToken + + So(model.RotatedAt, ShouldEqual, getTime().Unix()) + So(model.ClientIp, ShouldEqual, "192.168.10.12") + So(model.UserAgent, ShouldEqual, "a new user agent") + So(model.AuthTokenSeen, ShouldBeFalse) + So(model.SeenAt, ShouldEqual, 0) + So(model.PrevAuthToken, ShouldEqual, prevToken) + + // ability to auth using an old token + + lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + lookedUpModel, err := extractModelFromToken(lookedUpUserToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + So(lookedUpModel.SeenAt, ShouldEqual, getTime().Unix()) + + lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.Id, ShouldEqual, model.Id) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + + getTime = func() time.Time { + return t.Add(time.Hour + (2 * time.Minute)) + } + + lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev) + So(err, ShouldBeNil) + lookedUpModel, err = extractModelFromToken(lookedUpUserToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + + lookedUpModel, err = ctx.getAuthTokenByID(lookedUpModel.Id) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.AuthTokenSeen, ShouldBeFalse) + + rotated, err = userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + So(model.SeenAt, ShouldEqual, 0) + }) + + Convey("keeps prev token valid for 1 minute after it is confirmed", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + So(lookedUpUserToken, ShouldNotBeNil) + + getTime = func() time.Time { + return t.Add(10 * time.Minute) + } + + prevToken := model.UnhashedToken + rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + getTime = func() time.Time { + return t.Add(20 * time.Minute) + } + + currentUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + So(currentUserToken, ShouldNotBeNil) + + prevUserToken, err := userAuthTokenService.LookupToken(prevToken) + So(err, ShouldBeNil) + So(prevUserToken, ShouldNotBeNil) + }) + + Convey("will not mark token unseen when prev and current are the same", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + lookedUpModel, err := extractModelFromToken(lookedUpUserToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + + lookedUpUserToken, err = userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + lookedUpModel, err = extractModelFromToken(lookedUpUserToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + + lookedUpModel, err = ctx.getAuthTokenByID(lookedUpModel.Id) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + }) + + Convey("Rotate token", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + prevToken := model.AuthToken + + Convey("Should rotate current token and previous token when auth token seen", func() { + updated, err := ctx.markAuthTokenAsSeen(model.Id) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + + getTime = func() time.Time { + return t.Add(10 * time.Minute) + } + + rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + storedToken, err := ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(storedToken, ShouldNotBeNil) + So(storedToken.AuthTokenSeen, ShouldBeFalse) + So(storedToken.PrevAuthToken, ShouldEqual, prevToken) + So(storedToken.AuthToken, ShouldNotEqual, prevToken) + + prevToken = storedToken.AuthToken + + updated, err = ctx.markAuthTokenAsSeen(model.Id) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + + getTime = func() time.Time { + return t.Add(20 * time.Minute) + } + + rotated, err = userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + storedToken, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(storedToken, ShouldNotBeNil) + So(storedToken.AuthTokenSeen, ShouldBeFalse) + So(storedToken.PrevAuthToken, ShouldEqual, prevToken) + So(storedToken.AuthToken, ShouldNotEqual, prevToken) + }) + + Convey("Should rotate current token, but keep previous token when auth token not seen", func() { + model.RotatedAt = getTime().Add(-2 * time.Minute).Unix() + + getTime = func() time.Time { + return t.Add(2 * time.Minute) + } + + rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + storedToken, err := ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(storedToken, ShouldNotBeNil) + So(storedToken.AuthTokenSeen, ShouldBeFalse) + So(storedToken.PrevAuthToken, ShouldEqual, prevToken) + So(storedToken.AuthToken, ShouldNotEqual, prevToken) + }) + }) + + Reset(func() { + getTime = time.Now + }) + }) +} + +func createTestContext(t *testing.T) *testContext { + t.Helper() + + sqlstore := sqlstore.InitTestDB(t) + tokenService := &UserAuthTokenServiceImpl{ + SQLStore: sqlstore, + Cfg: &setting.Cfg{ + LoginCookieName: "grafana_session", + LoginCookieMaxDays: 7, + LoginDeleteExpiredTokensAfterDays: 30, + LoginCookieRotation: 10, + }, + log: log.New("test-logger"), + } + + return &testContext{ + sqlstore: sqlstore, + tokenService: tokenService, + } +} + +type testContext struct { + sqlstore *sqlstore.SqlStore + tokenService *UserAuthTokenServiceImpl +} + +func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) { + sess := c.sqlstore.NewSession() + var t userAuthToken + found, err := sess.ID(id).Get(&t) + if err != nil || !found { + return nil, err + } + + return &t, nil +} + +func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) { + sess := c.sqlstore.NewSession() + res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id) + if err != nil { + return false, err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return false, err + } + return rowsAffected == 1, nil +} diff --git a/pkg/services/auth/authtoken/model.go b/pkg/services/auth/authtoken/model.go new file mode 100644 index 00000000000..8bd89c68b04 --- /dev/null +++ b/pkg/services/auth/authtoken/model.go @@ -0,0 +1,76 @@ +package authtoken + +import ( + "errors" + "fmt" + + "github.com/grafana/grafana/pkg/services/auth" +) + +// Typed errors +var ( + ErrAuthTokenNotFound = errors.New("user auth token not found") +) + +type userAuthToken struct { + Id int64 + UserId int64 + AuthToken string + PrevAuthToken string + UserAgent string + ClientIp string + AuthTokenSeen bool + SeenAt int64 + RotatedAt int64 + CreatedAt int64 + UpdatedAt int64 + UnhashedToken string `xorm:"-"` +} + +func (uat *userAuthToken) toUserToken() (auth.UserToken, error) { + if uat == nil { + return nil, fmt.Errorf("needs pointer to userAuthToken struct") + } + + return &userTokenImpl{ + userAuthToken: uat, + }, nil +} + +type userToken interface { + auth.UserToken + GetModel() *userAuthToken +} + +type userTokenImpl struct { + *userAuthToken +} + +func (ut *userTokenImpl) GetUserId() int64 { + return ut.UserId +} + +func (ut *userTokenImpl) GetToken() string { + return ut.UnhashedToken +} + +func (ut *userTokenImpl) GetModel() *userAuthToken { + return ut.userAuthToken +} + +func extractModelFromToken(token auth.UserToken) (*userAuthToken, error) { + ut, ok := token.(userToken) + if !ok { + return nil, fmt.Errorf("failed to cast token") + } + + return ut.GetModel(), nil +} + +// UserAuthTokenService are used for generating and validating user auth tokens +type UserAuthTokenService interface { + CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) + LookupToken(unhashedToken string) (auth.UserToken, error) + TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) + RevokeToken(token auth.UserToken) error +} diff --git a/pkg/services/auth/session_cleanup.go b/pkg/services/auth/authtoken/session_cleanup.go similarity index 98% rename from pkg/services/auth/session_cleanup.go rename to pkg/services/auth/authtoken/session_cleanup.go index 7e523181a7b..cd2b766d6c0 100644 --- a/pkg/services/auth/session_cleanup.go +++ b/pkg/services/auth/authtoken/session_cleanup.go @@ -1,4 +1,4 @@ -package auth +package authtoken import ( "context" diff --git a/pkg/services/auth/session_cleanup_test.go b/pkg/services/auth/authtoken/session_cleanup_test.go similarity index 98% rename from pkg/services/auth/session_cleanup_test.go rename to pkg/services/auth/authtoken/session_cleanup_test.go index eef2cd74d04..101a279c374 100644 --- a/pkg/services/auth/session_cleanup_test.go +++ b/pkg/services/auth/authtoken/session_cleanup_test.go @@ -1,4 +1,4 @@ -package auth +package authtoken import ( "fmt" diff --git a/pkg/services/auth/model.go b/pkg/services/auth/model.go deleted file mode 100644 index 7a0f49539f2..00000000000 --- a/pkg/services/auth/model.go +++ /dev/null @@ -1,25 +0,0 @@ -package auth - -import ( - "errors" -) - -// Typed errors -var ( - ErrAuthTokenNotFound = errors.New("User auth token not found") -) - -type userAuthToken struct { - Id int64 - UserId int64 - AuthToken string - PrevAuthToken string - UserAgent string - ClientIp string - AuthTokenSeen bool - SeenAt int64 - RotatedAt int64 - CreatedAt int64 - UpdatedAt int64 - UnhashedToken string `xorm:"-"` -} From d53e64a32c48fca9149d2e291313a6dc8b04bb53 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 23:44:28 +0100 Subject: [PATCH 087/770] move auth token middleware/hooks to middleware package fix/adds auth token middleware tests --- pkg/api/common_test.go | 63 +++++++++-- pkg/api/http_server.go | 18 +-- pkg/api/login.go | 17 ++- pkg/middleware/middleware.go | 65 ++++++++++- pkg/middleware/middleware_test.go | 165 +++++++++++++++++++++++++--- pkg/middleware/org_redirect_test.go | 31 ++++-- pkg/middleware/quota_test.go | 16 ++- pkg/models/context.go | 2 + 8 files changed, 324 insertions(+), 53 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index fe02c94e277..853a04b5c11 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "gopkg.in/macaron.v1" . "github.com/smartystreets/goconvey/convey" @@ -129,24 +130,70 @@ func setupScenarioContext(url string) *scenarioContext { return sc } +type fakeUserToken interface { + auth.UserToken + SetToken(token string) +} + +type userTokenImpl struct { + userId int64 + token string +} + +func (ut *userTokenImpl) GetUserId() int64 { + return ut.userId +} + +func (ut *userTokenImpl) GetToken() string { + return ut.token +} + +func (ut *userTokenImpl) SetToken(token string) { + ut.token = token +} + type fakeUserAuthTokenService struct { - initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool + createTokenProvider func(userId int64, clientIP, userAgent string) (auth.UserToken, error) + tryRotateTokenProvider func(token auth.UserToken, clientIP, userAgent string) (bool, error) + lookupTokenProvider func(unhashedToken string) (auth.UserToken, error) + revokeTokenProvider func(token auth.UserToken) error } func newFakeUserAuthTokenService() *fakeUserAuthTokenService { return &fakeUserAuthTokenService{ - initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool { - return false + createTokenProvider: func(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 0, + token: "", + }, nil + }, + tryRotateTokenProvider: func(token auth.UserToken, clientIP, userAgent string) (bool, error) { + return false, nil + }, + lookupTokenProvider: func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 0, + token: "", + }, nil + }, + revokeTokenProvider: func(token auth.UserToken) error { + return nil }, } } -func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool { - return s.initContextWithTokenProvider(ctx, orgID) +func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + return s.createTokenProvider(userId, clientIP, userAgent) } -func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error { - return nil +func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (auth.UserToken, error) { + return s.lookupTokenProvider(unhashedToken) } -func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } +func (s *fakeUserAuthTokenService) TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) { + return s.tryRotateTokenProvider(token, clientIP, userAgent) +} + +func (s *fakeUserAuthTokenService) RevokeToken(token auth.UserToken) error { + return s.revokeTokenProvider(token) +} diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 7b7c1478a4c..a0a65d73244 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -21,7 +21,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtoken" "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/hooks" @@ -48,14 +48,14 @@ type HTTPServer struct { streamManager *live.StreamManager httpSrv *http.Server - RouteRegister routing.RouteRegister `inject:""` - Bus bus.Bus `inject:""` - RenderService rendering.Service `inject:""` - Cfg *setting.Cfg `inject:""` - HooksService *hooks.HooksService `inject:""` - CacheService *cache.CacheService `inject:""` - DatasourceCache datasources.CacheService `inject:""` - AuthTokenService auth.UserAuthTokenService `inject:""` + RouteRegister routing.RouteRegister `inject:""` + Bus bus.Bus `inject:""` + RenderService rendering.Service `inject:""` + Cfg *setting.Cfg `inject:""` + HooksService *hooks.HooksService `inject:""` + CacheService *cache.CacheService `inject:""` + DatasourceCache datasources.CacheService `inject:""` + AuthTokenService authtoken.UserAuthTokenService `inject:""` } func (hs *HTTPServer) Init() error { diff --git a/pkg/api/login.go b/pkg/api/login.go index 49da147724e..d25e83d34e8 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -5,11 +5,14 @@ import ( "net/http" "net/url" + "github.com/grafana/grafana/pkg/services/auth/authtoken" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" + "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -126,17 +129,23 @@ func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { if user == nil { - hs.log.Error("User login with nil user") + hs.log.Error("user login with nil user") } - err := hs.AuthTokenService.UserAuthenticatedHook(user, c) + userToken, err := hs.AuthTokenService.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) if err != nil { - hs.log.Error("User auth hook failed", "error", err) + hs.log.Error("failed to create auth token", "error", err) } + + middleware.WriteSessionCookie(c, userToken.GetToken(), middleware.OneYearInSeconds) } func (hs *HTTPServer) Logout(c *m.ReqContext) { - hs.AuthTokenService.SignOutUser(c) + if err := hs.AuthTokenService.RevokeToken(c.UserToken); err != nil && err != authtoken.ErrAuthTokenNotFound { + hs.log.Error("failed to revoke auth token", "error", err) + } + + middleware.WriteSessionCookie(c, "", -1) if setting.SignoutRedirectUrl != "" { c.Redirect(setting.SignoutRedirectUrl) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 3722ac3058f..6cf29340b82 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,13 +1,15 @@ package middleware import ( + "net/http" + "net/url" "strconv" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtoken" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -21,7 +23,7 @@ var ( ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN) ) -func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler { +func GetContextHandler(ats authtoken.UserAuthTokenService) macaron.Handler { return func(c *macaron.Context) { ctx := &m.ReqContext{ Context: c, @@ -49,7 +51,7 @@ func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler { case initContextWithApiKey(ctx): case initContextWithBasicAuth(ctx, orgId): case initContextWithAuthProxy(ctx, orgId): - case ats.InitContextWithToken(ctx, orgId): + case initContextWithToken(ats, ctx, orgId): case initContextWithAnonymousUser(ctx): } @@ -166,6 +168,63 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool { return true } +const cookieName = "grafana_session" +const OneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often. + +func initContextWithToken(authTokenService authtoken.UserAuthTokenService, ctx *m.ReqContext, orgID int64) bool { + rawToken := ctx.GetCookie(cookieName) + if rawToken == "" { + return false + } + + token, err := authTokenService.LookupToken(rawToken) + if err != nil { + ctx.Logger.Error("failed to look up user based on cookie", "error", err) + return false + } + + query := m.GetSignedInUserQuery{UserId: token.GetUserId(), OrgId: orgID} + if err := bus.Dispatch(&query); err != nil { + ctx.Logger.Error("failed to get user with id", "userId", token.GetUserId(), "error", err) + return false + } + + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + ctx.UserToken = token + + rotated, err := authTokenService.TryRotateToken(token, ctx.RemoteAddr(), ctx.Req.UserAgent()) + if err != nil { + ctx.Logger.Error("failed to rotate token", "error", err) + return true + } + + if rotated { + WriteSessionCookie(ctx, token.GetToken(), OneYearInSeconds) + } + + return true +} + +func WriteSessionCookie(ctx *m.ReqContext, value string, maxAge int) { + if setting.Env == setting.DEV { + ctx.Logger.Info("new token", "unhashed token", value) + } + + ctx.Resp.Header().Del("Set-Cookie") + cookie := http.Cookie{ + Name: cookieName, + Value: url.QueryEscape(value), + HttpOnly: true, + Path: setting.AppSubUrl + "/", + Secure: false, // TODO: use setting SecurityHTTPSCookies + MaxAge: maxAge, + SameSite: http.SameSiteLaxMode, // TODO: use setting LoginCookieSameSite + } + + http.SetCookie(ctx.Resp, &cookie) +} + func AddDefaultResponseHeaders() macaron.Handler { return func(ctx *m.ReqContext) { if ctx.IsApiRequest() && ctx.Req.Method == "GET" { diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 4679c449853..4e10ee39201 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -10,6 +10,8 @@ import ( msession "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtoken" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -146,17 +148,91 @@ func TestMiddlewareContext(t *testing.T) { }) }) - middlewareScenario("Auth token service", func(sc *scenarioContext) { - var wasCalled bool - sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { - wasCalled = true - return false + middlewareScenario("Non-expired auth token in cookie which not are being rotated", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: unhashedToken, + }, nil } sc.fakeReq("GET", "/").exec() - Convey("should call middleware", func() { - So(wasCalled, ShouldBeTrue) + Convey("should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 12) + So(sc.context.UserToken.GetUserId(), ShouldEqual, 12) + So(sc.context.UserToken.GetToken(), ShouldEqual, "token") + }) + + Convey("should not set cookie", func() { + So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, "") + }) + }) + + middlewareScenario("Non-expired auth token in cookie which are being rotated", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: unhashedToken, + }, nil + } + + sc.userAuthTokenService.tryRotateTokenProvider = func(userToken auth.UserToken, clientIP, userAgent string) (bool, error) { + userToken.(fakeUserToken).SetToken("rotated") + return true, nil + } + + expectedCookie := &http.Cookie{ + Name: cookieName, + Value: "rotated", + Path: setting.AppSubUrl + "/", + HttpOnly: true, + MaxAge: OneYearInSeconds, + SameSite: http.SameSiteLaxMode, + } + + sc.fakeReq("GET", "/").exec() + + Convey("should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 12) + So(sc.context.UserToken.GetUserId(), ShouldEqual, 12) + So(sc.context.UserToken.GetToken(), ShouldEqual, "rotated") + }) + + Convey("should set cookie", func() { + So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String()) + }) + }) + + middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return nil, authtoken.ErrAuthTokenNotFound + } + + sc.fakeReq("GET", "/").exec() + + Convey("should not init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeFalse) + So(sc.context.UserId, ShouldEqual, 0) + So(sc.context.UserToken, ShouldBeNil) }) }) @@ -508,6 +584,7 @@ type scenarioContext struct { resp *httptest.ResponseRecorder apiKey string authHeader string + tokenSessionCookie string respJson map[string]interface{} handlerFunc handlerFunc defaultHandler macaron.Handler @@ -522,6 +599,11 @@ func (sc *scenarioContext) withValidApiKey() *scenarioContext { return sc } +func (sc *scenarioContext) withTokenSessionCookie(unhashedToken string) *scenarioContext { + sc.tokenSessionCookie = unhashedToken + return sc +} + func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext { sc.authHeader = authHeader return sc @@ -571,6 +653,13 @@ func (sc *scenarioContext) exec() { sc.req.Header.Add("Authorization", sc.authHeader) } + if sc.tokenSessionCookie != "" { + sc.req.AddCookie(&http.Cookie{ + Name: cookieName, + Value: sc.tokenSessionCookie, + }) + } + sc.m.ServeHTTP(sc.resp, sc.req) if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { @@ -582,24 +671,70 @@ func (sc *scenarioContext) exec() { type scenarioFunc func(c *scenarioContext) type handlerFunc func(c *m.ReqContext) +type fakeUserToken interface { + auth.UserToken + SetToken(token string) +} + +type userTokenImpl struct { + userId int64 + token string +} + +func (ut *userTokenImpl) GetUserId() int64 { + return ut.userId +} + +func (ut *userTokenImpl) GetToken() string { + return ut.token +} + +func (ut *userTokenImpl) SetToken(token string) { + ut.token = token +} + type fakeUserAuthTokenService struct { - initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool + createTokenProvider func(userId int64, clientIP, userAgent string) (auth.UserToken, error) + tryRotateTokenProvider func(token auth.UserToken, clientIP, userAgent string) (bool, error) + lookupTokenProvider func(unhashedToken string) (auth.UserToken, error) + revokeTokenProvider func(token auth.UserToken) error } func newFakeUserAuthTokenService() *fakeUserAuthTokenService { return &fakeUserAuthTokenService{ - initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool { - return false + createTokenProvider: func(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 0, + token: "", + }, nil + }, + tryRotateTokenProvider: func(token auth.UserToken, clientIP, userAgent string) (bool, error) { + return false, nil + }, + lookupTokenProvider: func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 0, + token: "", + }, nil + }, + revokeTokenProvider: func(token auth.UserToken) error { + return nil }, } } -func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool { - return s.initContextWithTokenProvider(ctx, orgID) +func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + return s.createTokenProvider(userId, clientIP, userAgent) } -func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error { - return nil +func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (auth.UserToken, error) { + return s.lookupTokenProvider(unhashedToken) } -func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } +func (s *fakeUserAuthTokenService) TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) { + return s.tryRotateTokenProvider(token, clientIP, userAgent) +} + +func (s *fakeUserAuthTokenService) RevokeToken(token auth.UserToken) error { + return s.revokeTokenProvider(token) +} diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index 46b8776fdcc..c7479b3e9bc 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -3,6 +3,8 @@ package middleware import ( "testing" + "github.com/grafana/grafana/pkg/services/auth" + "fmt" "github.com/grafana/grafana/pkg/bus" @@ -14,14 +16,21 @@ func TestOrgRedirectMiddleware(t *testing.T) { Convey("Can redirect to correct org", t, func() { middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error { return nil }) - sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { - ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12} - ctx.IsSignedIn = true - return true + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 1, UserId: 12} + return nil + }) + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: "", + }, nil } sc.m.Get("/", sc.defaultHandler) @@ -33,21 +42,23 @@ func TestOrgRedirectMiddleware(t *testing.T) { }) middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error { return fmt.Errorf("") }) - sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { - ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12} - ctx.IsSignedIn = true - return true - } - bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { query.Result = &m.SignedInUser{OrgId: 1, UserId: 12} return nil }) + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: "", + }, nil + } + sc.m.Get("/", sc.defaultHandler) sc.fakeReq("GET", "/?orgId=3").exec() diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index 4f2203a5d3d..af22f41deba 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" @@ -74,10 +75,17 @@ func TestMiddlewareQuota(t *testing.T) { }) middlewareScenario("with user logged in", func(sc *scenarioContext) { - sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { - ctx.SignedInUser = &m.SignedInUser{OrgId: 2, UserId: 12} - ctx.IsSignedIn = true - return true + sc.withTokenSessionCookie("token") + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: "", + }, nil } bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error { diff --git a/pkg/models/context.go b/pkg/models/context.go index df970451304..da63db63f45 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" @@ -13,6 +14,7 @@ import ( type ReqContext struct { *macaron.Context *SignedInUser + UserToken auth.UserToken // This should only be used by the auth_proxy Session session.SessionStore From a7a964ec19cec8f0848d14eee6109d3656f61701 Mon Sep 17 00:00:00 2001 From: SamuelToh Date: Sun, 27 Jan 2019 21:49:22 +1000 Subject: [PATCH 088/770] Added PATCH verb end point for annotation op Added new PATCH verb annotation endpoint Removed unwanted fmt Added test cases for PATCH verb annotation endpoint Fixed formatting issue Check arr len before proceeding Updated doc to include PATCH verb annotation endpt --- docs/sources/http_api/annotations.md | 27 +++++++++++- pkg/api/annotations.go | 59 ++++++++++++++++++++++++++ pkg/api/annotations_test.go | 62 ++++++++++++++++++++++++++++ pkg/api/api.go | 1 + pkg/api/dtos/annotations.go | 8 ++++ 5 files changed, 156 insertions(+), 1 deletion(-) diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md index 6633714d77b..dee4ede0777 100644 --- a/docs/sources/http_api/annotations.md +++ b/docs/sources/http_api/annotations.md @@ -160,15 +160,18 @@ Content-Type: application/json } ``` -## Update Annotation +## Replace Annotation `PUT /api/annotations/:id` +Replaces the annotation that matches the specified id. + **Example Request**: ```json PUT /api/annotations/1141 HTTP/1.1 Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Content-Type: application/json { @@ -180,6 +183,28 @@ Content-Type: application/json } ``` +## Update Annotation + +`PATCH /api/annotations/:id` + +Updates one or more properties of an annotation that matches the specified id. + +**Example Request**: + +```json +PATCH /api/annotations/1145 HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +Content-Type: application/json + +{ + "time":1507037197000, + "timeEnd":1507180807095, + "text":"New Annotation Description", + "tags":["tag6","tag7","tag8"] +} +``` + ## Delete Annotation By Id `DELETE /api/annotations/:id` diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 242b5531f51..da9b55a1c16 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -210,6 +210,65 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response { return Success("Annotation updated") } +func PatchAnnotation(c *m.ReqContext, cmd dtos.PatchAnnotationsCmd) Response { + annotationID := c.ParamsInt64(":annotationId") + + repo := annotations.GetRepository() + + if resp := canSave(c, repo, annotationID); resp != nil { + return resp + } + + items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId}) + + if err != nil || len(items) == 0 { + return Error(500, "Could not find annotation to update", err) + } + + existing := annotations.Item{ + OrgId: c.OrgId, + UserId: c.UserId, + Id: annotationID, + Epoch: items[0].Time, + Text: items[0].Text, + Tags: items[0].Tags, + RegionId: items[0].RegionId, + } + + if cmd.Tags != nil { + existing.Tags = cmd.Tags + } + + if cmd.Text != "" && cmd.Text != existing.Text { + existing.Text = cmd.Text + } + + if cmd.Time > 0 && cmd.Time != existing.Epoch { + existing.Epoch = cmd.Time + } + + if err := repo.Update(&existing); err != nil { + return Error(500, "Failed to update annotation", err) + } + + // Update region end time if provided + if existing.RegionId != 0 && cmd.TimeEnd > 0 { + itemRight := existing + itemRight.RegionId = existing.Id + 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 != ... + itemRight.Id = 0 + + if err := repo.Update(&itemRight); err != nil { + return Error(500, "Failed to update annotation for region end time", err) + } + } + + return Success("Annotation patched") +} + func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response { repo := annotations.GetRepository() diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 08f3018c694..ebdd867a031 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -27,6 +27,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) { IsRegion: false, } + patchCmd := dtos.PatchAnnotationsCmd{ + Time: 1000, + Text: "annotation text", + Tags: []string{"tag1", "tag2"}, + } + Convey("When user is an Org Viewer", func() { role := m.ROLE_VIEWER Convey("Should not be allowed to save an annotation", func() { @@ -40,6 +46,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 403) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -67,6 +78,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 200) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -100,6 +116,13 @@ func TestAnnotationsApiEndpoint(t *testing.T) { Id: 1, } + patchCmd := dtos.PatchAnnotationsCmd{ + Time: 8000, + Text: "annotation text 50", + Tags: []string{"foo", "bar"}, + Id: 1, + } + deleteCmd := dtos.DeleteAnnotationsCmd{ DashboardId: 1, PanelId: 1, @@ -136,6 +159,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 403) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -163,6 +191,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 200) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -189,6 +222,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) { sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 200) }) + + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + deleteAnnotationsScenario("When calling POST on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 200) @@ -264,6 +303,29 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m. }) } +func patchAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := setupScenarioContext(url) + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = role + + return PatchAnnotation(c, cmd) + }) + + fakeAnnoRepo = &fakeAnnotationsRepo{} + annotations.SetRepository(fakeAnnoRepo) + + sc.m.Patch(routePattern, sc.defaultHandler) + + fn(sc) + }) +} + func deleteAnnotationsScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() diff --git a/pkg/api/api.go b/pkg/api/api.go index 980706d8355..0685ef3814d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -354,6 +354,7 @@ func (hs *HTTPServer) registerRoutes() { annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), Wrap(PostAnnotation)) annotationsRoute.Delete("/:annotationId", Wrap(DeleteAnnotationByID)) annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), Wrap(UpdateAnnotation)) + annotationsRoute.Patch("/:annotationId", bind(dtos.PatchAnnotationsCmd{}), Wrap(PatchAnnotation)) annotationsRoute.Delete("/region/:regionId", Wrap(DeleteAnnotationRegion)) annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), Wrap(PostGraphiteAnnotation)) }) diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index c917b0d9feb..b64329e56d1 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -22,6 +22,14 @@ type UpdateAnnotationsCmd struct { TimeEnd int64 `json:"timeEnd"` } +type PatchAnnotationsCmd struct { + Id int64 `json:"id"` + Time int64 `json:"time"` + Text string `json:"text"` + Tags []string `json:"tags"` + TimeEnd int64 `json:"timeEnd"` +} + type DeleteAnnotationsCmd struct { AlertId int64 `json:"alertId"` DashboardId int64 `json:"dashboardId"` From 2c255fd85a8646303e6b97455bc2869e25420609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 06:19:40 +0100 Subject: [PATCH 089/770] Renamed initialQueries to queries --- packages/grafana-ui/src/types/plugin.ts | 2 +- public/app/features/explore/QueryRow.tsx | 14 +++--- public/app/features/explore/state/actions.ts | 27 ++++------- public/app/features/explore/state/reducers.ts | 47 ++++++++++--------- .../loki/components/LokiQueryEditor.tsx | 2 +- .../loki/components/LokiQueryField.tsx | 13 ++--- .../prometheus/components/PromQueryField.tsx | 13 ++--- public/app/types/explore.ts | 4 +- 8 files changed, 54 insertions(+), 68 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index e674c9fbc32..c8f156c08dc 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -56,7 +56,7 @@ export interface QueryEditorProps { datasource: DSType; - initialQuery: TQuery; + query: TQuery; error?: string | JSX.Element; hint?: QueryHint; history: any[]; diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index 5e2e8442e54..bcb980e49e4 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -35,7 +35,7 @@ interface QueryRowProps { highlightLogsExpressionAction: typeof highlightLogsExpressionAction; history: HistoryItem[]; index: number; - initialQuery: DataQuery; + query: DataQuery; modifyQueries: typeof modifyQueries; queryTransactions: QueryTransaction[]; exploreEvents: Emitter; @@ -95,7 +95,7 @@ export class QueryRow extends PureComponent { }, 500); render() { - const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props; + const { datasourceInstance, history, index, query, queryTransactions, exploreEvents, range } = this.props; const transactions = queryTransactions.filter(t => t.rowIndex === index); const transactionWithError = transactions.find(t => t.error !== undefined); const hint = getFirstHintFromTransactions(transactions); @@ -110,7 +110,7 @@ export class QueryRow extends PureComponent { {QueryField ? ( { error={queryError} onQueryChange={this.onChangeQuery} onExecuteQuery={this.onExecuteQuery} - initialQuery={initialQuery} + initialQuery={query} exploreEvents={exploreEvents} range={range} /> @@ -155,9 +155,9 @@ export class QueryRow extends PureComponent { function mapStateToProps(state: StoreState, { exploreId, index }) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; - const { datasourceInstance, history, initialQueries, queryTransactions, range } = item; - const initialQuery = initialQueries[index]; - return { datasourceInstance, history, initialQuery, queryTransactions, range }; + const { datasourceInstance, history, queries, queryTransactions, range } = item; + const query = queries[index]; + return { datasourceInstance, history, query, queryTransactions, range }; } const mapDispatchToProps = { diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index a0357315484..f6fa5c05d63 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -87,7 +87,7 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun return async (dispatch, getState) => { const newDataSourceInstance = await getDatasourceSrv().get(datasource); const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance; - const queries = getState().explore[exploreId].initialQueries; + const queries = getState().explore[exploreId].queries; await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance)); @@ -494,7 +494,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) { return (dispatch, getState) => { const { datasourceInstance, - initialQueries, + queries, showingLogs, showingGraph, showingTable, @@ -503,7 +503,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) { supportsTable, } = getState().explore[exploreId]; - if (!hasNonEmptyQuery(initialQueries)) { + if (!hasNonEmptyQuery(queries)) { dispatch(runQueriesEmptyAction({ exploreId })); dispatch(stateSave()); // Remember to saves to state and update location return; @@ -565,14 +565,7 @@ function runQueriesForType( resultGetter?: any ) { return async (dispatch, getState) => { - const { - datasourceInstance, - eventBridge, - initialQueries: queries, - queryIntervals, - range, - scanning, - } = getState().explore[exploreId]; + const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId]; const datasourceId = datasourceInstance.meta.id; // Run all queries concurrently @@ -653,7 +646,7 @@ export function splitOpen(): ThunkResult { const itemState = { ...leftState, queryTransactions: [], - initialQueries: leftState.initialQueries.slice(), + queries: leftState.queries.slice(), }; dispatch(splitOpenAction({ itemState })); dispatch(stateSave()); @@ -670,7 +663,7 @@ export function stateSave() { const urlStates: { [index: string]: string } = {}; const leftUrlState: ExploreUrlState = { datasource: left.datasourceInstance.name, - queries: left.initialQueries.map(clearQueryKeys), + queries: left.queries.map(clearQueryKeys), range: left.range, ui: { showingGraph: left.showingGraph, @@ -682,13 +675,9 @@ export function stateSave() { if (split) { const rightUrlState: ExploreUrlState = { datasource: right.datasourceInstance.name, - queries: right.initialQueries.map(clearQueryKeys), + queries: right.queries.map(clearQueryKeys), range: right.range, - ui: { - showingGraph: right.showingGraph, - showingLogs: right.showingLogs, - showingTable: right.showingTable, - }, + ui: { showingGraph: right.showingGraph, showingLogs: right.showingLogs, showingTable: right.showingTable }, }; urlStates.right = serializeStateToUrlParam(rightUrlState, true); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 7c0a729d0ed..76fc7d5de32 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -60,7 +60,7 @@ export const makeExploreItemState = (): ExploreItemState => ({ datasourceMissing: false, exploreDatasources: [], history: [], - initialQueries: [], + queries: [], initialized: false, queryTransactions: [], queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, @@ -92,23 +92,26 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: addQueryRowAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, queryTransactions } = state; + const { queries, queryTransactions } = state; const { index, query } = action.payload; - // Add to initialQueries, which will cause a new row to be rendered - const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)]; + // Add to queries, which will cause a new row to be rendered + const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)]; // Ongoing transactions need to update their row indices const nextQueryTransactions = queryTransactions.map(qt => { if (qt.rowIndex > index) { - return { ...qt, rowIndex: qt.rowIndex + 1 }; + return { + ...qt, + rowIndex: qt.rowIndex + 1, + }; } return qt; }); return { ...state, - initialQueries: nextQueries, + queries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), @@ -118,12 +121,12 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: changeQueryAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, queryTransactions } = state; + const { queries, queryTransactions } = state; const { query, index } = action.payload; // Override path: queries are completely reset const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) }; - const nextQueries = [...initialQueries]; + const nextQueries = [...queries]; nextQueries[index] = nextQuery; // Discard ongoing transaction related to row query @@ -131,7 +134,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, - initialQueries: nextQueries, + queries: nextQueries, queryTransactions: nextQueryTransactions, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; @@ -162,7 +165,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta const queries = ensureQueries(); return { ...state, - initialQueries: queries.slice(), + queries: queries.slice(), queryTransactions: [], showingStartPage: Boolean(state.StartPage), queryKeys: getQueryKeys(queries, state.datasourceInstance), @@ -186,7 +189,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta eventBridge, exploreDatasources, range, - initialQueries: queries, + queries, initialized: true, queryKeys: getQueryKeys(queries, state.datasourceInstance), ...ui, @@ -197,7 +200,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: updateDatasourceInstanceAction, mapper: (state, action): ExploreItemState => { const { datasourceInstance } = action.payload; - return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.initialQueries, datasourceInstance) }; + return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) }; }, }) .addMapper({ @@ -254,13 +257,13 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: modifyQueriesAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, queryTransactions } = state; + const { queries, queryTransactions } = state; const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { // Modify all queries - nextQueries = initialQueries.map((query, i) => ({ + nextQueries = queries.map((query, i) => ({ ...modifier({ ...query }, modification), ...generateEmptyQuery(i), })); @@ -268,7 +271,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta nextQueryTransactions = []; } else { // Modify query only at index - nextQueries = initialQueries.map((query, i) => { + nextQueries = queries.map((query, i) => { // Synchronize all queries with local query cache to ensure consistency // TODO still needed? return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query; @@ -286,7 +289,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta } return { ...state, - initialQueries: nextQueries, + queries: nextQueries, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), queryTransactions: nextQueryTransactions, }; @@ -332,14 +335,14 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: removeQueryRowAction, mapper: (state, action): ExploreItemState => { - const { datasourceInstance, initialQueries, queryIntervals, queryTransactions, queryKeys } = state; + const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state; const { index } = action.payload; - if (initialQueries.length <= 1) { + if (queries.length <= 1) { return state; } - const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)]; // Discard transactions related to row query @@ -353,7 +356,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, ...results, - initialQueries: nextQueries, + queries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, queryKeys: nextQueryKeys, @@ -398,7 +401,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta const { queries } = action.payload; return { ...state, - initialQueries: queries.slice(), + queries: queries.slice(), queryKeys: getQueryKeys(queries, state.datasourceInstance), }; }, @@ -452,7 +455,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta const { queries } = action.payload; return { ...state, - initialQueries: queries, + queries, queryKeys: getQueryKeys(queries, state.datasourceInstance), }; }, diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index e9912522f16..a1b9e7a5df9 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -59,7 +59,7 @@ export class LokiQueryEditor extends PureComponent {
{ // Send text change to parent - const { initialQuery, onQueryChange, onExecuteQuery } = this.props; + const { query, onQueryChange, onExecuteQuery } = this.props; if (onQueryChange) { - const query = { - ...initialQuery, - expr: value, - }; - onQueryChange(query); + const nextQuery = { ...query, expr: value }; + onQueryChange(nextQuery); if (override && onExecuteQuery) { onExecuteQuery(); @@ -217,7 +214,7 @@ export class LokiQueryField extends React.PureComponent 0; @@ -237,7 +234,7 @@ export class LokiQueryField extends React.PureComponent { // Send text change to parent - const { initialQuery, onQueryChange, onExecuteQuery } = this.props; + const { query, onQueryChange, onExecuteQuery } = this.props; if (onQueryChange) { - const query: PromQuery = { - ...initialQuery, - expr: value, - }; - onQueryChange(query); + const nextQuery: PromQuery = { ...query, expr: value }; + onQueryChange(nextQuery); if (override && onExecuteQuery) { onExecuteQuery(); @@ -240,7 +237,7 @@ class PromQueryField extends React.PureComponent Date: Tue, 5 Feb 2019 07:03:16 +0100 Subject: [PATCH 090/770] Fixed so onBlur event trigger an QueryChange and QueryExecute if values differ --- public/app/features/explore/QueryField.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index a0e70e8066c..8ab7e56dc5a 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -50,6 +50,7 @@ export interface QueryFieldState { typeaheadPrefix: string; typeaheadText: string; value: Value; + lastExecutedValue: Value; } export interface TypeaheadInput { @@ -89,6 +90,7 @@ export class QueryField extends React.PureComponent { + handleBlur = (event, change) => { + const { lastExecutedValue } = this.state; + const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null; + const currentValue = Plain.serialize(change.value); + // If we dont wait here, menu clicks wont work because the menu // will be gone. this.resetTimer = setTimeout(this.resetTypeahead, 100); // Disrupting placeholder entry wipes all remaining placeholders needing input this.placeholdersBuffer.clearPlaceholders(); + + if (previousValue !== currentValue) { + this.executeOnQueryChangeAndExecuteQueries(); + } }; handleFocus = () => {}; From 275800cca94b79f6aac993e535e8d5f67bb9129a Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 4 Feb 2019 23:19:14 -0800 Subject: [PATCH 091/770] improve the stackdriver logo --- .../stackdriver/img/stackdriver_logo.png | Bin 15726 -> 0 bytes .../stackdriver/img/stackdriver_logo.svg | 1 + .../plugins/datasource/stackdriver/plugin.json | 4 ++-- 3 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png create mode 100644 public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg diff --git a/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png b/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png deleted file mode 100644 index cd52e773deb8e2fb4fbc27ff188541ce8e649025..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15726 zcmeIZ^;cAJ+devsASEr`T@r${gfu8E3=I;}-OYfAlG5GH(A^B8bVzr1gD`ZQ?Q`CD zo%1JrziT}|FswCe?>+Om^SZA4p6{y4a#$E-7$6V`OF>>*9RxyJ{r89V9C*iXvLgZb zhvcFzCkd(;q1Xd~L|GK1-+lB%I$AcPYeo5X%^6=wrPPdctc7MdpX^z8GNv*F6;=yCdXM~k@7FPmFX%#kU#OE!herg~q^zDS^hYreP$q9ExZZsE%w)Km%HkRgnFSq5pRr#ikKq6Ztz+&Kr3EuUPfOn>g|iuZ8=| zj~bUD&)1J>1iH5a{c$dMEHV=8TMQ%bvh|@tu)?f`#7U;4G@KoJ)iU@V-}e<%qjTx* z;)MyO0Bu{Ud^tEa61a*oY*VdHLQj&jpCm}dI4p7Fi!-w9E~%55jJGNgDZR^s-$5aj zkR>x0uH(Bv&>@&d<^@r5yKm2x2P5KZmycIS;5pc+RH?Z0c2aIMSU}W@BpPe^H#>4V zF%3w>;RE_J`)`5l;H>XOG8rw;MjA-JUdN#vjHDzUk=)@rDgsZB ze|7Vja97@q^Ps;ktPFMTd!Q6}n|z6QYRO@l!|ts$iBcQ$w#<5V23X(L-8vRx2!&}G zc=lYz`};3TQ%J*+pcD0Gief=GmMI#WT&qPzCN6=*Q`=a^i;r8{k zdK?DQ9WoOoiKqE%aaOk0yEyUc^a|cX|fX5M9XG{z|+i5xKL;vT09+agG{7L7`~^2%x`)FD;T_ zgZibF3ZWknGmUCK2&qY)JC>N4`zoQ5qOn|@zqYAXK2qWa+^fYTQ=aHIua>hoWg@?@oF?2Z?!jaS9| zW#P&|)MTV?^2uj&gqa@yvMl~h1XllZaYe~d@4Lr66m;r%`a?;cX!7!j|SzJ zE+hJJp>M(49+Vbyo>x9qEax`y@$w~ZxmX>_Lf?KqHK5@J+I?b(3kJJrDTz07lqiW^ zr#|61zF+-Pn$O+&O8VST=-vBFFJu$z z_MPwhP)ud}lnBe)#;eCsN9IAA6HDN`y0|>-O50%SjBEadlkD)G&@O~gy-vW=lDwq%34r~_DunUHdT%qf>M!FC?e}S*O^%8ji|N4WB?qmrwRdaL6Nsx+RRjOO=(iB~Zn(51 zGZ#;42gM-tR4ILRZY5`q|M`X2@UzYo;f^6&LD8Q;q@a2DH!D3Yg_S-XDh{SPpRf5M zhY==!T&NTa;=Fv-IzjH8J7Fx5xKxHW5DlWp8EL33aWT3Xa9X&l*KyxgFhtYF<3=^0 z#8c5x*!Lu>7c}3%nfV|xdKSb&6AwII)g5*oX?>t1p7cih{i2tZA_?1{UAXtC)L_-^ zygsvTQV+e>L>x903S0{%ySG(X9Y{R4IF`4{YK%{oMw?}Ew3ShO;o>!~*0oRGbB$tC z^k~=R${=C<4TR*0hQ@`7S;u%0I@EcOS;T+>zC=%+h*CbcoR107T0E=L#Tt5`jmdFumxnD3O&dwSB#pn!F;o3Rv zMy(E+smK(wmrCl0w0L-4eHCP|!-h!i3db(HI-F}WVh@ykYj_iqHhMeNs)Zl-9O*c) z`S~kM*Nbl!=pBj8@|Jn<&3Q3IMrdVl<(zJcGE;J*nQ3RJC_C73)@Qcxa#)A&VX?u? zwnk|#1`C{s($h1ME4F>?J(|caz>rhA_1&Xhx!t7C-jMExW*{g%AtXZzWfBf!1o8sQ ze61c6Y;ohUsyRJ97*V~eU|N`opb^0%p#cIhuq%2F<#T1zs!wZd4eg!UCEaQ|{Ck3e zT6xD4)_CD(v==MmE$3Y{Qz9+3GXEw=Pl((El*sgPT#Tr+P=^wDMh;(cpM) z#BcXH5E`8nfu-s)(a30UsxH)=*$niJJy+Skd@{Jg>}GDZFM8_o+V;1togP>>#@fCM zl2`$iH@{WfW5c2{S*%gCgfdM8MGn_d7VbMf)))9egpI}cn-k`Y@`E7|G2ik*9s)6k zm-mtOJcT{eZQ4uC(H0Neq3gv*K+7A4?0ZX_!)3*Gf>l>gufpdC#u~FOVaL9Ef<&T# zXY*!uQ1tFw2{oS-PC5Fd!Klc}Y6qgBtkcA_Oav9iV%0muXy zy41LWFfAn!jJ+h2?Z8(YF}I`!k#Gql@To-PwsVtK`Yo_j=B>O1aL6QxfOk9q1Bw1xIG|e^Y~c zUy?GHQMKgQI~w-l3J!ZK>fQ4bv16FLBN$2t#Cntj^Nvj}nP)viHyq|GP2nP+?W8Rk zc^&>oZuA+&a97lx>-HK_>-FoPf0fStwu{NA!M7&^$cwdAn@2e~2lTb|yjuG$@g9fi zB$qbt5jDLX?pY5n8hb7=+|EIz($6NXBhBJL_5ys`G)Fvz2(#v3qt9|Ng$b;hS-V;f zHHtTS8y$b7K>v5fPe*g$qxnhBEV^qViS;UaF!M(y^p1cZn$+-Kgy@-hltf&~34n__ zPU!B~J*P@BM`shqKhI|I3eo*Al7Kv`R4zx~@gBX=%RDJQ+wz%Fx-;o`Nr71eLJ~}W zNNv;-Py4*1*;uFJ5CBiFh2Z$;}dc268Cb$3uQ0HKL^YGT(pZU)sV6 zp|zn`)qgyx_C5&v&BXP55LLm)4*38?cMbONO!}3UmRK7Xht1!DH4?BgS_DKzxHNpo zd$a((v~@Lg*k*y6^T>7tBNw#t?0NN!V(~dvIkAeT1?2WwCPEKPRTR%*Tydq3SAfWT zPpEMqeW3aGT5r_yCV%pkAeSI`5ec=Za-ei?y5eyMt-~D8+4(z|02YgE1ar5}u*{2g z%Pn_C5v4>D+!eLVd4x2ii@V6FXIav+D&SUW(i9$ht5ihw)cPLU5jr6PabA)$P%Lcrq551*T- zlPZ#{i+KyCbi|-v&}JD_i^xrU->x5sCfN(^AG>L!RI9ca(Tr?HPe{0GSS7T%BQBiC zg-sW3U({bBk%Kughl&sIy6Y^(j{2DP;|)1_OObSZ>;|H3eVO2tRinbcM)nU6V@7d( zk6toz!N77b6almjt$OFe@Dd~)@j_^T;*AOG^AP+@P zjT9>gLL|O8Yh_WV*H$6gSn;f#^HUW;*j64VyAw%fB%Xxh%LtEJW#GWQ*Up29$~>rM z1-vE9cKs0_e3>Ocmj+4kMmd9tqfIUriv00e1VS{b>Hh$FZ%Hp!%^XX1;vd7R%XBaP zZFwy*yw#JBS*~9Urkak9_Uaw%COpo|j9R7pi%6g@U$4S$wF{FqeqA&!c5-tkunHSR}L7HVfi+CB}6mXE0zWMQS^PmHSH0$TmtGJlLomlXq ze+DB8J}O2DR(8z$7*?0Gm>IsIKZA7OFWAEAE_)QU?fu36R4IrhR)I~8jJ+rF?@ zxGairUjm^7fC8i>q-y(hI1Y$bDn00y?6_oh0!`>~KRVWW!t6t&8-rV7IlDr1+yLQq zuI@=n!SsM;oxi`Z-Tk$KDbwb3zi(PB=y0K;+OYa65hAvjkjOVZO2Y`gM4K#*&KTCL zoLwYPbqPM;uRLh#ZO3QrXGQAZa#frz@O7|k_xvf+I!=9BW|RbH{rm&&MeqGvKEp~b zSas1WrlMTQ|MRhx&HNH;lzKXVMrYAAiO(13=bk9KW@H@0gc-QSGU=|V#vLC}+gq7O zgvTms7xZY5W(`#!F0D0N_bDRkx6%;I0&e6-8b>WVaUv>=;%Kg-d&ecAiMM57nIu+# z8WH>%oLIoWl|rRjJ~C%y5s3xT4PbRVJ2QhL0s&W3^hrscI;aDjDgZ~z%(AdBYBA}_ z1zN?94hoB44S4%>A&#y24)Ghs_}dw#-qVGHpBgcnS{*33Dvas;@ry%;@7k=&<>1lV zgPFUIZzfoep4wR?)GBh-dt!FAV@3u=k{foD2#^HgT-MjULSlFdrqjoV4(kwVOE7lig-cxs^&_1T(mdiPYC#c8K(zPv) zgizal>abYyGCDOHE>;r*YMdA)HImpA6Q*iBgx&(6595rcI z1@cf27WKW`-=E>CxT&JA54Cu>Z3ve%sGgWgU)f-f&9OHXBxAmUM3h%_?)@UZuAl)? z4p2IRg!{|Zmqi~2(K?X!%6@3ps+V8+QgjK>v|GP;?|vbM`(r@1BNZrwtN2aUQy~`j zDvh4O00_;lmDK+2B75=O8;B6-o-QlTnWy*9;W)#km&w9%b-BPD*&7~A5$4D%AlHv* zFhQn7y6flVMp!uovDHPvT1PC?Nua}X!zbDE2%CktdE_ie+;6}yI#r{@H~C$2t}^F94qES zn(ckKzZj?pZ5^-a`Ol?N@6Rkx^r=T~g>C@jZ16s@XyQx5(RB3g!v3PPpHp+5W?t^LOnWJ^A z?QM9{Wte1GW?eEP&4yCCc%5HV9?S0$_-XT%zC?nuc58Rjeps@VcX|}9R7s;KtY9?){0~c(O*Ra(2+cVTOnL&2hz-u%s;1!9qYb=4r~3}Y=5I%9Vzt3 zh{1@azJG(~TLYJwLSR-3y{~LVcY3%RIQs0?IS1w1J>1TJBTlXg7d)pcqlFnIHB*X% zPIgMQPlU=0DwYp@VEda*2h0tJI5y#%OU(6elSPI9!+L78u)0qR zZ_Uq*QdM7Mlqju+kK+#_U;mZpt+KQ)$CIUMB zds4o(C)wJ&$e4%LyGPbkyDJq~^Rvq;t_s_Z-3uI_NCB_u%FqnlwOkblj-Fs<%xFwI zcrq=3t)d@YfUm*!`>UW{tR3vxZtU5N58_Ivh^l9F`mP0lLZNTY_7nWZK&qBXAktv9i^SAF)-z8%~ZNa3pkG@^ETv;)I zqL+>b2!I=@d^1t`mjuCx%-Z>?2_5JsZ&?bRF7MG1Hq^Hs=YVqH`*yV)?IQvm&gnK| z&d%B7prRP%S|S+s4&Z9^&nlmLozk@i#^5n?*k%Yb1LSd@1E7elGPRrk6XyjDSLSA~ z20yvet`@^BCNoar=f&g(Y0gj4Zd}{|)u(*u*zzvmB14sm?DgRuh)Waz?r6 zBZV+^M@3jKunWtkB2Ic5}G|LImn z%e59<;!jZqtnIW8bl7yOKP2*Qmm$tq2S4nQYxsWYU;=2?0?)FF+cw zM_D2Y^eS58nqhWsbs#?-z6hO$d3Ac_2qr@$u=?gnD=UTm0)cN#^Uknj9NQUHKUJ@* z_R^gx5#JY+Ck9Rnl||Ii2USWn;SFeE^_8j|D=*^ZFtyHqJPxE>?WnoQoe)3dTi(>^ z#9oaxe+M<4s>|czIV#R3KrcftZe2CgAK0+Bww*IMdZDgE2z33@k9d3T%o8dEzTqJH z50}4sVPlsDv(P|BD6h?}Oav5? zSSJ55BJlM_=FG^$gvkhv-6x_1|LQ;T>?(XBsr=BpiJimd`SFGY@*;k+`a-4FgvDg7-m)6I9NFL9;c`j= z+43djhNO4e{CmrPywARPxEF}8>U8o$W0lmv8|zkQ%-xf1E4d?dufZ~y7g~GOdG^U| zCXg`;=bxVd>`yJLJ|9(X&<}_x1#?bXH(-mB6MyuMEs(EKS9Im4jypJS_Q~PO)caeqt&>WB^zo%lJt2UG8rUO1`Ct zwRCs%zzFj?DQ@6k8)&&{`zDj1r`KQgGkoT}Mhwi+G*?&;~})!5lesb+B+@V0YGeBa@^Xy0E3j>^2) zvrO5?v*SGUl{FmQ-z=m-4SQg1myOhfth=&~`NG4#=g@AHp_S>c1Kn3J(FLD^YXp^l zM(ujl1V54zFVV0_>(;Xco(NCb`<{7EB+Ks7PF)Q~Qhd`qRvj$dVn~-j;vM9&B(g>R zHN8EEM0}o=rr?l5!vJ9*avtGr*c_0S$EhKCx6*F@J1R~*kBf8dqw+hNPKl7)Q-=>S z%WsxgSrZ+0Ai`YusPBn>q_!RZo$#MBaDJZdnGEVI4~>a$+$hgHcOVs!T!0vIrbohY zpXXbeQb8}XTO-I$Ek)gpYT{{jM0=#=la3&1r$u6q+G4lbb*~pug#|CM~x`4 zP9#V+U@Xn)@dw(elS^w87mu~pty~^PCo>da{u4ctwRrDthD2d2h%`&PsMh_P(aC`O z^#tuopP7iRl1M4p+npK3g`{FD1$|$weEB#Pi3iddcHRVmo%h|<k6&C)C4t8F z&F1sN$Vyqe%{_}K-unS{7ZO6`}U+m6UkrU>Nx(!5{JmzW~#;Ag#TpP8olJ=;uJ zxZ^uyV`AZ_c9EG1VgI(v8cZ2A=|3n#X;Tyc?VqoUzO-7GLPTRBv4c~G?oWGojn*|p z*tN|I*BWa+NWh9tcsI*R6}!k}w7POr?dx>yYkoc&4fcwER@VQ>Rbnz^POwfXHU z^-C9QtM|mKwI$<6VZybo<}O!f#MGkRkfbr~$?ptNQ4$}P9;q}<2R%Ba#%{;eCref8 zqbs)3e&q1CJ${tvsjqxmnF#2jlgx@0yp4MZuolr>GPdYg%cplB7u9!v-by&R{2Iw+ zq|&>%PGcd!ZcHY~!cFKm`L*xV(uvLasI@=mDHgiwGc>rp`*c~s-mS1xWSWe+mIQL? zRHz#LV4C~K;(`Zj>1p1yn?JB08Vu3z>$ktsa)p~Mh}ynEnl-s}k(ihaBD5VuGxh0rc( zH@S?K^Kz$?k&74{9cJOuB;5vgn0W7Hba_Q`);~l;2sr*Ajqj>lsq%5OdZVtlmdebc z4teOrx3`3m*bL4`*dVYS?;p!wDo4(7PLpF|X&1sQ*544orwsu&TCpoNG5}lpoXEK^P5i+pe)QOz`!}^u)6h*)kn|D1e$Y*gUEuiW) zA#We?IU4rv%pNlW5^jHO?Bf&ta>1t=^YR^}Nf$+mKDH6qCWcxptRlci!0Su zm5)gYPW{4T#}hspxy`Gx2>~4eev|2mX1HJ4n@JYAG?yOiv&6mjJDZGn7_kB4;}D*$ zBXS|aJ<-!AXStRGt~#smI{JXc2P#`Hy{wnp8QgkrkSB$aYYS|7vOe~2R|7uYz!ucL zmnD5Vz~h@=4z9cr&j*`M6yB?^U0XP6GASwE+0kAd z654rP6&!S4u`2bpx3sRw{3%%%a9I>mYjSnPG1G9&DAN4U4oc<8YTho$O=qOEbTw_v zv?N7{z!9zil-HvBX3UkpI5x-Rd>`!lGlJ-$xZKoyme1D$?M8tj5WwV#z$U5Pn9>xz zzUHtfUtaZm_fj<$n0hnt6D!A#%agu#835r#xO<<46ZKZPuEcZ0E$i-iY^6hmP2mCm zPE7e6zn%Z&=9RZ>vp=$N*41Ow$iUng$$|7;TN-v{rqEVjy7$Y~;rm^4bd~oQW;mtM znF7OEmrMnl?UQRn1aMj0m-7l|$ytQZ=HFc2_|9( zUc27wvw93Wq`QZ1Y^es?0fcaY#h`DuYl?hdu8)y3UK+0eEuxZhs|=og-ovR>cjZKU zID;K-wXVpT=J_MsNQC8mI~rU^^}Y?ZMB*-Gjsh6GB6@!^!$Il#1ee+z_+$g`&moiK zHwTR$>~EU6*yReGF>nK2#;-5nI7uGZF#MG>tD{^Uj`K!KeL z>x*TMv9BjJ=`;PFqk%ibVtuo>sD^C^-N{XU8@5k+?Pb@MTAe?KL6H*|z02m78VA#giXnTC{_3N1!-$fa4fxE{^XQFR`Vu9nkB>o z?r^w`gXh78lGoW$7>meUlJUrY1FoVkJN`WHa{_yRV7(=BoJh7b9#IWUwIZSoaSV}0 zZ6~D_IW+sXqXNl15y68hcbbhN1!`e&KrAu484R`7>a~7oN8u5fusT;nuoF~U$%V)d zhrYRYCimzHNk0^cS{d7 zluV~J-uj=Cym6`0T)=GCWRf$YCd?!+5#_9|sY&b9@+AkkdN|Z2j*boWubm!w#lJ5NBaKd66(JJNd1FEXld!6?a52PCy zt+kQP{+(IwzdSm%Dxgab>SYTGpav6jzR26t0-NP{7yFvjOxG?zcL4@guV&u38QGkF z)1Cg_@Is3w=Tn8J`N)HLEyL;g@aq_PJw-)2Oz_tqGYWI@)j#4HkO0vNwXC~Ls}ZJ> zmUjSzUU|@#BBY>jCK+Z}twSVVta^4&RLH3Fd@xCM;L1L2{I6vP0|jOUFq&%eOX}Rl zDVx*Qq$S=~`85^<#xpu;&Q^&+E~jjnEf9T(QVyMnB=ypYL&2&%NCZo9kADTt}mWuRDo5`Sw=w8X+fc!-R=YJ6qX zHGd^uwS6{keRB|muE4Er{2>I_DX7;te%&Z#^u-7_M~S6iiH(P$>pJH5T$N2=T@N?# z*_1(nW`jtM0q2nU;p{7PdEV1$-lINzEvR(HzR#+r-94>Fpw*vGd^;WoZT-XIWOZ@G zIWK8Ip*4D;Z>-N!pDM~@Bl0PJ#vT;BR#p;O7oaCr8M`xDBqC`4@V;jAbH$ad~b4&3ZCd&0awJs0@t1J>1$;;jT9+J@z;1D6ihq z1UAeFd;!Je^zM%?CQ;qtq}y5JG`^f5;en{g_>IGn+UVH9R^BeSsRWN6KR}HJD^!X> z0)(w*L1e9H;yUFAV2VV^JvyI7eaG&$XP;f9S7V}+6XS8Tta~0@m@1YsxUhPEGQwMx znr?XmY;?u?^O^iE4frSy$ZrT_Pm6DgcGdXyTaAv#)At`9SXp~uDF#?m5vUZ_Wfd*$ z=S6LA?RNbQL-hhhRvoWS*2;ad5sZks{H;*xZ`YaLe5Y(ZmBAB`uA0bHd0q}H+*+Yi zMa%j0cOqVR=t#>>bYsK8!OKcaA`ntxoM$qoX1jeHeNfO{AeHZv#Rj??GR-1uu`^-a%i}!0DK77?W<%E=H=}M4NB#)mV^bsUKFAIP%}% zvmWV{p7sr6#Sd% zuJRndEmb^g3Siq*R(ipE4M?%W8c+*?-_igPVNgtK*^ltl&mt}0pO`)bO4uQ1%=|)9 zS!R(9o-T8Xy1`|hcG@fsu~!yG2bb;l`5BP&mXbxe?=a5eWS1TQ;2IwvS^S*dr_pX@ zJQhb_)^np8mQ3Ny-qTjK7!YSXJQjyf)T<^LIBUxs;uj6lJ{bc$U%_K4Q8PTemwi6$ zuFX?tL2aDybmun_tlWkk-SEG|D%nZC6e(glp2PWD8P66*N5=@i&0C0%9qmbSINaC? zi|*}W?H`o9$7IAhbUH7b(LYH!@%Nhv@wqJ!^lze-g?#mRo85BnJ}T}rb<@#ZB$E*+HG`e*bjrfDzD=_0TU2v&%H`VvL58OtM z(tJfx>_b;Dsu@sHMAhem4FIfoeGxin2W zq?I|Imh}PvJgx?>FcfUC`MOD9L0m>n6n{?-aGvTJnea&wz3R#QtUQ{UWWYvFiHe;y zCekcZAGdiNu*bqx_`L9Iq0$uF-|5#!ENBl_BLKA0Xj)F3>>Z6>ble_@e0N$Db&T=l zK+$7{v#o%`JBK|1ukmXk6ri&qg?u>jj zZSG2MnX=HV5x+_Jj=rsu&mQ|&=W&iBWf0jai}2+4c#lHcZx>4d5r}dJo){Z3c$3OR z0JVWbt>oSwu{b{PXa6)%VAY)ux9Yd6mR$vugESt^1+%5%&9b0K`gfZE<%;P<#&~8{r-nIMmv0xM zamx@Qv^zlI`XqO{c@OkD;{Ob3}P2}*HS50!rBfWw>U}1ams249i9glclE4A@B zCUFJuXPxU2mWePK3E!5fM*~FO`eWRvHD8l3>T^T$I`)EiBuZiaCeb!$L*Vn;oGcaP z`)BnL6b4Tf1a^R-lG4pEGbGYlp85M!c0Q}RUGm(x6t{nr**&%TVG01EUQ4OuvuCh? zJd?%X4!b#RzI~zXa{;TB^_xx;l>mH#ZI7RiixD3Z5^pdArXzK407Q8>lX+&<&LexM ztVC?k0n5Bees}H^Y@0*4tCrQ?*2J+r2yiF4C;x{e!{tL@!pyn?z-zw+KC$xI;L78E zv&}|7Nb7UAtHI&Qla}Ad80FRzzr^^&XCY}Xm*l1*(+*(5YAF&xjz1ey5`%5lnnE2b!UN+8fhPROoO2Dczxw$kinZTvd8>PDD zW$$y-ov{~f&Pz*jDE}zxsKezp2QW~*EwW+aIJv+>)kMz@6o5E@a*Ca?p@lm>JnGBo z8^tcIq@G*pc7e`@Gdba+568ufmHig&Bi)Qp zV0~|UlD?r2Gx=FHmFBaW#YI*BOujCC3Eq(47B3dq<=oCz|oX#qeM+7@hJv%JptRK6V|C{FDhL58s96zTs8 z2SJ%9Ct~%l54bo?dl;XEZOH*^#H$%)uf&w87*zEh3;-uoqs9z2wG5V6wC}vzZWE;W z4?i8iwpbaNG<_Z*oQ242UblVeco|k-oohx2f}(JUO!4Y<9(+`ablT6eIFAXoMKi;@ z-bzf~RT6UdJERs}Zeo33eJGi21{|f|H9c9=*mufAiI?9@RBL2}t^U!0aw2Mg_QhSZ z0w3K$%J4`TK{>kDpa65-Maa^R%Vu_9UOFRkU%*#iY@WLn*&q;aB?oMXzU~%qw=$8d zZ1?D5Ys3MTR|7Efi0%Gr0clenAoxB2Kq)!jE$nR4y>|3AhB6}U+!*Lr&|XzLWmvkj z@042idyjVPEjem_0Z~*uUZ~i(E@isq4zo|*A17fUg@g5c@~?*teU|56&<6NFka9fP zRa?f=s>TP3LafweVg>QR;nTjw9kF_ft7CUhSyBv+ia9uw!mI_>ij(W zK!1=6ULUXzHB!`F{TJB`A+Cr>&@V~pYifDh%Y9V*v6CgMk&E@$z>&GX-KTE>O~;s> zvjpaH^T%ZXdDU65-05gC@BH*_K~aXAY;uWfB1&;9{N8I&6LhScF!aa&BHrmq_3vEz zw?Gu2zoRh3#?w}LF6CL%4BE)-sKSm7Wq}W|6i^mqF!o1ozla;3{vOr4Nm#q7j%Z`# zM@|Qrf{4ZYk;1@uYdh?EbIov9FXUy-;OYhb) zl{$(a=#U2hK%Io%w?~A+7SXBYV1I{Mkl5m#M^V)AZ&1brb$3I(pSBN2(k8)K`qw`B z36=QKHUX`0XKO9n4t&*c%$GN+gb&I_H}SfZZ{6^E$X9mmrocWoeGSTd38^eX}W|UbH_{R^#l|1U@ATs*|ms z?5J4JrEqcd;!1#^r|9}3TY4t;8}(~8BpiQR;oZQ}YP-$RWKwEm$>Nvm_S#}&3I^v5E UbDsl#Kn+rmQI@WdG!FWI0AbhjWdHyG diff --git a/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg b/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg new file mode 100644 index 00000000000..93878f20a06 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/app/plugins/datasource/stackdriver/plugin.json b/public/app/plugins/datasource/stackdriver/plugin.json index e68100c0e59..1ee3d57e9b1 100644 --- a/public/app/plugins/datasource/stackdriver/plugin.json +++ b/public/app/plugins/datasource/stackdriver/plugin.json @@ -14,8 +14,8 @@ "description": "Google Stackdriver Datasource for Grafana", "version": "1.0.0", "logos": { - "small": "img/stackdriver_logo.png", - "large": "img/stackdriver_logo.png" + "small": "img/stackdriver_logo.svg", + "large": "img/stackdriver_logo.svg" }, "author": { "name": "Grafana Project", From e4c92ae12433cefffc3b9855fc117def44a0ebb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 09:24:30 +0100 Subject: [PATCH 092/770] added comment to initDashboard --- public/app/features/dashboard/state/initDashboard.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 5419fcb41d7..14428bfa290 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -46,6 +46,15 @@ async function redirectToNewUrl(slug: string, dispatch: any, currentPath: string } } +/** + * This action (or saga) does everything needed to bootstrap a dashboard & dashboard model. + * First it handles the process of fetching the dashboard, correcting the url if required (causing redirects/url updates) + * + * This is used both for single dashboard & solo panel routes, home & new dashboard routes. + * + * Then it handles the initializing of the old angular services that the dashboard components & panels still depend on + * + */ export function initDashboard({ $injector, $scope, From bfdfb215f329eb5a04b5318db938a81bdddb3a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 09:32:42 +0100 Subject: [PATCH 093/770] added missing typing to explore props --- public/app/features/explore/Explore.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 8eb177b8ad4..b210bcccc18 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -21,7 +21,7 @@ import TimePicker, { parseTime } from './TimePicker'; import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions'; // Types -import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps } from '@grafana/ui'; +import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui'; import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { StoreState } from 'app/types'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore'; @@ -34,7 +34,7 @@ interface ExploreProps { changeSize: typeof changeSize; changeTime: typeof changeTime; datasourceError: string; - datasourceInstance: any; + datasourceInstance: ExploreDataSourceApi; datasourceLoading: boolean | null; datasourceMissing: boolean; exploreId: ExploreId; From 04f190c3e357476adb1fd95284b6392648be38bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 11:11:17 +0100 Subject: [PATCH 094/770] Updated playlist test --- public/app/features/playlist/specs/playlist_srv.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/public/app/features/playlist/specs/playlist_srv.test.ts b/public/app/features/playlist/specs/playlist_srv.test.ts index e6b7671c964..d2ff27e54e0 100644 --- a/public/app/features/playlist/specs/playlist_srv.test.ts +++ b/public/app/features/playlist/specs/playlist_srv.test.ts @@ -1,6 +1,6 @@ import { PlaylistSrv } from '../playlist_srv'; -const dashboards = [{ uri: 'dash1' }, { uri: 'dash2' }]; +const dashboards = [{ url: 'dash1' }, { url: 'dash2' }]; const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance }] => { const mockBackendSrv = { @@ -50,13 +50,12 @@ const mockWindowLocation = (): [jest.MockInstance, () => void] => { describe('PlaylistSrv', () => { let srv: PlaylistSrv; - let mockLocationService: { url: jest.MockInstance }; let hrefMock: jest.MockInstance; let unmockLocation: () => void; const initialUrl = 'http://localhost/playlist'; beforeEach(() => { - [srv, mockLocationService] = createPlaylistSrv(); + [srv] = createPlaylistSrv(); [hrefMock, unmockLocation] = mockWindowLocation(); // This will be cached in the srv when start() is called @@ -71,7 +70,6 @@ describe('PlaylistSrv', () => { await srv.start(1); for (let i = 0; i < 6; i++) { - expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); srv.next(); } @@ -84,7 +82,6 @@ describe('PlaylistSrv', () => { // 1 complete loop for (let i = 0; i < 3; i++) { - expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); srv.next(); } @@ -93,7 +90,6 @@ describe('PlaylistSrv', () => { // Another 2 loops for (let i = 0; i < 4; i++) { - expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); srv.next(); } From 4b5bfd3da54e8ee681da9c6d74750714b7a882ff Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 Feb 2019 14:01:06 +0300 Subject: [PATCH 095/770] azuremonitor: more autocomplete suggestions for built-in functions --- .../editor/KustoQueryField.tsx | 197 ++++++++---------- 1 file changed, 89 insertions(+), 108 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 9b2df96fcdd..bbe34b8f46a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -96,57 +96,51 @@ export default class KustoQueryField extends QueryField { const wrapperClasses = wrapperNode.classList; let typeaheadContext: string | null = null; + // Built-in functions if (wrapperClasses.contains('function-context')) { typeaheadContext = 'context-function'; - if (this.fields) { - suggestionGroups = this.getKeywordSuggestions(); - } else { - this._fetchFields(); - return; - } + suggestionGroups = this.getColumnSuggestions(); + + // where } else if (modelPrefix.match(/(where\s(\w+\b)?$)/i)) { typeaheadContext = 'context-where'; - const fullQuery = Plain.serialize(this.state.value); - const table = this.getTableFromContext(fullQuery); - if (table) { - suggestionGroups = this.getWhereSuggestions(table); - } else { - return; - } - } else if (modelPrefix.match(/(,\s*$)/)) { - typeaheadContext = 'context-multiple-fields'; - if (this.fields) { - suggestionGroups = this.getKeywordSuggestions(); - } else { - this._fetchFields(); - return; - } - } else if (modelPrefix.match(/(from\s$)/i)) { - typeaheadContext = 'context-from'; - if (this.events) { - suggestionGroups = this.getKeywordSuggestions(); - } else { - this._fetchEvents(); - return; - } - } else if (modelPrefix.match(/(^select\s\w*$)/i)) { - typeaheadContext = 'context-select'; - if (this.fields) { - suggestionGroups = this.getKeywordSuggestions(); - } else { - this._fetchFields(); - return; - } - } else if (modelPrefix.match(/from\s\S+\s\w*$/i)) { - prefix = ''; - typeaheadContext = 'context-since'; - suggestionGroups = this.getKeywordSuggestions(); - // } else if (modelPrefix.match(/\d+\s\w*$/)) { - // typeaheadContext = 'context-number'; - // suggestionGroups = this._getAfterNumberSuggestions(); - } else if (modelPrefix.match(/ago\b/i) || modelPrefix.match(/facet\b/i) || modelPrefix.match(/\$__timefilter\b/i)) { - typeaheadContext = 'context-timeseries'; - suggestionGroups = this.getKeywordSuggestions(); + suggestionGroups = this.getColumnSuggestions(); + + // summarize by + } else if (modelPrefix.match(/(summarize\s(\w+\b)?$)/i)) { + typeaheadContext = 'context-summarize'; + suggestionGroups = this.getFunctionSuggestions(); + } else if (modelPrefix.match(/(summarize\s(.+\s)?by\s+([^,\s]+,\s*)*([^,\s]+\b)?$)/i)) { + typeaheadContext = 'context-summarize-by'; + suggestionGroups = this.getColumnSuggestions(); + + // order by, top X by, ... by ... + } else if (modelPrefix.match(/(by\s+([^,\s]+,\s*)*([^,\s]+\b)?$)/i)) { + typeaheadContext = 'context-by'; + suggestionGroups = this.getColumnSuggestions(); + + // join + } else if (modelPrefix.match(/(on\s(.+\b)?$)/i)) { + typeaheadContext = 'context-join-on'; + suggestionGroups = this.getColumnSuggestions(); + } else if (modelPrefix.match(/(join\s+(\(\s+)?(\w+\b)?$)/i)) { + typeaheadContext = 'context-join'; + suggestionGroups = this.getTableSuggestions(); + + // distinct + } else if (modelPrefix.match(/(distinct\s(.+\b)?$)/i)) { + typeaheadContext = 'context-distinct'; + suggestionGroups = this.getColumnSuggestions(); + + // database() + } else if (modelPrefix.match(/(database\(\"(\w+)\"\)\.(.+\b)?$)/i)) { + typeaheadContext = 'context-database-table'; + const db = this.getDBFromDatabaseFunction(modelPrefix); + console.log(db); + suggestionGroups = this.getTableSuggestions(db); + prefix = prefix.replace('.', ''); + + // built-in } else if (prefix && !wrapperClasses.contains('argument')) { if (modelPrefix.match(/\s$/i)) { prefix = ''; @@ -156,7 +150,7 @@ export default class KustoQueryField extends QueryField { } else if (Plain.serialize(this.state.value) === '') { typeaheadContext = 'context-new'; if (this.schema) { - suggestionGroups = this._getInitialSuggestions(); + suggestionGroups = this.getInitialSuggestions(); } else { this.fetchSchema(); setTimeout(this.onTypeahead, 0); @@ -187,7 +181,7 @@ export default class KustoQueryField extends QueryField { .filter(group => group.items.length > 0); // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); - // console.log('onTypeahead', modelPrefix, prefix, typeaheadContext); + console.log('onTypeahead', modelPrefix, prefix, typeaheadContext); this.setState({ typeaheadPrefix: prefix, @@ -293,6 +287,10 @@ export default class KustoQueryField extends QueryField { // ]; // } + private getInitialSuggestions(): SuggestionGroup[] { + return this.getTableSuggestions(); + } + private getKeywordSuggestions(): SuggestionGroup[] { return [ { @@ -323,50 +321,28 @@ export default class KustoQueryField extends QueryField { ]; } - private _getInitialSuggestions(): SuggestionGroup[] { + private getFunctionSuggestions(): SuggestionGroup[] { return [ { prefixMatch: true, - label: 'Tables', - items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name })) + label: 'Functions', + items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Macros', + items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) } ]; - - // return [ - // { - // prefixMatch: true, - // label: 'Keywords', - // items: KEYWORDS.map(wrapText) - // }, - // { - // prefixMatch: true, - // label: 'Operators', - // items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) - // }, - // { - // prefixMatch: true, - // label: 'Functions', - // items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) - // }, - // { - // prefixMatch: true, - // label: 'Macros', - // items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) - // } - // ]; } - private getWhereSuggestions(table: string): SuggestionGroup[] { - const tableSchema = this.schema.Databases.Default.Tables[table]; - if (tableSchema) { + getTableSuggestions(db = 'Default'): SuggestionGroup[] { + if (this.schema.Databases[db]) { return [ { prefixMatch: true, - label: 'Fields', - items: _.map(tableSchema.OrderedColumns, (f: any) => ({ - text: f.Name, - hint: f.Type - })) + label: 'Tables', + items: _.map(this.schema.Databases[db].Tables, (t: any) => ({ text: t.Name })) } ]; } else { @@ -374,7 +350,28 @@ export default class KustoQueryField extends QueryField { } } - private getTableFromContext(query: string) { + private getColumnSuggestions(): SuggestionGroup[] { + const table = this.getTableFromContext(); + if (table) { + const tableSchema = this.schema.Databases.Default.Tables[table]; + if (tableSchema) { + return [ + { + prefixMatch: true, + label: 'Fields', + items: _.map(tableSchema.OrderedColumns, (f: any) => ({ + text: f.Name, + hint: f.Type + })) + } + ]; + } + } + return []; + } + + private getTableFromContext() { + const query = Plain.serialize(this.state.value); const tablePattern = /^\s*(\w+)\s*|/g; const normalizedQuery = normalizeQuery(query); const match = tablePattern.exec(normalizedQuery); @@ -385,30 +382,14 @@ export default class KustoQueryField extends QueryField { } } - private async _fetchEvents() { - // const query = 'events'; - // const result = await this.request(query); - - // if (result === undefined) { - // this.events = []; - // } else { - // this.events = result; - // } - // setTimeout(this.onTypeahead, 0); - - //Stub - this.events = []; - } - - private async _fetchFields() { - // const query = 'fields'; - // const result = await this.request(query); - - // this.fields = result || []; - - // setTimeout(this.onTypeahead, 0); - // Stub - this.fields = []; + private getDBFromDatabaseFunction(prefix: string) { + const databasePattern = /database\(\"(\w+)\"\)/gi; + const match = databasePattern.exec(prefix); + if (match && match.length > 1 && match[0] && match[1]) { + return match[1]; + } else { + return null; + } } private async fetchSchema() { From 0642c5269315cfa2acbb61648daf2d1de20004e6 Mon Sep 17 00:00:00 2001 From: ijin08 Date: Tue, 5 Feb 2019 12:05:02 +0100 Subject: [PATCH 096/770] created new color variables, changed primary to blue, changed success-btns to primary-btns. --- .../ColorPicker/SeriesColorPickerPopover.tsx | 4 +- .../PanelOptionsGroup/_PanelOptionsGroup.scss | 4 +- .../ThresholdsEditor/_ThresholdsEditor.scss | 2 +- .../components/EmptyListCTA/EmptyListCTA.tsx | 2 +- .../__snapshots__/EmptyListCTA.test.tsx.snap | 2 +- .../components/OrgActionBar/OrgActionBar.tsx | 2 +- .../__snapshots__/OrgActionBar.test.tsx.snap | 2 +- .../PermissionList/AddPermission.tsx | 2 +- .../SharedPreferences/SharedPreferences.tsx | 2 +- .../manage_dashboards/manage_dashboards.html | 6 +- .../app/features/admin/partials/edit_org.html | 2 +- .../features/admin/partials/edit_user.html | 8 +- .../app/features/admin/partials/new_user.html | 2 +- public/app/features/admin/partials/orgs.html | 2 +- public/app/features/admin/partials/users.html | 2 +- .../alerting/partials/notification_edit.html | 2 +- .../alerting/partials/notifications_list.html | 2 +- .../features/annotations/partials/editor.html | 8 +- .../annotations/partials/event_editor.html | 2 +- public/app/features/api-keys/ApiKeysPage.tsx | 4 +- .../__snapshots__/ApiKeysPage.test.tsx.snap | 2 +- .../AddPanelWidget/AddPanelWidget.tsx | 2 +- .../components/DashExportModal/template.html | 2 +- .../components/DashLinks/editor.html | 8 +- .../DashboardPermissions.tsx | 2 +- .../DashboardSettings/template.html | 6 +- .../components/ExportDataModal/template.html | 2 +- .../components/RowOptions/template.html | 2 +- .../SaveModals/SaveDashboardAsModalCtrl.ts | 2 +- .../SaveModals/SaveDashboardModalCtrl.ts | 4 +- .../SaveProvisionedDashboardModalCtrl.ts | 2 +- .../components/ShareModal/template.html | 2 +- .../UnsavedChangesModalCtrl.ts | 2 +- .../components/VersionHistory/template.html | 2 +- .../datasources/settings/ButtonRow.tsx | 2 +- .../__snapshots__/ButtonRow.test.tsx.snap | 4 +- .../features/folders/FolderPermissions.tsx | 2 +- .../features/folders/FolderSettingsPage.tsx | 2 +- .../FolderSettingsPage.test.tsx.snap | 4 +- .../folders/partials/create_folder.html | 2 +- .../MoveToFolderModal/template.html | 2 +- .../uploadDashboardDirective.ts | 2 +- .../partials/dashboard_import.html | 2 +- public/app/features/org/OrgProfile.tsx | 2 +- .../__snapshots__/OrgProfile.test.tsx.snap | 2 +- public/app/features/org/partials/invite.html | 2 +- public/app/features/org/partials/newOrg.html | 2 +- .../app/features/org/partials/select_org.html | 2 +- .../features/playlist/partials/playlist.html | 4 +- .../features/playlist/partials/playlists.html | 2 +- .../plugins/partials/plugin_edit.html | 4 +- .../profile/partials/change_password.html | 2 +- .../features/profile/partials/profile.html | 2 +- public/app/features/teams/TeamGroupSync.tsx | 6 +- public/app/features/teams/TeamList.tsx | 2 +- public/app/features/teams/TeamMembers.tsx | 4 +- public/app/features/teams/TeamSettings.tsx | 2 +- .../__snapshots__/TeamGroupSync.test.tsx.snap | 8 +- .../__snapshots__/TeamList.test.tsx.snap | 2 +- .../__snapshots__/TeamMembers.test.tsx.snap | 6 +- .../__snapshots__/TeamSettings.test.tsx.snap | 2 +- .../features/teams/partials/create_team.html | 2 +- .../features/templating/partials/editor.html | 8 +- public/app/features/users/UsersActionBar.tsx | 4 +- .../UsersActionBar.test.tsx.snap | 4 +- public/app/partials/confirm_modal.html | 2 +- public/app/partials/edit_json.html | 2 +- public/app/partials/login.html | 2 +- public/app/partials/reset_password.html | 6 +- public/app/partials/signup_invited.html | 2 +- public/app/partials/signup_step2.html | 2 +- public/sass/_variables.dark.scss | 87 +++++++------- public/sass/_variables.light.scss | 110 +++++++++--------- public/sass/base/_type.scss | 4 +- public/sass/components/_buttons.scss | 34 +----- public/sass/components/_navbar.scss | 2 +- .../components/_panel_gettingstarted.scss | 2 +- public/vendor/angular-ui/ui-bootstrap-tpls.js | 2 +- 78 files changed, 212 insertions(+), 243 deletions(-) diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 3fa7a1f4a45..75727f18dcb 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -69,8 +69,8 @@ export class AxisSelector extends React.PureComponent diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss index b5b815cf57c..ddcb8971275 100644 --- a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -29,14 +29,14 @@ &:hover { .panel-options-group__add-circle { - background-color: $btn-success-bg; + background-color: $btn-primary-bg; color: $text-color-strong; } } } .panel-options-group__add-circle { - @include gradientBar($btn-success-bg, $btn-success-bg-hl, $text-color); + @include gradientBar($btn-primary-bg, $btn-primary-bg-hl, #fff); border-radius: 50px; width: 20px; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 200adfbfd75..923244af781 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -21,7 +21,7 @@ } .thresholds-row-add-button { - @include buttonBackground($btn-success-bg, $btn-success-bg-hl, $text-color); + @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl, #fff); align-self: center; margin-right: 5px; diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index d63af72ae4d..6b5c6ebb7ca 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -20,7 +20,7 @@ class EmptyListCTA extends Component { return (
{title}
- + {buttonTitle} diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap index b85660bcc6f..21c2ed294b4 100644 --- a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap @@ -10,7 +10,7 @@ exports[`EmptyListCTA renders correctly 1`] = ` Title
diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index 8fc34a018e1..b6b2046736f 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -35,7 +35,7 @@ export default class OrgActionBar extends PureComponent { onSetLayoutMode(mode)} />
diff --git a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap index dc53e7863ea..25de037930a 100644 --- a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap +++ b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap @@ -29,7 +29,7 @@ exports[`Render should render component 1`] = ` className="page-action-bar__spacer" /> diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 30219371257..80afcedf873 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -130,7 +130,7 @@ class AddPermissions extends Component {
-
diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 33aca1de2aa..171e0e8109e 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -126,7 +126,7 @@ export class SharedPreferences extends PureComponent { />
-
diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 6fbd65afaf5..6036ead3ef1 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -5,15 +5,15 @@
-
+ Dashboard - + Folder - + Import diff --git a/public/app/features/admin/partials/edit_org.html b/public/app/features/admin/partials/edit_org.html index 975d663e9b0..911181ef999 100644 --- a/public/app/features/admin/partials/edit_org.html +++ b/public/app/features/admin/partials/edit_org.html @@ -10,7 +10,7 @@
- +
diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 5b0efa8bdf3..7e6457a8a76 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -21,7 +21,7 @@
- +
@@ -34,7 +34,7 @@
- +
@@ -46,7 +46,7 @@
- +
@@ -65,7 +65,7 @@
- +
diff --git a/public/app/features/admin/partials/new_user.html b/public/app/features/admin/partials/new_user.html index 5199d957c33..e3374d080ca 100644 --- a/public/app/features/admin/partials/new_user.html +++ b/public/app/features/admin/partials/new_user.html @@ -24,7 +24,7 @@
- +
diff --git a/public/app/features/admin/partials/orgs.html b/public/app/features/admin/partials/orgs.html index d28cf4dc967..b40aed6faab 100644 --- a/public/app/features/admin/partials/orgs.html +++ b/public/app/features/admin/partials/orgs.html @@ -3,7 +3,7 @@
- + New Org diff --git a/public/app/features/admin/partials/users.html b/public/app/features/admin/partials/users.html index 806c10851e5..08704dc0459 100644 --- a/public/app/features/admin/partials/users.html +++ b/public/app/features/admin/partials/users.html @@ -7,7 +7,7 @@
- + Add new user diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index b2cd2f21e4d..5e7201cdfdd 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -68,7 +68,7 @@
- + Back
diff --git a/public/app/features/alerting/partials/notifications_list.html b/public/app/features/alerting/partials/notifications_list.html index 246cb45b4db..6624a1d1132 100644 --- a/public/app/features/alerting/partials/notifications_list.html +++ b/public/app/features/alerting/partials/notifications_list.html @@ -7,7 +7,7 @@
- + New Channel diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index 65ee7e52bd0..9a7a8cb738a 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -9,7 +9,7 @@
@@ -48,7 +48,7 @@
There are no custom annotation queries added yet
- + Add Annotation Query @@ -105,8 +105,8 @@
- - + +
diff --git a/public/app/features/annotations/partials/event_editor.html b/public/app/features/annotations/partials/event_editor.html index 529434755f1..286decb34ce 100644 --- a/public/app/features/annotations/partials/event_editor.html +++ b/public/app/features/annotations/partials/event_editor.html @@ -26,7 +26,7 @@
- + Cancel
diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 41b9b0c8a55..21d1ca54a66 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -169,7 +169,7 @@ export class ApiKeysPage extends PureComponent {
- +
@@ -199,7 +199,7 @@ export class ApiKeysPage extends PureComponent {
-
diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index f40894426ae..03f11f79cc3 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -137,7 +137,7 @@ exports[`Render should render CTA if there are no API keys 1`] = ` className="gf-form" > diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 8c1ab93cec1..dbd2fb1ffeb 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -142,7 +142,7 @@ export class AddPanelWidget extends React.Component {
- {addCopyButton} diff --git a/public/app/features/dashboard/components/DashExportModal/template.html b/public/app/features/dashboard/components/DashExportModal/template.html index 3c14c4b184d..e399d166d04 100644 --- a/public/app/features/dashboard/components/DashExportModal/template.html +++ b/public/app/features/dashboard/components/DashExportModal/template.html @@ -12,7 +12,7 @@
-
@@ -126,10 +126,10 @@ - - diff --git a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx index ce6a866ce57..8cc26c4a1f2 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx @@ -76,7 +76,7 @@ export class DashboardPermissions extends PureComponent {
-
diff --git a/public/app/features/dashboard/components/DashboardSettings/template.html b/public/app/features/dashboard/components/DashboardSettings/template.html index 97002f7bf92..99edc035bd5 100644 --- a/public/app/features/dashboard/components/DashboardSettings/template.html +++ b/public/app/features/dashboard/components/DashboardSettings/template.html @@ -10,7 +10,7 @@
-
-
@@ -128,7 +128,7 @@

Make Editable

-
diff --git a/public/app/features/dashboard/components/ExportDataModal/template.html b/public/app/features/dashboard/components/ExportDataModal/template.html index 8b766889c33..f59bd629e03 100644 --- a/public/app/features/dashboard/components/ExportDataModal/template.html +++ b/public/app/features/dashboard/components/ExportDataModal/template.html @@ -29,7 +29,7 @@ diff --git a/public/app/features/dashboard/components/RowOptions/template.html b/public/app/features/dashboard/components/RowOptions/template.html index 3d5c6116679..13e00b631ed 100644 --- a/public/app/features/dashboard/components/RowOptions/template.html +++ b/public/app/features/dashboard/components/RowOptions/template.html @@ -22,7 +22,7 @@
- +
diff --git a/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts index 6a470785fdb..60fa031f71c 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts @@ -32,7 +32,7 @@ const template = `
- + Cancel
diff --git a/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts index 88fba13f711..ed187befb95 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts @@ -52,8 +52,8 @@ const template = ` diff --git a/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts b/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts index cb83a1baa0c..b08a733d877 100644 --- a/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts +++ b/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts @@ -20,7 +20,7 @@ const template = `
- +
diff --git a/public/app/features/dashboard/components/VersionHistory/template.html b/public/app/features/dashboard/components/VersionHistory/template.html index 5a053c46cc6..c7e94682d28 100644 --- a/public/app/features/dashboard/components/VersionHistory/template.html +++ b/public/app/features/dashboard/components/VersionHistory/template.html @@ -64,7 +64,7 @@ Show more versions diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx index 08bc84775dc..efd2802178f 100644 --- a/public/app/features/folders/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -82,7 +82,7 @@ export class FolderSettingsPage extends PureComponent { />
-
-
diff --git a/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html b/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html index 8a67517aa92..fd805465a55 100644 --- a/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html +++ b/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html @@ -26,7 +26,7 @@
- + Cancel
diff --git a/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts b/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts index 0c38a1247f1..44f831af0c2 100644 --- a/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts +++ b/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts @@ -4,7 +4,7 @@ import angular from 'angular'; const template = ` -
@@ -317,8 +317,8 @@
- - + +
diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 28ed4754d01..c7ce8c6f894 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -65,12 +65,12 @@ export class UsersActionBar extends PureComponent { )}
{canInvite && ( - + Invite )} {externalUserMngLinkUrl && ( - + {externalUserMngLinkName} )} diff --git a/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap b/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap index e69accb011b..a73d298581e 100644 --- a/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap +++ b/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap @@ -105,7 +105,7 @@ exports[`Render should show external user management button 1`] = ` className="page-action-bar__spacer" /> @@ -143,7 +143,7 @@ exports[`Render should show invite button 1`] = ` className="page-action-bar__spacer" /> diff --git a/public/app/partials/confirm_modal.html b/public/app/partials/confirm_modal.html index d0b0d260f78..5d80f59a41f 100644 --- a/public/app/partials/confirm_modal.html +++ b/public/app/partials/confirm_modal.html @@ -26,7 +26,7 @@
- +
diff --git a/public/app/partials/edit_json.html b/public/app/partials/edit_json.html index 91552f95d41..fb411e662fc 100644 --- a/public/app/partials/edit_json.html +++ b/public/app/partials/edit_json.html @@ -15,7 +15,7 @@
diff --git a/public/app/partials/reset_password.html b/public/app/partials/reset_password.html index bba38af0235..085cc34d111 100644 --- a/public/app/partials/reset_password.html +++ b/public/app/partials/reset_password.html @@ -16,7 +16,7 @@
-
diff --git a/public/app/partials/signup_invited.html b/public/app/partials/signup_invited.html index c4c08c9ded8..966dba2d352 100644 --- a/public/app/partials/signup_invited.html +++ b/public/app/partials/signup_invited.html @@ -30,7 +30,7 @@
-
diff --git a/public/app/partials/signup_step2.html b/public/app/partials/signup_step2.html index b01c8160b16..5fae3563600 100644 --- a/public/app/partials/signup_step2.html +++ b/public/app/partials/signup_step2.html @@ -37,7 +37,7 @@
- diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 7b0ed869bdc..66943bb733a 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -3,6 +3,18 @@ $theme-name: dark; +// New Colors +// ------------------------- +$sapphire-faint: #041126; +$sapphire-bright: #5794F2; +$sapphire-base: #3274D9; +$sapphire-shade: #1F60C4; +$lobster-base: #E02F44; +$lobster-shade: #C4162A; +$forest-light: #96D98D; +$forest-base: #37872D; +$forest-shade: #19730E; + // Grays // ------------------------- $black: #000; @@ -30,31 +42,29 @@ $white: #fff; // Accent colors // ------------------------- $blue: #33b5e5; -$blue-dark: #005f81; $green: #299c46; -$red: #d44a3a; +$red: $lobster-base; $yellow: #ecbb13; -$pink: #ff4444; $purple: #9933cc; $variable: #32d1df; $orange: #eb7b18; $brand-primary: $orange; -$brand-success: $green; +$brand-success: $forest-base; $brand-warning: $brand-primary; -$brand-danger: $red; +$brand-danger: $lobster-base; -$query-red: #e24d42; -$query-green: #74e680; +$query-red: $lobster-base; +$query-green: $forest-light; $query-purple: #fe85fc; $query-keyword: #66d9ef; $query-orange: $orange; // Status colors // ------------------------- -$online: #10a345; +$online: $forest-base; $warn: #f79520; -$critical: #ed2e18; +$critical: $lobster-base; // Scaffolding // ------------------------- @@ -68,7 +78,6 @@ $text-color-weak: $gray-2; $text-color-faint: $dark-5; $text-color-emphasis: $gray-5; -$text-shadow-strong: 1px 1px 4px $black; $text-shadow-faint: 1px 1px 4px rgb(45, 45, 45); // gradients @@ -87,7 +96,7 @@ $edit-gradient: linear-gradient(180deg, rgb(22, 23, 25) 50%, #090909); $link-color: darken($white, 11%); $link-color-disabled: darken($link-color, 30%); $link-hover-color: $white; -$external-link-color: $blue; +$external-link-color: $sapphire-bright; // Typography // ------------------------- @@ -100,8 +109,7 @@ $hr-border-color: $dark-4; // Panel // ------------------------- $panel-bg: #212124; -$panel-border-color: $dark-1; -$panel-border: solid 1px $panel-border-color; +$panel-border: solid 1px $dark-1; $panel-header-hover-bg: $dark-4; $panel-corner: $panel-bg; @@ -110,7 +118,7 @@ $page-header-bg: linear-gradient(90deg, #292a2d, black); $page-header-shadow: inset 0px -4px 14px $dark-2; $page-header-border-color: $dark-4; -$divider-border-color: #555; +$divider-border-color: $gray-1; // Graphite Target Editor $tight-form-bg: $dark-3; @@ -153,29 +161,20 @@ $table-bg-hover: $dark-3; // Buttons // ------------------------- -$btn-primary-bg: #ff6600; -$btn-primary-bg-hl: #bc3e06; +$btn-primary-bg: $sapphire-base; +$btn-primary-bg-hl: $sapphire-shade; -$btn-secondary-bg-hl: lighten($blue-dark, 5%); -$btn-secondary-bg: $blue-dark; +$btn-secondary-bg: $sapphire-base; +$btn-secondary-bg-hl: $sapphire-shade; -$btn-success-bg: $green; -$btn-success-bg-hl: darken($green, 6%); - -$btn-warning-bg: $brand-warning; -$btn-warning-bg-hl: lighten($brand-warning, 8%); - -$btn-danger-bg: $red; -$btn-danger-bg-hl: darken($red, 8%); +$btn-danger-bg: $lobster-base; +$btn-danger-bg-hl: $lobster-shade; $btn-inverse-bg: $dark-3; $btn-inverse-bg-hl: lighten($dark-3, 4%); $btn-inverse-text-color: $link-color; $btn-inverse-text-shadow: 0px 1px 0 rgba(0, 0, 0, 0.1); -$btn-active-bg: $gray-4; -$btn-active-text-color: $blue-dark; - $btn-link-color: $gray-3; $iconContainerBackground: $black; @@ -281,11 +280,11 @@ $toolbar-bg: $input-black; // ------------------------- $warning-text-color: $warn; $error-text-color: #e84d4d; -$success-text-color: #12d95a; -$info-text-color: $blue-dark; +$success-text-color: $forest-light; +//$info-text-color: $blue-dark; $alert-error-bg: linear-gradient(90deg, #d44939, #e0603d); -$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); +$alert-success-bg: linear-gradient(90deg, $forest-base, $forest-shade); $alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d); $alert-info-bg: linear-gradient(100deg, #1a4552, #00374a); @@ -317,7 +316,7 @@ $tooltipBackgroundBrand: $brand-primary; $checkboxImageUrl: '../img/checkbox.png'; // info box -$info-box-border-color: darken($blue, 12%); +$info-box-border-color: $sapphire-base; // footer $footer-link-color: $gray-2; @@ -348,8 +347,8 @@ $diff-arrow-color: $white; $diff-json-bg: $dark-4; $diff-json-fg: $gray-5; -$diff-json-added: #457740; -$diff-json-deleted: #a04338; +$diff-json-added: $sapphire-shade; +$diff-json-deleted: $lobster-shade; $diff-json-old: #a04338; $diff-json-new: #457740; @@ -360,21 +359,21 @@ $diff-json-changed-num: $text-color; $diff-json-icon: $gray-7; //Submenu -$variable-option-bg: $blue-dark; +$variable-option-bg: $sapphire-shade; //Switch Slider // ------------------------- $switch-bg: $input-bg; $switch-slider-color: $dark-2; $switch-slider-off-bg: $gray-1; -$switch-slider-on-bg: linear-gradient(90deg, $orange, $red); +$switch-slider-on-bg: linear-gradient(90deg, #eb7b18, #d44a3a); $switch-slider-shadow: 0 0 3px black; //Checkbox // ------------------------- $checkbox-bg: $dark-1; $checkbox-border: 1px solid $gray-1; -$checkbox-checked-bg: linear-gradient(0deg, $orange, $red); +$checkbox-checked-bg: linear-gradient(0deg, #eb7b18, #d44a3a); $checkbox-color: $dark-1; //Panel Edit @@ -385,24 +384,24 @@ $panel-editor-side-menu-shadow: drop-shadow(0 0 10px $black); $panel-editor-toolbar-view-bg: $input-black; $panel-editor-viz-item-shadow: 0 0 8px $dark-5; $panel-editor-viz-item-border: 1px solid $dark-5; -$panel-editor-viz-item-shadow-hover: 0 0 4px $blue; -$panel-editor-viz-item-border-hover: 1px solid $blue; +$panel-editor-viz-item-shadow-hover: 0 0 4px $sapphire-shade; +$panel-editor-viz-item-border-hover: 1px solid $sapphire-shade; $panel-editor-viz-item-bg: $input-black; $panel-editor-tabs-line-color: #e3e3e3; -$panel-editor-viz-item-bg-hover: darken($blue, 47%); +$panel-editor-viz-item-bg-hover: $sapphire-faint; $panel-editor-viz-item-bg-hover-active: darken($orange, 45%); $panel-options-group-border: none; $panel-options-group-header-bg: $gray-blue; -$panel-grid-placeholder-bg: darken($blue, 47%); -$panel-grid-placeholder-shadow: 0 0 4px $blue; +$panel-grid-placeholder-bg: $sapphire-faint; +$panel-grid-placeholder-shadow: 0 0 4px $sapphire-shade; // logs $logs-color-unkown: $gray-2; // toggle-group -$button-toggle-group-btn-active-bg: linear-gradient(90deg, $orange, $red); +$button-toggle-group-btn-active-bg: linear-gradient(90deg, #eb7b18, #d44a3a); $button-toggle-group-btn-active-shadow: inset 0 0 4px $black; $button-toggle-group-btn-seperator-border: 1px solid $page-bg; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 10c074e1481..85cb047be25 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -7,6 +7,19 @@ $theme-name: light; +// New Colors +// ------------------------- +$sapphire-faint: #F5F9FF; +$sapphire-light: #A8CAFF; +$sapphire-base: #3274D9; +$sapphire-shade: #1F60C4; +$lobster-base: #E02F44; +$lobster-shade: #C4162A; +$green-base: #37872D; +$green-shade: #19730E; +$purple-shade: #8F3BB8; +$yellow-base: #F2CC0C; + // Grays // ------------------------- $black: #000; @@ -31,32 +44,29 @@ $white: #fff; // Accent colors // ------------------------- $blue: #0083b3; -$blue-dark: #005f81; -$blue-light: #00a8e6; $green: #3aa655; -$red: #d44939; +$red: $lobster-base; $yellow: #ff851b; $orange: #ff7941; -$pink: #e671b8; $purple: #9954bb; -$variable: $blue; +$variable: $purple-shade; $brand-primary: $orange; $brand-success: $green; $brand-warning: $orange; -$brand-danger: $red; +$brand-danger: $lobster-base; -$query-red: $red; +$query-red: $lobster-base; $query-green: $green; $query-purple: $purple; $query-orange: $orange; -$query-keyword: $blue; +$query-keyword: $sapphire-base; // Status colors // ------------------------- -$online: #01a64f; +$online: $green-shade; $warn: #f79520; -$critical: #ec2128; +$critical: $lobster-shade; // Scaffolding // ------------------------- @@ -70,9 +80,7 @@ $text-color-weak: $gray-2; $text-color-faint: $gray-4; $text-color-emphasis: $dark-5; -$text-shadow-strong: none; $text-shadow-faint: none; -$textShadow: none; // gradients $brand-gradient: linear-gradient(to right, rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); @@ -84,7 +92,7 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%); $link-color: $gray-1; $link-color-disabled: lighten($link-color, 30%); $link-hover-color: darken($link-color, 20%); -$external-link-color: $blue-light; +$external-link-color: $sapphire-shade; // Typography // ------------------------- @@ -98,8 +106,7 @@ $hr-border-color: $dark-3 !default; // ------------------------- $panel-bg: $white; -$panel-border-color: $gray-5; -$panel-border: solid 1px $panel-border-color; +$panel-border: solid 1px $gray-5; $panel-header-hover-bg: $gray-6; $panel-corner: $gray-4; @@ -150,29 +157,20 @@ $scrollbarBorder: $gray-4; // Buttons // ------------------------- -$btn-primary-bg: $brand-primary; -$btn-primary-bg-hl: lighten($brand-primary, 8%); +$btn-primary-bg: $sapphire-base; +$btn-primary-bg-hl: $sapphire-shade; -$btn-secondary-bg: $blue; -$btn-secondary-bg-hl: lighten($blue, 4%); +$btn-secondary-bg: rgba(0,0,0,0); +$btn-secondary-bg-hl: rgba(0,0,0,0); -$btn-success-bg: lighten($green, 3%); -$btn-success-bg-hl: darken($green, 3%); - -$btn-warning-bg: lighten($orange, 3%); -$btn-warning-bg-hl: darken($orange, 3%); - -$btn-danger-bg: lighten($red, 3%); -$btn-danger-bg-hl: darken($red, 3%); +$btn-danger-bg: $lobster-base; +$btn-danger-bg-hl: $lobster-shade; $btn-inverse-bg: $gray-6; $btn-inverse-bg-hl: darken($gray-6, 5%); $btn-inverse-text-color: $gray-1; $btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4); -$btn-active-bg: $white; -$btn-active-text-color: $blue; - $btn-link-color: $gray-1; $btn-divider-left: $gray-4; @@ -189,8 +187,8 @@ $input-bg-disabled: $gray-5; $input-color: $dark-3; $input-border-color: $gray-5; $input-box-shadow: none; -$input-border-focus: $blue !default; -$input-box-shadow-focus: $blue !default; +$input-border-focus: $sapphire-light !default; +$input-box-shadow-focus: $sapphire-light !default; $input-color-placeholder: $gray-4 !default; $input-label-bg: $gray-5; $input-label-border-color: $gray-5; @@ -285,14 +283,14 @@ $navbar-button-border: $gray-4; // Form states and alerts // ------------------------- $warning-text-color: lighten($orange, 10%); -$error-text-color: lighten($red, 10%); +$error-text-color: $lobster-shade; $success-text-color: lighten($green, 10%); -$info-text-color: $blue; +$info-text-color: $sapphire-shade; -$alert-error-bg: linear-gradient(90deg, #d44939, #e04d3d); -$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); -$alert-warning-bg: linear-gradient(90deg, #d44939, #e04d3d); -$alert-info-bg: $blue; +$alert-error-bg: linear-gradient(90deg, $lobster-base, $lobster-shade); +$alert-success-bg: linear-gradient(90deg, $green-base, $green-shade); +$alert-warning-bg: linear-gradient(90deg, $lobster-base, $lobster-shade); +$alert-info-bg: $sapphire-base; // popover $popover-bg: $page-bg; @@ -300,7 +298,7 @@ $popover-color: $text-color; $popover-border-color: $gray-5; $popover-shadow: 0 0 20px $white; -$popover-help-bg: $blue; +$popover-help-bg: $sapphire-base; $popover-help-color: $gray-6; $popover-error-bg: $btn-danger-bg; @@ -321,7 +319,7 @@ $tooltipBackgroundBrand: $brand-primary; $checkboxImageUrl: '../img/checkbox_white.png'; // info box -$info-box-border-color: lighten($blue, 20%); +$info-box-border-color: $sapphire-base; // footer $footer-link-color: $gray-3; @@ -332,16 +330,16 @@ $footer-link-hover: $dark-5; // json explorer $json-explorer-default-color: black; $json-explorer-string-color: green; -$json-explorer-number-color: blue; -$json-explorer-boolean-color: red; +$json-explorer-number-color: $sapphire-base; +$json-explorer-boolean-color: $lobster-base; $json-explorer-null-color: #855a00; $json-explorer-undefined-color: rgb(202, 11, 105); $json-explorer-function-color: #ff20ed; $json-explorer-rotate-time: 100ms; $json-explorer-toggler-opacity: 0.6; -$json-explorer-bracket-color: blue; +$json-explorer-bracket-color: $sapphire-base; $json-explorer-key-color: #00008b; -$json-explorer-url-color: blue; +$json-explorer-url-color: $sapphire-base; // Changelog and diff // ------------------------- @@ -355,34 +353,34 @@ $diff-arrow-color: $dark-3; $diff-group-bg: $gray-7; $diff-json-bg: $gray-5; -$diff-json-fg: $gray-2; +$diff-json-fg: $gray-1; -$diff-json-added: lighten(desaturate($green, 30%), 10%); -$diff-json-deleted: desaturate($red, 35%); +$diff-json-added: $sapphire-shade; +$diff-json-deleted: $lobster-shade; $diff-json-old: #5a372a; $diff-json-new: #664e33; -$diff-json-changed-fg: $gray-6; +$diff-json-changed-fg: $gray-7; $diff-json-changed-num: $gray-4; $diff-json-icon: $gray-4; //Submenu -$variable-option-bg: $blue-light; +$variable-option-bg: $sapphire-light; //Switch Slider // ------------------------- $switch-bg: $white; $switch-slider-color: $gray-7; $switch-slider-off-bg: $gray-5; -$switch-slider-on-bg: linear-gradient(90deg, $yellow, $red); +$switch-slider-on-bg: linear-gradient(90deg, #FF9830, #E55400); $switch-slider-shadow: 0 0 3px $dark-5; //Checkbox // ------------------------- $checkbox-bg: $gray-6; $checkbox-border: 1px solid $gray-3; -$checkbox-checked-bg: linear-gradient(0deg, $yellow, $red); +$checkbox-checked-bg: linear-gradient(0deg, #FF9830, #E55400); $checkbox-color: $gray-7; //Panel Edit @@ -393,18 +391,18 @@ $panel-editor-side-menu-shadow: drop-shadow(0 0 2px $gray-3); $panel-editor-toolbar-view-bg: $white; $panel-editor-viz-item-shadow: 0 0 4px $gray-3; $panel-editor-viz-item-border: 1px solid $gray-3; -$panel-editor-viz-item-shadow-hover: 0 0 4px $blue-light; -$panel-editor-viz-item-border-hover: 1px solid $blue-light; +$panel-editor-viz-item-shadow-hover: 0 0 4px $sapphire-light; +$panel-editor-viz-item-border-hover: 1px solid $sapphire-light; $panel-editor-viz-item-bg: $white; $panel-editor-tabs-line-color: $dark-5; -$panel-editor-viz-item-bg-hover: lighten($blue, 62%); +$panel-editor-viz-item-bg-hover: $sapphire-faint; $panel-editor-viz-item-bg-hover-active: lighten($orange, 34%); $panel-options-group-border: none; $panel-options-group-header-bg: $gray-5; -$panel-grid-placeholder-bg: lighten($blue, 62%); -$panel-grid-placeholder-shadow: 0 0 4px $blue-light; +$panel-grid-placeholder-bg: $sapphire-faint; +$panel-grid-placeholder-shadow: 0 0 4px $sapphire-light; // logs $logs-color-unkown: $gray-5; diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index 1a005b0d511..e5a20a80659 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -59,13 +59,13 @@ a.text-error:focus { color: darken($error-text-color, 10%); } -.text-info { +/*.text-info { color: $info-text-color; } a.text-info:hover, a.text-info:focus { color: darken($info-text-color, 10%); -} +}*/ .text-success { color: $success-text-color; diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 4e032d7b9d1..84e2665f582 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -89,35 +89,12 @@ .btn-secondary { @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); } -// Warning appears are orange -.btn-warning { - @include buttonBackground($btn-warning-bg, $btn-warning-bg-hl); -} // Danger and error appear as red .btn-danger { @include buttonBackground($btn-danger-bg, $btn-danger-bg-hl); } -// Success appears as green -.btn-success { - @include buttonBackground($btn-success-bg, $btn-success-bg-hl); - &--processing { - @include button-outline-variant($gray-1); - @include box-shadow(none); - cursor: default; - - &:hover, - &:active, - &:active:hover, - &:focus, - &:disabled { - color: $gray-1; - background-color: transparent; - border-color: $gray-1; - } - } -} // Info appears as a neutral blue .btn-secondary { @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); @@ -138,20 +115,15 @@ @include button-outline-variant($btn-primary-bg); } .btn-outline-secondary { - @include button-outline-variant($btn-secondary-bg); + @include button-outline-variant($btn-secondary-bg-hl); } .btn-outline-inverse { @include button-outline-variant($btn-inverse-bg); } -.btn-outline-success { - @include button-outline-variant($btn-success-bg); -} -.btn-outline-warning { - @include button-outline-variant($btn-warning-bg); -} .btn-outline-danger { - @include button-outline-variant($btn-danger-bg); + @include button-outline-variant(green); } + .btn-outline-disabled { @include button-outline-variant($gray-1); @include box-shadow(none); diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index b3733b694fc..088dd72f37b 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -158,7 +158,7 @@ } &--primary { - @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); + @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl); } } diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 5bbc4ba29ca..b51bd3a9ef9 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -118,7 +118,7 @@ $path-position: $marker-size-half - ($path-height / 2); .progress-step-cta { @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $btn-border-radius); - @include buttonBackground($btn-success-bg, $btn-success-bg-hl); + @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl); display: none; } diff --git a/public/vendor/angular-ui/ui-bootstrap-tpls.js b/public/vendor/angular-ui/ui-bootstrap-tpls.js index 87120b66ce1..ad6f3b4b4bc 100644 --- a/public/vendor/angular-ui/ui-bootstrap-tpls.js +++ b/public/vendor/angular-ui/ui-bootstrap-tpls.js @@ -1245,7 +1245,7 @@ angular.module("template/datepicker/popup.html", []).run(["$templateCache", func " \n" + " \n" + " \n" + - " \n" + + " \n" + " \n" + "\n" + ""); From fd1ef0a2be8ae3115ddeb65f44b1a50dbd5cb650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 12:10:42 +0100 Subject: [PATCH 097/770] Added custom scrollbar and remember scroll pos to jump back to same scroll pos when going back to dashboard from edit mode --- .../CustomScrollbar/CustomScrollbar.tsx | 1 + public/app/core/components/Page/Page.tsx | 7 --- .../dashboard/containers/DashboardPage.tsx | 45 +++++++++++++------ public/app/routes/GrafanaCtrl.ts | 1 + public/app/routes/ReactContainer.tsx | 3 ++ 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 40f6c6c3c37..17c511826fb 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -45,6 +45,7 @@ export class CustomScrollbar extends PureComponent { if (this.props.scrollTop > 10000) { ref.scrollToBottom(); } else { + console.log('scrollbar set scrollTop'); ref.scrollTop(this.props.scrollTop); } } diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index c4846ecf85d..997f02b700c 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -17,13 +17,10 @@ interface Props { } class Page extends Component { - private bodyClass = 'is-react'; - private body = document.body; static Header = PageHeader; static Contents = PageContents; componentDidMount() { - this.body.classList.add(this.bodyClass); this.updateTitle(); } @@ -33,10 +30,6 @@ class Page extends Component { } } - componentWillUnmount() { - this.body.classList.remove(this.bodyClass); - } - updateTitle = () => { const title = this.getPageTitle; document.title = title ? title + ' - Grafana' : 'Grafana'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 404c953eecb..33f2a602b0c 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -1,6 +1,6 @@ // Libraries import $ from 'jquery'; -import React, { PureComponent } from 'react'; +import React, { PureComponent, MouseEvent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import classNames from 'classnames'; @@ -9,11 +9,11 @@ import classNames from 'classnames'; import { createErrorNotification } from 'app/core/copy/appNotification'; // Components -import { LoadingPlaceholder } from '@grafana/ui'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashNav } from '../components/DashNav'; import { SubMenu } from '../components/SubMenu'; import { DashboardSettings } from '../components/DashboardSettings'; +import { CustomScrollbar } from '@grafana/ui'; // Redux import { initDashboard } from '../state/initDashboard'; @@ -50,6 +50,8 @@ interface State { isEditing: boolean; isFullscreen: boolean; fullscreenPanel: PanelModel | null; + scrollTop: number; + rememberScrollTop: number; } export class DashboardPage extends PureComponent { @@ -58,6 +60,8 @@ export class DashboardPage extends PureComponent { isEditing: false, isFullscreen: false, fullscreenPanel: null, + scrollTop: 0, + rememberScrollTop: 0, }; async componentDidMount() { @@ -121,6 +125,7 @@ export class DashboardPage extends PureComponent { isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel, + rememberScrollTop: this.state.scrollTop, }); this.setPanelFullscreenClass(urlFullscreen); } else { @@ -135,9 +140,17 @@ export class DashboardPage extends PureComponent { dashboard.setViewMode(this.state.fullscreenPanel, false, false); } - this.setState({ isEditing: false, isFullscreen: false, fullscreenPanel: null }, () => { - dashboard.render(); - }); + this.setState( + { + isEditing: false, + isFullscreen: false, + fullscreenPanel: null, + scrollTop: this.state.rememberScrollTop, + }, + () => { + dashboard.render(); + } + ); this.setPanelFullscreenClass(false); } @@ -160,9 +173,10 @@ export class DashboardPage extends PureComponent { $('body').toggleClass('panel-in-fullscreen', isFullscreen); } - renderLoadingState() { - return ; - } + setScrollTop = (e: MouseEvent): void => { + const target = e.target as HTMLElement; + this.setState({ scrollTop: target.scrollTop }); + }; renderDashboard() { const { dashboard, editview } = this.props; @@ -186,7 +200,7 @@ export class DashboardPage extends PureComponent { render() { const { dashboard, editview, $injector } = this.props; - const { isSettingsOpening, isEditing, isFullscreen } = this.state; + const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state; if (!dashboard) { return null; @@ -201,6 +215,7 @@ export class DashboardPage extends PureComponent { 'dashboard-container': true, 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, }); + return (
{ $injector={$injector} />
- {dashboard && editview && } + + {dashboard && editview && } -
- {dashboard.meta.submenuEnabled && } - -
+
+ {dashboard.meta.submenuEnabled && } + +
+
); diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 07d99725113..9157c189ab2 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -45,6 +45,7 @@ export class GrafanaCtrl { }; $rootScope.colors = colors; + $rootScope.onAppEvent = function(name, callback, localScope) { const unbind = $rootScope.$on(name, callback); let callerScope = this; diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index a56c8878fb1..d64e74e3949 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -47,9 +47,12 @@ export function reactContainer( routeInfo: $route.current.$$route.routeInfo, }; + document.body.classList.add('is-react'); + ReactDOM.render(WrapInProvider(store, component, props), elem[0]); scope.$on('$destroy', () => { + document.body.classList.remove('is-react'); ReactDOM.unmountComponentAtNode(elem[0]); }); }, From 139fb65fa926c17cdb68f15c69da95c8eeefd7ee Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 5 Feb 2019 12:36:12 +0100 Subject: [PATCH 098/770] docs: fixes #14940 --- docs/sources/installation/configuration.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 46bab83654e..ac3dc6ebfd0 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -393,9 +393,7 @@ Analytics ID here. By default this feature is disabled. ### check_for_updates -Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used -in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor -send any sensitive information. +Set to false to disable all checks to https://grafana.com for new versions of installed plugins and to the Grafana GitHub repository to check for a newer version of Grafana. The version information is used in some UI views to notify that a new Grafana update or a plugin update exists. This option does not cause any auto updates, nor send any sensitive information. The check is run every 10 minutes.
From 181b4f9e80fbe7a6aa39c957dc79c863dcdbf11a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 Feb 2019 14:39:24 +0300 Subject: [PATCH 099/770] azuremonitor: improve autocomplete experence --- .../editor/KustoQueryField.tsx | 24 ++++++++++--------- .../editor/query_field.tsx | 6 ++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index bbe34b8f46a..0a484794e8f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -65,7 +65,7 @@ export default class KustoQueryField extends QueryField { this.fetchSchema(); } - onTypeahead = () => { + onTypeahead = (force = false) => { const selection = window.getSelection(); if (selection.anchorNode) { const wrapperNode = selection.anchorNode.parentElement; @@ -140,14 +140,8 @@ export default class KustoQueryField extends QueryField { suggestionGroups = this.getTableSuggestions(db); prefix = prefix.replace('.', ''); - // built-in - } else if (prefix && !wrapperClasses.contains('argument')) { - if (modelPrefix.match(/\s$/i)) { - prefix = ''; - } - typeaheadContext = 'context-builtin'; - suggestionGroups = this.getKeywordSuggestions(); - } else if (Plain.serialize(this.state.value) === '') { + // new + } else if (normalizeQuery(Plain.serialize(this.state.value)).match(/^\s*\w*$/i)) { typeaheadContext = 'context-new'; if (this.schema) { suggestionGroups = this.getInitialSuggestions(); @@ -156,7 +150,15 @@ export default class KustoQueryField extends QueryField { setTimeout(this.onTypeahead, 0); return; } - } else { + + // built-in + } else if (prefix && !wrapperClasses.contains('argument')) { + if (modelPrefix.match(/\s$/i)) { + prefix = ''; + } + typeaheadContext = 'context-builtin'; + suggestionGroups = this.getKeywordSuggestions(); + } else if (force === true) { typeaheadContext = 'context-builtin'; if (modelPrefix.match(/\s$/i)) { prefix = ''; @@ -181,7 +183,7 @@ export default class KustoQueryField extends QueryField { .filter(group => group.items.length > 0); // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); - console.log('onTypeahead', modelPrefix, prefix, typeaheadContext); + // console.log('onTypeahead', prefix, typeaheadContext); this.setState({ typeaheadPrefix: prefix, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 0acd53cabff..e62337c4982 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -104,11 +104,11 @@ class QueryField extends React.Component { const changed = value.document !== this.state.value.document; this.setState({ value }, () => { if (changed) { + // call typeahead only if query changed + window.requestAnimationFrame(this.onTypeahead); this.onChangeQuery(); } }); - - window.requestAnimationFrame(this.onTypeahead); }; request = (url?) => { @@ -143,7 +143,7 @@ class QueryField extends React.Component { case ' ': { if (event.ctrlKey) { event.preventDefault(); - this.onTypeahead(); + this.onTypeahead(true); return true; } break; From 2802569529197d48e602da0f67bc9f2e1b1e75a1 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 5 Feb 2019 12:47:42 +0100 Subject: [PATCH 100/770] minor layout change, simple render test --- .../AddPanelWidget/AddPanelWidget.test.tsx | 23 ++++++ .../AddPanelWidget/AddPanelWidget.tsx | 39 +++++---- .../AddPanelWidget/_AddPanelWidget.scss | 27 ++++--- .../AddPanelWidget.test.tsx.snap | 81 +++++++++++++++++++ 4 files changed, 146 insertions(+), 24 deletions(-) create mode 100644 public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx create mode 100644 public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx new file mode 100644 index 00000000000..91da066e4cc --- /dev/null +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { AddPanelWidget, Props } from './AddPanelWidget'; +import { DashboardModel, PanelModel } from '../../state'; + +const setup = (propOverrides?: object) => { + const props: Props = { + dashboard: {} as DashboardModel, + panel: {} as PanelModel, + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 21c4451d9b9..e70615bde39 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -1,8 +1,8 @@ import React from 'react'; import _ from 'lodash'; import config from 'app/core/config'; -import { PanelModel } from '../../state/PanelModel'; -import { DashboardModel } from '../../state/DashboardModel'; +import { PanelModel } from '../../state'; +import { DashboardModel } from '../../state'; import store from 'app/core/store'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { updateLocation } from 'app/core/actions'; @@ -57,6 +57,7 @@ export class AddPanelWidget extends React.Component { copiedPanels.push(pluginCopy); } } + return _.sortBy(copiedPanels, 'sort'); } @@ -65,14 +66,6 @@ export class AddPanelWidget extends React.Component { this.props.dashboard.removePanel(this.props.dashboard.panels[0]); } - copyButton(panel) { - return ( - - ); - } - moveToEdit(location) { reduxStore.dispatch(updateLocation(location)); } @@ -151,7 +144,7 @@ export class AddPanelWidget extends React.Component { renderOptionLink = (icon, text, onClick) => { return (
- +
@@ -162,6 +155,8 @@ export class AddPanelWidget extends React.Component { }; render() { + const { copiedPanelPlugins } = this.state; + return (
@@ -172,9 +167,25 @@ export class AddPanelWidget extends React.Component {
- {this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} - {this.renderOptionLink('visualization', 'Choose Panel type', () => this.onCreateNewPanel('visualization'))} - {this.renderOptionLink('queries', 'Convert to row', this.onCreateNewRow)} +
+ {this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} + {this.renderOptionLink('visualization', 'Choose Panel type', () => + this.onCreateNewPanel('visualization') + )} +
+
+
+ Convert to row +
+ {copiedPanelPlugins.length === 1 && ( +
this.onPasteCopiedPanel(copiedPanelPlugins[0])} + > + Paste copied panel +
+ )} +
diff --git a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss index 587daa2703f..ab6ff8556d8 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss +++ b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss @@ -27,11 +27,8 @@ } .add-panel-widget__link { - display: block; margin: 0 8px; - width: 130px; - text-align: center; - padding: 8px 0; + width: 150px; } .add-panel-widget__icon { @@ -54,14 +51,24 @@ margin-right: -10px; } +.add-panel-widget__create { + display: inherit; + margin-bottom: 24px; +} + +.add-panel-widget__actions { + display: inherit; +} + +.add-panel-widget__action { + cursor: pointer; + margin: 0 4px; +} + .add-panel-widget__btn-container { + height: 100%; display: flex; justify-content: center; align-items: center; - height: 100%; - flex-direction: row; - - .btn { - margin-bottom: 10px; - } + flex-direction: column; } diff --git a/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap new file mode 100644 index 00000000000..585f45210af --- /dev/null +++ b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` + +`; From bbc5dff7bd719b4410eb685a8ff31dbd81ab96d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 12:56:03 +0100 Subject: [PATCH 101/770] Fixed add panel should scroll to top --- .../dashboard/components/DashNav/DashNav.tsx | 137 ++++++++---------- .../dashboard/containers/DashboardPage.tsx | 31 ++-- 2 files changed, 77 insertions(+), 91 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 374fd6dcd36..297d7ca7ea7 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -23,6 +23,7 @@ export interface Props { isFullscreen: boolean; $injector: any; updateLocation: typeof updateLocation; + onAddPanel: () => void; } export class DashNav extends PureComponent { @@ -39,7 +40,8 @@ export class DashNav extends PureComponent { componentDidMount() { const loader = getAngularLoader(); - const template = ''; + const template = + ''; const scopeProps = { dashboard: this.props.dashboard }; this.timepickerCmp = loader.load(this.timePickerEl, scopeProps, template); @@ -55,21 +57,6 @@ export class DashNav extends PureComponent { appEvents.emit('show-dash-search'); }; - onAddPanel = () => { - const { dashboard } = this.props; - - // Return if the "Add panel" exists already - if (dashboard.panels.length > 0 && dashboard.panels[0].type === 'add-panel') { - return; - } - - dashboard.addPanel({ - type: 'add-panel', - gridPos: { x: 0, y: 0, w: 12, h: 8 }, - title: 'Panel Title', - }); - }; - onClose = () => { if (this.props.editview) { this.props.updateLocation({ @@ -137,7 +124,7 @@ export class DashNav extends PureComponent { }; render() { - const { dashboard, isFullscreen, editview } = this.props; + const { dashboard, isFullscreen, editview, onAddPanel } = this.props; const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; @@ -186,73 +173,73 @@ export class DashNav extends PureComponent { tooltip="Add panel" classSuffix="add-panel" icon="gicon gicon-add-panel" - onClick={this.onAddPanel} + onClick={onAddPanel} /> - )} + )} - {canStar && ( - - )} + {canStar && ( + + )} - {canShare && ( - - )} + {canShare && ( + + )} - {canSave && ( - - )} + {canSave && ( + + )} - {snapshotUrl && ( - - )} + {snapshotUrl && ( + + )} - {showSettings && ( - - )} -
+ {showSettings && ( + + )} + -
- -
+
+ +
-
(this.timePickerEl = element)} /> +
(this.timePickerEl = element)} /> - {(isFullscreen || editview) && ( -
- -
- )} -
+ {(isFullscreen || editview) && ( +
+ +
+ )} +
); } } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 33f2a602b0c..1d1882f277d 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -178,25 +178,23 @@ export class DashboardPage extends PureComponent { this.setState({ scrollTop: target.scrollTop }); }; - renderDashboard() { - const { dashboard, editview } = this.props; - const { isEditing, isFullscreen } = this.state; + onAddPanel = () => { + const { dashboard } = this.props; - const classes = classNames({ - 'dashboard-container': true, - 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, + // Return if the "Add panel" exists already + if (dashboard.panels.length > 0 && dashboard.panels[0].type === 'add-panel') { + return; + } + + dashboard.addPanel({ + type: 'add-panel', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + title: 'Panel Title', }); - return ( -
- {dashboard && editview && } - -
- -
-
- ); - } + // scroll to top after adding panel + this.setState({ scrollTop: 0 }); + }; render() { const { dashboard, editview, $injector } = this.props; @@ -224,6 +222,7 @@ export class DashboardPage extends PureComponent { isFullscreen={isFullscreen} editview={editview} $injector={$injector} + onAddPanel={this.onAddPanel} />
From 9ba98b87035e07714bf65c7a11c63a3f1c0c952f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 13:13:52 +0100 Subject: [PATCH 102/770] Fixes #15223 by handling onPaste event because of bug in Slate --- public/app/features/explore/QueryField.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 8ab7e56dc5a..810bca9ef5a 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -468,6 +468,14 @@ export class QueryField extends React.PureComponent { + const pastedValue = event.clipboardData.getData('Text'); + const newValue = change.value.change().insertText(pastedValue); + this.onChange(newValue); + + return true; + }; + render() { const { disabled } = this.props; const wrapperClassName = classnames('slate-query-field__wrapper', { @@ -484,6 +492,7 @@ export class QueryField extends React.PureComponent Date: Tue, 5 Feb 2019 13:49:35 +0100 Subject: [PATCH 103/770] Basic loading state for slow dashboards --- .../CustomScrollbar/CustomScrollbar.tsx | 1 - public/app/core/redux/index.ts | 6 ++---- .../dashboard/containers/DashboardPage.tsx | 19 ++++++++++++++++++- .../app/features/dashboard/state/actions.ts | 4 ++-- .../features/dashboard/state/initDashboard.ts | 10 +++++++++- .../app/features/dashboard/state/reducers.ts | 13 +++++++++++-- public/app/types/dashboard.ts | 1 + public/sass/pages/_dashboard.scss | 11 +++++++++++ 8 files changed, 54 insertions(+), 11 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 17c511826fb..40f6c6c3c37 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -45,7 +45,6 @@ export class CustomScrollbar extends PureComponent { if (this.props.scrollTop > 10000) { ref.scrollToBottom(); } else { - console.log('scrollbar set scrollTop'); ref.scrollTop(this.props.scrollTop); } } diff --git a/public/app/core/redux/index.ts b/public/app/core/redux/index.ts index 359f160b9ce..bf45d7d22df 100644 --- a/public/app/core/redux/index.ts +++ b/public/app/core/redux/index.ts @@ -1,4 +1,2 @@ -import { actionCreatorFactory } from './actionCreatorFactory'; -import { reducerFactory } from './reducerFactory'; - -export { actionCreatorFactory, reducerFactory }; +export { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from './actionCreatorFactory'; +export { reducerFactory } from './reducerFactory'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 1d1882f277d..5fa48f45375 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -38,6 +38,7 @@ interface Props { urlEdit: boolean; urlFullscreen: boolean; loadingState: DashboardLoadingState; + isLoadingSlow: boolean; dashboard: DashboardModel; initDashboard: typeof initDashboard; setDashboardModel: typeof setDashboardModel; @@ -52,6 +53,7 @@ interface State { fullscreenPanel: PanelModel | null; scrollTop: number; rememberScrollTop: number; + showLoadingState: boolean; } export class DashboardPage extends PureComponent { @@ -59,6 +61,7 @@ export class DashboardPage extends PureComponent { isSettingsOpening: false, isEditing: false, isFullscreen: false, + showLoadingState: false, fullscreenPanel: null, scrollTop: 0, rememberScrollTop: 0, @@ -196,11 +199,24 @@ export class DashboardPage extends PureComponent { this.setState({ scrollTop: 0 }); }; + renderLoadingState() { + return ( +
+
+ Dashboard {this.props.loadingState} +
+
+ ); + } + render() { - const { dashboard, editview, $injector } = this.props; + const { dashboard, editview, $injector, isLoadingSlow } = this.props; const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state; if (!dashboard) { + if (isLoadingSlow) { + return this.renderLoadingState(); + } return null; } @@ -249,6 +265,7 @@ const mapStateToProps = (state: StoreState) => ({ urlFullscreen: state.location.query.fullscreen === true, urlEdit: state.location.query.edit === true, loadingState: state.dashboard.loadingState, + isLoadingSlow: state.dashboard.isLoadingSlow, dashboard: state.dashboard.model as DashboardModel, }); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index bc57b8e5f10..da4c195c953 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -3,8 +3,7 @@ import { ThunkAction } from 'redux-thunk'; // Services & Utils import { getBackendSrv } from 'app/core/services/backend_srv'; -import { actionCreatorFactory } from 'app/core/redux'; -import { ActionOf } from 'app/core/redux/actionCreatorFactory'; +import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux'; import { createSuccessNotification } from 'app/core/copy/appNotification'; // Actions @@ -25,6 +24,7 @@ import { DashboardLoadingState, MutableDashboard } from 'app/types/dashboard'; export const loadDashboardPermissions = actionCreatorFactory('LOAD_DASHBOARD_PERMISSIONS').create(); export const setDashboardLoadingState = actionCreatorFactory('SET_DASHBOARD_LOADING_STATE').create(); export const setDashboardModel = actionCreatorFactory('SET_DASHBOARD_MODEL').create(); +export const setDashboardLoadingSlow = noPayloadActionCreatorFactory('SET_DASHBOARD_LOADING_SLOW').create(); export type Action = ActionOf; export type ThunkResult = ThunkAction; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 14428bfa290..d529ca0b531 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -12,7 +12,7 @@ import { config } from 'app/core/config'; import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; import locationUtil from 'app/core/utils/location_util'; -import { setDashboardLoadingState, ThunkResult, setDashboardModel } from './actions'; +import { setDashboardLoadingState, ThunkResult, setDashboardModel, setDashboardLoadingSlow } from './actions'; import { removePanel } from '../utils/panel'; // Types @@ -71,6 +71,14 @@ export function initDashboard({ // set fetching state dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + // Detect slow loading / initializing and set state flag + // This is in order to not show loading indication for fast loading dashboards as it creates blinking/flashing + setTimeout(() => { + if (getState().dashboard.model === null) { + dispatch(setDashboardLoadingSlow()); + } + }, 500); + try { switch (routeInfo) { // handle old urls with no uid diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index 2f4e5df5c14..5566363c996 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -1,10 +1,11 @@ import { DashboardState, DashboardLoadingState } from 'app/types/dashboard'; -import { loadDashboardPermissions, setDashboardLoadingState, setDashboardModel } from './actions'; +import { loadDashboardPermissions, setDashboardLoadingState, setDashboardModel, setDashboardLoadingSlow } from './actions'; import { reducerFactory } from 'app/core/redux'; import { processAclItems } from 'app/core/utils/acl'; export const initialState: DashboardState = { loadingState: DashboardLoadingState.NotStarted, + isLoadingSlow: false, model: null, permissions: [], }; @@ -28,7 +29,15 @@ export const dashboardReducer = reducerFactory(initialState) filter: setDashboardModel, mapper: (state, action) => ({ ...state, - model: action.payload + model: action.payload, + isLoadingSlow: false, + }), + }) + .addMapper({ + filter: setDashboardLoadingSlow, + mapper: (state, action) => ({ + ...state, + isLoadingSlow: true, }), }) .create(); diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 36c0a420f28..39d7e3cba8a 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -25,5 +25,6 @@ export enum DashboardLoadingState { export interface DashboardState { model: MutableDashboard | null; loadingState: DashboardLoadingState; + isLoadingSlow: boolean; permissions: DashboardAcl[] | null; } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 9ca4e092f02..0f37ffc850e 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -276,3 +276,14 @@ div.flot-text { .panel-full-edit { padding-top: $dashboard-padding; } + +.dashboard-loading { + height: 60vh; + display: flex; + align-items: center; + justify-content: center; +} + +.dashboard-loading__text { + font-size: $font-size-lg; +} From aa2bf07c71d0e42e9e2df7ef40ae939f2a60c016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 14:12:32 +0100 Subject: [PATCH 104/770] Expand rows for panels in collapsed rows --- .../dashboard/containers/DashboardPage.tsx | 7 ++++++- .../dashboard/containers/SoloPanelPage.tsx | 8 +++++++- .../app/features/dashboard/state/DashboardModel.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 5fa48f45375..0d28ccb19a2 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -120,7 +120,12 @@ export class DashboardPage extends PureComponent { onEnterFullscreen() { const { dashboard, urlEdit, urlFullscreen, urlPanelId } = this.props; - const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + const panelId = parseInt(urlPanelId, 10); + + // need to expand parent row if this panel is inside a row + dashboard.expandParentRowFor(panelId); + + const panel = dashboard.getPanelById(panelId); if (panel) { dashboard.setViewMode(panel, urlFullscreen, urlEdit); diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index beb45b6904d..915d2e03965 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -59,7 +59,13 @@ export class SoloPanelPage extends Component { // we just got the dashboard! if (!prevProps.dashboard) { - const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + const panelId = parseInt(urlPanelId, 10); + + // need to expand parent row if this panel is inside a row + dashboard.expandParentRowFor(panelId); + + const panel = dashboard.getPanelById(panelId); + if (!panel) { this.setState({ notFound: true }); return; diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 8756af2ceea..743eb61f97d 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -904,4 +904,18 @@ export class DashboardModel { this.processRepeats(); this.events.emit('template-variable-value-updated'); } + + expandParentRowFor(panelId: number) { + for (const panel of this.panels) { + if (panel.collapsed) { + for (const rowPanel of panel.panels) { + if (rowPanel.id === panelId) { + this.toggleRow(panel); + return; + } + } + } + } + } + } From da531032812565840c2e29c90b2f6e280adcb454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 14:18:35 +0100 Subject: [PATCH 105/770] Prevent viewers from going into edit mode --- public/app/features/dashboard/containers/DashboardPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 0d28ccb19a2..1bd5218fd60 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -130,7 +130,7 @@ export class DashboardPage extends PureComponent { if (panel) { dashboard.setViewMode(panel, urlFullscreen, urlEdit); this.setState({ - isEditing: urlEdit, + isEditing: urlEdit && dashboard.meta.canEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel, rememberScrollTop: this.state.scrollTop, From ae0b027d69ce0fe2946aabfe55267150151a4038 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 09:34:04 +0100 Subject: [PATCH 106/770] chore: Replace sizeMe with AutoSizer in DashboardGrid --- .../dashboard/dashgrid/DashboardGrid.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 658bfad3816..5a65fadd74b 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -5,13 +5,12 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core import { DashboardPanel } from './DashboardPanel'; import { DashboardModel, PanelModel } from '../state'; import classNames from 'classnames'; -import sizeMe from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; let lastGridWidth = 1200; let ignoreNextWidthChange = false; -interface GridWrapperProps { - size: { width: number; }; +interface SizedReactLayoutGridProps { layout: ReactGridLayout.Layout[]; onLayoutChange: (layout: ReactGridLayout.Layout[]) => void; children: JSX.Element | JSX.Element[]; @@ -25,8 +24,12 @@ interface GridWrapperProps { isFullscreen?: boolean; } +interface GridWrapperProps extends SizedReactLayoutGridProps { + sizedWidth: number; +} + function GridWrapper({ - size, + sizedWidth, layout, onLayoutChange, children, @@ -38,8 +41,8 @@ function GridWrapper({ isResizable, isDraggable, isFullscreen, -}: GridWrapperProps) { - const width = size.width > 0 ? size.width : lastGridWidth; +}: GridWrapperProps) { + const width = sizedWidth > 0 ? sizedWidth : lastGridWidth; // logic to ignore width changes (optimization) if (width !== lastGridWidth) { @@ -74,7 +77,16 @@ function GridWrapper({ ); } -const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper); +const SizedReactLayoutGrid = (props: SizedReactLayoutGridProps) => ( + + {({width}) => ( + + )} + +); export interface DashboardGridProps { dashboard: DashboardModel; From 097396c517e1db4e5633f7625168c255511deb1c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 09:57:54 +0100 Subject: [PATCH 107/770] chore: Replace withSize with AutoSizer in explore/Graph.tsx --- public/app/features/explore/Graph.test.tsx | 1 + public/app/features/explore/Graph.tsx | 27 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/public/app/features/explore/Graph.test.tsx b/public/app/features/explore/Graph.test.tsx index fe4deaf17aa..8976c677592 100644 --- a/public/app/features/explore/Graph.test.tsx +++ b/public/app/features/explore/Graph.test.tsx @@ -5,6 +5,7 @@ import { mockData } from './__mocks__/mockData'; const setup = (propOverrides?: object) => { const props = { + size: { width: 10, height: 20 }, data: mockData().slice(0, 19), range: { from: 'now-6h', to: 'now' }, ...propOverrides, diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 5d64dde28ce..b087f6a457d 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -1,7 +1,7 @@ import $ from 'jquery'; import React, { PureComponent } from 'react'; import moment from 'moment'; -import { withSize } from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.time'; @@ -80,12 +80,15 @@ interface GraphProps { id?: string; range: RawTimeRange; split?: boolean; - size?: { width: number; height: number }; userOptions?: any; onChangeTime?: (range: RawTimeRange) => void; onToggleSeries?: (alias: string, hiddenSeries: Set) => void; } +interface SizedGraphProps extends GraphProps { + size: { width: number; height: number }; +} + interface GraphState { /** * Type parameter refers to the `alias` property of a `TimeSeries`. @@ -95,7 +98,7 @@ interface GraphState { showAllTimeSeries: boolean; } -export class Graph extends PureComponent { +export class Graph extends PureComponent { $el: any; dynamicOptions = null; @@ -116,7 +119,7 @@ export class Graph extends PureComponent { this.$el.bind('plotselected', this.onPlotSelected); } - componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { + componentDidUpdate(prevProps: SizedGraphProps, prevState: GraphState) { if ( prevProps.data !== this.props.data || prevProps.range !== this.props.range || @@ -261,4 +264,18 @@ export class Graph extends PureComponent { } } -export default withSize()(Graph); +export default (props: GraphProps) => ( + + {({width, height}) => { + return ( + + ); + }} + +); From d68df9d704d2fb637ba302c86e11107fe5e1953e Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 12:09:24 +0100 Subject: [PATCH 108/770] fix: Calculation issue with AutoSizer in explore --- public/app/features/explore/Graph.tsx | 28 ++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index b087f6a457d..10006557349 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -265,17 +265,19 @@ export class Graph extends PureComponent { } export default (props: GraphProps) => ( - - {({width, height}) => { - return ( - - ); - }} - +
{/* div needed for AutoSizer to calculate, https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#observation */} + + {({width, height}) => ( +
+ {width > 0 && } +
+ )} +
+
); From 260b6f5de83133f668aa15a3874eeeb20d46cbe7 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 12:12:24 +0100 Subject: [PATCH 109/770] chore: Remove react-sizeme --- package.json | 1 - yarn.lock | 28 ---------------------------- 2 files changed, 29 deletions(-) diff --git a/package.json b/package.json index 77fd92baf57..18c0a56f0c4 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,6 @@ "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", "react-redux": "^5.0.7", - "react-sizeme": "^2.3.6", "react-table": "^6.8.6", "react-transition-group": "^2.2.1", "react-virtualized": "^9.21.0", diff --git a/yarn.lock b/yarn.lock index 169abd40ee4..fd0c446fbce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3972,11 +3972,6 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" -batch-processor@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" - integrity sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg= - batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -6613,13 +6608,6 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -element-resize-detector@^1.1.12: - version "1.2.0" - resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.0.tgz#63344fd6f4e5ecff6f018d027e17b281fd4fa338" - integrity sha512-UmhNB8sIJVZeg56gEjgmMd6p37sCg8j8trVW0LZM7Wzv+kxQ5CnRHcgRKBTB/kFUSn3e7UP59kl2V2U8Du1hmg== - dependencies: - batch-processor "1.0.0" - elliptic@^6.0.0: version "6.4.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" @@ -10900,11 +10888,6 @@ lodash.tail@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= -lodash.throttle@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= - 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" @@ -14215,17 +14198,6 @@ react-resizable@1.x: prop-types "15.x" react-draggable "^2.2.6 || ^3.0.3" -react-sizeme@^2.3.6: - version "2.5.2" - resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.5.2.tgz#e7041390cfb895ed15d896aa91d76e147e3b70b5" - integrity sha512-hYvcncV1FxVzPm2EhVwlOLf7Tk+k/ttO6rI7bfKUL/aL1gYzrY3DXJsdZ6nFaFgGSU/i8KC6gCoptOhBbRJpXQ== - dependencies: - element-resize-detector "^1.1.12" - invariant "^2.2.2" - lodash.debounce "^4.0.8" - lodash.throttle "^4.1.1" - shallowequal "^1.0.2" - react-split-pane@^0.1.84: version "0.1.85" resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.85.tgz#64819946a99b617ffa2d20f6f45a0056b6ee4faa" From f5431f521082743d930f54d1d93f6dcaface7f7a Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 13:48:00 +0100 Subject: [PATCH 110/770] chore: Explore: Remove inner AutoSizer, spread the size-object to width/height, change height type to number --- public/app/features/explore/Explore.tsx | 2 +- public/app/features/explore/Graph.tsx | 38 +++++-------------- .../app/features/explore/GraphContainer.tsx | 6 ++- public/app/features/explore/Logs.tsx | 2 +- 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b210bcccc18..437b50db63c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -211,7 +211,7 @@ export class Explore extends React.PureComponent { {showingStartPage && } {!showingStartPage && ( <> - {supportsGraph && !supportsLogs && } + {supportsGraph && !supportsLogs && } {supportsTable && } {supportsLogs && ( ) => void; } -interface SizedGraphProps extends GraphProps { - size: { width: number; height: number }; -} - interface GraphState { /** * Type parameter refers to the `alias` property of a `TimeSeries`. @@ -98,7 +94,7 @@ interface GraphState { showAllTimeSeries: boolean; } -export class Graph extends PureComponent { +export class Graph extends PureComponent { $el: any; dynamicOptions = null; @@ -119,13 +115,13 @@ export class Graph extends PureComponent { this.$el.bind('plotselected', this.onPlotSelected); } - componentDidUpdate(prevProps: SizedGraphProps, prevState: GraphState) { + componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { if ( prevProps.data !== this.props.data || prevProps.range !== this.props.range || prevProps.split !== this.props.split || prevProps.height !== this.props.height || - (prevProps.size && prevProps.size.width !== this.props.size.width) || + prevProps.width !== this.props.width || !equal(prevState.hiddenSeries, this.state.hiddenSeries) ) { this.draw(); @@ -147,8 +143,8 @@ export class Graph extends PureComponent { }; getDynamicOptions() { - const { range, size } = this.props; - const ticks = (size.width || 0) / 100; + const { range, width } = this.props; + const ticks = (width || 0) / 100; let { from, to } = range; if (!moment.isMoment(from)) { from = dateMath.parse(from, false); @@ -240,7 +236,7 @@ export class Graph extends PureComponent { } render() { - const { height = '100px', id = 'graph' } = this.props; + const { height = 100, id = 'graph' } = this.props; const { hiddenSeries } = this.state; const data = this.getGraphData(); @@ -264,20 +260,4 @@ export class Graph extends PureComponent { } } -export default (props: GraphProps) => ( -
{/* div needed for AutoSizer to calculate, https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#observation */} - - {({width, height}) => ( -
- {width > 0 && } -
- )} -
-
-); +export default Graph; diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index 7263fd09288..3950d89c11f 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -20,6 +20,7 @@ interface GraphContainerProps { split: boolean; toggleGraph: typeof toggleGraph; changeTime: typeof changeTime; + width: number; } export class GraphContainer extends PureComponent { @@ -32,8 +33,8 @@ export class GraphContainer extends PureComponent { }; render() { - const { exploreId, graphResult, loading, showingGraph, showingTable, range, split } = this.props; - const graphHeight = showingGraph && showingTable ? '200px' : '400px'; + const { exploreId, graphResult, loading, showingGraph, showingTable, range, split, width } = this.props; + const graphHeight = showingGraph && showingTable ? 200 : 400; if (!graphResult) { return null; @@ -48,6 +49,7 @@ export class GraphContainer extends PureComponent { onChangeTime={this.onChangeTime} range={range} split={split} + width={width} /> ); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 490257cb9a9..b6c903bc504 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -214,7 +214,7 @@ export default class Logs extends PureComponent {
Date: Tue, 5 Feb 2019 14:09:25 +0100 Subject: [PATCH 111/770] fix: Update snapshot --- .../app/features/explore/__snapshots__/Graph.test.tsx.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap index a7ec6deb22c..c38fb26a252 100644 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Render should render component 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> @@ -480,7 +480,7 @@ exports[`Render should render component with disclaimer 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> @@ -962,7 +962,7 @@ exports[`Render should show query return no time series 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> From 6d874dd1f160f8e3124dec7ed892cf0f3e54c82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 14:42:29 +0100 Subject: [PATCH 112/770] Improved error handling --- public/app/core/copy/appNotification.ts | 26 ++++++++++++++----- .../features/dashboard/state/initDashboard.ts | 7 ++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/public/app/core/copy/appNotification.ts b/public/app/core/copy/appNotification.ts index c34480d7aad..0062cd08fa6 100644 --- a/public/app/core/copy/appNotification.ts +++ b/public/app/core/copy/appNotification.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types'; const defaultSuccessNotification: AppNotification = { @@ -31,12 +32,25 @@ export const createSuccessNotification = (title: string, text?: string): AppNoti id: Date.now(), }); -export const createErrorNotification = (title: string, text?: string): AppNotification => ({ - ...defaultErrorNotification, - title: title, - text: text, - id: Date.now(), -}); +export const createErrorNotification = (title: string, text?: any): AppNotification => { + // Handling if text is an error object + if (text && !_.isString(text)) { + if (text.message) { + text = text.message; + } else if (text.data && text.data.message) { + text = text.data.message; + } else { + text = text.toString(); + } + } + + return { + ...defaultErrorNotification, + title: title, + text: text, + id: Date.now(), + }; +}; export const createWarningNotification = (title: string, text?: string): AppNotification => ({ ...defaultWarningNotification, diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index d529ca0b531..b8eed6c4e64 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -81,7 +81,6 @@ export function initDashboard({ try { switch (routeInfo) { - // handle old urls with no uid case DashboardRouteInfo.Home: { // load home dash dashDTO = await getBackendSrv().get('/api/dashboards/home'); @@ -130,6 +129,7 @@ export function initDashboard({ } } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + dispatch(notifyApp(createErrorNotification('Dashboard fetch failed', err))); console.log(err); return; } @@ -143,6 +143,7 @@ export function initDashboard({ dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + dispatch(notifyApp(createErrorNotification('Dashboard model initializing failure', err))); console.log(err); return; } @@ -168,7 +169,7 @@ export function initDashboard({ try { await variableSrv.init(dashboard); } catch (err) { - dispatch(notifyApp(createErrorNotification('Templating init failed'))); + dispatch(notifyApp(createErrorNotification('Templating init failed', err))); console.log(err); } @@ -194,7 +195,7 @@ export function initDashboard({ keybindingSrv.setupDashboardBindings($scope, dashboard, onRemovePanel); } catch (err) { - dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); + dispatch(notifyApp(createErrorNotification('Dashboard init failed', err))); console.log(err); } From a624c9713aac9e1132099223e07e7b06ab0223ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 15:09:56 +0100 Subject: [PATCH 113/770] Removed unused controllers and services --- public/app/core/components/gf_page.ts | 40 ----- .../app/core/components/scroll/page_scroll.ts | 43 ----- public/app/core/core.ts | 4 - .../dashboard/containers/DashboardCtrl.ts | 150 ----------------- public/app/features/dashboard/index.ts | 2 - .../services/DashboardViewStateSrv.ts | 155 ------------------ public/app/partials/dashboard.html | 17 -- public/views/index-template.html | 2 +- 8 files changed, 1 insertion(+), 412 deletions(-) delete mode 100644 public/app/core/components/gf_page.ts delete mode 100644 public/app/core/components/scroll/page_scroll.ts delete mode 100644 public/app/features/dashboard/containers/DashboardCtrl.ts delete mode 100644 public/app/features/dashboard/services/DashboardViewStateSrv.ts delete mode 100644 public/app/partials/dashboard.html diff --git a/public/app/core/components/gf_page.ts b/public/app/core/components/gf_page.ts deleted file mode 100644 index 057a307f205..00000000000 --- a/public/app/core/components/gf_page.ts +++ /dev/null @@ -1,40 +0,0 @@ -import coreModule from 'app/core/core_module'; - -const template = ` -
- -
- - -
-
-
-
-`; - -export function gfPageDirective() { - return { - restrict: 'E', - template: template, - scope: { - model: '=', - }, - transclude: { - header: '?gfPageHeader', - body: 'gfPageBody', - }, - link: (scope, elem, attrs) => { - console.log(scope); - }, - }; -} - -coreModule.directive('gfPage', gfPageDirective); diff --git a/public/app/core/components/scroll/page_scroll.ts b/public/app/core/components/scroll/page_scroll.ts deleted file mode 100644 index 2d6e27f8b22..00000000000 --- a/public/app/core/components/scroll/page_scroll.ts +++ /dev/null @@ -1,43 +0,0 @@ -import coreModule from 'app/core/core_module'; -import appEvents from 'app/core/app_events'; - -export function pageScrollbar() { - return { - restrict: 'A', - link: (scope, elem, attrs) => { - let lastPos = 0; - - appEvents.on( - 'dash-scroll', - evt => { - if (evt.restore) { - elem[0].scrollTop = lastPos; - return; - } - - lastPos = elem[0].scrollTop; - - if (evt.animate) { - elem.animate({ scrollTop: evt.pos }, 500); - } else { - elem[0].scrollTop = evt.pos; - } - }, - scope - ); - - scope.$on('$routeChangeSuccess', () => { - lastPos = 0; - elem[0].scrollTop = 0; - // Focus page to enable scrolling by keyboard - elem[0].focus({ preventScroll: true }); - }); - - elem[0].tabIndex = -1; - // Focus page to enable scrolling by keyboard - elem[0].focus({ preventScroll: true }); - }, - }; -} - -coreModule.directive('pageScrollbar', pageScrollbar); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index fb38cefd435..1f289fc4b27 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -43,8 +43,6 @@ import { helpModal } from './components/help/help'; import { JsonExplorer } from './components/json_explorer/json_explorer'; import { NavModelSrv, NavModel } from './nav_model_srv'; import { geminiScrollbar } from './components/scroll/scroll'; -import { pageScrollbar } from './components/scroll/page_scroll'; -import { gfPageDirective } from './components/gf_page'; import { orgSwitcher } from './components/org_switcher'; import { profiler } from './profiler'; import { registerAngularDirectives } from './angular_wrappers'; @@ -79,8 +77,6 @@ export { NavModelSrv, NavModel, geminiScrollbar, - pageScrollbar, - gfPageDirective, orgSwitcher, manageDashboardsDirective, TimeSeries, diff --git a/public/app/features/dashboard/containers/DashboardCtrl.ts b/public/app/features/dashboard/containers/DashboardCtrl.ts deleted file mode 100644 index 0151f8f7331..00000000000 --- a/public/app/features/dashboard/containers/DashboardCtrl.ts +++ /dev/null @@ -1,150 +0,0 @@ -// Utils -import config from 'app/core/config'; -import appEvents from 'app/core/app_events'; -import coreModule from 'app/core/core_module'; -import { removePanel } from 'app/features/dashboard/utils/panel'; - -// Services -import { AnnotationsSrv } from '../../annotations/annotations_srv'; - -// Types -import { DashboardModel } from '../state/DashboardModel'; - -export class DashboardCtrl { - dashboard: DashboardModel; - dashboardViewState: any; - loadedFallbackDashboard: boolean; - editTab: number; - - /** @ngInject */ - constructor( - private $scope, - private keybindingSrv, - private timeSrv, - private variableSrv, - private dashboardSrv, - private unsavedChangesSrv, - private dashboardViewStateSrv, - private annotationsSrv: AnnotationsSrv, - public playlistSrv - ) { - // temp hack due to way dashboards are loaded - // can't use controllerAs on route yet - $scope.ctrl = this; - } - - setupDashboard(data) { - try { - this.setupDashboardInternal(data); - } catch (err) { - this.onInitFailed(err, 'Dashboard init failed', true); - } - } - - setupDashboardInternal(data) { - const dashboard = this.dashboardSrv.create(data.dashboard, data.meta); - this.dashboardSrv.setCurrent(dashboard); - - // init services - this.timeSrv.init(dashboard); - this.annotationsSrv.init(dashboard); - - // template values service needs to initialize completely before - // the rest of the dashboard can load - this.variableSrv - .init(dashboard) - // template values failes are non fatal - .catch(this.onInitFailed.bind(this, 'Templating init failed', false)) - // continue - .finally(() => { - this.dashboard = dashboard; - this.dashboard.processRepeats(); - this.dashboard.updateSubmenuVisibility(); - this.dashboard.autoFitPanels(window.innerHeight); - - this.unsavedChangesSrv.init(dashboard, this.$scope); - - // TODO refactor ViewStateSrv - this.$scope.dashboard = dashboard; - this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope); - - this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); - this.setWindowTitleAndTheme(); - - appEvents.emit('dashboard-initialized', dashboard); - }) - .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); - } - - onInitFailed(msg, fatal, err) { - console.log(msg, err); - - if (err.data && err.data.message) { - err.message = err.data.message; - } else if (!err.message) { - err = { message: err.toString() }; - } - - this.$scope.appEvent('alert-error', [msg, err.message]); - - // protect against recursive fallbacks - if (fatal && !this.loadedFallbackDashboard) { - this.loadedFallbackDashboard = true; - this.setupDashboard({ dashboard: { title: 'Dashboard Init failed' } }); - } - } - - templateVariableUpdated() { - this.dashboard.processRepeats(); - } - - setWindowTitleAndTheme() { - window.document.title = config.windowTitlePrefix + this.dashboard.title; - } - - showJsonEditor(evt, options) { - const model = { - object: options.object, - updateHandler: options.updateHandler, - }; - - this.$scope.appEvent('show-dash-editor', { - src: 'public/app/partials/edit_json.html', - model: model, - }); - } - - getDashboard() { - return this.dashboard; - } - - getPanelContainer() { - return this; - } - - onRemovingPanel(evt, options) { - options = options || {}; - if (!options.panelId) { - return; - } - - const panelInfo = this.dashboard.getPanelInfoById(options.panelId); - removePanel(this.dashboard, panelInfo.panel, true); - } - - onDestroy() { - if (this.dashboard) { - this.dashboard.destroy(); - } - } - - init(dashboard) { - this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this)); - this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this)); - this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this)); - this.$scope.$on('$destroy', this.onDestroy.bind(this)); - this.setupDashboard(dashboard); - } -} - -coreModule.controller('DashboardCtrl', DashboardCtrl); diff --git a/public/app/features/dashboard/index.ts b/public/app/features/dashboard/index.ts index 9f2935660ef..d9a03b0aad6 100644 --- a/public/app/features/dashboard/index.ts +++ b/public/app/features/dashboard/index.ts @@ -1,8 +1,6 @@ -import './containers/DashboardCtrl'; import './dashgrid/DashboardGridDirective'; // Services -import './services/DashboardViewStateSrv'; import './services/UnsavedChangesSrv'; import './services/DashboardLoaderSrv'; import './services/DashboardSrv'; diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts deleted file mode 100644 index 7cb4c1de7ab..00000000000 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.ts +++ /dev/null @@ -1,155 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; -import config from 'app/core/config'; -import appEvents from 'app/core/app_events'; -import { DashboardModel } from '../state/DashboardModel'; - -// represents the transient view state -// like fullscreen panel & edit -export class DashboardViewStateSrv { - state: any; - panelScopes: any; - $scope: any; - dashboard: DashboardModel; - fullscreenPanel: any; - oldTimeRange: any; - - /** @ngInject */ - constructor($scope, private $location, private $timeout) { - const self = this; - self.state = {}; - self.panelScopes = []; - self.$scope = $scope; - self.dashboard = $scope.dashboard; - - $scope.onAppEvent('$routeUpdate', () => { - // const urlState = self.getQueryStringState(); - // if (self.needsSync(urlState)) { - // self.update(urlState, true); - // } - }); - - $scope.onAppEvent('panel-change-view', (evt, payload) => { - // self.update(payload); - }); - - // this marks changes to location during this digest cycle as not to add history item - // don't want url changes like adding orgId to add browser history - // $location.replace(); - // this.update(this.getQueryStringState()); - } - - needsSync(urlState) { - return _.isEqual(this.state, urlState) === false; - } - - getQueryStringState() { - const state = this.$location.search(); - state.panelId = parseInt(state.panelId, 10) || null; - state.fullscreen = state.fullscreen ? true : null; - state.edit = state.edit === 'true' || state.edit === true || null; - state.editview = state.editview || null; - state.orgId = config.bootData.user.orgId; - return state; - } - - serializeToUrl() { - const urlState = _.clone(this.state); - urlState.fullscreen = this.state.fullscreen ? true : null; - urlState.edit = this.state.edit ? true : null; - return urlState; - } - - update(state, fromRouteUpdated?) { - // implement toggle logic - if (state.toggle) { - delete state.toggle; - if (this.state.fullscreen && state.fullscreen) { - if (this.state.edit === state.edit) { - state.fullscreen = !state.fullscreen; - } - } - } - - _.extend(this.state, state); - - if (!this.state.fullscreen) { - this.state.fullscreen = null; - this.state.edit = null; - // clear panel id unless in solo mode - if (!this.dashboard.meta.soloMode) { - this.state.panelId = null; - } - } - - if ((this.state.fullscreen || this.dashboard.meta.soloMode) && this.state.panelId) { - // Trying to render panel in fullscreen when it's in the collapsed row causes an issue. - // So in this case expand collapsed row first. - this.toggleCollapsedPanelRow(this.state.panelId); - } - - // if no edit state cleanup tab parm - if (!this.state.edit) { - delete this.state.tab; - } - - // do not update url params if we are here - // from routeUpdated event - if (fromRouteUpdated !== true) { - this.$location.search(this.serializeToUrl()); - } - } - - toggleCollapsedPanelRow(panelId) { - for (const panel of this.dashboard.panels) { - if (panel.collapsed) { - for (const rowPanel of panel.panels) { - if (rowPanel.id === panelId) { - this.dashboard.toggleRow(panel); - return; - } - } - } - } - } - - leaveFullscreen() { - const panel = this.fullscreenPanel; - - this.dashboard.setViewMode(panel, false, false); - - delete this.fullscreenPanel; - - this.$timeout(() => { - appEvents.emit('dash-scroll', { restore: true }); - - if (this.oldTimeRange !== this.dashboard.time) { - this.dashboard.startRefresh(); - } else { - this.dashboard.render(); - } - }); - } - - enterFullscreen(panel) { - const isEditing = this.state.edit && this.dashboard.meta.canEdit; - - this.oldTimeRange = this.dashboard.time; - this.fullscreenPanel = panel; - - // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode() - this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 }); - this.dashboard.setViewMode(panel, true, isEditing); - } -} - -/** @ngInject */ -export function dashboardViewStateSrv($location, $timeout) { - return { - create: $scope => { - return new DashboardViewStateSrv($scope, $location, $timeout); - }, - }; -} - -angular.module('grafana.services').factory('dashboardViewStateSrv', dashboardViewStateSrv); diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html deleted file mode 100644 index 32acdc435f2..00000000000 --- a/public/app/partials/dashboard.html +++ /dev/null @@ -1,17 +0,0 @@ -
- - -
- - - -
- - - - -
-
-
diff --git a/public/views/index-template.html b/public/views/index-template.html index 770ab74eccc..895b0e4ae19 100644 --- a/public/views/index-template.html +++ b/public/views/index-template.html @@ -192,7 +192,7 @@
-
+
From 9fc87e417447a280ea8dd02428124b213069fa4b Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 5 Feb 2019 15:12:04 +0100 Subject: [PATCH 114/770] first working draft --- .../grafana-ui/src/components/Gauge/Gauge.tsx | 20 ++++---- public/app/plugins/panel/gauge/GaugePanel.tsx | 49 +++++++++++++++---- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 04d89bf3f57..9842903b394 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -184,17 +184,15 @@ export class Gauge extends PureComponent { const { height, width } = this.props; return ( -
-
(this.canvasElement = element)} - /> -
+
(this.canvasElement = element)} + /> ); } } diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index b6f37dde94f..928f9a43909 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -16,6 +16,7 @@ interface Props extends PanelProps {} export class GaugePanel extends PureComponent { render() { + console.log('renduru'); const { panelData, width, height, onInterpolate, options } = this.props; const prefix = onInterpolate(options.prefix); @@ -28,7 +29,33 @@ export class GaugePanel extends PureComponent { nullValueMode: NullValueMode.Null, }); - if (vmSeries[0]) { + const gauges = []; + if (vmSeries.length > 1) { + for (let i = 0; i < vmSeries.length; i++) { + gauges.push( + + {theme => ( +
+ +
Gauge {i}
+
+ )} +
+ ); + } + return [gauges]; + } else if (vmSeries.length > 0) { value = vmSeries[0].stats[options.stat]; } else { value = null; @@ -40,15 +67,17 @@ export class GaugePanel extends PureComponent { return ( {theme => ( - +
+ +
)}
); From 49a597fcd0018dfd833da5796bf95800cbdd42c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 15:15:15 +0100 Subject: [PATCH 115/770] Moved remove panel logic to dashboard srv --- public/app/core/services/keybindingSrv.ts | 4 ++-- .../app/features/dashboard/services/DashboardSrv.ts | 13 ++++++++++--- .../app/features/dashboard/state/initDashboard.ts | 10 +--------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index dfacc483b8e..917d1801c0e 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -144,7 +144,7 @@ export class KeybindingSrv { this.$location.search(search); } - setupDashboardBindings(scope, dashboard, onRemovePanel) { + setupDashboardBindings(scope, dashboard) { this.bind('mod+o', () => { dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3; appEvents.emit('graph-hover-clear'); @@ -212,7 +212,7 @@ export class KeybindingSrv { // delete panel this.bind('p r', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { - onRemovePanel(dashboard.meta.focusPanelId); + appEvents.emit('remove-panel', dashboard.meta.focusPanelId); dashboard.meta.focusPanelId = 0; } }); diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 38fadfecdc1..88eb58ad345 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -2,28 +2,35 @@ import coreModule from 'app/core/core_module'; import { appEvents } from 'app/core/app_events'; import locationUtil from 'app/core/utils/location_util'; import { DashboardModel } from '../state/DashboardModel'; +import { removePanel } from '../utils/panel'; export class DashboardSrv { - dash: any; + dash: DashboardModel; /** @ngInject */ constructor(private backendSrv, private $rootScope, private $location) { appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); appEvents.on('panel-change-view', this.onPanelChangeView); + appEvents.on('remove-panel', this.onRemovePanel); } create(dashboard, meta) { return new DashboardModel(dashboard, meta); } - setCurrent(dashboard) { + setCurrent(dashboard: DashboardModel) { this.dash = dashboard; } - getCurrent() { + getCurrent(): DashboardModel { return this.dash; } + onRemovePanel = (panelId: number) => { + const dashboard = this.getCurrent(); + removePanel(dashboard, dashboard.getPanelById(panelId), true); + }; + onPanelChangeView = (options) => { const urlParams = this.$location.search(); diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index b8eed6c4e64..c774c4926f4 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -13,7 +13,6 @@ import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; import locationUtil from 'app/core/utils/location_util'; import { setDashboardLoadingState, ThunkResult, setDashboardModel, setDashboardLoadingSlow } from './actions'; -import { removePanel } from '../utils/panel'; // Types import { DashboardLoadingState, DashboardRouteInfo } from 'app/types'; @@ -185,15 +184,8 @@ export function initDashboard({ // init unsaved changes tracking unsavedChangesSrv.init(dashboard, $scope); + keybindingSrv.setupDashboardBindings($scope, dashboard); - // dashboard keybindings should not live in core, this needs a bigger refactoring - // So declaring this here so it can depend on the removePanel util function - // Long term onRemovePanel should be handled via react prop callback - const onRemovePanel = (panelId: number) => { - removePanel(dashboard, dashboard.getPanelById(panelId), true); - }; - - keybindingSrv.setupDashboardBindings($scope, dashboard, onRemovePanel); } catch (err) { dispatch(notifyApp(createErrorNotification('Dashboard init failed', err))); console.log(err); From e2ffaef88a0a67a9b155c0ce115bb3654d6af9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 15:25:19 +0100 Subject: [PATCH 116/770] Fixed so that we close angular TimePicker when user clicks outside the dropdown --- public/app/routes/GrafanaCtrl.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 70bdf49e5e4..a860ba87e6b 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -280,6 +280,24 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop if (popover.length > 0 && target.parents('.graph-legend').length === 0) { popover.hide(); } + + // hide time picker + const timePickerDropDownIsOpen = elem.find('.gf-timepicker-dropdown').length > 0; + const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0; + const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0; + const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0; + const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0; + if ( + timePickerDropDownIsOpen && + !targetIsInTimePickerNav && + !targetIsInTimePickerDropDown && + !targetIsDatePickerRowBtn && + !targetIsDatePickerHeaderBtn + ) { + scope.$apply(() => { + scope.appEvent('closeTimepicker'); + }); + } }); }, }; From 0302c7afa7446b28baf02dec1b2c43e5e7f8d3d6 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 5 Feb 2019 15:28:03 +0100 Subject: [PATCH 117/770] stackdriver: add some more typings --- .../stackdriver/components/QueryEditor.tsx | 8 +++--- .../datasource/stackdriver/datasource.ts | 7 ++--- .../datasource/stackdriver/query_ctrl.ts | 4 +-- .../plugins/datasource/stackdriver/types.ts | 26 +++++++++++-------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx b/public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx index 94521041416..c3bd9212b21 100644 --- a/public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx @@ -10,21 +10,21 @@ import { Alignments } from './Alignments'; import { AlignmentPeriods } from './AlignmentPeriods'; import { AliasBy } from './AliasBy'; import { Help } from './Help'; -import { Target, MetricDescriptor } from '../types'; +import { StackdriverQuery, MetricDescriptor } from '../types'; import { getAlignmentPickerData } from '../functions'; import StackdriverDatasource from '../datasource'; import { SelectOptionItem } from '@grafana/ui'; export interface Props { - onQueryChange: (target: Target) => void; + onQueryChange: (target: StackdriverQuery) => void; onExecuteQuery: () => void; - target: Target; + target: StackdriverQuery; events: any; datasource: StackdriverDatasource; templateSrv: TemplateSrv; } -interface State extends Target { +interface State extends StackdriverQuery { alignOptions: SelectOptionItem[]; lastQuery: string; lastQueryError: string; diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 025955105a7..15c6350c8a0 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -2,9 +2,10 @@ import { stackdriverUnitMappings } from './constants'; import appEvents from 'app/core/app_events'; import _ from 'lodash'; import StackdriverMetricFindQuery from './StackdriverMetricFindQuery'; -import { MetricDescriptor } from './types'; +import { StackdriverQuery, MetricDescriptor } from './types'; +import { DataSourceApi, DataQueryOptions } from '@grafana/ui/src/types'; -export default class StackdriverDatasource { +export default class StackdriverDatasource implements DataSourceApi { id: number; url: string; baseUrl: string; @@ -103,7 +104,7 @@ export default class StackdriverDatasource { return unit; } - async query(options) { + async query(options: DataQueryOptions) { const result = []; const data = await this.getTimeSeries(options); if (data.results) { diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index c6a8a4d9782..3a2d0bb970a 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { QueryCtrl } from 'app/plugins/sdk'; -import { Target } from './types'; +import { StackdriverQuery } from './types'; import { TemplateSrv } from 'app/features/templating/template_srv'; export class StackdriverQueryCtrl extends QueryCtrl { @@ -16,7 +16,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.onExecuteQuery = this.onExecuteQuery.bind(this); } - onQueryChange(target: Target) { + onQueryChange(target: StackdriverQuery) { Object.assign(this.target, target); } diff --git a/public/app/plugins/datasource/stackdriver/types.ts b/public/app/plugins/datasource/stackdriver/types.ts index 29b12b4289d..83909bbafce 100644 --- a/public/app/plugins/datasource/stackdriver/types.ts +++ b/public/app/plugins/datasource/stackdriver/types.ts @@ -1,3 +1,5 @@ +import { DataQuery } from '@grafana/ui/src/types'; + export enum MetricFindQueryTypes { Services = 'services', MetricTypes = 'metricTypes', @@ -20,20 +22,22 @@ export interface VariableQueryData { services: Array<{ value: string; name: string }>; } -export interface Target { - defaultProject: string; - unit: string; +export interface StackdriverQuery extends DataQuery { + defaultProject?: string; + unit?: string; metricType: string; - service: string; + service?: string; refId: string; crossSeriesReducer: string; - alignmentPeriod: string; - perSeriesAligner: string; - groupBys: string[]; - filters: string[]; - aliasBy: string; - metricKind: string; - valueType: string; + alignmentPeriod?: string; + perSeriesAligner?: string; + groupBys?: string[]; + filters?: string[]; + aliasBy?: string; + metricKind?: string; + valueType?: string; + datasourceId: number; + view: string; } export interface AnnotationTarget { From a344091d82e74a8054fed8279ca271a41278980e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 15:29:19 +0100 Subject: [PATCH 118/770] Optimized so we only do checks when dropdown is opened --- public/app/routes/GrafanaCtrl.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index a860ba87e6b..c6945f26d08 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -283,17 +283,21 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop // hide time picker const timePickerDropDownIsOpen = elem.find('.gf-timepicker-dropdown').length > 0; - const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0; - const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0; - const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0; - const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0; - if ( - timePickerDropDownIsOpen && - !targetIsInTimePickerNav && - !targetIsInTimePickerDropDown && - !targetIsDatePickerRowBtn && - !targetIsDatePickerHeaderBtn - ) { + if (timePickerDropDownIsOpen) { + const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0; + const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0; + const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0; + const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0; + + if ( + targetIsInTimePickerNav || + targetIsInTimePickerDropDown || + targetIsDatePickerRowBtn || + targetIsDatePickerHeaderBtn + ) { + return; + } + scope.$apply(() => { scope.appEvent('closeTimepicker'); }); From e42b670f5c29df535d4cb5477077011ab5ec9842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 15:41:00 +0100 Subject: [PATCH 119/770] Closing timepicker when clicking outside the picker --- public/app/features/explore/ExploreToolbar.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 35f06d11c81..4d9620e311e 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -8,6 +8,7 @@ import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { StoreState } from 'app/types/store'; import { changeDatasource, clearQueries, splitClose, runQueries, splitOpen } from './state/actions'; import TimePicker from './TimePicker'; +import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper'; enum IconSide { left = 'left', @@ -79,6 +80,10 @@ export class UnConnectedExploreToolbar extends PureComponent { this.props.runQuery(this.props.exploreId); }; + onCloseTimePicker = () => { + this.props.timepickerRef.current.setState({ isOpen: false }); + }; + render() { const { datasourceMissing, @@ -137,7 +142,9 @@ export class UnConnectedExploreToolbar extends PureComponent {
) : null}
- + + +
- {this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} - {this.renderOptionLink('visualization', 'Choose Panel type', () => + {this.renderOptionLink('queries', 'Add Query', this.onCreateNewPanel)} + {this.renderOptionLink('visualization', 'Choose Visualization', () => this.onCreateNewPanel('visualization') )}
-
- Convert to row -
+ {copiedPanelPlugins.length === 1 && ( -
this.onPasteCopiedPanel(copiedPanelPlugins[0])} > Paste copied panel -
+ )}
diff --git a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss index ab6ff8556d8..288b2e7a410 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss +++ b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss @@ -14,6 +14,9 @@ align-items: center; width: 100%; cursor: move; + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; .gicon { font-size: 30px; @@ -26,9 +29,15 @@ } } +.add-panel-widget__title { + font-size: $font-size-md; + font-weight: $font-weight-semi-bold; + margin-right: $spacer*2; +} + .add-panel-widget__link { margin: 0 8px; - width: 150px; + width: 154px; } .add-panel-widget__icon { @@ -54,6 +63,8 @@ .add-panel-widget__create { display: inherit; margin-bottom: 24px; + // this is to have the big button appear centered + margin-top: 55px; } .add-panel-widget__actions { @@ -61,7 +72,6 @@ } .add-panel-widget__action { - cursor: pointer; margin: 0 4px; } diff --git a/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap index 585f45210af..00faf48d8df 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap +++ b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap @@ -13,6 +13,11 @@ exports[`Render should render component 1`] = ` + + New Panel +
- Add query + Add Query
@@ -60,7 +65,7 @@ exports[`Render should render component 1`] = ` />
- Choose Panel type + Choose Visualization
@@ -68,12 +73,12 @@ exports[`Render should render component 1`] = `
-
Convert to row -
+
diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index d7aafb89e55..bfdc13bc8f2 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import { QueriesTab } from './QueriesTab'; -import { VisualizationTab } from './VisualizationTab'; +import VisualizationTab from './VisualizationTab'; import { GeneralTab } from './GeneralTab'; import { AlertTab } from '../../alerting/AlertTab'; @@ -38,7 +38,7 @@ export class PanelEditor extends PureComponent { onChangeTab = (tab: PanelEditorTab) => { store.dispatch( updateLocation({ - query: { tab: tab.id }, + query: { tab: tab.id, openVizPicker: null }, partial: true, }) ); diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index 1ca290d4051..94a403c11bf 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -3,7 +3,9 @@ import React, { PureComponent } from 'react'; // Utils & Services import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; -import { store } from 'app/store/store'; +import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; +import { StoreState } from 'app/types'; +import { updateLocation } from 'app/core/actions'; // Components import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; @@ -22,6 +24,8 @@ interface Props { plugin: PanelPlugin; angularPanel?: AngularComponent; onTypeChanged: (newType: PanelPlugin) => void; + updateLocation: typeof updateLocation; + urlOpenVizPicker: boolean; } interface State { @@ -39,7 +43,7 @@ export class VisualizationTab extends PureComponent { super(props); this.state = { - isVizPickerOpen: store.getState().location.query.isVizPickerOpen === true, + isVizPickerOpen: this.props.urlOpenVizPicker, searchQuery: '', scrollTop: 0, }; @@ -150,6 +154,10 @@ export class VisualizationTab extends PureComponent { }; onCloseVizPicker = () => { + if (this.props.urlOpenVizPicker) { + this.props.updateLocation({ query: { openVizPicker: null }, partial: true }); + } + this.setState({ isVizPickerOpen: false }); }; @@ -237,3 +245,13 @@ export class VisualizationTab extends PureComponent { ); } } + +const mapStateToProps = (state: StoreState) => ({ + urlOpenVizPicker: !!state.location.query.openVizPicker +}); + +const mapDispatchToProps = { + updateLocation +}; + +export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps); diff --git a/public/img/icons_dark_theme/icon_advanced.svg b/public/img/icons_dark_theme/icon_advanced.svg index 5fd18a86dd5..dea3ddff685 100644 --- a/public/img/icons_dark_theme/icon_advanced.svg +++ b/public/img/icons_dark_theme/icon_advanced.svg @@ -4,7 +4,7 @@ diff --git a/public/img/icons_dark_theme/icon_advanced_active.svg b/public/img/icons_dark_theme/icon_advanced_active.svg index 80672a2595b..1227ddc868c 100644 --- a/public/img/icons_dark_theme/icon_advanced_active.svg +++ b/public/img/icons_dark_theme/icon_advanced_active.svg @@ -5,7 +5,7 @@ width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve"> - From 4d2cff41ff15fb801960f8a5a5cd17a7fc8f5136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 19:38:51 +0100 Subject: [PATCH 131/770] Minor code simplification --- public/app/features/dashboard/containers/DashboardPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 1bd5218fd60..a620ac848b4 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -247,7 +247,7 @@ export class DashboardPage extends PureComponent { />
- {dashboard && editview && } + {editview && }
{dashboard.meta.submenuEnabled && } From 3e129dffa0490d4ef33f18786031bd95a2e33845 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 5 Feb 2019 19:40:46 +0100 Subject: [PATCH 132/770] changelog: add notes about closing #8207 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 286b013a30f..48551ae13db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Minor * **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae) +* **Cloudwatch**: Add `resource_arns` template variable query function [#8207](https://github.com/grafana/grafana/issues/8207), thx [@jeroenvollenbrock](https://github.com/jeroenvollenbrock) * **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson) * **Stackdriver**: Template variables in filters using globbing format [#15182](https://github.com/grafana/grafana/issues/15182) From 749320002b02d15c9d9cd287d87d285bba3791dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20J=C3=B8rgensen?= Date: Tue, 5 Feb 2019 20:57:51 +0100 Subject: [PATCH 133/770] Added ServerlessDatabaseCapacity metric to list of AWS RDS metrics. --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index f898a65f911..44e0333335c 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -99,7 +99,7 @@ func init() { "AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"}, "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"}, - "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"}, + "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"}, "AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"}, "AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"}, "AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"}, From 1d1b617cee3cfedeb03586e0db00f5219d187761 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 5 Feb 2019 21:08:55 +0100 Subject: [PATCH 134/770] remove unused code --- pkg/api/common_test.go | 87 ++++-------------------------------------- 1 file changed, 8 insertions(+), 79 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 853a04b5c11..3f3a50aae69 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/auth" "gopkg.in/macaron.v1" . "github.com/smartystreets/goconvey/convey" @@ -95,14 +94,13 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map } type scenarioContext struct { - m *macaron.Macaron - context *m.ReqContext - resp *httptest.ResponseRecorder - handlerFunc handlerFunc - defaultHandler macaron.Handler - req *http.Request - url string - userAuthTokenService *fakeUserAuthTokenService + m *macaron.Macaron + context *m.ReqContext + resp *httptest.ResponseRecorder + handlerFunc handlerFunc + defaultHandler macaron.Handler + req *http.Request + url string } func (sc *scenarioContext) exec() { @@ -124,76 +122,7 @@ func setupScenarioContext(url string) *scenarioContext { Delims: macaron.Delims{Left: "[[", Right: "]]"}, })) - sc.userAuthTokenService = newFakeUserAuthTokenService() - sc.m.Use(middleware.GetContextHandler(sc.userAuthTokenService)) + sc.m.Use(middleware.GetContextHandler(nil)) return sc } - -type fakeUserToken interface { - auth.UserToken - SetToken(token string) -} - -type userTokenImpl struct { - userId int64 - token string -} - -func (ut *userTokenImpl) GetUserId() int64 { - return ut.userId -} - -func (ut *userTokenImpl) GetToken() string { - return ut.token -} - -func (ut *userTokenImpl) SetToken(token string) { - ut.token = token -} - -type fakeUserAuthTokenService struct { - createTokenProvider func(userId int64, clientIP, userAgent string) (auth.UserToken, error) - tryRotateTokenProvider func(token auth.UserToken, clientIP, userAgent string) (bool, error) - lookupTokenProvider func(unhashedToken string) (auth.UserToken, error) - revokeTokenProvider func(token auth.UserToken) error -} - -func newFakeUserAuthTokenService() *fakeUserAuthTokenService { - return &fakeUserAuthTokenService{ - createTokenProvider: func(userId int64, clientIP, userAgent string) (auth.UserToken, error) { - return &userTokenImpl{ - userId: 0, - token: "", - }, nil - }, - tryRotateTokenProvider: func(token auth.UserToken, clientIP, userAgent string) (bool, error) { - return false, nil - }, - lookupTokenProvider: func(unhashedToken string) (auth.UserToken, error) { - return &userTokenImpl{ - userId: 0, - token: "", - }, nil - }, - revokeTokenProvider: func(token auth.UserToken) error { - return nil - }, - } -} - -func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) { - return s.createTokenProvider(userId, clientIP, userAgent) -} - -func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (auth.UserToken, error) { - return s.lookupTokenProvider(unhashedToken) -} - -func (s *fakeUserAuthTokenService) TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) { - return s.tryRotateTokenProvider(token, clientIP, userAgent) -} - -func (s *fakeUserAuthTokenService) RevokeToken(token auth.UserToken) error { - return s.revokeTokenProvider(token) -} From 3c2fd02bc00ac631b1429533e898b848ed22dc47 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 5 Feb 2019 21:09:55 +0100 Subject: [PATCH 135/770] refactor login/auth token configuration settings remove login section and reuse existing sections security and auth --- conf/defaults.ini | 41 +++++++++++---------- conf/sample.ini | 43 +++++++++++----------- pkg/setting/setting.go | 82 ++++++++++++++++++++++++------------------ 3 files changed, 89 insertions(+), 77 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index d021d342fbf..c65cb93d426 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -106,25 +106,6 @@ path = grafana.db # For "sqlite3" only. cache mode setting used for connecting to the database cache_mode = private -#################################### Login ############################### - -[login] - -# Login cookie name -cookie_name = grafana_session - -# Login cookie same site setting. defaults to `lax`. can be set to "lax", "strict" and "none" -cookie_samesite = lax - -# How many days an session can be unused before we inactivate it -login_remember_days = 7 - -# How often should the login token be rotated. default to '10m' -rotate_token_minutes = 10 - -# How long should Grafana keep expired tokens before deleting them -delete_expired_token_after_days = 30 - #################################### Session ############################# [session] # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file" @@ -206,8 +187,11 @@ data_source_proxy_whitelist = # disable protection against brute force login attempts disable_brute_force_login_protection = false -# set cookies as https only. default is false -https_flag_cookies = false +# set to true if you host Grafana behind HTTPS. default is false. +cookie_secure = false + +# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict" and "none" +cookie_samesite = lax #################################### Snapshots ########################### [snapshots] @@ -260,6 +244,21 @@ external_manage_info = viewers_can_edit = false [auth] +# Login cookie name +login_cookie_name = grafana_session + +# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days. +login_maximum_inactive_lifetime_days = 7 + +# The maximum lifetime (days) an autenticated user can be logged in since login time before being required to login. Default is 30 days. +login_maximum_lifetime_days = 30 + +# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. +token_rotation_interval_minutes = 10 + +# How often should expired auth tokens be deleted from the database. The default is 7 days. +expired_tokens_cleanup_interval_days = 7 + # Set to true to disable (hide) the login form, useful if you use OAuth disable_login_form = false diff --git a/conf/sample.ini b/conf/sample.ini index ef677320686..39feb31441e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -102,25 +102,6 @@ log_queries = # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared) ;cache_mode = private -#################################### Login ############################### - -[login] - -# Login cookie name -;cookie_name = grafana_session - -# Login cookie same site setting. defaults to `lax`. can be set to "lax", "strict" and "none" -;cookie_samesite = lax - -# How many days an session can be unused before we inactivate it -;login_remember_days = 7 - -# How often should the login token be rotated. default to '10' -;rotate_token_minutes = 10 - -# How long should Grafana keep expired tokens before deleting them -;delete_expired_token_after_days = 30 - #################################### Session #################################### [session] # Either "memory", "file", "redis", "mysql", "postgres", default is "file" @@ -193,8 +174,11 @@ log_queries = # disable protection against brute force login attempts ;disable_brute_force_login_protection = false -# set cookies as https only. default is false -;https_flag_cookies = false +# set to true if you host Grafana behind HTTPS. default is false. +;cookie_secure = false + +# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict" and "none" +;cookie_samesite = lax #################################### Snapshots ########################### [snapshots] @@ -240,6 +224,21 @@ log_queries = ;viewers_can_edit = false [auth] +# Login cookie name +;login_cookie_name = grafana_session + +# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days, +;login_maximum_inactive_lifetime_days = 7 + +# The maximum lifetime (days) an autenticated user can be logged in since login time before being required to login. Default is 30 days. +;login_maximum_lifetime_days = 30 + +# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. +;token_rotation_interval_minutes = 10 + +# How often should expired auth tokens be deleted from the database. The default is 7 days. +;expired_tokens_cleanup_interval_days = 7 + # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false ;disable_login_form = false @@ -253,7 +252,7 @@ log_queries = # This setting is ignored if multiple OAuth providers are configured. ;oauth_auto_login = false -#################################### Anonymous Auth ########################## +#################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access ;enabled = false diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index c3c78d10fec..9f7d03bb472 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -89,6 +89,8 @@ var ( EmailCodeValidMinutes int DataProxyWhiteList map[string]bool DisableBruteForceLoginProtection bool + CookieSecure bool + CookieSameSite http.SameSite // Snapshots ExternalSnapshotUrl string @@ -118,8 +120,10 @@ var ( ViewersCanEdit bool // Http auth - AdminUser string - AdminPassword string + AdminUser string + AdminPassword string + LoginCookieName string + LoginMaxLifetimeDays int AnonymousEnabled bool AnonymousOrgName string @@ -215,7 +219,11 @@ type Cfg struct { RendererLimit int RendererLimitAlerting int + // Security DisableBruteForceLoginProtection bool + CookieSecure bool + CookieSameSite http.SameSite + TempDataLifetime time.Duration MetricsEndpointEnabled bool MetricsEndpointBasicAuthUsername string @@ -224,13 +232,12 @@ type Cfg struct { DisableSanitizeHtml bool EnterpriseLicensePath string - LoginCookieName string - LoginCookieMaxDays int - LoginCookieRotation int - LoginDeleteExpiredTokensAfterDays int - LoginCookieSameSite http.SameSite - - SecurityHTTPSCookies bool + // Auth + LoginCookieName string + LoginMaxInactiveLifetimeDays int + LoginMaxLifetimeDays int + TokenRotationIntervalMinutes int + ExpiredTokensCleanupIntervalDays int } type CommandLineArgs struct { @@ -554,30 +561,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { ApplicationName = APP_NAME_ENTERPRISE } - //login - login := iniFile.Section("login") - cfg.LoginCookieName = login.Key("cookie_name").MustString("grafana_session") - cfg.LoginCookieMaxDays = login.Key("login_remember_days").MustInt(7) - cfg.LoginDeleteExpiredTokensAfterDays = login.Key("delete_expired_token_after_days").MustInt(30) - - samesiteString := login.Key("cookie_samesite").MustString("lax") - validSameSiteValues := map[string]http.SameSite{ - "lax": http.SameSiteLaxMode, - "strict": http.SameSiteStrictMode, - "none": http.SameSiteDefaultMode, - } - - if samesite, ok := validSameSiteValues[samesiteString]; ok { - cfg.LoginCookieSameSite = samesite - } else { - cfg.LoginCookieSameSite = http.SameSiteLaxMode - } - - cfg.LoginCookieRotation = login.Key("rotate_token_minutes").MustInt(10) - if cfg.LoginCookieRotation < 2 { - cfg.LoginCookieRotation = 2 - } - 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) @@ -621,9 +604,26 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { SecretKey = security.Key("secret_key").String() DisableGravatar = security.Key("disable_gravatar").MustBool(true) cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) - cfg.SecurityHTTPSCookies = security.Key("https_flag_cookies").MustBool(false) DisableBruteForceLoginProtection = cfg.DisableBruteForceLoginProtection + CookieSecure = security.Key("cookie_secure").MustBool(false) + cfg.CookieSecure = CookieSecure + + samesiteString := security.Key("cookie_samesite").MustString("lax") + validSameSiteValues := map[string]http.SameSite{ + "lax": http.SameSiteLaxMode, + "strict": http.SameSiteStrictMode, + "none": http.SameSiteDefaultMode, + } + + if samesite, ok := validSameSiteValues[samesiteString]; ok { + CookieSameSite = samesite + cfg.CookieSameSite = CookieSameSite + } else { + CookieSameSite = http.SameSiteLaxMode + cfg.CookieSameSite = CookieSameSite + } + // read snapshots settings snapshots := iniFile.Section("snapshots") ExternalSnapshotUrl = snapshots.Key("external_snapshot_url").String() @@ -661,6 +661,20 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { // auth auth := iniFile.Section("auth") + + LoginCookieName = auth.Key("login_cookie_name").MustString("grafana_session") + cfg.LoginCookieName = LoginCookieName + cfg.LoginMaxInactiveLifetimeDays = auth.Key("login_maximum_inactive_lifetime_days").MustInt(7) + + LoginMaxLifetimeDays = auth.Key("login_maximum_lifetime_days").MustInt(30) + cfg.LoginMaxLifetimeDays = LoginMaxLifetimeDays + + cfg.TokenRotationIntervalMinutes = auth.Key("token_rotation_interval_minutes").MustInt(10) + if cfg.TokenRotationIntervalMinutes < 2 { + cfg.TokenRotationIntervalMinutes = 2 + } + cfg.ExpiredTokensCleanupIntervalDays = auth.Key("expired_tokens_cleanup_interval_days").MustInt(7) + DisableLoginForm = auth.Key("disable_login_form").MustBool(false) DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false) From 80d0943d9d1c0fb20ceb5236dad7ee672b6dc522 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 5 Feb 2019 21:10:56 +0100 Subject: [PATCH 136/770] document login, short-lived tokens and secure cookie configurations --- docs/sources/auth/overview.md | 32 ++++++++++++++++++++++ docs/sources/installation/configuration.md | 8 ++++++ 2 files changed, 40 insertions(+) diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index 0480ee88adc..e3d4c08ca5d 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -36,6 +36,38 @@ Grafana of course has a built in user authentication system with password authen disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth provider (listed above). There is also options for allowing self sign up. +### Login and short-lived tokens + +> The followung applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration. + +Grafana are using short-lived tokens as a mechanism for verifying authenticated users. +These short-lived tokens are rotated each `token_rotation_interval_minutes` for an active authenticated user. + +An active authenticated user that gets it token rotated will extend the `login_maximum_inactive_lifetime_days` time from "now" that Grafana will remember the user. +This means that a user can close its browser and come back before `now + login_maximum_inactive_lifetime_days` and still being authenticated. + This is true as long as the time since user login is less than `login_maximum_lifetime_days`. + +Example: + +```bash +[auth] + +# Login cookie name +login_cookie_name = grafana_session + +# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days. +login_maximum_inactive_lifetime_days = 7 + +# The maximum lifetime (days) an autenticated user can be logged in since login time before being required to login. Default is 30 days. +login_maximum_lifetime_days = 30 + +# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. +token_rotation_interval_minutes = 10 + +# How often should expired auth tokens be deleted from the database. The default is 7 days. +expired_tokens_cleanup_interval_days = 7 +``` + ### Anonymous authentication You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 46bab83654e..b4b53d7557b 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -287,6 +287,14 @@ Default is `false`. Define a white list of allowed ips/domains to use in data sources. Format: `ip_or_domain:port` separated by spaces +### cookie_secure + +Set to `true` if you host Grafana behind HTTPS. Default is `false`. + +### cookie_samesite + +Sets the `SameSite` cookie attribute and prevents the browser from sending this cookie along with cross-site requests. The main goal is mitigate the risk of cross-origin information leakage. It also provides some protection against cross-site request forgery attacks (CSRF), [read more here](https://www.owasp.org/index.php/SameSite). Valid values are `lax`, `strict` and `none`. Default is `lax`. +
## [users] From 0915f931ae6cff86ad04d3e531fd969b802d0b4d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 5 Feb 2019 21:12:30 +0100 Subject: [PATCH 137/770] change configuration settings in auth package --- pkg/services/auth/authtoken/auth_token.go | 4 ++-- pkg/services/auth/authtoken/auth_token_test.go | 8 ++++---- pkg/services/auth/authtoken/session_cleanup.go | 4 ++-- pkg/services/auth/authtoken/session_cleanup_test.go | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/services/auth/authtoken/auth_token.go b/pkg/services/auth/authtoken/auth_token.go index 4e4bd375501..1fdad4dbea5 100644 --- a/pkg/services/auth/authtoken/auth_token.go +++ b/pkg/services/auth/authtoken/auth_token.go @@ -81,7 +81,7 @@ func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (auth.UserT s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) } - expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix() + expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginMaxInactiveLifetimeDays) * time.Second).Unix() var model userAuthToken exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&model) @@ -148,7 +148,7 @@ func (s *UserAuthTokenServiceImpl) TryRotateToken(token auth.UserToken, clientIP needsRotation := false rotatedAt := time.Unix(model.RotatedAt, 0) if model.AuthTokenSeen { - needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute)) + needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.TokenRotationIntervalMinutes) * time.Minute)) } else { needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime)) } diff --git a/pkg/services/auth/authtoken/auth_token_test.go b/pkg/services/auth/authtoken/auth_token_test.go index 7809e235f5c..51d361a9f4f 100644 --- a/pkg/services/auth/authtoken/auth_token_test.go +++ b/pkg/services/auth/authtoken/auth_token_test.go @@ -341,10 +341,10 @@ func createTestContext(t *testing.T) *testContext { tokenService := &UserAuthTokenServiceImpl{ SQLStore: sqlstore, Cfg: &setting.Cfg{ - LoginCookieName: "grafana_session", - LoginCookieMaxDays: 7, - LoginDeleteExpiredTokensAfterDays: 30, - LoginCookieRotation: 10, + LoginMaxInactiveLifetimeDays: 7, + LoginMaxLifetimeDays: 30, + TokenRotationIntervalMinutes: 10, + ExpiredTokensCleanupIntervalDays: 1, }, log: log.New("test-logger"), } diff --git a/pkg/services/auth/authtoken/session_cleanup.go b/pkg/services/auth/authtoken/session_cleanup.go index cd2b766d6c0..ecee82767e4 100644 --- a/pkg/services/auth/authtoken/session_cleanup.go +++ b/pkg/services/auth/authtoken/session_cleanup.go @@ -7,12 +7,12 @@ import ( func (srv *UserAuthTokenServiceImpl) Run(ctx context.Context) error { ticker := time.NewTicker(time.Hour * 12) - deleteSessionAfter := time.Hour * 24 * time.Duration(srv.Cfg.LoginDeleteExpiredTokensAfterDays) + deleteSessionAfter := time.Hour * 24 * time.Duration(srv.Cfg.ExpiredTokensCleanupIntervalDays) for { select { case <-ticker.C: - srv.ServerLockService.LockAndExecute(ctx, "delete old sessions", time.Hour*12, func() { + srv.ServerLockService.LockAndExecute(ctx, "delete expired auth tokens", time.Hour*12, func() { srv.deleteOldSession(deleteSessionAfter) }) diff --git a/pkg/services/auth/authtoken/session_cleanup_test.go b/pkg/services/auth/authtoken/session_cleanup_test.go index 101a279c374..bca1aa824eb 100644 --- a/pkg/services/auth/authtoken/session_cleanup_test.go +++ b/pkg/services/auth/authtoken/session_cleanup_test.go @@ -14,7 +14,7 @@ func TestUserAuthTokenCleanup(t *testing.T) { ctx := createTestContext(t) insertToken := func(token string, prev string, rotatedAt int64) { - ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""} + ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: rotatedAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""} _, err := ctx.sqlstore.NewSession().Insert(&ut) So(err, ShouldBeNil) } From 871c84d195417e51839db1f1fb33e47e196e18ef Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 5 Feb 2019 21:14:23 +0100 Subject: [PATCH 138/770] changes needed for api/middleware due to configuration settings --- pkg/api/login.go | 5 +++-- pkg/api/login_oauth.go | 3 ++- pkg/middleware/middleware.go | 24 +++++++++++++++--------- pkg/middleware/middleware_test.go | 16 ++++++++++++---- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/pkg/api/login.go b/pkg/api/login.go index d25e83d34e8..def24f983c1 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -137,7 +137,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { hs.log.Error("failed to create auth token", "error", err) } - middleware.WriteSessionCookie(c, userToken.GetToken(), middleware.OneYearInSeconds) + middleware.WriteSessionCookie(c, userToken.GetToken(), hs.Cfg.LoginMaxLifetimeDays) } func (hs *HTTPServer) Logout(c *m.ReqContext) { @@ -185,7 +185,8 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *m.ReqContext, cookieName string Value: hex.EncodeToString(encryptedError), HttpOnly: true, Path: setting.AppSubUrl + "/", - Secure: hs.Cfg.SecurityHTTPSCookies, + Secure: hs.Cfg.CookieSecure, + SameSite: hs.Cfg.CookieSameSite, }) return nil diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 4160d48733e..87a8ecc876f 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -214,7 +214,8 @@ func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value stri Value: value, HttpOnly: true, Path: setting.AppSubUrl + "/", - Secure: hs.Cfg.SecurityHTTPSCookies, + Secure: hs.Cfg.CookieSecure, + SameSite: hs.Cfg.CookieSameSite, }) } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 6cf29340b82..9a3e5e1e01c 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -4,6 +4,7 @@ import ( "net/http" "net/url" "strconv" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" @@ -168,11 +169,8 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool { return true } -const cookieName = "grafana_session" -const OneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often. - func initContextWithToken(authTokenService authtoken.UserAuthTokenService, ctx *m.ReqContext, orgID int64) bool { - rawToken := ctx.GetCookie(cookieName) + rawToken := ctx.GetCookie(setting.LoginCookieName) if rawToken == "" { return false } @@ -200,26 +198,34 @@ func initContextWithToken(authTokenService authtoken.UserAuthTokenService, ctx * } if rotated { - WriteSessionCookie(ctx, token.GetToken(), OneYearInSeconds) + WriteSessionCookie(ctx, token.GetToken(), setting.LoginMaxLifetimeDays) } return true } -func WriteSessionCookie(ctx *m.ReqContext, value string, maxAge int) { +func WriteSessionCookie(ctx *m.ReqContext, value string, maxLifetimeDays int) { if setting.Env == setting.DEV { ctx.Logger.Info("new token", "unhashed token", value) } + var maxAge int + if maxLifetimeDays <= 0 { + maxAge = -1 + } else { + maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour) + time.Hour + maxAge = int(maxAgeHours.Seconds()) + } + ctx.Resp.Header().Del("Set-Cookie") cookie := http.Cookie{ - Name: cookieName, + Name: setting.LoginCookieName, Value: url.QueryEscape(value), HttpOnly: true, Path: setting.AppSubUrl + "/", - Secure: false, // TODO: use setting SecurityHTTPSCookies + Secure: setting.CookieSecure, MaxAge: maxAge, - SameSite: http.SameSiteLaxMode, // TODO: use setting LoginCookieSameSite + SameSite: setting.CookieSameSite, } http.SetCookie(ctx.Resp, &cookie) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 4e10ee39201..fdcc56da3bf 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "path/filepath" "testing" + "time" msession "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/bus" @@ -197,13 +198,17 @@ func TestMiddlewareContext(t *testing.T) { return true, nil } + maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour) + maxAge := (maxAgeHours + time.Hour).Seconds() + expectedCookie := &http.Cookie{ - Name: cookieName, + Name: setting.LoginCookieName, Value: "rotated", Path: setting.AppSubUrl + "/", HttpOnly: true, - MaxAge: OneYearInSeconds, - SameSite: http.SameSiteLaxMode, + MaxAge: int(maxAge), + Secure: setting.CookieSecure, + SameSite: setting.CookieSameSite, } sc.fakeReq("GET", "/").exec() @@ -545,6 +550,9 @@ func middlewareScenario(desc string, fn scenarioFunc) { Convey(desc, func() { defer bus.ClearBusHandlers() + setting.LoginCookieName = "grafana_session" + setting.LoginMaxLifetimeDays = 30 + sc := &scenarioContext{} viewsPath, _ := filepath.Abs("../../public/views") @@ -655,7 +663,7 @@ func (sc *scenarioContext) exec() { if sc.tokenSessionCookie != "" { sc.req.AddCookie(&http.Cookie{ - Name: cookieName, + Name: setting.LoginCookieName, Value: sc.tokenSessionCookie, }) } From 948350659094f140806c829a52b23eac172ec400 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 5 Feb 2019 21:20:11 +0100 Subject: [PATCH 139/770] auth token clean up job now runs on schedule and deletes all expired tokens delete tokens having created_at <= LoginMaxLifetimeDays or rotated_at <= LoginMaxInactiveLifetimeDays --- .../auth/authtoken/session_cleanup.go | 34 ++++++++--- .../auth/authtoken/session_cleanup_test.go | 56 +++++++++++++++---- 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/pkg/services/auth/authtoken/session_cleanup.go b/pkg/services/auth/authtoken/session_cleanup.go index ecee82767e4..2b8dfb7b4e2 100644 --- a/pkg/services/auth/authtoken/session_cleanup.go +++ b/pkg/services/auth/authtoken/session_cleanup.go @@ -6,14 +6,23 @@ import ( ) func (srv *UserAuthTokenServiceImpl) Run(ctx context.Context) error { - ticker := time.NewTicker(time.Hour * 12) - deleteSessionAfter := time.Hour * 24 * time.Duration(srv.Cfg.ExpiredTokensCleanupIntervalDays) + if srv.Cfg.ExpiredTokensCleanupIntervalDays <= 0 { + srv.log.Debug("cleanup of expired auth tokens are disabled") + return nil + } + + jobInterval := time.Duration(srv.Cfg.ExpiredTokensCleanupIntervalDays) * 24 * time.Hour + srv.log.Debug("cleanup of expired auth tokens are enabled", "intervalDays", srv.Cfg.ExpiredTokensCleanupIntervalDays) + + ticker := time.NewTicker(jobInterval) + maxInactiveLifetime := time.Duration(srv.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour + maxLifetime := time.Duration(srv.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour for { select { case <-ticker.C: - srv.ServerLockService.LockAndExecute(ctx, "delete expired auth tokens", time.Hour*12, func() { - srv.deleteOldSession(deleteSessionAfter) + srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() { + srv.deleteExpiredTokens(maxInactiveLifetime, maxLifetime) }) case <-ctx.Done(): @@ -22,17 +31,24 @@ func (srv *UserAuthTokenServiceImpl) Run(ctx context.Context) error { } } -func (srv *UserAuthTokenServiceImpl) deleteOldSession(deleteSessionAfter time.Duration) (int64, error) { - sql := `DELETE from user_auth_token WHERE rotated_at < ?` +func (srv *UserAuthTokenServiceImpl) deleteExpiredTokens(maxInactiveLifetime, maxLifetime time.Duration) (int64, error) { + createdBefore := getTime().Add(-maxLifetime) + rotatedBefore := getTime().Add(-maxInactiveLifetime) - deleteBefore := getTime().Add(-deleteSessionAfter) - res, err := srv.SQLStore.NewSession().Exec(sql, deleteBefore.Unix()) + srv.log.Debug("starting cleanup of expired auth tokens", "createdBefore", createdBefore, "rotatedBefore", rotatedBefore) + + sql := `DELETE from user_auth_token WHERE created_at <= ? OR rotated_at <= ?` + res, err := srv.SQLStore.NewSession().Exec(sql, createdBefore.Unix(), rotatedBefore.Unix()) if err != nil { return 0, err } affected, err := res.RowsAffected() - srv.log.Info("deleted old sessions", "count", affected) + if err != nil { + srv.log.Error("failed to cleanup expired auth tokens", "error", err) + return 0, nil + } + srv.log.Info("cleanup of expired auth tokens done", "count", affected) return affected, err } diff --git a/pkg/services/auth/authtoken/session_cleanup_test.go b/pkg/services/auth/authtoken/session_cleanup_test.go index bca1aa824eb..7b611b3263c 100644 --- a/pkg/services/auth/authtoken/session_cleanup_test.go +++ b/pkg/services/auth/authtoken/session_cleanup_test.go @@ -12,25 +12,57 @@ func TestUserAuthTokenCleanup(t *testing.T) { Convey("Test user auth token cleanup", t, func() { ctx := createTestContext(t) + ctx.tokenService.Cfg.LoginMaxInactiveLifetimeDays = 7 + ctx.tokenService.Cfg.LoginMaxLifetimeDays = 30 - insertToken := func(token string, prev string, rotatedAt int64) { - ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: rotatedAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""} + insertToken := func(token string, prev string, createdAt, rotatedAt int64) { + ut := userAuthToken{AuthToken: token, PrevAuthToken: prev, CreatedAt: createdAt, RotatedAt: rotatedAt, UserAgent: "", ClientIp: ""} _, err := ctx.sqlstore.NewSession().Insert(&ut) So(err, ShouldBeNil) } - // insert three old tokens that should be deleted - for i := 0; i < 3; i++ { - insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), int64(i)) + t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC) + getTime = func() time.Time { + return t } - // insert three active tokens that should not be deleted - for i := 0; i < 3; i++ { - insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), getTime().Unix()) - } + Convey("should delete tokens where token rotation age is older than or equal 7 days", func() { + from := t.Add(-7 * 24 * time.Hour) - affected, err := ctx.tokenService.deleteOldSession(time.Hour) - So(err, ShouldBeNil) - So(affected, ShouldEqual, 3) + // insert three old tokens that should be deleted + for i := 0; i < 3; i++ { + insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), from.Unix(), from.Unix()) + } + + // insert three active tokens that should not be deleted + for i := 0; i < 3; i++ { + from = from.Add(time.Second) + insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), from.Unix()) + } + + affected, err := ctx.tokenService.deleteExpiredTokens(7*24*time.Hour, 30*24*time.Hour) + So(err, ShouldBeNil) + So(affected, ShouldEqual, 3) + }) + + Convey("should delete tokens where token age is older than or equal 30 days", func() { + from := t.Add(-30 * 24 * time.Hour) + fromRotate := t.Add(-time.Second) + + // insert three old tokens that should be deleted + for i := 0; i < 3; i++ { + insertToken(fmt.Sprintf("oldA%d", i), fmt.Sprintf("oldB%d", i), from.Unix(), fromRotate.Unix()) + } + + // insert three active tokens that should not be deleted + for i := 0; i < 3; i++ { + from = from.Add(time.Second) + insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), fromRotate.Unix()) + } + + affected, err := ctx.tokenService.deleteExpiredTokens(7*24*time.Hour, 30*24*time.Hour) + So(err, ShouldBeNil) + So(affected, ShouldEqual, 3) + }) }) } From 4de9e3598b8bb35b6f700719a6e626e77e36c197 Mon Sep 17 00:00:00 2001 From: SamuelToh Date: Wed, 6 Feb 2019 06:41:39 +1000 Subject: [PATCH 140/770] Address review comments --- docs/sources/http_api/annotations.md | 4 ++-- pkg/api/annotations.go | 2 +- pkg/api/dtos/annotations.go | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md index dee4ede0777..ca589581f96 100644 --- a/docs/sources/http_api/annotations.md +++ b/docs/sources/http_api/annotations.md @@ -189,6 +189,8 @@ Content-Type: application/json Updates one or more properties of an annotation that matches the specified id. +The `PATCH` operation currently supports updating of the `text`, `tags`, `time` and `timeEnd` properties. It does not handle updating of the `isRegion` and `regionId` properties. To make an annotation regional or vice versa, consider using the `PUT` operation. + **Example Request**: ```json @@ -198,8 +200,6 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Content-Type: application/json { - "time":1507037197000, - "timeEnd":1507180807095, "text":"New Annotation Description", "tags":["tag6","tag7","tag8"] } diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index da9b55a1c16..de9d2517caa 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -222,7 +222,7 @@ func PatchAnnotation(c *m.ReqContext, cmd dtos.PatchAnnotationsCmd) Response { items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId}) if err != nil || len(items) == 0 { - return Error(500, "Could not find annotation to update", err) + return Error(404, "Could not find annotation to update", err) } existing := annotations.Item{ diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index b64329e56d1..bdee8599fea 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -23,11 +23,11 @@ type UpdateAnnotationsCmd struct { } type PatchAnnotationsCmd struct { - Id int64 `json:"id"` - Time int64 `json:"time"` - Text string `json:"text"` - Tags []string `json:"tags"` - TimeEnd int64 `json:"timeEnd"` + Id int64 `json:"id"` + Time int64 `json:"time"` + Text string `json:"text"` + Tags []string `json:"tags"` + TimeEnd int64 `json:"timeEnd"` } type DeleteAnnotationsCmd struct { From 0be43948e2cab8685714e9ee93d085fc069e0501 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 5 Feb 2019 22:01:00 +0100 Subject: [PATCH 141/770] changelog: add notes about closing #15265 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48551ae13db..4cf2262f7d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ ### Minor * **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae) +* **Stackdriver**: Template variables in filters using globbing format [#15182](https://github.com/grafana/grafana/issues/15182) * **Cloudwatch**: Add `resource_arns` template variable query function [#8207](https://github.com/grafana/grafana/issues/8207), thx [@jeroenvollenbrock](https://github.com/jeroenvollenbrock) * **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson) -* **Stackdriver**: Template variables in filters using globbing format [#15182](https://github.com/grafana/grafana/issues/15182) +* **Cloudwatch**: Add AWS RDS ServerlessDatabaseCapacity metric [#15265](https://github.com/grafana/grafana/pull/15265), thx [@larsjoergensen](https://github.com/larsjoergensen) # 6.0.0-beta1 (2019-01-30) From d8658a765c568d62cfeb3e5bac6d2a55969c9e65 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Feb 2019 08:30:14 +0100 Subject: [PATCH 142/770] enhanced expiration logic for lookup token tokens are not expired if created_at > now - LoginMaxLifetimeDays and rotated_at > now - LoginMaxInactiveLifetimeDays --- pkg/services/auth/authtoken/auth_token.go | 7 +- .../auth/authtoken/auth_token_test.go | 70 +++++++++++++++++-- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/pkg/services/auth/authtoken/auth_token.go b/pkg/services/auth/authtoken/auth_token.go index 1fdad4dbea5..47aa925fd4d 100644 --- a/pkg/services/auth/authtoken/auth_token.go +++ b/pkg/services/auth/authtoken/auth_token.go @@ -81,10 +81,13 @@ func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (auth.UserT s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) } - expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginMaxInactiveLifetimeDays) * time.Second).Unix() + tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour + tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour + createdAfter := getTime().Add(-tokenMaxLifetime).Unix() + rotatedAfter := getTime().Add(-tokenMaxInactiveLifetime).Unix() var model userAuthToken - exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&model) + exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, createdAfter, rotatedAfter).Get(&model) if err != nil { return nil, err } diff --git a/pkg/services/auth/authtoken/auth_token_test.go b/pkg/services/auth/authtoken/auth_token_test.go index 51d361a9f4f..7ecb67b2ebf 100644 --- a/pkg/services/auth/authtoken/auth_token_test.go +++ b/pkg/services/auth/authtoken/auth_token_test.go @@ -105,12 +105,56 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldBeNil) So(stillGood, ShouldNotBeNil) - getTime = func() time.Time { - return t.Add(24 * 7 * time.Hour) - } - notGood, err := userAuthTokenService.LookupToken(model.UnhashedToken) - So(err, ShouldEqual, ErrAuthTokenNotFound) - So(notGood, ShouldBeNil) + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + + Convey("when rotated_at is 6:59:59 ago should find token", func() { + getTime = func() time.Time { + return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour).Add(-time.Second) + } + + stillGood, err = userAuthTokenService.LookupToken(stillGood.GetToken()) + So(err, ShouldBeNil) + So(stillGood, ShouldNotBeNil) + }) + + Convey("when rotated_at is 7:00:00 ago should not find token", func() { + getTime = func() time.Time { + return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour) + } + + notGood, err := userAuthTokenService.LookupToken(userToken.GetToken()) + So(err, ShouldEqual, ErrAuthTokenNotFound) + So(notGood, ShouldBeNil) + }) + + Convey("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func() { + updated, err := ctx.updateRotatedAt(model.Id, time.Unix(model.CreatedAt, 0).Add(24*25*time.Hour).Unix()) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + + getTime = func() time.Time { + return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour).Add(-time.Second) + } + + stillGood, err = userAuthTokenService.LookupToken(stillGood.GetToken()) + So(err, ShouldBeNil) + So(stillGood, ShouldNotBeNil) + }) + + Convey("when rotated_at is 5 days ago and created_at is 30 days ago should not find token", func() { + updated, err := ctx.updateRotatedAt(model.Id, time.Unix(model.CreatedAt, 0).Add(24*25*time.Hour).Unix()) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + + getTime = func() time.Time { + return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour) + } + + notGood, err := userAuthTokenService.LookupToken(userToken.GetToken()) + So(err, ShouldEqual, ErrAuthTokenNotFound) + So(notGood, ShouldBeNil) + }) }) Convey("can properly rotate tokens", func() { @@ -384,3 +428,17 @@ func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) { } return rowsAffected == 1, nil } + +func (c *testContext) updateRotatedAt(id, rotatedAt int64) (bool, error) { + sess := c.sqlstore.NewSession() + res, err := sess.Exec("UPDATE user_auth_token SET rotated_at = ? WHERE id = ?", rotatedAt, id) + if err != nil { + return false, err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return false, err + } + return rowsAffected == 1, nil +} From 44275d9660feabcb42ca41db2b6866b16314c340 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Feb 2019 08:45:01 +0100 Subject: [PATCH 143/770] middleware fix --- pkg/middleware/middleware.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 9a3e5e1e01c..817372292b9 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -178,6 +178,7 @@ func initContextWithToken(authTokenService authtoken.UserAuthTokenService, ctx * token, err := authTokenService.LookupToken(rawToken) if err != nil { ctx.Logger.Error("failed to look up user based on cookie", "error", err) + WriteSessionCookie(ctx, "", -1) return false } From 1fbdd02464ff5b1c917c7ca4b13f66d4153065c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 6 Feb 2019 09:04:38 +0100 Subject: [PATCH 144/770] wip: tests --- public/app/core/redux/index.ts | 2 +- public/app/features/dashboard/containers/DashboardPage.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/core/redux/index.ts b/public/app/core/redux/index.ts index bf45d7d22df..1ed23a8d744 100644 --- a/public/app/core/redux/index.ts +++ b/public/app/core/redux/index.ts @@ -1,2 +1,2 @@ -export { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from './actionCreatorFactory'; +export { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf, ActionCreator } from './actionCreatorFactory'; export { reducerFactory } from './reducerFactory'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 1bd5218fd60..dfc0c1d2758 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -25,7 +25,7 @@ import { notifyApp } from 'app/core/actions'; import { StoreState, DashboardLoadingState, DashboardRouteInfo } from 'app/types'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -interface Props { +export interface Props { urlUid?: string; urlSlug?: string; urlType?: string; @@ -46,7 +46,7 @@ interface Props { updateLocation: typeof updateLocation; } -interface State { +export interface State { isSettingsOpening: boolean; isEditing: boolean; isFullscreen: boolean; From 85ef2ca738c8e976d0a387728429934637841012 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Feb 2019 09:43:45 +0100 Subject: [PATCH 145/770] fix spelling --- conf/defaults.ini | 2 +- conf/sample.ini | 2 +- docs/sources/auth/overview.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index c65cb93d426..41b948e53af 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -250,7 +250,7 @@ login_cookie_name = grafana_session # The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days. login_maximum_inactive_lifetime_days = 7 -# The maximum lifetime (days) an autenticated user can be logged in since login time before being required to login. Default is 30 days. +# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days. login_maximum_lifetime_days = 30 # How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. diff --git a/conf/sample.ini b/conf/sample.ini index 39feb31441e..831fa31253e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -230,7 +230,7 @@ log_queries = # The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days, ;login_maximum_inactive_lifetime_days = 7 -# The maximum lifetime (days) an autenticated user can be logged in since login time before being required to login. Default is 30 days. +# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days. ;login_maximum_lifetime_days = 30 # How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index e3d4c08ca5d..fba8da00a5e 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -58,7 +58,7 @@ login_cookie_name = grafana_session # The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days. login_maximum_inactive_lifetime_days = 7 -# The maximum lifetime (days) an autenticated user can be logged in since login time before being required to login. Default is 30 days. +# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days. login_maximum_lifetime_days = 30 # How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes. From 9c64e3b4b98b1ad3b7c89d814554e58c1370033d Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 6 Feb 2019 09:45:03 +0100 Subject: [PATCH 146/770] Revert "chore: Remove react-sizeme" This reverts commit 260b6f5de83133f668aa15a3874eeeb20d46cbe7. --- package.json | 1 + yarn.lock | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/package.json b/package.json index 18c0a56f0c4..77fd92baf57 100644 --- a/package.json +++ b/package.json @@ -177,6 +177,7 @@ "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", "react-redux": "^5.0.7", + "react-sizeme": "^2.3.6", "react-table": "^6.8.6", "react-transition-group": "^2.2.1", "react-virtualized": "^9.21.0", diff --git a/yarn.lock b/yarn.lock index fd0c446fbce..169abd40ee4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3972,6 +3972,11 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +batch-processor@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" + integrity sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg= + batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -6608,6 +6613,13 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= +element-resize-detector@^1.1.12: + version "1.2.0" + resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.0.tgz#63344fd6f4e5ecff6f018d027e17b281fd4fa338" + integrity sha512-UmhNB8sIJVZeg56gEjgmMd6p37sCg8j8trVW0LZM7Wzv+kxQ5CnRHcgRKBTB/kFUSn3e7UP59kl2V2U8Du1hmg== + dependencies: + batch-processor "1.0.0" + elliptic@^6.0.0: version "6.4.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" @@ -10888,6 +10900,11 @@ lodash.tail@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= + 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" @@ -14198,6 +14215,17 @@ react-resizable@1.x: prop-types "15.x" react-draggable "^2.2.6 || ^3.0.3" +react-sizeme@^2.3.6: + version "2.5.2" + resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.5.2.tgz#e7041390cfb895ed15d896aa91d76e147e3b70b5" + integrity sha512-hYvcncV1FxVzPm2EhVwlOLf7Tk+k/ttO6rI7bfKUL/aL1gYzrY3DXJsdZ6nFaFgGSU/i8KC6gCoptOhBbRJpXQ== + dependencies: + element-resize-detector "^1.1.12" + invariant "^2.2.2" + lodash.debounce "^4.0.8" + lodash.throttle "^4.1.1" + shallowequal "^1.0.2" + react-split-pane@^0.1.84: version "0.1.85" resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.85.tgz#64819946a99b617ffa2d20f6f45a0056b6ee4faa" From c47c2528aa9d5075a7c3ddc0a3a22220140652c3 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 6 Feb 2019 09:45:09 +0100 Subject: [PATCH 147/770] Revert "chore: Replace sizeMe with AutoSizer in DashboardGrid" This reverts commit ae0b027d69ce0fe2946aabfe55267150151a4038. --- .../dashboard/dashgrid/DashboardGrid.tsx | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 5a65fadd74b..658bfad3816 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -5,12 +5,13 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core import { DashboardPanel } from './DashboardPanel'; import { DashboardModel, PanelModel } from '../state'; import classNames from 'classnames'; -import { AutoSizer } from 'react-virtualized'; +import sizeMe from 'react-sizeme'; let lastGridWidth = 1200; let ignoreNextWidthChange = false; -interface SizedReactLayoutGridProps { +interface GridWrapperProps { + size: { width: number; }; layout: ReactGridLayout.Layout[]; onLayoutChange: (layout: ReactGridLayout.Layout[]) => void; children: JSX.Element | JSX.Element[]; @@ -24,12 +25,8 @@ interface SizedReactLayoutGridProps { isFullscreen?: boolean; } -interface GridWrapperProps extends SizedReactLayoutGridProps { - sizedWidth: number; -} - function GridWrapper({ - sizedWidth, + size, layout, onLayoutChange, children, @@ -41,8 +38,8 @@ function GridWrapper({ isResizable, isDraggable, isFullscreen, -}: GridWrapperProps) { - const width = sizedWidth > 0 ? sizedWidth : lastGridWidth; +}: GridWrapperProps) { + const width = size.width > 0 ? size.width : lastGridWidth; // logic to ignore width changes (optimization) if (width !== lastGridWidth) { @@ -77,16 +74,7 @@ function GridWrapper({ ); } -const SizedReactLayoutGrid = (props: SizedReactLayoutGridProps) => ( - - {({width}) => ( - - )} - -); +const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper); export interface DashboardGridProps { dashboard: DashboardModel; From 4caea91164bed003b589476e77ceeae64949b568 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 6 Feb 2019 13:00:26 +0300 Subject: [PATCH 148/770] azuremonitor: fix autocomplete menu height --- .../editor/query_field.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index e62337c4982..adab7fc5414 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -105,7 +105,7 @@ class QueryField extends React.Component { this.setState({ value }, () => { if (changed) { // call typeahead only if query changed - window.requestAnimationFrame(this.onTypeahead); + requestAnimationFrame(() => this.onTypeahead()); this.onChangeQuery(); } }); @@ -283,12 +283,18 @@ class QueryField extends React.Component { const rect = node.parentElement.getBoundingClientRect(); const scrollX = window.scrollX; const scrollY = window.scrollY; + const screenHeight = window.innerHeight; + + const menuLeft = rect.left + scrollX - 2; + const menuTop = rect.top + scrollY + rect.height + 4; + const menuHeight = screenHeight - menuTop - 10; // Write DOM requestAnimationFrame(() => { menu.style.opacity = 1; - menu.style.top = `${rect.top + scrollY + rect.height + 4}px`; - menu.style.left = `${rect.left + scrollX - 2}px`; + menu.style.top = `${menuTop}px`; + menu.style.left = `${menuLeft}px`; + menu.style.maxHeight = `${menuHeight}px`; }); } }; From 865d1567fc0da20bd5388ce425d42857bd9d5e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 6 Feb 2019 11:30:42 +0100 Subject: [PATCH 149/770] Added DashboardPage tests that tests view mode transition logic --- .../containers/DashboardPage.test.tsx | 126 ++++++++++ .../dashboard/containers/DashboardPage.tsx | 2 +- .../__snapshots__/DashboardPage.test.tsx.snap | 220 ++++++++++++++++++ 3 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 public/app/features/dashboard/containers/DashboardPage.test.tsx create mode 100644 public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx new file mode 100644 index 00000000000..59e71c69757 --- /dev/null +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { DashboardPage, Props, State } from './DashboardPage'; +import { DashboardModel } from '../state'; +import { setDashboardModel } from '../state/actions'; +import { DashboardRouteInfo, DashboardLoadingState } from 'app/types'; + +jest.mock('sass/_variables.scss', () => ({ + panelhorizontalpadding: 10, + panelVerticalPadding: 10, +})); + +jest.mock('app/features/dashboard/components/DashboardSettings/SettingsCtrl', () => ({ +})); + +function setup(propOverrides?: Partial): ShallowWrapper { + const props: Props = { + urlUid: '11', + urlSlug: 'my-dash', + $scope: {}, + $injector: {}, + routeInfo: DashboardRouteInfo.Normal, + urlEdit: false, + urlFullscreen: false, + loadingState: DashboardLoadingState.Done, + isLoadingSlow: false, + initDashboard: jest.fn(), + updateLocation: jest.fn(), + notifyApp: jest.fn(), + dashboard: null, + setDashboardModel: setDashboardModel, + }; + + Object.assign(props, propOverrides); + return shallow(); +} + +describe('DashboardPage', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + wrapper = setup(); + }); + + describe('Given dashboard has not loaded yet', () => { + it('should render nothing', () => { + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('Given dashboard model', () => { + let dashboard: DashboardModel; + + beforeEach(() => { + dashboard = new DashboardModel({ + title: 'My dashboard', + panels: [ + { + id: 1, + type: 'graph', + title: 'My graph', + gridPos: { x: 0, y: 0, w: 1, h: 1 } + } + ] + }, { + canEdit: true, + canSave: true, + }); + wrapper.setProps({ dashboard, loadingState: DashboardLoadingState.Done }); + }); + + it('Should update title', () => { + expect(document.title).toBe('My dashboard - Grafana'); + }); + + it('After render dashboard', () => { + expect(wrapper).toMatchSnapshot(); + }); + + describe('Given user has scrolled down and goes into fullscreen edit', () => { + beforeEach(() => { + wrapper.setState({ scrollTop: 100 }); + wrapper.setProps({ + urlFullscreen: true, + urlEdit: true, + urlPanelId: '1', + }); + }); + + it('Should update model state to fullscreen & edit', () => { + expect(dashboard.meta.fullscreen).toBe(true); + expect(dashboard.meta.isEditing).toBe(true); + }); + + it('Should update component state to fullscreen and edit', () => { + const state = wrapper.state(); + expect(state.isEditing).toBe(true); + expect(state.isFullscreen).toBe(true); + expect(state.rememberScrollTop).toBe(100); + }); + + describe('Given user goes back to dashboard', () => { + beforeEach(() => { + wrapper.setState({ scrollTop: 0 }); + wrapper.setProps({ + urlFullscreen: false, + urlEdit: false, + urlPanelId: null, + }); + }); + + it('Should update model state normal state', () => { + expect(dashboard.meta.fullscreen).toBe(false); + expect(dashboard.meta.isEditing).toBe(false); + }); + + it('Should update component state to normal and restore scrollTop', () => { + const state = wrapper.state(); + expect(state.isEditing).toBe(false); + expect(state.isFullscreen).toBe(false); + expect(state.scrollTop).toBe(100); + }); + }); + }); + }); +}); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index dfc0c1d2758..f71838d2aa0 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -39,7 +39,7 @@ export interface Props { urlFullscreen: boolean; loadingState: DashboardLoadingState; isLoadingSlow: boolean; - dashboard: DashboardModel; + dashboard: DashboardModel | null; initDashboard: typeof initDashboard; setDashboardModel: typeof setDashboardModel; notifyApp: typeof notifyApp; diff --git a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap new file mode 100644 index 00000000000..d3808513e7b --- /dev/null +++ b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap @@ -0,0 +1,220 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DashboardPage Given dashboard has not loaded yet should render nothing 1`] = `""`; + +exports[`DashboardPage Given dashboard model After render dashboard 1`] = ` +
+ +
+ +
+ +
+
+
+
+`; From e4446f0340eef3edf3e6d54f50364fdc821732cc Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 6 Feb 2019 13:52:35 +0300 Subject: [PATCH 150/770] azuremonitor: improve autocomplete UX --- .../editor/KustoQueryField.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 0a484794e8f..2a578176674 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -65,7 +65,7 @@ export default class KustoQueryField extends QueryField { this.fetchSchema(); } - onTypeahead = (force = false) => { + onTypeahead = (force?: boolean) => { const selection = window.getSelection(); if (selection.anchorNode) { const wrapperNode = selection.anchorNode.parentElement; @@ -152,14 +152,17 @@ export default class KustoQueryField extends QueryField { } // built-in - } else if (prefix && !wrapperClasses.contains('argument')) { + } else if (prefix && !wrapperClasses.contains('argument') && !force) { + // Use only last typed word as a prefix for searching if (modelPrefix.match(/\s$/i)) { prefix = ''; + return; } + prefix = getLastWord(prefix); typeaheadContext = 'context-builtin'; suggestionGroups = this.getKeywordSuggestions(); } else if (force === true) { - typeaheadContext = 'context-builtin'; + typeaheadContext = 'context-builtin-forced'; if (modelPrefix.match(/\s$/i)) { prefix = ''; } @@ -183,7 +186,7 @@ export default class KustoQueryField extends QueryField { .filter(group => group.items.length > 0); // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); - // console.log('onTypeahead', prefix, typeaheadContext); + // console.log('onTypeahead', prefix, typeaheadContext, force); this.setState({ typeaheadPrefix: prefix, @@ -422,3 +425,12 @@ function normalizeQuery(query: string): string { normalizedQuery = normalizedQuery.replace('\n', ' '); return normalizedQuery; } + +function getLastWord(str: string): string { + const lastWordPattern = /(?:.*\s)?([^\s]+\s*)$/gi; + const match = lastWordPattern.exec(str); + if (match && match.length > 1) { + return match[1]; + } + return ''; +} From 6848fe0edf0480eda09efa300f075ac4f560b3ee Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Feb 2019 14:26:43 +0100 Subject: [PATCH 151/770] docs: update annotaions http api --- docs/sources/http_api/annotations.md | 50 +++++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md index ca589581f96..e1d2876f48a 100644 --- a/docs/sources/http_api/annotations.md +++ b/docs/sources/http_api/annotations.md @@ -97,7 +97,7 @@ Creates an annotation in the Grafana database. The `dashboardId` and `panelId` f **Example Request**: -```json +```http POST /api/annotations HTTP/1.1 Accept: application/json Content-Type: application/json @@ -115,7 +115,7 @@ Content-Type: application/json **Example Response**: -```json +```http HTTP/1.1 200 Content-Type: application/json @@ -135,7 +135,7 @@ format (string with multiple tags being separated by a space). **Example Request**: -```json +```http POST /api/annotations/graphite HTTP/1.1 Accept: application/json Content-Type: application/json @@ -150,7 +150,7 @@ Content-Type: application/json **Example Response**: -```json +```http HTTP/1.1 200 Content-Type: application/json @@ -160,15 +160,15 @@ Content-Type: application/json } ``` -## Replace Annotation +## Update Annotation `PUT /api/annotations/:id` -Replaces the annotation that matches the specified id. +Updates all properties of an annotation that matches the specified id. To only update certain property, consider using the [Patch Annotation](#patch-annotation) operation. **Example Request**: -```json +```http PUT /api/annotations/1141 HTTP/1.1 Accept: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -183,17 +183,28 @@ Content-Type: application/json } ``` -## Update Annotation +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "message":"Annotation updated" +} +``` + +## Patch Annotation `PATCH /api/annotations/:id` Updates one or more properties of an annotation that matches the specified id. -The `PATCH` operation currently supports updating of the `text`, `tags`, `time` and `timeEnd` properties. It does not handle updating of the `isRegion` and `regionId` properties. To make an annotation regional or vice versa, consider using the `PUT` operation. +This operation currently supports updating of the `text`, `tags`, `time` and `timeEnd` properties. It does not handle updating of the `isRegion` and `regionId` properties. To make an annotation regional or vice versa, consider using the [Update Annotation](#update-annotation) operation. **Example Request**: -```json +```http PATCH /api/annotations/1145 HTTP/1.1 Accept: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -205,6 +216,17 @@ Content-Type: application/json } ``` +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "message":"Annotation patched" +} +``` + ## Delete Annotation By Id `DELETE /api/annotations/:id` @@ -226,7 +248,9 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk HTTP/1.1 200 Content-Type: application/json -{"message":"Annotation deleted"} +{ + "message":"Annotation deleted" +} ``` ## Delete Annotation By RegionId @@ -250,5 +274,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk HTTP/1.1 200 Content-Type: application/json -{"message":"Annotation region deleted"} +{ + "message":"Annotation region deleted" +} ``` From a53c3b45fcf75b8b6c375f80f730066a9c3ed802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 6 Feb 2019 14:35:53 +0100 Subject: [PATCH 152/770] Added a basic test for initDashboard thunk --- package.json | 1 + .../dashboard/state/initDashboard.test.ts | 130 ++++++++++++++++++ .../features/dashboard/state/initDashboard.ts | 6 +- public/app/features/profile/state/reducers.ts | 14 ++ public/app/store/configureStore.ts | 2 + public/app/types/user.ts | 4 +- yarn.lock | 7 + 7 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 public/app/features/dashboard/state/initDashboard.test.ts create mode 100644 public/app/features/profile/state/reducers.ts diff --git a/package.json b/package.json index 77fd92baf57..5ac751ced3f 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "prettier": "1.9.2", "react-hot-loader": "^4.3.6", "react-test-renderer": "^16.5.0", + "redux-mock-store": "^1.5.3", "regexp-replace-loader": "^1.0.1", "sass-lint": "^1.10.2", "sass-loader": "^7.0.1", diff --git a/public/app/features/dashboard/state/initDashboard.test.ts b/public/app/features/dashboard/state/initDashboard.test.ts new file mode 100644 index 00000000000..eebeb5010fb --- /dev/null +++ b/public/app/features/dashboard/state/initDashboard.test.ts @@ -0,0 +1,130 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { initDashboard, InitDashboardArgs } from './initDashboard'; +import { DashboardRouteInfo, DashboardLoadingState } from 'app/types'; + +const mockStore = configureMockStore([thunk]); + +interface ScenarioContext { + args: InitDashboardArgs; + timeSrv: any; + annotationsSrv: any; + unsavedChangesSrv: any; + variableSrv: any; + dashboardSrv: any; + keybindingSrv: any; + setup: (fn: () => void) => void; + actions: any[]; + storeState: any; +} + +type ScenarioFn = (ctx: ScenarioContext) => void; + +function describeInitScenario(description: string, scenarioFn: ScenarioFn) { + describe(description, () => { + const timeSrv = { init: jest.fn() }; + const annotationsSrv = { init: jest.fn() }; + const unsavedChangesSrv = { init: jest.fn() }; + const variableSrv = { init: jest.fn() }; + const dashboardSrv = { setCurrent: jest.fn() }; + const keybindingSrv = { setupDashboardBindings: jest.fn() }; + + const injectorMock = { + get: (name: string) => { + switch (name) { + case 'timeSrv': + return timeSrv; + case 'annotationsSrv': + return annotationsSrv; + case 'unsavedChangesSrv': + return unsavedChangesSrv; + case 'dashboardSrv': + return dashboardSrv; + case 'variableSrv': + return variableSrv; + case 'keybindingSrv': + return keybindingSrv; + default: + throw { message: 'Unknown service ' + name }; + } + }, + }; + + let setupFn = () => {}; + + const ctx: ScenarioContext = { + args: { + $injector: injectorMock, + $scope: {}, + fixUrl: false, + routeInfo: DashboardRouteInfo.Normal, + }, + timeSrv, + annotationsSrv, + unsavedChangesSrv, + variableSrv, + dashboardSrv, + keybindingSrv, + actions: [], + storeState: { + location: { + query: {}, + }, + user: {}, + }, + setup: (fn: () => void) => { + setupFn = fn; + }, + }; + + beforeEach(async () => { + setupFn(); + + const store = mockStore(ctx.storeState); + + await store.dispatch(initDashboard(ctx.args)); + + ctx.actions = store.getActions(); + }); + + scenarioFn(ctx); + }); +} + +describeInitScenario('Initializing new dashboard', ctx => { + ctx.setup(() => { + ctx.storeState.user.orgId = 12; + ctx.args.routeInfo = DashboardRouteInfo.New; + }); + + it('Should send action to set loading state to fetching', () => { + expect(ctx.actions[0].type).toBe('SET_DASHBOARD_LOADING_STATE'); + expect(ctx.actions[0].payload).toBe(DashboardLoadingState.Fetching); + }); + + it('Should send action to set loading state to Initializing', () => { + expect(ctx.actions[1].type).toBe('SET_DASHBOARD_LOADING_STATE'); + expect(ctx.actions[1].payload).toBe(DashboardLoadingState.Initializing); + }); + + it('Should update location with orgId query param', () => { + expect(ctx.actions[2].type).toBe('UPDATE_LOCATION'); + expect(ctx.actions[2].payload.query.orgId).toBe(12); + }); + + it('Should send action to set dashboard model', () => { + expect(ctx.actions[3].type).toBe('SET_DASHBOARD_MODEL'); + expect(ctx.actions[3].payload.title).toBe('New dashboard'); + }); + + it('Should Initializing services', () => { + expect(ctx.timeSrv.init).toBeCalled(); + expect(ctx.annotationsSrv.init).toBeCalled(); + expect(ctx.variableSrv.init).toBeCalled(); + expect(ctx.unsavedChangesSrv.init).toBeCalled(); + expect(ctx.keybindingSrv.setupDashboardBindings).toBeCalled(); + expect(ctx.dashboardSrv.setCurrent).toBeCalled(); + }); +}); + + diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index ba218c1583d..2c68435b313 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -9,7 +9,6 @@ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { AnnotationsSrv } from 'app/features/annotations/annotations_srv'; import { VariableSrv } from 'app/features/templating/variable_srv'; import { KeybindingSrv } from 'app/core/services/keybindingSrv'; -import { config } from 'app/core/config'; // Actions import { updateLocation } from 'app/core/actions'; @@ -150,8 +149,9 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult { } // add missing orgId query param - if (!getState().location.query.orgId) { - dispatch(updateLocation({ query: { orgId: config.bootData.user.orgId }, partial: true, replace: true })); + const storeState = getState() ; + if (!storeState.location.query.orgId) { + dispatch(updateLocation({ query: { orgId: storeState.user.orgId }, partial: true, replace: true })); } // init services diff --git a/public/app/features/profile/state/reducers.ts b/public/app/features/profile/state/reducers.ts new file mode 100644 index 00000000000..dc6e841449e --- /dev/null +++ b/public/app/features/profile/state/reducers.ts @@ -0,0 +1,14 @@ +import { UserState } from 'app/types'; +import config from 'app/core/config'; + +export const initialState: UserState = { + orgId: config.bootData.user.orgId, +}; + +export const userReducer = (state = initialState, action: any): UserState => { + return state; +}; + +export default { + user: userReducer, +}; diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 570a387cd74..e2c33523271 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -11,6 +11,7 @@ import exploreReducers from 'app/features/explore/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; +import userReducers from 'app/features/profile/state/reducers'; import organizationReducers from 'app/features/org/state/reducers'; import { setStore } from './store'; @@ -25,6 +26,7 @@ const rootReducers = { ...pluginReducers, ...dataSourcesReducers, ...usersReducers, + ...userReducers, ...organizationReducers, }; diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 365411147bb..7691558ce90 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,5 +1,3 @@ -import { DashboardSearchHit } from './search'; - export interface OrgUser { avatarUrl: string; email: string; @@ -47,5 +45,5 @@ export interface UsersState { } export interface UserState { - starredDashboards: DashboardSearchHit[]; + orgId: number; } diff --git a/yarn.lock b/yarn.lock index 169abd40ee4..df2e1cea37e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14582,6 +14582,13 @@ redux-logger@^3.0.6: dependencies: deep-diff "^0.3.5" +redux-mock-store@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d" + integrity sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA== + dependencies: + lodash.isplainobject "^4.0.6" + redux-thunk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" From 809d4b040aedb388f5836a39c8f75316321bfca0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Feb 2019 14:52:17 +0100 Subject: [PATCH 153/770] changelog: add notes about closing #12546 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf2262f7d2..4bddf5e0f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * **Cloudwatch**: Add `resource_arns` template variable query function [#8207](https://github.com/grafana/grafana/issues/8207), thx [@jeroenvollenbrock](https://github.com/jeroenvollenbrock) * **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson) * **Cloudwatch**: Add AWS RDS ServerlessDatabaseCapacity metric [#15265](https://github.com/grafana/grafana/pull/15265), thx [@larsjoergensen](https://github.com/larsjoergensen) +* **Annotations**: Support PATCH verb in annotations http api [#12546](https://github.com/grafana/grafana/issues/12546), thx [@SamuelToh](https://github.com/SamuelToh) # 6.0.0-beta1 (2019-01-30) From 7eb2558fc5d8d99588f41734c887fab8ac7c1f47 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 6 Feb 2019 15:06:27 +0100 Subject: [PATCH 154/770] Fix issue with graph legend color picker disapearing on color selection --- public/app/core/utils/ConfigProvider.tsx | 3 +-- public/app/plugins/panel/graph/graph.ts | 5 ++++- scripts/webpack/getThemeVariable.js | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/public/app/core/utils/ConfigProvider.tsx b/public/app/core/utils/ConfigProvider.tsx index 56b6fc3d8b9..cb3ad88b191 100644 --- a/public/app/core/utils/ConfigProvider.tsx +++ b/public/app/core/utils/ConfigProvider.tsx @@ -21,8 +21,7 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { return ( {config => { - const currentTheme = getCurrentThemeName(); - return {children}; + return {children}; }} ); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 3800e147d9d..846d11ea475 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -28,6 +28,8 @@ import { GraphCtrl } from './module'; import { GrafanaThemeType, getValueFormat } from '@grafana/ui'; import { provideTheme } from 'app/core/utils/ConfigProvider'; +const LegendWithThemeProvider = provideTheme(Legend); + class GraphElement { ctrl: GraphCtrl; tooltip: any; @@ -44,6 +46,7 @@ class GraphElement { legendElem: HTMLElement; constructor(private scope, private elem, private timeSrv) { + this.ctrl = scope.ctrl; this.dashboard = this.ctrl.dashboard; this.panel = this.ctrl.panel; @@ -110,7 +113,7 @@ class GraphElement { onToggleAxis: this.ctrl.onToggleAxis, }; - const legendReactElem = React.createElement(provideTheme(Legend), legendProps); + const legendReactElem = React.createElement(LegendWithThemeProvider, legendProps); ReactDOM.render(legendReactElem, this.legendElem, () => this.renderPanel()); } diff --git a/scripts/webpack/getThemeVariable.js b/scripts/webpack/getThemeVariable.js index 0db0a9842a8..6726f95d47c 100644 --- a/scripts/webpack/getThemeVariable.js +++ b/scripts/webpack/getThemeVariable.js @@ -29,7 +29,7 @@ function getThemeVariable(variablePath, themeName) { const variable = get(theme, variablePath.getValue()); if (!variable) { - throw new Error(`${variablePath} is not defined fo ${themeName}`); + throw new Error(`${variablePath.getValue()} is not defined for ${themeName.getValue()} theme`); } if (isHex(variable)) { From 71cfcd58ba79e55bb483d37894a6e1e3c4a2f2e7 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 6 Feb 2019 15:38:51 +0100 Subject: [PATCH 155/770] combine mode with avg value --- .../src/components/Switch/Switch.tsx | 2 +- .../panel/gauge/GaugeOptionsEditor.tsx | 39 ++++++++++++++++--- public/app/plugins/panel/gauge/GaugePanel.tsx | 17 ++++++-- .../plugins/panel/gauge/GaugePanelOptions.tsx | 9 ++--- public/app/plugins/panel/gauge/types.ts | 1 + 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/src/components/Switch/Switch.tsx b/packages/grafana-ui/src/components/Switch/Switch.tsx index feee58386b8..8cdd7c481f2 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.tsx @@ -34,7 +34,7 @@ export class Switch extends PureComponent { const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`; return ( -
-
+ ); } diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index f40894426ae..9a9daab76c3 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -35,118 +35,114 @@ exports[`Render should render CTA if there are no API keys 1`] = ` -
- - + +
-
- -
- Add API Key -
-
+ +
+ Add API Key +
+ +
-
- - Key name - - + +
+
+ + Role + + + - - - - - -
-
- -
+ Viewer + + + + +
- -
- -
+
+ +
+
+ +
+
`; diff --git a/public/app/features/org/OrgDetailsPage.tsx b/public/app/features/org/OrgDetailsPage.tsx index ee644f0006f..236558db40a 100644 --- a/public/app/features/org/OrgDetailsPage.tsx +++ b/public/app/features/org/OrgDetailsPage.tsx @@ -36,18 +36,16 @@ export class OrgDetailsPage extends PureComponent { return ( - diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index f8c59d82130..f564991f291 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -73,7 +73,13 @@ export class FolderPermissions extends PureComponent { const { isAdding } = this.state; if (folder.id === 0) { - return ; + return ( + + + + + + ); } const folderInfo = { title: folder.title, url: folder.url, id: folder.id }; @@ -90,7 +96,7 @@ export class FolderPermissions extends PureComponent {
From 5195954681527f277f3ce6f4f52f7b6efe215760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 16:50:12 +0100 Subject: [PATCH 293/770] style tweak to alert --- public/sass/components/_alerts.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_alerts.scss b/public/sass/components/_alerts.scss index dc98cba82bd..1c4f1b7fcb7 100644 --- a/public/sass/components/_alerts.scss +++ b/public/sass/components/_alerts.scss @@ -6,7 +6,7 @@ // ------------------------- .alert { - padding: 1.25rem 2rem 1.25rem 1.5rem; + padding: 15px 20px; margin-bottom: $panel-margin / 2; text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5); background: $alert-error-bg; From 1693f083cc8e35efaa9daf9bc8398c9d2c70b525 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 11 Feb 2019 16:57:49 +0100 Subject: [PATCH 294/770] Move deduplication calculation from Logs component to redux selector --- package.json | 2 + public/app/core/utils/reselect.ts | 5 +++ public/app/features/explore/Logs.tsx | 16 +++---- public/app/features/explore/LogsContainer.tsx | 42 ++++++++++++++++++- .../app/features/explore/state/actionTypes.ts | 13 +++++- public/app/features/explore/state/reducers.ts | 11 +++++ public/app/types/explore.ts | 7 +++- yarn.lock | 12 ++++++ 8 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 public/app/core/utils/reselect.ts diff --git a/package.json b/package.json index fae51a1d856..22cfe33a4d0 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "dependencies": { "@babel/polyfill": "^7.0.0", "@torkelo/react-select": "2.1.1", + "@types/reselect": "^2.2.0", "angular": "1.6.6", "angular-bindonce": "0.3.1", "angular-native-dragdrop": "1.2.2", @@ -187,6 +188,7 @@ "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", "remarkable": "^1.7.1", + "reselect": "^4.0.0", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^6.3.3", "slate": "^0.33.4", diff --git a/public/app/core/utils/reselect.ts b/public/app/core/utils/reselect.ts new file mode 100644 index 00000000000..7c8fc7727b0 --- /dev/null +++ b/public/app/core/utils/reselect.ts @@ -0,0 +1,5 @@ +import { memoize } from 'lodash'; +import { createSelectorCreator } from 'reselect'; + +const hashFn = (...args) => args.reduce((acc, val) => acc + '-' + JSON.stringify(val), ''); +export const createLodashMemoizedSelector = createSelectorCreator(memoize, hashFn); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index f41555b9121..0e3b3f3558e 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -9,8 +9,6 @@ import { LogsDedupDescription, LogsDedupStrategy, LogsModel, - dedupLogRows, - filterLogLevels, LogLevel, LogsMetaKind, } from 'app/core/logs_model'; @@ -51,6 +49,7 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { interface Props { data?: LogsModel; + dedupedData?: LogsModel; width: number; exploreId: string; highlighterExpressions: string[]; @@ -59,16 +58,17 @@ interface Props { scanning?: boolean; scanRange?: RawTimeRange; dedupStrategy: LogsDedupStrategy; + hiddenLogLevels: Set; onChangeTime?: (range: RawTimeRange) => void; onClickLabel?: (label: string, value: string) => void; onStartScanning?: () => void; onStopScanning?: () => void; onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void; + onToggleLogLevel: (hiddenLogLevels: Set) => void; } interface State { deferLogs: boolean; - hiddenLogLevels: Set; renderAll: boolean; showLabels: boolean | null; // Tristate: null means auto showLocalTime: boolean; @@ -81,7 +81,6 @@ export default class Logs extends PureComponent { state = { deferLogs: true, - hiddenLogLevels: new Set(), renderAll: false, showLabels: null, showLocalTime: true, @@ -142,7 +141,7 @@ export default class Logs extends PureComponent { onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set) => { const hiddenLogLevels: Set = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level])); - this.setState({ hiddenLogLevels }); + this.props.onToggleLogLevel(hiddenLogLevels); }; onClickScan = (event: React.SyntheticEvent) => { @@ -166,21 +165,18 @@ export default class Logs extends PureComponent { scanning, scanRange, width, + dedupedData, } = this.props; if (!data) { return null; } - const { deferLogs, hiddenLogLevels, renderAll, showLocalTime, showUtc, } = this.state; + const { deferLogs, renderAll, showLocalTime, showUtc } = this.state; let { showLabels } = this.state; const { dedupStrategy } = this.props; const hasData = data && data.rows && data.rows.length > 0; const showDuplicates = dedupStrategy !== LogsDedupStrategy.none; - - // Filtering - const filteredData = filterLogLevels(data, hiddenLogLevels); - const dedupedData = dedupLogRows(filteredData, dedupStrategy); const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0); const meta = [...data.meta]; diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 190c1c43b5a..9fd06afae9b 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -4,18 +4,21 @@ import { connect } from 'react-redux'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; -import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model'; +import { LogsModel, LogsDedupStrategy, LogLevel, filterLogLevels, dedupLogRows } from 'app/core/logs_model'; import { StoreState } from 'app/types'; import { toggleLogs, changeDedupStrategy } from './state/actions'; import Logs from './Logs'; import Panel from './Panel'; +import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes'; +import { createLodashMemoizedSelector } from 'app/core/utils/reselect'; interface LogsContainerProps { exploreId: ExploreId; loading: boolean; logsHighlighterExpressions?: string[]; logsResult?: LogsModel; + dedupedResult?: LogsModel; onChangeTime: (range: TimeRange) => void; onClickLabel: (key: string, value: string) => void; onStartScanning: () => void; @@ -25,8 +28,10 @@ interface LogsContainerProps { scanRange?: RawTimeRange; showingLogs: boolean; toggleLogs: typeof toggleLogs; + toggleLogLevelAction: typeof toggleLogLevelAction; changeDedupStrategy: typeof changeDedupStrategy; dedupStrategy: LogsDedupStrategy; + hiddenLogLevels: Set; width: number; } @@ -39,12 +44,21 @@ export class LogsContainer extends PureComponent { this.props.changeDedupStrategy(this.props.exploreId, dedupStrategy); }; + hangleToggleLogLevel = (hiddenLogLevels: Set) => { + const { exploreId } = this.props; + this.props.toggleLogLevelAction({ + exploreId, + hiddenLogLevels, + }); + }; + render() { const { exploreId, loading, logsHighlighterExpressions, logsResult, + dedupedResult, onChangeTime, onClickLabel, onStartScanning, @@ -54,6 +68,7 @@ export class LogsContainer extends PureComponent { scanning, scanRange, width, + hiddenLogLevels, } = this.props; return ( @@ -61,6 +76,7 @@ export class LogsContainer extends PureComponent { { onStartScanning={onStartScanning} onStopScanning={onStopScanning} onDedupStrategyChange={this.handleDedupStrategyChange} + onToggleLogLevel={this.hangleToggleLogLevel} range={range} scanning={scanning} scanRange={scanRange} width={width} + hiddenLogLevels={hiddenLogLevels} /> ); @@ -90,12 +108,29 @@ const selectItemUIState = (itemState: ExploreItemState) => { dedupStrategy, }; }; + +const logsSelector = (state: ExploreItemState) => state.logsResult; +const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels; +const dedupStrategySelector = (state: ExploreItemState) => state.dedupStrategy; +const deduplicatedLogsSelector = createLodashMemoizedSelector( + logsSelector, hiddenLogLevelsSelector, dedupStrategySelector, + (logs, hiddenLogLevels, dedupStrategy) => { + if (!logs) { + return null; + } + const filteredData = filterLogLevels(logs, new Set(hiddenLogLevels)); + return dedupLogRows(filteredData, dedupStrategy); + } +); + function mapStateToProps(state: StoreState, { exploreId }) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, range } = item; const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); - const {showingLogs, dedupStrategy} = selectItemUIState(item); + const { showingLogs, dedupStrategy } = selectItemUIState(item); + const hiddenLogLevels = new Set(item.hiddenLogLevels); + const dedupedResult = deduplicatedLogsSelector(item); return { loading, @@ -106,12 +141,15 @@ function mapStateToProps(state: StoreState, { exploreId }) { showingLogs, range, dedupStrategy, + hiddenLogLevels, + dedupedResult, }; } const mapDispatchToProps = { toggleLogs, changeDedupStrategy, + toggleLogLevelAction, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index d54a8754c3d..c54eef97a43 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -18,6 +18,7 @@ import { ExploreUIState, } from 'app/types/explore'; import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; +import { LogLevel } from 'app/core/logs_model'; /** Higher order actions * @@ -201,6 +202,11 @@ export interface UpdateDatasourceInstancePayload { datasourceInstance: DataSourceApi; } +export interface ToggleLogLevelPayload { + exploreId: ExploreId; + hiddenLogLevels: Set; +} + export interface QueriesImportedPayload { exploreId: ExploreId; queries: DataQuery[]; @@ -397,6 +403,10 @@ export const updateDatasourceInstanceAction = actionCreatorFactory( + 'explore/TOGGLE_LOG_LEVEL' +).create(); + /** * Resets state for explore. */ @@ -436,4 +446,5 @@ export type Action = | ActionOf | ActionOf | ActionOf - | ActionOf; + | ActionOf + | ActionOf; diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 255591ee6e3..db3e9a95858 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -38,6 +38,7 @@ import { toggleTableAction, queriesImportedAction, updateUIStateAction, + toggleLogLevelAction, } from './actionTypes'; export const DEFAULT_RANGE = { @@ -467,6 +468,16 @@ export const itemReducer = reducerFactory({} as ExploreItemSta }; }, }) + .addMapper({ + filter: toggleLogLevelAction, + mapper: (state, action): ExploreItemState => { + const { hiddenLogLevels } = action.payload; + return { + ...state, + hiddenLogLevels: Array.from(hiddenLogLevels) + }; + }, + }) .create(); /** diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 066ca226157..7a6af04b2ee 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -11,7 +11,7 @@ import { } from '@grafana/ui'; import { Emitter } from 'app/core/core'; -import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model'; +import { LogsModel, LogsDedupStrategy, LogLevel } from 'app/core/logs_model'; import TableModel from 'app/core/table_model'; export interface CompletionItem { @@ -242,6 +242,11 @@ export interface ExploreItemState { * Current logs deduplication strategy */ dedupStrategy?: LogsDedupStrategy; + + /** + * Currently hidden log series + */ + hiddenLogLevels?: LogLevel[]; } export interface ExploreUIState { diff --git a/yarn.lock b/yarn.lock index df2e1cea37e..3c86bdf810f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1819,6 +1819,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/reselect@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/reselect/-/reselect-2.2.0.tgz#c667206cfdc38190e1d379babe08865b2288575f" + integrity sha1-xmcgbP3DgZDh03m6vgiGWyKIV18= + dependencies: + reselect "*" + "@types/storybook__addon-actions@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@types/storybook__addon-actions/-/storybook__addon-actions-3.4.1.tgz#8f90d76b023b58ee794170f2fe774a3fddda2c1d" @@ -14894,6 +14901,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@*, reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" From 951e5932d4e650e160aeae2bbde850b7985dd237 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 11 Feb 2019 17:00:16 +0100 Subject: [PATCH 295/770] changelog: adds note for #15363 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a82fc7050b4..6589099e178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ * **AzureMonitor**: improve autocomplete for Log Analytics and App Insights editor [#15131](https://github.com/grafana/grafana/issues/15131) * **LDAP**: Fix IPA/FreeIPA v4.6.4 does not allow LDAP searches with empty attributes [#14432](https://github.com/grafana/grafana/issues/14432) +### Breaking changes + +* **Internal Metrics** Edition has been added to the build_info metric. This will break any Graphite queries using this metric. Edition will be a new label for the Prometheus metric. [#15363](https://github.com/grafana/grafana/pull/15363) + ### 6.0.0-beta1 fixes * **Postgres**: Fix default port not added when port not configured [#15189](https://github.com/grafana/grafana/issues/15189) From 0493d905f17f6b1b7ffbb7afbf181a91798d5bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 17:42:31 +0100 Subject: [PATCH 296/770] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6589099e178..7b97da0e81c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ -# 6.0.0-beta2 (unreleased) +# 6.0.0-beta3 (unreleased) + +# 6.0.0-beta2 (2019-02-11) ### New Features * **AzureMonitor**: Enable alerting by converting Azure Monitor API to Go [#14623](https://github.com/grafana/grafana/issues/14623) From e38cfc1a7183bd30e6e3eb022077e96fe6167d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 17:43:02 +0100 Subject: [PATCH 297/770] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f44291a86a..f004ee07732 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "6.0.0-prebeta2", + "version": "6.0.0-pre3", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From fc91e1cf57a22e4a4a5cd1e99f7bee70ad3ec65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 17:47:47 +0100 Subject: [PATCH 298/770] Fixed issue with gauge requests being cancelled --- public/app/features/dashboard/dashgrid/DataPanel.tsx | 3 +-- public/app/features/dashboard/dashgrid/PanelChrome.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 2183548000b..b81d66fa7f5 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -28,7 +28,7 @@ interface RenderProps { export interface Props { datasource: string | null; queries: any[]; - panelId?: number; + panelId: number; dashboardId?: number; isVisible?: boolean; timeRange?: TimeRange; @@ -50,7 +50,6 @@ export interface State { export class DataPanel extends Component { static defaultProps = { isVisible: true, - panelId: 1, dashboardId: 1, }; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b02d9479dcc..b29be4b389d 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -149,6 +149,7 @@ export class PanelChrome extends PureComponent { this.renderPanel(false, panel.snapshotData, width, height) ) : ( Date: Mon, 11 Feb 2019 21:11:19 +0100 Subject: [PATCH 299/770] Fix error caused by named colors that are not part of named colors palette --- packages/grafana-ui/src/utils/namedColorsPalette.test.ts | 8 ++++---- packages/grafana-ui/src/utils/namedColorsPalette.ts | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts index aa57b46636c..e544548aa4a 100644 --- a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts +++ b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts @@ -44,10 +44,6 @@ describe('colors', () => { }); describe('getColorFromHexRgbOrName', () => { - it('returns undefined for unknown color', () => { - expect(() => getColorFromHexRgbOrName('aruba-sunshine')).toThrow(); - }); - it('returns dark hex variant for known color if theme not specified', () => { expect(getColorFromHexRgbOrName(SemiDarkBlue.name)).toBe(SemiDarkBlue.variants.dark); }); @@ -64,5 +60,9 @@ describe('colors', () => { expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)'); expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)'); }); + + it('returns hex for named color that is not a part of named colors palette', () => { + expect(getColorFromHexRgbOrName('lime')).toBe('#00ff00'); + }); }); }); diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.ts b/packages/grafana-ui/src/utils/namedColorsPalette.ts index ee5741e794e..88ae510a6d8 100644 --- a/packages/grafana-ui/src/utils/namedColorsPalette.ts +++ b/packages/grafana-ui/src/utils/namedColorsPalette.ts @@ -1,5 +1,6 @@ import { flatten } from 'lodash'; import { GrafanaThemeType } from '../types'; +import tinycolor from 'tinycolor2'; type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple'; @@ -106,7 +107,7 @@ export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaThemeType const colorDefinition = getColorByName(color); if (!colorDefinition) { - throw new Error('Unknown color'); + return new tinycolor(color).toHexString(); } return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark; From a5d158c014a65fded992bab069368eb61adbe1a8 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 11 Feb 2019 21:22:53 +0100 Subject: [PATCH 300/770] Added one more test case for color resolving helper --- packages/grafana-ui/src/utils/namedColorsPalette.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts index e544548aa4a..19c7d9c84ad 100644 --- a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts +++ b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts @@ -44,6 +44,10 @@ describe('colors', () => { }); describe('getColorFromHexRgbOrName', () => { + it('returns black for unknown color', () => { + expect(getColorFromHexRgbOrName('aruba-sunshine')).toBe("#000000"); + }); + it('returns dark hex variant for known color if theme not specified', () => { expect(getColorFromHexRgbOrName(SemiDarkBlue.name)).toBe(SemiDarkBlue.variants.dark); }); From edd9576f1586178501b1d75726944d1df70fcc1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 12 Feb 2019 08:04:30 +0100 Subject: [PATCH 301/770] Fixed elastic5 docker compose block --- devenv/docker/blocks/elastic5/docker-compose.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/devenv/docker/blocks/elastic5/docker-compose.yaml b/devenv/docker/blocks/elastic5/docker-compose.yaml index 33a7d9855b0..3a2ef39faba 100644 --- a/devenv/docker/blocks/elastic5/docker-compose.yaml +++ b/devenv/docker/blocks/elastic5/docker-compose.yaml @@ -1,6 +1,3 @@ -# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine -version: '2' -services: elasticsearch5: image: elasticsearch:5 command: elasticsearch From 3b9105e1bebb24e518548faf258576e0b30f70a8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 12 Feb 2019 08:45:21 +0100 Subject: [PATCH 302/770] enable testing provsioned datasources closes #12164 --- .../datasources/settings/ButtonRow.test.tsx | 1 + .../datasources/settings/ButtonRow.tsx | 8 +- .../settings/DataSourceSettingsPage.tsx | 89 ++++++++++--------- .../__snapshots__/ButtonRow.test.tsx.snap | 7 ++ .../DataSourceSettingsPage.test.tsx.snap | 4 + 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/public/app/features/datasources/settings/ButtonRow.test.tsx b/public/app/features/datasources/settings/ButtonRow.test.tsx index 0acab8941ff..84b16d829d5 100644 --- a/public/app/features/datasources/settings/ButtonRow.test.tsx +++ b/public/app/features/datasources/settings/ButtonRow.test.tsx @@ -7,6 +7,7 @@ const setup = (propOverrides?: object) => { isReadOnly: true, onSubmit: jest.fn(), onDelete: jest.fn(), + onTest: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/features/datasources/settings/ButtonRow.tsx b/public/app/features/datasources/settings/ButtonRow.tsx index 6b85e21405c..3e8ac060010 100644 --- a/public/app/features/datasources/settings/ButtonRow.tsx +++ b/public/app/features/datasources/settings/ButtonRow.tsx @@ -4,14 +4,20 @@ export interface Props { isReadOnly: boolean; onDelete: () => void; onSubmit: (event) => void; + onTest: (event) => void; } -const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit }) => { +const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit, onTest }) => { return (
+ {isReadOnly && ( + + )} diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index ff840390cf5..fe1121ed73e 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -72,6 +72,12 @@ export class DataSourceSettingsPage extends PureComponent { this.testDataSource(); }; + onTest = async (evt: React.FormEvent) => { + evt.preventDefault(); + + this.testDataSource(); + }; + onDelete = () => { appEvents.emit('confirm-modal', { title: 'Delete', @@ -180,52 +186,55 @@ export class DataSourceSettingsPage extends PureComponent { return ( - {this.hasDataSource &&
-
-
- {this.isReadOnly() && this.renderIsReadOnlyMessage()} - {this.shouldRenderInfoBox() &&
{this.getInfoText()}
} + {this.hasDataSource && ( +
+
+ + {this.isReadOnly() && this.renderIsReadOnlyMessage()} + {this.shouldRenderInfoBox() &&
{this.getInfoText()}
} - setIsDefault(state)} - onNameChange={name => setDataSourceName(name)} - /> - - {dataSourceMeta.module && ( - setIsDefault(state)} + onNameChange={name => setDataSourceName(name)} /> - )} -
- {testingMessage && ( -
-
- {testingStatus === 'error' ? ( - - ) : ( - - )} -
-
-
{testingMessage}
-
-
+ {dataSourceMeta.module && ( + )} -
- this.onSubmit(event)} - isReadOnly={this.isReadOnly()} - onDelete={this.onDelete} - /> - +
+ {testingMessage && ( +
+
+ {testingStatus === 'error' ? ( + + ) : ( + + )} +
+
+
{testingMessage}
+
+
+ )} +
+ + this.onSubmit(event)} + isReadOnly={this.isReadOnly()} + onDelete={this.onDelete} + onTest={event => this.onTest(event)} + /> + +
-
} + )} ); diff --git a/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap index bd190f60b03..d4ec7eeea1e 100644 --- a/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap +++ b/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap @@ -12,6 +12,13 @@ exports[`Render should render component 1`] = ` > Save & Test +
@@ -202,6 +203,7 @@ exports[`Render should render beta info text 1`] = ` isReadOnly={false} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} />
@@ -302,6 +304,7 @@ exports[`Render should render component 1`] = ` isReadOnly={false} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} />
- {!isLoading && ( -
- this.onOrgNameChange(name)} - onSubmit={this.onUpdateOrganization} - orgName={organization.name} - /> - -
- )} + {!isLoading && ( +
+ this.onOrgNameChange(name)} + onSubmit={this.onUpdateOrganization} + orgName={organization.name} + /> +
+ )} ); diff --git a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap index 9e13a73901e..2339975ca8b 100644 --- a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap +++ b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap @@ -15,11 +15,7 @@ exports[`Render should render component 1`] = ` > -
- + /> `; @@ -39,19 +35,15 @@ exports[`Render should render organization and preferences 1`] = ` -
-
- - -
+
+ +
From 93f1a48641b9e9219fa5f8869757fcdb4d1187ae Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 11 Feb 2019 15:21:02 +0100 Subject: [PATCH 284/770] changelog: adds note for #14623 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6221b7bcc93..a82fc7050b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # 6.0.0-beta2 (unreleased) +### New Features +* **AzureMonitor**: Enable alerting by converting Azure Monitor API to Go [#14623](https://github.com/grafana/grafana/issues/14623) + ### Minor * **Alerting**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae) * **Graphite/InfluxDB/OpenTSDB**: Fix always take dashboard timezone into consideration when handle custom time ranges [#15284](https://github.com/grafana/grafana/issues/15284) From c4fa64e6dc082bdb813edb13f34652f4163b9cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 15:23:17 +0100 Subject: [PATCH 285/770] Updated lint-staged --- package.json | 2 +- yarn.lock | 286 ++++++++++++++++++++++----------------------------- 2 files changed, 122 insertions(+), 166 deletions(-) diff --git a/package.json b/package.json index fae51a1d856..2f44291a86a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "husky": "^0.14.3", "jest": "^23.6.0", "jest-date-mock": "^1.0.6", - "lint-staged": "^6.0.0", + "lint-staged": "^8.1.3", "load-grunt-tasks": "3.5.2", "mini-css-extract-plugin": "^0.4.0", "mocha": "^4.0.1", diff --git a/yarn.lock b/yarn.lock index df2e1cea37e..2fb4a5d3ee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1040,6 +1040,20 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.1.3.tgz#b700d97385fa91affed60c71dfd51c67e9dad762" integrity sha512-QsYGKdhhuDFNq7bjm2r44y0mp5xW3uO3csuTPDWZc0OIiMQv+AIY5Cqwd4mJiC5N8estVl7qlvOx1hbtOuUWbw== +"@iamstarkov/listr-update-renderer@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e" + integrity sha512-IJyxQWsYDEkf8C8QthBn5N8tIUR9V9je6j3sMIpAkonaadjbvxmRC6RAhpa3RKxndhNnU2M6iNbtJwd7usQYIA== + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^2.3.0" + strip-ansi "^3.0.1" + "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -2468,7 +2482,7 @@ ansi-colors@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== -ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: +ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= @@ -2525,11 +2539,6 @@ ansistyles@~0.1.3: resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk= -any-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242" - integrity sha1-xnhwBYADV5AJCD9UrAq6+1wz0kI= - any-observable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -2548,11 +2557,6 @@ app-root-dir@^1.0.2: resolved "https://registry.yarnpkg.com/app-root-dir/-/app-root-dir-1.0.2.tgz#38187ec2dea7577fff033ffcb12172692ff6e118" integrity sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg= -app-root-path@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.1.0.tgz#98bf6599327ecea199309866e8140368fd2e646a" - integrity sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo= - append-transform@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" @@ -4588,7 +4592,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, 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, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -4783,7 +4787,7 @@ cli-columns@^3.1.2: string-width "^2.0.0" strip-ansi "^3.0.1" -cli-cursor@^1.0.1, cli-cursor@^1.0.2: +cli-cursor@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= @@ -4797,11 +4801,6 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-spinners@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" - integrity sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw= - cli-table2@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97" @@ -5075,7 +5074,7 @@ comma-separated-tokens@^1.0.0: dependencies: trim "0.0.1" -commander@2, commander@^2.11.0, commander@^2.12.1, commander@^2.13.0, commander@^2.19.0, commander@^2.8.1, commander@^2.9.0: +commander@2, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.19.0, commander@^2.8.1, commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -5312,7 +5311,7 @@ cosmiconfig@^4.0.0: parse-json "^4.0.0" require-from-string "^2.0.1" -cosmiconfig@^5.0.5, cosmiconfig@^5.0.7: +cosmiconfig@^5.0.2, cosmiconfig@^5.0.5, cosmiconfig@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" integrity sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA== @@ -6085,7 +6084,7 @@ debug@^3.1.0, debug@^3.2.5: dependencies: ms "^2.1.1" -debug@^4.1.0: +debug@^4.0.1, debug@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -6915,7 +6914,7 @@ escape-html@^1.0.3, escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.4, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -7129,19 +7128,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" - integrity sha1-2NdrvBtVIX7RkP1t1J08d07PyNo= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -7631,6 +7617,11 @@ flush-write-stream@^1.0.0: inherits "^2.0.1" readable-stream "^2.0.4" +fn-name@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7" + integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc= + follow-redirects@^1.0.0, follow-redirects@^1.2.5: version "1.6.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.6.1.tgz#514973c44b5757368bad8bddfe52f81f015c94cb" @@ -7848,6 +7839,15 @@ fuse.js@^3.0.1, fuse.js@^3.3.0: resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.3.0.tgz#1e4fe172a60687230fb54a5cb247eb96e2e7e885" integrity sha512-ESBRkGLWMuVkapqYCcNO1uqMg5qbCKkgb+VS6wsy17Rix0/cMS9kSOZoYkjH8Ko//pgJ/EEGu0GTjk2mjX2LGQ== +g-status@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/g-status/-/g-status-2.0.2.tgz#270fd32119e8fc9496f066fe5fe88e0a6bc78b97" + integrity sha512-kQoE9qH+T1AHKgSSD0Hkv98bobE90ILQcXAF4wvGgsr7uFqNvwmh8j+Lq3l0RVt3E3HjSbv2B9biEGcEtpHLCA== + dependencies: + arrify "^1.0.1" + matcher "^1.0.0" + simple-git "^1.85.0" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -9497,13 +9497,6 @@ is-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA= -is-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2" - integrity sha1-s2ExHYPG5dcmyr9eJQsCNxBvWuI= - dependencies: - symbol-observable "^0.2.2" - is-observable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" @@ -9917,11 +9910,6 @@ jest-environment-node@^23.4.0: jest-mock "^23.2.0" jest-util "^23.4.0" -jest-get-type@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23" - integrity sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q== - jest-get-type@^22.1.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" @@ -10094,16 +10082,6 @@ jest-util@^23.4.0: slash "^1.0.0" source-map "^0.6.0" -jest-validate@^21.1.0: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7" - integrity sha512-k4HLI1rZQjlU+EC682RlQ6oZvLrE5SCh3brseQc24vbZTxzT/k/3urar5QMCVgjadmSO7lECeGdc6YxnM3yEGg== - dependencies: - chalk "^2.0.1" - jest-get-type "^21.2.0" - leven "^2.1.0" - pretty-format "^21.2.1" - jest-validate@^23.6.0: version "23.6.0" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.6.0.tgz#36761f99d1ed33fcd425b4e4c5595d62b6597474" @@ -10566,51 +10544,42 @@ libnpx@^10.2.0: y18n "^4.0.0" yargs "^11.0.0" -lint-staged@^6.0.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.1.1.tgz#cd08c4d9b8ccc2d37198d1c47ce77d22be6cf324" - integrity sha512-M/7bwLdXbeG7ZNLcasGeLMBDg60/w6obj3KOtINwJyxAxb53XGY0yH5FSZlWklEzuVbTtqtIfAajh6jYIN90AA== +lint-staged@^8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.1.3.tgz#bb069db5466c0fe16710216e633a84f2b362fa60" + integrity sha512-6TGkikL1B+6mIOuSNq2TV6oP21IhPMnV8q0cf9oYZ296ArTVNcbFh1l1pfVOHHbBIYLlziWNsQ2q45/ffmJ4AA== dependencies: - app-root-path "^2.0.0" - chalk "^2.1.0" - commander "^2.11.0" - cosmiconfig "^4.0.0" + "@iamstarkov/listr-update-renderer" "0.4.1" + chalk "^2.3.1" + commander "^2.14.1" + cosmiconfig "^5.0.2" debug "^3.1.0" dedent "^0.7.0" - execa "^0.8.0" + del "^3.0.0" + execa "^1.0.0" find-parent-dir "^0.3.0" + g-status "^2.0.2" is-glob "^4.0.0" - jest-validate "^21.1.0" - listr "^0.13.0" - lodash "^4.17.4" - log-symbols "^2.0.0" - minimatch "^3.0.0" + is-windows "^1.0.2" + listr "^0.14.2" + lodash "^4.17.5" + log-symbols "^2.2.0" + micromatch "^3.1.8" npm-which "^3.0.1" p-map "^1.1.1" path-is-inside "^1.0.2" pify "^3.0.0" - staged-git-files "1.0.0" - stringify-object "^3.2.0" + please-upgrade-node "^3.0.2" + staged-git-files "1.1.2" + string-argv "^0.0.2" + stringify-object "^3.2.2" + yup "^0.26.10" listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= -listr-update-renderer@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7" - integrity sha1-NE2YDaLKLosUW6MFkI8yrj9MyKc= - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - strip-ansi "^3.0.1" - listr-update-renderer@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" @@ -10625,16 +10594,6 @@ listr-update-renderer@^0.5.0: log-update "^2.3.0" strip-ansi "^3.0.1" -listr-verbose-renderer@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35" - integrity sha1-ggb0z21S3cWCfl/RSYng6WWTOjU= - dependencies: - chalk "^1.1.3" - cli-cursor "^1.0.2" - date-fns "^1.27.2" - figures "^1.7.0" - listr-verbose-renderer@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" @@ -10645,30 +10604,7 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d" - integrity sha1-ILsLowuuZg7oTMBQPfS+PVYjiH0= - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - figures "^1.7.0" - indent-string "^2.1.0" - is-observable "^0.2.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.4.0" - listr-verbose-renderer "^0.4.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - ora "^0.2.3" - p-map "^1.1.1" - rxjs "^5.4.2" - stream-to-observable "^0.2.0" - strip-ansi "^3.0.1" - -listr@^0.14.1: +listr@^0.14.1, listr@^0.14.2: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -10949,14 +10885,6 @@ log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-update@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" - integrity sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE= - dependencies: - ansi-escapes "^1.0.0" - cli-cursor "^1.0.2" - log-update@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" @@ -11154,6 +11082,13 @@ marksy@^6.1.0: he "^1.1.1" marked "^0.3.12" +matcher@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/matcher/-/matcher-1.1.1.tgz#51d8301e138f840982b338b116bb0c09af62c1c2" + integrity sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg== + dependencies: + escape-string-regexp "^1.0.4" + material-colors@^1.2.1: version "1.2.6" resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" @@ -12512,16 +12447,6 @@ optionator@^0.8.1: type-check "~0.3.2" wordwrap "~1.0.0" -ora@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" - integrity sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q= - dependencies: - chalk "^1.1.1" - cli-cursor "^1.0.2" - cli-spinners "^0.1.2" - object-assign "^4.0.1" - ordered-ast-traverse@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ordered-ast-traverse/-/ordered-ast-traverse-1.1.1.tgz#6843a170bc0eee8b520cc8ddc1ddd3aa30fa057c" @@ -13023,6 +12948,13 @@ pkg-up@^1.0.0: dependencies: find-up "^1.0.0" +please-upgrade-node@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac" + integrity sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ== + dependencies: + semver-compare "^1.0.0" + pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" @@ -13560,14 +13492,6 @@ pretty-error@^2.0.2, pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36" - integrity sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A== - dependencies: - ansi-regex "^3.0.0" - ansi-styles "^3.2.0" - pretty-format@^23.6.0: version "23.6.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" @@ -13670,6 +13594,11 @@ prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, pr loose-envify "^1.3.1" object-assign "^4.1.1" +property-expr@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.5.1.tgz#22e8706894a0c8e28d58735804f6ba3a3673314f" + integrity sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g== + property-information@^5.0.0, property-information@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.0.1.tgz#c3b09f4f5750b1634c0b24205adbf78f18bdf94f" @@ -15078,7 +15007,7 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= -rxjs@^5.4.2, rxjs@^5.5.2: +rxjs@^5.5.2: version "5.5.12" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== @@ -15247,6 +15176,11 @@ selfsigned@^1.9.1: dependencies: node-forge "0.7.5" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -15482,6 +15416,13 @@ simple-get@^2.7.0: once "^1.3.1" simple-concat "^1.0.0" +simple-git@^1.85.0: + version "1.107.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.107.0.tgz#12cffaf261c14d6f450f7fdb86c21ccee968b383" + integrity sha512-t4OK1JRlp4ayKRfcW6owrWcRVLyHRUlhGd0uN6ZZTqfDq8a5XpcUdOKiGRNobHEuMtNqzp0vcJNvhYWwh5PsQA== + dependencies: + debug "^4.0.1" + simple-is@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0" @@ -15929,10 +15870,10 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== -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" - integrity sha1-zbhHg3wfzFLAioctSIPMCHdmioA= +staged-git-files@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.2.tgz#4326d33886dc9ecfa29a6193bf511ba90a46454b" + integrity sha512-0Eyrk6uXW6tg9PYkhi/V/J4zHp33aNyi2hOCmhFLqLTIhbgqWn5jlSzI+IU0VqrZq6+DbHcabQl/WP6P3BG0QA== static-extend@^0.1.1: version "0.1.2" @@ -16009,13 +15950,6 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= -stream-to-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10" - integrity sha1-WdbqOT2HwsDdrBCqDVYbxrpvDhA= - dependencies: - any-observable "^0.2.0" - strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -16026,6 +15960,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-argv@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.0.2.tgz#dac30408690c21f3c3630a3ff3a05877bdcbd736" + integrity sha1-2sMECGkMIfPDYwo/86BYd73L1zY= + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -16131,7 +16070,7 @@ stringifier@^1.3.0: traverse "^0.6.6" type-name "^2.0.1" -stringify-object@^3.2.0: +stringify-object@^3.2.2: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== @@ -16336,11 +16275,6 @@ symbol-observable@1.0.1: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ= -symbol-observable@^0.2.2: - version "0.2.4" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" - integrity sha1-lag9smGG1q9+ehjb2XYKL4bQj0A= - symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -16358,6 +16292,11 @@ symbol.prototype.description@^1.0.0: dependencies: has-symbols "^1.0.0" +synchronous-promise@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.6.tgz#de76e0ea2b3558c1e673942e47e714a930fa64aa" + integrity sha512-TyOuWLwkmtPL49LHCX1caIwHjRzcVd62+GF6h8W/jHOeZUFHpnd2XJDVuUlaTaLPH1nuu2M69mfHr5XbQJnf/g== + systemjs-plugin-css@^0.1.36: version "0.1.37" resolved "https://registry.yarnpkg.com/systemjs-plugin-css/-/systemjs-plugin-css-0.1.37.tgz#684847252ca69b7da24a1201094c86274324e82f" @@ -16649,6 +16588,11 @@ toposort@^1.0.0: resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + touch@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" @@ -18075,6 +18019,18 @@ yeoman-generator@^2.0.5: through2 "^2.0.0" yeoman-environment "^2.0.5" +yup@^0.26.10: + version "0.26.10" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.26.10.tgz#3545839663289038faf25facfc07e11fd67c0cb1" + integrity sha512-keuNEbNSnsOTOuGCt3UJW69jDE3O4P+UHAakO7vSeFMnjaitcmlbij/a3oNb9g1Y1KvSKH/7O1R2PQ4m4TRylw== + dependencies: + "@babel/runtime" "7.0.0" + fn-name "~2.0.1" + lodash "^4.17.10" + property-expr "^1.5.0" + synchronous-promise "^2.0.5" + toposort "^2.0.2" + zip-stream@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04" From e4e42fcd08c156d556b4896b323580e997edc2f7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 11 Feb 2019 15:42:12 +0100 Subject: [PATCH 286/770] adds edition to build_info metric --- pkg/metrics/metrics.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 718a63ee768..bab2fb45127 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -3,6 +3,8 @@ package metrics import ( "runtime" + "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" ) @@ -282,7 +284,7 @@ func init() { Name: "build_info", Help: "A metric with a constant '1' value labeled by version, revision, branch, and goversion from which Grafana was built.", Namespace: exporterName, - }, []string{"version", "revision", "branch", "goversion"}) + }, []string{"version", "revision", "branch", "goversion", "edition"}) } // SetBuildInformation sets the build information for this binary @@ -291,8 +293,13 @@ func SetBuildInformation(version, revision, branch string) { // Once this have been released for some time we should be able to remote `M_Grafana_Version` // The reason we added a new one is that its common practice in the prometheus community // to name this metric `*_build_info` so its easy to do aggregation on all programs. + edition := "oss" + if setting.IsEnterprise { + edition = "enterprise" + } + M_Grafana_Version.WithLabelValues(version).Set(1) - grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version()).Set(1) + grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version(), edition).Set(1) } func initMetricVars() { From c332e106a24f800d673a04eddaa4e3bc55a4942e Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 11 Feb 2019 16:00:08 +0100 Subject: [PATCH 287/770] Removing default thresholds values. --- public/vendor/flot/jquery.flot.gauge.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js index d256a5db7ed..b6468d5824f 100644 --- a/public/vendor/flot/jquery.flot.gauge.js +++ b/public/vendor/flot/jquery.flot.gauge.js @@ -935,16 +935,7 @@ } }, values: [ - { - value: 50, - color: "lightgreen" - }, { - value: 80, - color: "yellow" - }, { - value: 100, - color: "red" - } + ] } } From 0019e0ffc9fc4d9e7768165b2022d18624528d2f Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 11 Feb 2019 16:20:32 +0100 Subject: [PATCH 288/770] chore: Only show Queries tab for panel plugins with isDataPanel set to true --- .../dashboard/panel_editor/PanelEditor.tsx | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index bfdc13bc8f2..37240389373 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -30,6 +30,32 @@ interface PanelEditorTab { text: string; } +enum PanelEditorTabIds { + Queries = 'queries', + Visualization = 'visualization', + Advanced = 'advanced', + Alert = 'alert' +} + +interface PanelEditorTab { + id: string; + text: string; +} + +const panelEditorTabTexts = { + [PanelEditorTabIds.Queries]: 'Queries', + [PanelEditorTabIds.Visualization]: 'Visualization', + [PanelEditorTabIds.Advanced]: 'Panel Options', + [PanelEditorTabIds.Alert]: 'Alert', +}; + +const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => { + return { + id: tabId, + text: panelEditorTabTexts[tabId] + }; +}; + export class PanelEditor extends PureComponent { constructor(props) { super(props); @@ -72,31 +98,26 @@ export class PanelEditor extends PureComponent { render() { const { plugin } = this.props; - let activeTab = store.getState().location.query.tab || 'queries'; + let activeTab: PanelEditorTabIds = store.getState().location.query.tab || PanelEditorTabIds.Queries; const tabs: PanelEditorTab[] = [ - { id: 'queries', text: 'Queries' }, - { id: 'visualization', text: 'Visualization' }, - { id: 'advanced', text: 'Panel Options' }, + getPanelEditorTab(PanelEditorTabIds.Queries), + getPanelEditorTab(PanelEditorTabIds.Visualization), + getPanelEditorTab(PanelEditorTabIds.Advanced), ]; // handle panels that do not have queries tab - if (plugin.exports.PanelCtrl) { - if (!plugin.exports.PanelCtrl.prototype.onDataReceived) { - // remove queries tab - tabs.shift(); - // switch tab - if (activeTab === 'queries') { - activeTab = 'visualization'; - } + if (!plugin.isDataPanel) { + // remove queries tab + tabs.shift(); + // switch tab + if (activeTab === PanelEditorTabIds.Queries) { + activeTab = PanelEditorTabIds.Visualization; } } if (config.alertingEnabled && plugin.id === 'graph') { - tabs.push({ - id: 'alert', - text: 'Alert', - }); + tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert)); } return ( From b93cdf56fb7d5828900ad60f5f2fc42e420adf00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 16:26:02 +0100 Subject: [PATCH 289/770] Removed double page container --- public/app/features/teams/TeamList.tsx | 4 +- public/app/features/teams/TeamPages.tsx | 2 +- .../__snapshots__/TeamList.test.tsx.snap | 582 +++++++++--------- .../__snapshots__/TeamPages.test.tsx.snap | 22 +- 4 files changed, 297 insertions(+), 313 deletions(-) diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index efd279184d4..2e399b34860 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -86,7 +86,7 @@ export class TeamList extends PureComponent { const { teams, searchQuery } = this.props; return ( -
+ <>
-
+ ); } diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index ebbde595601..7a38197ff71 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -84,7 +84,7 @@ export class TeamPages extends PureComponent { return ( - {team && Object.keys(team).length !== 0 &&
{this.renderPage()}
} + {team && Object.keys(team).length !== 0 && this.renderPage()}
); diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index 812fe05c424..5d969cd9d83 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -36,320 +36,316 @@ exports[`Render should render teams table 1`] = ` isLoading={false} >
-
- -
-
+ - - - - - - - + + + + + + + + + + + + + + + + + + + +
- - Name - - Email - - Members - + +
+ + + + + + + + + + + + + + + - - - + + + - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - + + + + + - - - - - - -
+ + Name + + Email + + Members + +
+ + + + + + test-1 + + + + test-1@test.com + + + + 1 + + + -
- - - - - - - test-1 - - - - test-1@test.com - - - - 1 - - - -
- - - - - - - test-2 - - - - test-2@test.com - - - - 2 - - - -
- - - - - - - test-3 - - - - test-3@test.com - - - - 3 - - - -
- - - - - - - test-4 - - - - test-4@test.com - - - - 4 - - - -
- +
+ - - - - - - test-5 - - - - test-5@test.com - - - - 5 - - - -
-
+ + +
+ + test-3 + + + + test-3@test.com + + + + 3 + + + +
+ + + + + + test-4 + + + + test-4@test.com + + + + 4 + + + +
+ + + + + + test-5 + + + + test-5@test.com + + + + 5 + + + +
diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 0c09eb3f82d..70f37cea4c5 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -17,11 +17,7 @@ exports[`Render should render group sync page 1`] = ` -
- -
+
`; @@ -33,13 +29,9 @@ exports[`Render should render member page if team not empty 1`] = ` -
- -
+
`; @@ -51,11 +43,7 @@ exports[`Render should render settings and preferences page 1`] = ` -
- -
+
`; From 8e93b68e6d883c8f2bb9ee4942a3888d98afcdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 16:38:05 +0100 Subject: [PATCH 290/770] restoring green CTA --- .../PanelOptionsGroup/_PanelOptionsGroup.scss | 3 +-- .../ThresholdsEditor/_ThresholdsEditor.scss | 2 +- .../manage_dashboards/manage_dashboards.html | 11 ++++------- .../alerting/partials/notifications_list.html | 1 - public/app/features/api-keys/ApiKeysPage.tsx | 2 +- public/app/features/playlist/partials/playlists.html | 1 - public/sass/_variables.dark.scss | 11 +++++++---- public/sass/_variables.light.scss | 11 +++++++---- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss index f8a3a408bab..4ce9c5264ea 100644 --- a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -36,8 +36,7 @@ } .panel-options-group__add-circle { - - @include gradientBar($btn-secondary-bg, $btn-secondary-bg-hl, #fff); + @include gradientBar($btn-success-bg, $btn-success-bg-hl, #fff); border-radius: 50px; width: 20px; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index e2cbfc372a9..8ef59bf08af 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -21,7 +21,7 @@ } .thresholds-row-add-button { - @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl, #fff); + @include buttonBackground($btn-success-bg, $btn-success-bg-hl, #fff); align-self: center; margin-right: 5px; diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 6036ead3ef1..4ef2d7c9a66 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -6,15 +6,12 @@
- - Dashboard + New Dashboard - - - Folder + + New Folder - - + Import
diff --git a/public/app/features/alerting/partials/notifications_list.html b/public/app/features/alerting/partials/notifications_list.html index 6624a1d1132..ce4fea9ff49 100644 --- a/public/app/features/alerting/partials/notifications_list.html +++ b/public/app/features/alerting/partials/notifications_list.html @@ -8,7 +8,6 @@
- New Channel
diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 7bed498e2ac..2627b1a6862 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -200,7 +200,7 @@ export class ApiKeysPage extends PureComponent {
diff --git a/public/app/features/playlist/partials/playlists.html b/public/app/features/playlist/partials/playlists.html index 2ec919f8157..22e41ac7104 100644 --- a/public/app/features/playlist/partials/playlists.html +++ b/public/app/features/playlist/partials/playlists.html @@ -5,7 +5,6 @@ diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 1ed3ecf2cf7..6181590985f 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -159,11 +159,14 @@ $table-bg-hover: $dark-3; // Buttons // ------------------------- -$btn-primary-bg: $sapphire-base; -$btn-primary-bg-hl: $sapphire-shade; +$btn-secondary-bg: $sapphire-base; +$btn-secondary-bg-hl: $sapphire-shade; -$btn-secondary-bg: $green-base; -$btn-secondary-bg-hl: $green-shade; +$btn-primary-bg: $green-base; +$btn-primary-bg-hl: $green-shade; + +$btn-success-bg: $green-base; +$btn-success-bg-hl: $green-shade; $btn-danger-bg: $lobster-base; $btn-danger-bg-hl: $lobster-shade; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 077c6598a4d..f0e0a535653 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -155,11 +155,14 @@ $table-bg-hover: $gray-5; // Buttons // ------------------------- -$btn-secondary-bg: $green-base; -$btn-secondary-bg-hl: $green-shade; +$btn-primary-bg: $green-base; +$btn-primary-bg-hl: $green-shade; -$btn-primary-bg: $sapphire-base; -$btn-primary-bg-hl: $sapphire-shade; +$btn-secondary-bg: $sapphire-base; +$btn-secondary-bg-hl: $sapphire-shade; + +$btn-success-bg: $green-base; +$btn-success-bg-hl: $green-shade; $btn-danger-bg: $lobster-base; $btn-danger-bg-hl: $lobster-shade; From a2dad6157a0e77dbdae2f6c7440b55d6a40e3864 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 11 Feb 2019 16:44:09 +0100 Subject: [PATCH 291/770] hard move --- .../features/dashboard/dashgrid/DataPanel.tsx | 23 ++----- .../dashboard/dashgrid/PanelChrome.tsx | 62 ++++++++++++++----- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 2183548000b..5b0b8588ad0 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -2,7 +2,6 @@ import React, { Component } from 'react'; import { Tooltip } from '@grafana/ui'; -import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary'; // Services import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Utils @@ -18,8 +17,6 @@ import { TimeSeries, } from '@grafana/ui'; -const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; - interface RenderProps { loading: LoadingState; panelData: PanelData; @@ -203,22 +200,10 @@ export class DataPanel extends Component { return ( <> {this.renderLoadingStates()} - - {({ error, errorInfo }) => { - if (errorInfo) { - this.onError(error.message || DEFAULT_PLUGIN_ERROR); - return null; - } - return ( - <> - {this.props.children({ - loading, - panelData, - })} - - ); - }} - + {this.props.children({ + loading, + panelData, + })} ); } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b02d9479dcc..1f69fb81d30 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -8,6 +8,7 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; // Components import { PanelHeader } from './PanelHeader/PanelHeader'; import { DataPanel } from './DataPanel'; +import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary'; // Utils import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; @@ -23,6 +24,8 @@ import variables from 'sass/_variables.scss'; import templateSrv from 'app/features/templating/template_srv'; import { DataQueryResponse } from '@grafana/ui/src'; +const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; + export interface Props { panel: PanelModel; dashboard: DashboardModel; @@ -34,6 +37,9 @@ export interface State { renderCounter: number; timeInfo?: string; timeRange?: TimeRange; + loading: LoadingState; + isFirstLoad: boolean; + errorMessage: string; } export class PanelChrome extends PureComponent { @@ -43,8 +49,11 @@ export class PanelChrome extends PureComponent { super(props); this.state = { + loading: LoadingState.NotStarted, refreshCounter: 0, renderCounter: 0, + isFirstLoad: false, + errorMessage: '', }; } @@ -94,6 +103,16 @@ export class PanelChrome extends PureComponent { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } + onError = (errorMessage: string) => { + if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) { + this.setState({ + loading: LoadingState.Error, + isFirstLoad: false, + errorMessage: errorMessage, + }); + } + }; + renderPanel(loading, panelData, width, height): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; @@ -145,23 +164,32 @@ export class PanelChrome extends PureComponent { scopedVars={panel.scopedVars} links={panel.links} /> - {panel.snapshotData ? ( - this.renderPanel(false, panel.snapshotData, width, height) - ) : ( - - {({ loading, panelData }) => { - return this.renderPanel(loading, panelData, width, height); - }} - - )} + + {({ error, errorInfo }) => { + if (errorInfo) { + this.onError(error.message || DEFAULT_PLUGIN_ERROR); + return null; + } + + return panel.snapshotData ? ( + this.renderPanel(false, panel.snapshotData, width, height) + ) : ( + + {({ loading, panelData }) => { + return this.renderPanel(loading, panelData, width, height); + }} + + ); + }} +
); }} From afc2efa56ddb53334b3a4c73d3f30b3de9ad2c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 16:45:47 +0100 Subject: [PATCH 292/770] Removed plus icons --- .../DashboardPermissions/DashboardPermissions.tsx | 4 +--- public/app/features/folders/FolderPermissions.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx index 8cc26c4a1f2..e5fb0da71fc 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx @@ -76,9 +76,7 @@ export class DashboardPermissions extends PureComponent {
- +
@@ -407,6 +410,7 @@ exports[`Render should render is ready only message 1`] = ` isReadOnly={true} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} />
From 5388541fd7dbefc7300c46b23e280872bdf61881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 12 Feb 2019 08:03:43 +0100 Subject: [PATCH 303/770] Fixes bug #12972 with a new type of input that escapes and unescapes special regexp characters --- .../components/OrgActionBar/OrgActionBar.tsx | 10 ++-- .../__snapshots__/OrgActionBar.test.tsx.snap | 5 +- .../RegExpSafeInput/RegExpSafeInput.tsx | 48 +++++++++++++++++++ .../features/alerting/AlertRuleList.test.tsx | 5 +- .../app/features/alerting/AlertRuleList.tsx | 11 ++--- .../__snapshots__/AlertRuleList.test.tsx.snap | 6 +-- .../features/api-keys/ApiKeysPage.test.tsx | 9 ++-- public/app/features/api-keys/ApiKeysPage.tsx | 16 ++----- .../panel_editor/VisualizationTab.tsx | 7 ++- .../datasources/NewDataSourcePage.tsx | 10 ++-- public/app/features/teams/TeamList.test.tsx | 9 ++-- public/app/features/teams/TeamList.tsx | 12 ++--- .../app/features/teams/TeamMembers.test.tsx | 3 +- public/app/features/teams/TeamMembers.tsx | 8 ++-- .../__snapshots__/TeamMembers.test.tsx.snap | 9 ++-- public/app/features/users/UsersActionBar.tsx | 6 +-- .../UsersActionBar.test.tsx.snap | 20 ++++---- 17 files changed, 109 insertions(+), 85 deletions(-) create mode 100644 public/app/core/components/RegExpSafeInput/RegExpSafeInput.tsx diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index b6b2046736f..9bf1eacc515 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react'; import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector'; +import { RegExpSafeInput } from '../RegExpSafeInput/RegExpSafeInput'; export interface Props { searchQuery: string; @@ -23,12 +24,11 @@ export default class OrgActionBar extends PureComponent {
diff --git a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap index 25de037930a..db453b8cc3f 100644 --- a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap +++ b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap @@ -10,11 +10,10 @@ exports[`Render should render component 1`] = `