From 3fab4d6f8caa15790b4f8a6103a0998a56ce348c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 13 Feb 2019 19:52:27 +0100 Subject: [PATCH 01/22] docs: fix typo --- docs/sources/auth/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index 1d0b0d89b3f..fc22d6b2d7f 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -38,7 +38,7 @@ 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. +> The following 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. From 592f57d0ba5487f8c6621a98cae5446f435ded67 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 13 Feb 2019 19:53:25 +0100 Subject: [PATCH 02/22] docs: add note regarding auth proxy and user session requirement --- docs/sources/tutorials/ha_setup.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md index f141392e223..3464f124458 100644 --- a/docs/sources/tutorials/ha_setup.md +++ b/docs/sources/tutorials/ha_setup.md @@ -26,6 +26,8 @@ Grafana will now persist all long term data in the database. How to configure th ## User sessions +> Beginning with Grafana v6.0 and above the following only applies when using [Auth Proxy Authentication](/auth/auth-proxy/). + The second thing to consider is how to deal with user sessions and how to configure your load balancer in front of Grafana. Grafana supports two ways of storing session data: locally on disk or in a database/cache-server. If you want to store sessions on disk you can use `sticky sessions` in your load balancer. If you prefer to store session data in a database/cache-server From 1bd9573f8ec9a40dd87f79f5fa103bff10fa6593 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 13 Feb 2019 19:53:55 +0100 Subject: [PATCH 03/22] docs: add upgrade notes for v6.0 --- docs/sources/installation/upgrading.md | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index a476a38c3c5..18c6cb649b1 100644 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -117,3 +117,32 @@ One of the database migrations included in this release will update all annotati We've got one report where using systemd, PostgreSQL and a large amount of annotations (table size 1645mb) took 8-20 minutes for the database migration to complete. However, the grafana-server process was killed after 90 seconds by systemd. Any database migration queries in progress when systemd kills the grafana-server process continues to execute in database until finished. If you're using systemd and have a large amount of annotations consider temporary adjusting the systemd `TimeoutStartSec` setting to something high like `30m` before upgrading. + +## Upgrading to v6.0 + +If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitzied HTML. +Read more [here](/installation/configuration/#disable-sanitize-html) about this new setting. + +### Authentication and security + +If your using Grafana's builtin, LDAP (without Auth Proxy) or OAuth authentication all users will be required to login upon the next visit after the upgrade. + +If you have `cookie_secure` set to `true` in the `session` section you probably want to change the `cookie_secure` to `true` in the `security` section as well. Ending up with a configuration like this: + +```ini +[session] +cookie_secure = true + +[security] +cookie_secure = true +``` + +The `login_remember_days`, `cookie_username` and `cookie_remember_name` settings in the `security` section are no longer being used so they're safe to remove. + +If you have `login_remember_days` configured to 0 (zero) you should change your configuration to this to accomplish similar behavior, i.e. a logged in user will maximum be logged in for 1 day until being forced to login again: + +```ini +[auth] +login_maximum_inactive_lifetime_days = 1 +login_maximum_lifetime_days = 1 +``` From de2f3db4130c4afc6b57cdb614151e21060daeed Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 13 Feb 2019 19:54:45 +0100 Subject: [PATCH 04/22] docs: improve removal of session storage for what's new in v6.0 --- docs/sources/guides/whats-new-in-v6-0.md | 32 +++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v6-0.md b/docs/sources/guides/whats-new-in-v6-0.md index 61eec0ac390..2cf26723062 100644 --- a/docs/sources/guides/whats-new-in-v6-0.md +++ b/docs/sources/guides/whats-new-in-v6-0.md @@ -27,6 +27,7 @@ The main highlights are: - [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource - [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins. - [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker. +- [Removal of user session storage]({{< relref "#easier-to-deploy-more-secure" >}}) makes Grafana easier to deploy & more secure. ## Explore @@ -113,30 +114,39 @@ will be shared closer to or just after release. {{< docs-imagebox img="/img/docs/v60/react_panels.png" max-width="600px" caption="React Panel" >}}
-### Google Stackdriver Datasource +## Google Stackdriver Datasource Built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) is officially released in Grafana 6.0. Beta support was added in Grafana 5.3 and we have added lots of improvements since then. To get started read the guide: [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/). -### Azure Monitor Datasource +## Azure Monitor Datasource One of the goals of the Grafana v6.0 release is to add support for the three major clouds. Amazon Cloudwatch has been a core datasource for years and Google Stackdriver is also now supported. We developed an external plugin for Azure Monitor last year and for this release the [plugin](https://grafana.com/plugins/grafana-azure-monitor-datasource) is being moved into Grafana to be one of the built-in datasources. For users of the external plugin, Grafana will automatically start using the built-in version. As a core datasource, the Azure Monitor datasource will get alerting support for the official 6.0 release. The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics. -### Provisioning support for alert notifiers +## Provisioning support for alert notifiers Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance. -### Auth and session token improvements +## Easier to deploy & more secure -The previous session storage implementation in Grafana was causing problems in larger HA setups due to too many write requests to the database. The remember me token also have several security issues which is why we decided to rewrite auth middleware in Grafana and remove the session storage since most operations using the session storage could be rewritten to use cookies or data already made available earlier in the request. -If you are using `Auth proxy` for authentication the session storage will still be used but our goal is to remove this ASAP as well. +Grafana 6.0 removes the need of configuring and setup of additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a +high availability setup and/or if you're using a stateless user session storage like Redis, Memcache, Postgres or MySQL. -This release will force all users to log in again since their previous token is not valid anymore. +Instead of user sessions a solution based on short-lived tokens that are rotated frequently have been implemented. This also replaces the old "remember me cookie" +solution, which allowed a user to be logged in between browser sessions, and which have been subject to several security holes throughout the years. +Read more about the short-lived token solution and how to configure it [here](/auth/overview/#login-and-short-lived-tokens). -### Named Colors +> Please note that due to these changes, all users will be required to login upon next visit after upgrade. + +Besides these changes there have also been security improvements regarding protection against Cross-Site Request Forgery (CSRF) and Cross-site Scripting (XSS) attacks. + +> If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured +but our goal is to remove this requirements in a near future. + +## Named Colors {{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}} @@ -148,12 +158,16 @@ Named colors also enables Grafana to adapt colors to the current theme.
-### Other features +## Other features - The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per bucket computations like the difference or ratio between two metrics. - Support for Google Hangouts Chat alert notifications - New built in template variables for the current time range in `$__from` and `$__to` +## Upgrading + +See [upgrade notes](/installation/upgrading/#upgrading-to-v6-0). + ## Changelog Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list of new features, changes, and bug fixes. From 7f7cc85ef0b0319a12cb02aa541952414c14f154 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 14 Feb 2019 13:24:41 +0100 Subject: [PATCH 05/22] docs: adds note about new login cookie name --- docs/sources/installation/upgrading.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index 18c6cb649b1..d230efb1888 100644 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -146,3 +146,5 @@ If you have `login_remember_days` configured to 0 (zero) you should change your login_maximum_inactive_lifetime_days = 1 login_maximum_lifetime_days = 1 ``` + +The default cookie name for storing the auth token is `grafana_sess`. you can configure this with `login_cookie_name` in `[auth]` settings. \ No newline at end of file From 933d4729db6062b99ad614316aa63312e8e6f6a1 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 14 Feb 2019 14:06:50 +0100 Subject: [PATCH 06/22] docs: mention samesite setting --- docs/sources/guides/whats-new-in-v6-0.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v6-0.md b/docs/sources/guides/whats-new-in-v6-0.md index 2cf26723062..e411704f757 100644 --- a/docs/sources/guides/whats-new-in-v6-0.md +++ b/docs/sources/guides/whats-new-in-v6-0.md @@ -130,7 +130,7 @@ The Azure Monitor datasource integrates four Azure services with Grafana - Azure Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance. -## Easier to deploy & more secure +## Easier to deploy & more secure authentication Grafana 6.0 removes the need of configuring and setup of additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a high availability setup and/or if you're using a stateless user session storage like Redis, Memcache, Postgres or MySQL. @@ -141,7 +141,7 @@ Read more about the short-lived token solution and how to configure it [here](/a > Please note that due to these changes, all users will be required to login upon next visit after upgrade. -Besides these changes there have also been security improvements regarding protection against Cross-Site Request Forgery (CSRF) and Cross-site Scripting (XSS) attacks. +Besides these changes we have also introduced [SameSite](https://www.owasp.org/index.php/SameSite) setting to protect against Cross-Site Request Forgery (CSRF) and Cross-site Scripting (XSS) attacks. This setting enables more control of when the browser include cookies in requests. Its set to `lax` by default but can be configured using `cookie_samesite` under `[security]` > If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured but our goal is to remove this requirements in a near future. From 94c1335f1101c5f177536c76c925ff6c13ffc775 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 14 Feb 2019 14:17:02 +0100 Subject: [PATCH 07/22] docs: move alerting above session --- docs/sources/tutorials/ha_setup.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md index 3464f124458..f20a9933126 100644 --- a/docs/sources/tutorials/ha_setup.md +++ b/docs/sources/tutorials/ha_setup.md @@ -15,7 +15,7 @@ Setting up Grafana for high availability is fairly simple. It comes down to two 2. Decide how to store session data.
- +
## Configure multiple servers to use the same database @@ -24,6 +24,10 @@ First, you need to do is to setup MySQL or Postgres on another server and config You can find the configuration for doing that in the [[database]]({{< relref "configuration.md" >}}#database) section in the grafana config. Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on for the database you're using. +## Alerting + +Currently alerting supports a limited form of high availability. Since v4.2.0, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. + ## User sessions > Beginning with Grafana v6.0 and above the following only applies when using [Auth Proxy Authentication](/auth/auth-proxy/). @@ -43,6 +47,4 @@ If you use MySQL/Postgres for session storage, you first need a table to store t For Grafana itself it doesn't really matter if you store the session data on disk or database/redis/memcache. But we recommend using a database/redis/memcache since it makes it easier manage the grafana servers. -## Alerting -Currently alerting supports a limited form of high availability. Since v4.2.0, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support distributing the alert rule execution between servers. That might be added in the future but right now prefer to keep it simple. From 3c2f6094b2a90676558f4c4c58d2871bedd2b0df Mon Sep 17 00:00:00 2001 From: Maxim Neverov Date: Thu, 14 Feb 2019 16:31:15 +0100 Subject: [PATCH 08/22] Fix percent_diff calculation when points are nulls --- pkg/services/alerting/conditions/reducer.go | 19 +++++++++++-------- .../alerting/conditions/reducer_test.go | 12 ++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 1e8ae792746..485ea43561b 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -131,14 +131,17 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { break } } - // get the oldest point - points = points[0:i] - for i := 0; i < len(points); i++ { - if points[i][0].Valid { - allNull = false - val := (first - points[i][0].Float64) / points[i][0].Float64 * 100 - value = math.Abs(val) - break + + if i >= 1 { + // get the oldest point + points = points[0:i] + for i := 0; i < len(points); i++ { + if points[i][0].Valid { + allNull = false + val := (first - points[i][0].Float64) / points[i][0].Float64 * 100 + value = math.Abs(val) + break + } } } case "count_non_null": diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 7f11fc498bd..70b03f1dba8 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -157,6 +157,18 @@ func TestSimpleReducer(t *testing.T) { result := testReducer("percent_diff", 30, 40, 40) So(result, ShouldEqual, float64(33.33333333333333)) }) + + Convey("percent_diff with only nulls", func() { + reducer := NewSimpleReducer("percent_diff") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + + So(reducer.Reduce(series).Valid, ShouldEqual, false) + }) }) } From 191f9c8407220f2702c1fb43d1b2134629bc8bef Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 14 Feb 2019 19:42:39 +0100 Subject: [PATCH 09/22] whats new: rename security section --- docs/sources/guides/whats-new-in-v6-0.md | 8 +++++--- docs/sources/installation/upgrading.md | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v6-0.md b/docs/sources/guides/whats-new-in-v6-0.md index e411704f757..656e563a7f8 100644 --- a/docs/sources/guides/whats-new-in-v6-0.md +++ b/docs/sources/guides/whats-new-in-v6-0.md @@ -27,7 +27,7 @@ The main highlights are: - [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource - [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins. - [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker. -- [Removal of user session storage]({{< relref "#easier-to-deploy-more-secure" >}}) makes Grafana easier to deploy & more secure. +- [Removal of user session storage]({{< relref "#easier-to-deploy-improved security" >}}) makes Grafana easier to deploy & improved security. ## Explore @@ -130,7 +130,7 @@ The Azure Monitor datasource integrates four Azure services with Grafana - Azure Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance. -## Easier to deploy & more secure authentication +## Easier to deploy & improved security Grafana 6.0 removes the need of configuring and setup of additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a high availability setup and/or if you're using a stateless user session storage like Redis, Memcache, Postgres or MySQL. @@ -141,11 +141,13 @@ Read more about the short-lived token solution and how to configure it [here](/a > Please note that due to these changes, all users will be required to login upon next visit after upgrade. -Besides these changes we have also introduced [SameSite](https://www.owasp.org/index.php/SameSite) setting to protect against Cross-Site Request Forgery (CSRF) and Cross-site Scripting (XSS) attacks. This setting enables more control of when the browser include cookies in requests. Its set to `lax` by default but can be configured using `cookie_samesite` under `[security]` +Besides these changes we have also introduced [SameSite](https://www.owasp.org/index.php/SameSite) setting to protect against Cross-Site Request Forgery (CSRF). This setting enables more control of when the browser include cookies in requests. Its set to `lax` by default but can be configured using `cookie_samesite` under `[security]` > If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured but our goal is to remove this requirements in a near future. +We also disable script tags in text panels by defult to avoid Cross-site Scripting (XSS) attacks. + ## Named Colors {{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}} diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index d230efb1888..e235f25b9e9 100644 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -147,4 +147,4 @@ login_maximum_inactive_lifetime_days = 1 login_maximum_lifetime_days = 1 ``` -The default cookie name for storing the auth token is `grafana_sess`. you can configure this with `login_cookie_name` in `[auth]` settings. \ No newline at end of file +The default cookie name for storing the auth token is `grafana_session`. you can configure this with `login_cookie_name` in `[auth]` settings. \ No newline at end of file From af5a2468e88bffbe6fc7412c4a859b51af0feb82 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 14 Feb 2019 20:18:24 +0100 Subject: [PATCH 10/22] fix spelling error --- docs/sources/guides/whats-new-in-v6-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/guides/whats-new-in-v6-0.md b/docs/sources/guides/whats-new-in-v6-0.md index 656e563a7f8..e789b6e71b2 100644 --- a/docs/sources/guides/whats-new-in-v6-0.md +++ b/docs/sources/guides/whats-new-in-v6-0.md @@ -146,7 +146,7 @@ Besides these changes we have also introduced [SameSite](https://www.owasp.org/i > If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured but our goal is to remove this requirements in a near future. -We also disable script tags in text panels by defult to avoid Cross-site Scripting (XSS) attacks. +We also disable script tags in text panels by default to avoid Cross-site Scripting (XSS) attacks. ## Named Colors From 28eaac3a9c7082e8c496005c1cb66b4b70a4f82f Mon Sep 17 00:00:00 2001 From: Maxim Neverov Date: Thu, 14 Feb 2019 21:52:59 +0100 Subject: [PATCH 11/22] Extracted common code for diff calculation --- pkg/services/alerting/conditions/reducer.go | 87 +++++++++---------- .../alerting/conditions/reducer_test.go | 12 +++ 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 485ea43561b..f55545be311 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -95,55 +95,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { } } case "diff": - var ( - points = series.Points - first float64 - i int - ) - // get the newest point - for i = len(points) - 1; i >= 0; i-- { - if points[i][0].Valid { - allNull = false - first = points[i][0].Float64 - break - } - } - // get the oldest point - points = points[0:i] - for i := 0; i < len(points); i++ { - if points[i][0].Valid { - allNull = false - value = first - points[i][0].Float64 - break - } - } + allNull, value = calculateDiff(series, allNull, value, diff) case "percent_diff": - var ( - points = series.Points - first float64 - i int - ) - // get the newest point - for i = len(points) - 1; i >= 0; i-- { - if points[i][0].Valid { - allNull = false - first = points[i][0].Float64 - break - } - } - - if i >= 1 { - // get the oldest point - points = points[0:i] - for i := 0; i < len(points); i++ { - if points[i][0].Valid { - allNull = false - val := (first - points[i][0].Float64) / points[i][0].Float64 * 100 - value = math.Abs(val) - break - } - } - } + allNull, value = calculateDiff(series, allNull, value, percentDiff) case "count_non_null": for _, v := range series.Points { if v[0].Valid { @@ -166,3 +120,40 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { func NewSimpleReducer(typ string) *SimpleReducer { return &SimpleReducer{Type: typ} } + +func calculateDiff(series *tsdb.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) { + var ( + points = series.Points + first float64 + i int + ) + // get the newest point + for i = len(points) - 1; i >= 0; i-- { + if points[i][0].Valid { + allNull = false + first = points[i][0].Float64 + break + } + } + if i >= 1 { + // get the oldest point + points = points[0:i] + for i := 0; i < len(points); i++ { + if points[i][0].Valid { + allNull = false + val := fn(first, points[i][0].Float64) + value = math.Abs(val) + break + } + } + } + return allNull, value +} + +var diff = func(newest, oldest float64) float64 { + return newest - oldest +} + +var percentDiff = func(newest, oldest float64) float64 { + return (newest - oldest) / oldest * 100 +} diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 70b03f1dba8..d2c21771d0b 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -143,6 +143,18 @@ func TestSimpleReducer(t *testing.T) { So(result, ShouldEqual, float64(10)) }) + Convey("diff with only nulls", func() { + reducer := NewSimpleReducer("diff") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + + So(reducer.Reduce(series).Valid, ShouldEqual, false) + }) + Convey("percent_diff one point", func() { result := testReducer("percent_diff", 40) So(result, ShouldEqual, float64(0)) From 7699706e65ffd48c03196907de6b89d2b0be1c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 Feb 2019 11:23:31 +0100 Subject: [PATCH 12/22] fixed handling of alert urls with true flags, fixes #15454 --- pkg/services/alerting/eval_context.go | 2 +- .../app/features/alerting/AlertRuleItem.tsx | 2 +- .../__snapshots__/AlertRuleItem.test.tsx.snap | 4 +-- .../containers/DashboardPage.test.tsx | 34 ++++++++++++++++++- .../dashboard/containers/DashboardPage.tsx | 6 ++-- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 4db942e0a55..02b9955662f 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) { return c.dashboardRef, nil } -const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d" +const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d" func (c *EvalContext) GetRuleUrl() (string, error) { if c.IsTestRun { diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 86bb0207460..3fec37d19b8 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent { 'fa-pause': rule.state !== 'paused', }); - const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; + const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`; return (
  • diff --git a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap index f686127ebf3..8e076ffd22e 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Render should render component 1`] = ` className="alert-rule-item__name" > { expect(ctx.cleanUpDashboardMock.calls).toBe(1); }); }); + + describe('mapStateToProps with bool fullscreen', () => { + const props = mapStateToProps({ + location: { + routeParams: {}, + query: { + fullscreen: true, + edit: false, + }, + }, + dashboard: {}, + } as any); + + expect(props.urlFullscreen).toBe(true); + expect(props.urlEdit).toBe(false); + }); + + describe('mapStateToProps with string edit true', () => { + const props = mapStateToProps({ + location: { + routeParams: {}, + query: { + fullscreen: false, + edit: 'true', + }, + }, + dashboard: {}, + } as any); + + expect(props.urlFullscreen).toBe(false); + expect(props.urlEdit).toBe(true); + }); }); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 27118e297b5..bdb601a692f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -284,15 +284,15 @@ export class DashboardPage extends PureComponent { } } -const mapStateToProps = (state: StoreState) => ({ +export 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, urlFolderId: state.location.query.folderId, - urlFullscreen: state.location.query.fullscreen === true, - urlEdit: state.location.query.edit === true, + urlFullscreen: !!state.location.query.fullscreen, + urlEdit: !!state.location.query.edit, initPhase: state.dashboard.initPhase, isInitSlow: state.dashboard.isInitSlow, initError: state.dashboard.initError, From 88fbe3adcab52f47224584dba91324a6db342a4e Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 15 Feb 2019 11:27:21 +0100 Subject: [PATCH 13/22] docs: fix header --- docs/sources/installation/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index d85e39146dc..2352bd15b89 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -594,7 +594,7 @@ Default setting for new alert rules. Defaults to categorize error and timeouts a Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) -# concurrent_render_limit +### concurrent_render_limit > Available in 5.3 and above From ff87f6d303961f5c3f8a9304b7bfb6f57233e593 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 15 Feb 2019 11:31:29 +0100 Subject: [PATCH 14/22] docs: suggested changes --- docs/sources/guides/whats-new-in-v6-0.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v6-0.md b/docs/sources/guides/whats-new-in-v6-0.md index e789b6e71b2..2ba7e86b385 100644 --- a/docs/sources/guides/whats-new-in-v6-0.md +++ b/docs/sources/guides/whats-new-in-v6-0.md @@ -27,7 +27,7 @@ The main highlights are: - [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource - [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins. - [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker. -- [Removal of user session storage]({{< relref "#easier-to-deploy-improved security" >}}) makes Grafana easier to deploy & improved security. +- [Removal of user session storage]({{< relref "#easier-to-deploy-improved-security" >}}) makes Grafana easier to deploy & improves security. ## Explore @@ -141,13 +141,14 @@ Read more about the short-lived token solution and how to configure it [here](/a > Please note that due to these changes, all users will be required to login upon next visit after upgrade. -Besides these changes we have also introduced [SameSite](https://www.owasp.org/index.php/SameSite) setting to protect against Cross-Site Request Forgery (CSRF). This setting enables more control of when the browser include cookies in requests. Its set to `lax` by default but can be configured using `cookie_samesite` under `[security]` +Besides these changes we have also made security improvements regarding Cross-Site Request Forgery (CSRF) and Cross-site Scripting (XSS) vulnerabilities: + +* Cookies are per default using the [SameSite](/installation/configuration/#cookie-samesite) attribute to protect against CSRF attacks +* Script tags in text panels are per default [disabled](/installation/configuration/#disable-sanitize-html) to protect against XSS attacks > If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured but our goal is to remove this requirements in a near future. -We also disable script tags in text panels by default to avoid Cross-site Scripting (XSS) attacks. - ## Named Colors {{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}} From 2d5fd7fdfd78a930bddff5ae4d48f77f03ee8798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 16 Feb 2019 15:45:19 +0100 Subject: [PATCH 15/22] Fixed prettier issue (#15471) Fixed prettier CI issue that caused build failures --- package.json | 2 +- .../features/explore/LogMessageAnsi.test.tsx | 20 +++++++++++--- .../app/features/explore/LogMessageAnsi.tsx | 27 +++++++++++-------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 0bf3e972a72..70067a166a6 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", "storybook": "cd packages/grafana-ui && yarn storybook", - "prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\"" + "prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"" }, "husky": { "hooks": { diff --git a/public/app/features/explore/LogMessageAnsi.test.tsx b/public/app/features/explore/LogMessageAnsi.test.tsx index 6560fd7b7dd..8513ed4c114 100644 --- a/public/app/features/explore/LogMessageAnsi.test.tsx +++ b/public/app/features/explore/LogMessageAnsi.test.tsx @@ -16,9 +16,21 @@ describe('', () => { const wrapper = shallow(); expect(wrapper.find('span')).toHaveLength(1); - expect(wrapper.find('span').first().prop('style')).toMatchObject(expect.objectContaining({ - color: expect.any(String) - })); - expect(wrapper.find('span').first().text()).toBe('ipsum'); + expect( + wrapper + .find('span') + .first() + .prop('style') + ).toMatchObject( + expect.objectContaining({ + color: expect.any(String), + }) + ); + expect( + wrapper + .find('span') + .first() + .text() + ).toBe('ipsum'); }); }); diff --git a/public/app/features/explore/LogMessageAnsi.tsx b/public/app/features/explore/LogMessageAnsi.tsx index e4df16fa13c..ea751879c2e 100644 --- a/public/app/features/explore/LogMessageAnsi.tsx +++ b/public/app/features/explore/LogMessageAnsi.tsx @@ -46,15 +46,15 @@ export class LogMessageAnsi extends PureComponent { const parsed = ansicolor.parse(props.value); return { - chunks: parsed.spans.map((span) => { - return span.css ? - { - style: convertCSSToStyle(span.css), - text: span.text - } : - { text: span.text }; + chunks: parsed.spans.map(span => { + return span.css + ? { + style: convertCSSToStyle(span.css), + text: span.text, + } + : { text: span.text }; }), - prevValue: props.value + prevValue: props.value, }; } @@ -62,9 +62,14 @@ export class LogMessageAnsi extends PureComponent { const { chunks } = this.state; return chunks.map( - (chunk, index) => chunk.style ? - {chunk.text} : - chunk.text + (chunk, index) => + chunk.style ? ( + + {chunk.text} + + ) : ( + chunk.text + ) ); } } From 89ad52598610632f3370add4f0853b9ecacf80ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Feb 2019 08:11:57 +0100 Subject: [PATCH 16/22] Fixed issue with PanelHeader and grid-drag-handle class still being applied in fullscreen, fixes #15480 --- .../features/dashboard/dashgrid/DashboardPanel.tsx | 4 ++-- .../app/features/dashboard/dashgrid/PanelChrome.tsx | 4 +++- .../dashboard/dashgrid/PanelHeader/PanelHeader.tsx | 11 +++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index b9c56e36382..bb2470cff17 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -131,10 +131,10 @@ export class DashboardPanel extends PureComponent { }; renderReactPanel() { - const { dashboard, panel } = this.props; + const { dashboard, panel, isFullscreen } = this.props; const { plugin } = this.state; - return ; + return ; } renderAngularPanel() { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 5a993293946..29fe307e941 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -29,6 +29,7 @@ export interface Props { panel: PanelModel; dashboard: DashboardModel; plugin: PanelPlugin; + isFullscreen: boolean; } export interface State { @@ -193,7 +194,7 @@ export class PanelChrome extends PureComponent { }; render() { - const { dashboard, panel } = this.props; + const { dashboard, panel, isFullscreen } = this.props; const { errorMessage, timeInfo } = this.state; const { transparent } = panel; @@ -216,6 +217,7 @@ export class PanelChrome extends PureComponent { scopedVars={panel.scopedVars} links={panel.links} error={errorMessage} + isFullscreen={isFullscreen} /> {({ error, errorInfo }) => { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 49e32ae058c..0f6563836f0 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -19,6 +19,7 @@ export interface Props { scopedVars?: string; links?: []; error?: string; + isFullscreen: boolean; } interface ClickCoordinates { @@ -69,10 +70,9 @@ export class PanelHeader extends Component { }; render() { - const isFullscreen = false; - const isLoading = false; + const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props; + const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - const { panel, dashboard, timeInfo, scopedVars, error } = this.props; const title = templateSrv.replaceWithText(panel.title, scopedVars); return ( @@ -86,11 +86,6 @@ export class PanelHeader extends Component { error={error} />
    - {isLoading && ( - - - - )}
    From b488892f5faf342605bfe1b6a643524a8ec48f1f Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 17 Feb 2019 17:38:56 -0300 Subject: [PATCH 17/22] Added enable_gzip documentation (#15322) --- docs/sources/installation/configuration.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2352bd15b89..f0418ad31a6 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -160,6 +160,13 @@ The path to the directory where the front end files (HTML, JS, and CSS files). Default to `public` which is why the Grafana binary needs to be executed with working directory set to the installation path. +### enable_gzip + +Set this option to `true` to enable HTTP compression, this can improve +transfer speed and bandwidth utilization. It is recommended that most +users set it to `true`. By default it is set to `false` for compatibility +reasons. + ### cert_file Path to the certificate file (if `protocol` is set to `https`). From 9e55fef544e35dc8955be9379a88324e91169f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Feb 2019 09:52:31 +0100 Subject: [PATCH 18/22] improved formatting of variable docs --- docs/sources/reference/templating.md | 83 ++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index bf3fbd6a229..825fc5b7ebf 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -38,22 +38,81 @@ documentation article for details on value escaping during interpolation. ### Advanced Formatting Options -> Only available in Grafana v5.1+. - The formatting of the variable interpolation depends on the data source but there are some situations where you might want to change the default formatting. For example, the default for the MySql datasource is to join multiple values as comma-separated with quotes: `'server01','server02'`. In some cases you might want to have a comma-separated string without quotes: `server01,server02`. This is now possible with the advanced formatting options. Syntax: `${var_name:option}` -Filter Option | Example | Raw | Interpolated | Description ------------- | ------------- | ------------- | ------------- | ------------- -`glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | (Default) Formats multi-value variable into a glob (for Graphite queries) -`regex` | ${servers:regex} | `'test.', 'test2'` | (test\.|test2) | Formats multi-value variable into a regex string -`pipe` | ${servers:pipe} | `'test.', 'test2'` | test.|test2 | Formats multi-value variable into a pipe-separated string -`csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string -`json`| ${servers:json} | `'test1', 'test2'` | `["test1","test2"]` | Formats multi-value variable as a JSON string -`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB. -`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression. -`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded. +#### Glob +Formats multi-value variable into a glob (for Graphite queries). + +```bash +servers = ['test1', 'test2'] +String to interpolate: '${servers:glob}' +Interpolation result: '{test1,test2}' +``` + +### Regex +Formats multi-value variable into a regex string. + +```bash +servers = ['test1.', 'test2'] +String to interpolate: '${servers:regex}' +Interpolation result: '(test\.|test2)' +``` + +### Pipe +Formats multi-value variable into a pipe-separated string. + +```bash +servers = ['test1.', 'test2'] +String to interpolate: '${servers:pipe}' +Interpolation result: 'test.|test2' +``` + +### Csv +Formats multi-value variable as a comma-separated string. + +```bash +servers = ['test1', 'test2'] +String to interpolate: '${servers:csv}' +Interpolation result: 'test,test2' +``` + +### Json +Formats multi-value variable as a comma-separated string. + +```bash +servers = ['test1', 'test2'] +String to interpolate: '${servers:json}' +Interpolation result: '["test1", "test2"]' +``` + +### Distributed +Formats multi-value variable in custom format for OpenTSDB. + +```bash +servers = ['test1', 'test2'] +String to interpolate: '${servers:distributed}' +Interpolation result: 'test1,servers=test2' +``` + +### Lucene +Formats multi-value variable in lucene format for Elasticsearch. + +```bash +servers = ['test1', 'test2'] +String to interpolate: '${servers:lucene}' +Interpolation result: '("test1" OR "test2")' +``` + +### Percentencode +Formats single & multi valued varaibles for use in URL parameters. + +```bash +servers = ['foo()bar BAZ', 'test2'] +String to interpolate: '${servers:lucene}' +Interpolation result: 'foo%28%29bar%20BAZ%2Ctest2' +``` Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1). From bf826d7c81ef9db2cd359926d2e32e82c25e6169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Feb 2019 17:35:46 +0100 Subject: [PATCH 19/22] Removed primary class from Add Query button, and changed name of Panel Options tab o General Options --- public/app/features/dashboard/panel_editor/GeneralTab.tsx | 2 +- public/app/features/dashboard/panel_editor/PanelEditor.tsx | 2 +- public/app/features/dashboard/panel_editor/QueriesTab.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/GeneralTab.tsx b/public/app/features/dashboard/panel_editor/GeneralTab.tsx index d91737195f1..01a6e39cedb 100644 --- a/public/app/features/dashboard/panel_editor/GeneralTab.tsx +++ b/public/app/features/dashboard/panel_editor/GeneralTab.tsx @@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent { render() { return ( - +
    (this.element = element)} /> ); diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index 74870b25f07..1bc42a2fd88 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -45,7 +45,7 @@ interface PanelEditorTab { const panelEditorTabTexts = { [PanelEditorTabIds.Queries]: 'Queries', [PanelEditorTabIds.Visualization]: 'Visualization', - [PanelEditorTabIds.Advanced]: 'Panel Options', + [PanelEditorTabIds.Advanced]: 'General', [PanelEditorTabIds.Alert]: 'Alert', }; diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index d46ff020906..bef23c03496 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -135,7 +135,7 @@ export class QueriesTab extends PureComponent {
    {!isAddingMixed && ( - )} From 14ba3f58914b8f6e95bf616adb34db3cd75e5d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Feb 2019 10:59:22 +0100 Subject: [PATCH 20/22] Fixed spelling issue in templating docs --- docs/sources/reference/templating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 825fc5b7ebf..b00e44943ef 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -106,7 +106,7 @@ Interpolation result: '("test1" OR "test2")' ``` ### Percentencode -Formats single & multi valued varaibles for use in URL parameters. +Formats single & multi valued variables for use in URL parameters. ```bash servers = ['foo()bar BAZ', 'test2'] From 75dd7d00360d337c6b8a5cf0023249cb2e33a8ae Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 18 Feb 2019 11:08:42 +0100 Subject: [PATCH 21/22] Fix typo in view mode cykle button --- public/app/features/dashboard/components/DashNav/DashNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 6db07b5d42e..8806e35c6b6 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -248,7 +248,7 @@ export class DashNav extends PureComponent {
    Date: Mon, 18 Feb 2019 11:41:14 +0100 Subject: [PATCH 22/22] Changed how react panels store their options (#15468) Changed how react panels store their options * Added a ReactPanelPlugin as the interface that react panels export, this way react panels have clearer api, and gives us hooks to handle migrations and a way for panel to handle panel changes in the future * Moved gauge value options into a sub oject and made editor more generic, will be moved out of gauge pane later and shared between singlestat, gauge, bargauge, honecomb * Also remove nested options prop that was there due to bug * Added missing Gauge props * Fixed gauge issue that will require migration later and also value options editor did not handle null decimals or 0 decimals * Fixed unit tests * More fixes for react panels --- .../grafana-ui/src/components/Gauge/Gauge.tsx | 2 +- packages/grafana-ui/src/types/panel.ts | 21 +++++++++- packages/grafana-ui/src/types/plugin.ts | 6 +-- public/app/core/constants.ts | 1 - .../__snapshots__/DashboardPage.test.tsx.snap | 10 ++--- .../dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../features/dashboard/dashgrid/DataPanel.tsx | 21 +++++----- .../dashboard/dashgrid/PanelChrome.tsx | 4 +- .../dashgrid/PanelPluginNotFound.tsx | 4 +- .../panel_editor/VisualizationTab.tsx | 28 ++++++-------- .../dashboard/state/DashboardMigrator.test.ts | 2 +- .../dashboard/state/DashboardMigrator.ts | 26 ++++++++++++- .../dashboard/state/PanelModel.test.ts | 14 +++++++ .../features/dashboard/state/PanelModel.ts | 27 +++++-------- ...eOptionsEditor.tsx => GaugeOptionsBox.tsx} | 9 ++++- public/app/plugins/panel/gauge/GaugePanel.tsx | 16 ++++++-- ...ePanelOptions.tsx => GaugePanelEditor.tsx} | 38 +++++++------------ ...eOptions.tsx => SingleStatValueEditor.tsx} | 35 +++++++++++++---- public/app/plugins/panel/gauge/module.tsx | 12 ++++-- public/app/plugins/panel/gauge/types.ts | 30 ++++++++++++--- ...hPanelOptions.tsx => GraphPanelEditor.tsx} | 4 +- public/app/plugins/panel/graph2/module.tsx | 4 +- public/app/plugins/panel/text2/module.tsx | 4 +- 23 files changed, 203 insertions(+), 117 deletions(-) rename public/app/plugins/panel/gauge/{GaugeOptionsEditor.tsx => GaugeOptionsBox.tsx} (85%) rename public/app/plugins/panel/gauge/{GaugePanelOptions.tsx => GaugePanelEditor.tsx} (52%) rename public/app/plugins/panel/gauge/{ValueOptions.tsx => SingleStatValueEditor.tsx} (74%) rename public/app/plugins/panel/graph2/{GraphPanelOptions.tsx => GraphPanelEditor.tsx} (91%) diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index a7435a56b3c..b8c257f4138 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -9,7 +9,7 @@ import { Themeable } from '../../index'; type TimeSeriesValue = string | number | null; export interface Props extends Themeable { - decimals: number; + decimals?: number | null; height: number; valueMappings: ValueMapping[]; maxValue: number; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 4eda85f9a28..2da48b0fec6 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,3 +1,4 @@ +import { ComponentClass } from 'react'; import { TimeSeries, LoadingState, TableData } from './data'; import { TimeRange } from './time'; @@ -19,11 +20,29 @@ export interface PanelData { tableData?: TableData; } -export interface PanelOptionsProps { +export interface PanelEditorProps { options: T; onChange: (options: T) => void; } +export class ReactPanelPlugin { + panel: ComponentClass>; + editor?: ComponentClass>; + defaults?: TOptions; + + constructor(panel: ComponentClass>) { + this.panel = panel; + } + + setEditor(editor: ComponentClass>) { + this.editor = editor; + } + + setDefaults(defaults: TOptions) { + this.defaults = defaults; + } +} + export interface PanelSize { width: number; height: number; diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index c8f156c08dc..e2dda8ad407 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -1,5 +1,5 @@ import { ComponentClass } from 'react'; -import { PanelProps, PanelOptionsProps } from './panel'; +import { ReactPanelPlugin } from './panel'; import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource'; export interface DataSourceApi { @@ -81,9 +81,7 @@ export interface PluginExports { // Panel plugin PanelCtrl?: any; - Panel?: ComponentClass; - PanelOptions?: ComponentClass; - PanelDefaults?: any; + reactPanel: ReactPanelPlugin; } export interface PluginMeta { diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index 7d295b27726..d51c4cf83d6 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20; export const PANEL_HEADER_HEIGHT = 27; export const PANEL_BORDER = 2; -export const PANEL_OPTIONS_KEY_PREFIX = 'options-'; diff --git a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap index 002cac2306e..f60e60c43a8 100644 --- a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap +++ b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap @@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -190,7 +190,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -313,7 +313,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -423,7 +423,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], @@ -518,7 +518,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti ], "refresh": undefined, "revision": undefined, - "schemaVersion": 17, + "schemaVersion": 18, "snapshot": undefined, "style": "dark", "tags": Array [], diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index bb2470cff17..9aeddd5a0d9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -173,7 +173,7 @@ export class DashboardPanel extends PureComponent { onMouseLeave={this.onMouseLeave} style={styles} > - {plugin.exports.Panel && this.renderReactPanel()} + {plugin.exports.reactPanel && this.renderReactPanel()} {plugin.exports.PanelCtrl && this.renderAngularPanel()}
    )} diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 0675c7afa60..9718e150e2a 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -162,7 +162,7 @@ export class DataPanel extends Component { } onError(message, err); - this.setState({ isFirstLoad: false }); + this.setState({ isFirstLoad: false, loading: LoadingState.Error }); } }; @@ -187,7 +187,8 @@ export class DataPanel extends Component { const { loading, isFirstLoad } = this.state; const panelData = this.getPanelData(); - if (isFirstLoad && loading === LoadingState.Loading) { + // do not render component until we have first data + if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { return this.renderLoadingState(); } @@ -201,21 +202,17 @@ export class DataPanel extends Component { return ( <> - {this.renderLoadingState()} + {loading === LoadingState.Loading && this.renderLoadingState()} {this.props.children({ loading, panelData })} ); } private renderLoadingState(): JSX.Element { - const { loading } = this.state; - if (loading === LoadingState.Loading) { - return ( -
    - -
    - ); - } - return null; + return ( +
    + +
    + ); } } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 29fe307e941..23c92b23837 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -140,7 +140,7 @@ export class PanelChrome extends PureComponent { renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; - const PanelComponent = plugin.exports.Panel; + const PanelComponent = plugin.exports.reactPanel.panel; // This is only done to increase a counter that is used by backend // image rendering (phantomjs/headless chrome) to know when to capture image @@ -154,7 +154,7 @@ export class PanelChrome extends PureComponent { loading={loading} panelData={panelData} timeRange={timeRange} - options={panel.getOptions(plugin.exports.PanelDefaults)} + options={panel.getOptions(plugin.exports.reactPanel.defaults)} width={width - 2 * variables.panelhorizontalpadding} height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding} renderCounter={renderCounter} diff --git a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx index 3f835bdbac2..4067f361f06 100644 --- a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx +++ b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelProps } from '@grafana/ui'; +import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; import { PanelPlugin } from 'app/types'; interface Props { @@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin { }, exports: { - Panel: NotFound, + reactPanel: new ReactPanelPlugin(NotFound), }, }; } diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index f9d8b3df607..8a904961a4f 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -50,33 +50,27 @@ export class VisualizationTab extends PureComponent { }; } - getPanelDefaultOptions = () => { + getReactPanelOptions = () => { const { panel, plugin } = this.props; - - if (plugin.exports.PanelDefaults) { - return panel.getOptions(plugin.exports.PanelDefaults.options); - } - - return panel.getOptions(plugin.exports.PanelDefaults); + return panel.getOptions(plugin.exports.reactPanel.defaults); }; renderPanelOptions() { const { plugin, angularPanel } = this.props; - const { PanelOptions } = plugin.exports; if (angularPanel) { return
    (this.element = element)} />; } - return ( - <> - {PanelOptions ? ( - - ) : ( -

    Visualization has no options

    - )} - - ); + if (plugin.exports.reactPanel) { + const PanelEditor = plugin.exports.reactPanel.editor; + + if (PanelEditor) { + return ; + } + } + + return

    Visualization has no options

    ; } componentDidMount() { diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index fdb309b5db5..e4b29eeddfc 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -127,7 +127,7 @@ describe('DashboardModel', () => { }); it('dashboard schema version should be set to latest', () => { - expect(model.schemaVersion).toBe(17); + expect(model.schemaVersion).toBe(18); }); it('graph thresholds should be migrated', () => { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index ba631102b81..1aa310308d5 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -22,7 +22,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades = []; - this.dashboard.schemaVersion = 17; + this.dashboard.schemaVersion = 18; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -387,6 +387,30 @@ export class DashboardMigrator { }); } + if (oldVersion < 18) { + // migrate change to gauge options + panelUpgrades.push(panel => { + if (panel['options-gauge']) { + panel.options = panel['options-gauge']; + panel.options.valueOptions = { + unit: panel.options.unit, + stat: panel.options.stat, + decimals: panel.options.decimals, + prefix: panel.options.prefix, + suffix: panel.options.suffix, + }; + // this options prop was due to a bug + delete panel.options.options; + delete panel.options.unit; + delete panel.options.stat; + delete panel.options.decimals; + delete panel.options.prefix; + delete panel.options.suffix; + delete panel['options-gauge']; + } + }); + } + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index a7e112c7ba5..d96838dc640 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -55,5 +55,19 @@ describe('PanelModel', () => { expect(model.alert).toBe(undefined); }); }); + + describe('get panel options', () => { + it('should apply defaults', () => { + model.options = { existingProp: 10 }; + const options = model.getOptions({ + defaultProp: true, + existingProp: 0, + }); + + expect(options.defaultProp).toBe(true); + expect(options.existingProp).toBe(10); + expect(model.options).toBe(options); + }); + }); }); }); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 2c0ff674e8a..fda586d2776 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -3,7 +3,6 @@ import _ from 'lodash'; // Types import { Emitter } from 'app/core/utils/emitter'; -import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants'; import { DataQuery, TimeSeries } from '@grafana/ui'; import { TableData } from '@grafana/ui/src'; @@ -92,6 +91,7 @@ export class PanelModel { timeFrom?: any; timeShift?: any; hideTimeOverride?: any; + options: object; maxDataPoints?: number; interval?: string; @@ -105,8 +105,6 @@ export class PanelModel { hasRefreshed: boolean; events: Emitter; cacheTimeout?: any; - - // cache props between plugins cachedPluginOptions?: any; constructor(model) { @@ -134,20 +132,14 @@ export class PanelModel { } getOptions(panelDefaults) { - return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults); + return _.defaultsDeep(this.options || {}, panelDefaults); } updateOptions(options: object) { - const update: any = {}; - update[this.getOptionsKey()] = options; - Object.assign(this, update); + this.options = options; this.render(); } - private getOptionsKey() { - return PANEL_OPTIONS_KEY_PREFIX + this.type; - } - getSaveModel() { const model: any = {}; for (const property in this) { @@ -240,14 +232,15 @@ export class PanelModel { // for angular panels only we need to remove all events and let angular panels do some cleanup if (fromAngularPanel) { this.destroy(); + } - for (const key of _.keys(this)) { - if (mustKeepProps[key]) { - continue; - } - - delete this[key]; + // remove panel type specific options + for (const key of _.keys(this)) { + if (mustKeepProps[key]) { + continue; } + + delete this[key]; } this.restorePanelOptions(pluginId); diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsBox.tsx similarity index 85% rename from public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx rename to public/app/plugins/panel/gauge/GaugeOptionsBox.tsx index 50e2a344a9b..b5d6acca806 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsBox.tsx @@ -1,9 +1,14 @@ +// Libraries import React, { PureComponent } from 'react'; -import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui'; +// Components +import { Switch, PanelOptionsGroup } from '@grafana/ui'; + +// Types +import { FormField, PanelEditorProps } from '@grafana/ui'; import { GaugeOptions } from './types'; -export default class GaugeOptionsEditor extends PureComponent> { +export class GaugeOptionsBox extends PureComponent> { onToggleThresholdLabels = () => this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels }); diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 5cb256ee1aa..e7e60a7c417 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -16,9 +16,10 @@ interface Props extends PanelProps {} export class GaugePanel extends PureComponent { render() { const { panelData, width, height, onInterpolate, options } = this.props; + const { valueOptions } = options; - const prefix = onInterpolate(options.prefix); - const suffix = onInterpolate(options.suffix); + const prefix = onInterpolate(valueOptions.prefix); + const suffix = onInterpolate(valueOptions.suffix); let value: TimeSeriesValue; if (panelData.timeSeries) { @@ -28,7 +29,7 @@ export class GaugePanel extends PureComponent { }); if (vmSeries[0]) { - value = vmSeries[0].stats[options.stat]; + value = vmSeries[0].stats[valueOptions.stat]; } else { value = null; } @@ -41,11 +42,18 @@ export class GaugePanel extends PureComponent { {theme => ( )} diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx similarity index 52% rename from public/app/plugins/panel/gauge/GaugePanelOptions.tsx rename to public/app/plugins/panel/gauge/GaugePanelEditor.tsx index 84726ac88bf..63031f9d895 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { - PanelOptionsProps, + PanelEditorProps, ThresholdsEditor, Threshold, PanelOptionsGrid, @@ -8,29 +8,11 @@ import { ValueMapping, } from '@grafana/ui'; -import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; -import GaugeOptionsEditor from './GaugeOptionsEditor'; -import { GaugeOptions } from './types'; - -export const defaultProps = { - options: { - minValue: 0, - maxValue: 100, - prefix: '', - showThresholdMarkers: true, - showThresholdLabels: false, - suffix: '', - decimals: 0, - stat: 'avg', - unit: 'none', - valueMappings: [], - thresholds: [], - }, -}; - -export default class GaugePanelOptions extends PureComponent> { - static defaultProps = defaultProps; +import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor'; +import { GaugeOptionsBox } from './GaugeOptionsBox'; +import { GaugeOptions, SingleStatValueOptions } from './types'; +export class GaugePanelEditor extends PureComponent> { onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, @@ -43,14 +25,20 @@ export default class GaugePanelOptions extends PureComponent + this.props.onChange({ + ...this.props.options, + valueOptions, + }); + render() { const { onChange, options } = this.props; return ( <> - - + + diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx similarity index 74% rename from public/app/plugins/panel/gauge/ValueOptions.tsx rename to public/app/plugins/panel/gauge/SingleStatValueEditor.tsx index 1fdccadddf2..86c177bb5e5 100644 --- a/public/app/plugins/panel/gauge/ValueOptions.tsx +++ b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx @@ -1,7 +1,12 @@ +// Libraries import React, { PureComponent } from 'react'; -import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui'; + +// Components import UnitPicker from 'app/core/components/Select/UnitPicker'; -import { GaugeOptions } from './types'; +import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui'; + +// Types +import { SingleStatValueOptions } from './types'; const statOptions = [ { value: 'min', label: 'Min' }, @@ -19,24 +24,40 @@ const statOptions = [ const labelWidth = 6; -export default class ValueOptions extends PureComponent> { - onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); +export interface Props { + options: SingleStatValueOptions; + onChange: (valueOptions: SingleStatValueOptions) => void; +} +export class SingleStatValueEditor extends PureComponent { + onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value }); onDecimalChange = event => { if (!isNaN(event.target.value)) { - this.props.onChange({ ...this.props.options, decimals: event.target.value }); + this.props.onChange({ + ...this.props.options, + decimals: parseInt(event.target.value, 10), + }); + } else { + this.props.onChange({ + ...this.props.options, + decimals: null, + }); } }; onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); - onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); render() { const { stat, unit, decimals, prefix, suffix } = this.props.options; + let decimalsString = ''; + if (Number.isFinite(decimals)) { + decimalsString = decimals.toString(); + } + return (
    @@ -57,7 +78,7 @@ export default class ValueOptions extends PureComponent diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 783e4825657..a32cb7cd538 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -1,4 +1,10 @@ -import GaugePanelOptions, { defaultProps } from './GaugePanelOptions'; -import { GaugePanel } from './GaugePanel'; +import { ReactPanelPlugin } from '@grafana/ui'; -export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, defaultProps as PanelDefaults }; +import { GaugePanelEditor } from './GaugePanelEditor'; +import { GaugePanel } from './GaugePanel'; +import { GaugeOptions, defaults } from './types'; + +export const reactPanel = new ReactPanelPlugin(GaugePanel); + +reactPanel.setEditor(GaugePanelEditor); +reactPanel.setDefaults(defaults); diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts index 42262178dc8..10dd475eff5 100644 --- a/public/app/plugins/panel/gauge/types.ts +++ b/public/app/plugins/panel/gauge/types.ts @@ -1,15 +1,35 @@ import { Threshold, ValueMapping } from '@grafana/ui'; export interface GaugeOptions { - decimals: number; valueMappings: ValueMapping[]; maxValue: number; minValue: number; - prefix: string; showThresholdLabels: boolean; showThresholdMarkers: boolean; - stat: string; - suffix: string; thresholds: Threshold[]; - unit: string; + valueOptions: SingleStatValueOptions; } + +export interface SingleStatValueOptions { + unit: string; + suffix: string; + stat: string; + prefix: string; + decimals?: number | null; +} + +export const defaults: GaugeOptions = { + minValue: 0, + maxValue: 100, + showThresholdMarkers: true, + showThresholdLabels: false, + valueOptions: { + prefix: '', + suffix: '', + decimals: null, + stat: 'avg', + unit: 'none', + }, + valueMappings: [], + thresholds: [], +}; diff --git a/public/app/plugins/panel/graph2/GraphPanelOptions.tsx b/public/app/plugins/panel/graph2/GraphPanelEditor.tsx similarity index 91% rename from public/app/plugins/panel/graph2/GraphPanelOptions.tsx rename to public/app/plugins/panel/graph2/GraphPanelEditor.tsx index a9c2d299589..80b17ccd5c4 100644 --- a/public/app/plugins/panel/graph2/GraphPanelOptions.tsx +++ b/public/app/plugins/panel/graph2/GraphPanelEditor.tsx @@ -3,10 +3,10 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelOptionsProps, Switch } from '@grafana/ui'; +import { PanelEditorProps, Switch } from '@grafana/ui'; import { Options } from './types'; -export class GraphPanelOptions extends PureComponent> { +export class GraphPanelEditor extends PureComponent> { onToggleLines = () => { this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines }); }; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 762d5609541..a3a3fadf6bf 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,4 +1,4 @@ import { GraphPanel } from './GraphPanel'; -import { GraphPanelOptions } from './GraphPanelOptions'; +import { GraphPanelEditor } from './GraphPanelEditor'; -export { GraphPanel as Panel, GraphPanelOptions as PanelOptions }; +export { GraphPanel as Panel, GraphPanelEditor as PanelOptions }; diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index cc3ec016273..884a5927a19 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { PanelProps } from '@grafana/ui'; +import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; export class Text2 extends PureComponent { constructor(props: PanelProps) { @@ -11,4 +11,4 @@ export class Text2 extends PureComponent { } } -export { Text2 as Panel }; +export const reactPanel = new ReactPanelPlugin(Text2);