diff --git a/CHANGELOG.md b/CHANGELOG.md index 3befbc40eb8..1a623d5ea8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,28 @@ # 5.3.0 (unreleased) +### New Major Features + +* **Alerting**: Notification reminders [#7330](https://github.com/grafana/grafana/issues/7330), thx [@jbaublitz](https://github.com/jbaublitz) +* **Dashboard**: TV & Kiosk mode changes, new cycle view mode button in dashboard toolbar [#13025](https://github.com/grafana/grafana/pull/13025) * **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht) -* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano) -* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon) +* **Postgres**: Graphical query builder [#10095](https://github.com/grafana/grafana/issues/10095), thx [svenklemm](https://github.com/svenklemm) + +### New Features + * **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622) -* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda) -* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos) -* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476) * **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi) +* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476) +* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos) +* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano) +* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda) * **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm) +* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon) ### Minor +* **Units**: Adds bitcoin axes unit. [#13125](https://github.com/grafana/grafana/pull/13125) +* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079) +* **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme) * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) * **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705) @@ -41,7 +52,6 @@ * **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z) * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) * **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766) -om/grafana/grafana/issues/12668) * **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964) * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2) * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) @@ -57,10 +67,13 @@ om/grafana/grafana/issues/12668) * **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen) * **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229) * **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486) +* **Login**: Show loading animation while waiting for authentication response on login [#12865](https://github.com/grafana/grafana/issues/12865) ### Breaking changes * Postgres datasource no longer automatically adds time column alias when using the $__timeGroup alias. However, there's code in place which should make this change backward compatible and shouldn't create any issues. +* Kiosk mode now also hides submenu (variables) +* ?inactive url parameter no longer supported, replaced with kiosk=tv url parameter ### New experimental features @@ -72,6 +85,12 @@ These are new features that's still being worked on and are in an experimental p * **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224) +# 5.2.3 (2018-08-29) + +### Important fix for LDAP & OAuth login vulnerability + +See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-6-4-security-update/10050) for details. + # 5.2.2 (2018-07-25) ### Minor @@ -440,6 +459,12 @@ The following properties have been deprecated and will be removed in a future re - `uri` property in `GET /api/search` -> Use new `url` or `uid` property instead - `meta.slug` property in `GET /api/dashboards/uid/:uid` and `GET /api/dashboards/db/:slug` -> Use new `meta.url` or `dashboard.uid` property instead +# 4.6.4 (2018-08-29) + +### Important fix for LDAP & OAuth login vulnerability + +See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-6-4-security-update/10050) for details. + # 4.6.3 (2017-12-14) ## Fixes diff --git a/Gopkg.lock b/Gopkg.lock index 6f08e208ecd..bd247d691dd 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -427,12 +427,6 @@ revision = "1744e2970ca51c86172c8190fadad617561ed6e7" version = "v1.0.0" -[[projects]] - branch = "master" - name = "github.com/shurcooL/sanitized_anchor_name" - packages = ["."] - revision = "86672fcb3f950f35f2e675df2240550f2a50762f" - [[projects]] name = "github.com/smartystreets/assertions" packages = [ @@ -679,6 +673,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb" + inputs-digest = "81a37e747b875cf870c1b9486fa3147e704dea7db8ba86f7cb942d3ddc01d3e3" solver-name = "gps-cdcl" solver-version = 1 diff --git a/conf/defaults.ini b/conf/defaults.ini index 90fc144c6e0..fff9f630690 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -538,3 +538,8 @@ container_name = [external_image_storage.local] # does not require any configuration + +[rendering] +# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer +server_url = +callback_url = diff --git a/conf/sample.ini b/conf/sample.ini index 4291071e026..2b2ae497e36 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -460,3 +460,8 @@ log_queries = [external_image_storage.local] # does not require any configuration + +[rendering] +# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer +;server_url = +;callback_url = diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 7fff41fb805..f3d4091defa 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -155,7 +155,7 @@ Since not all datasources have the same configuration settings we only have the | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | | timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source | -| esVersion | string | Elastic | Elasticsearch version as an number (2/5/56) | +| esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) | | timeField | string | Elastic | Which field that should be used as timestamp | | interval | string | Elastic | Index date time format | | authType | string | Cloudwatch | Auth provider. keys/credentials/arn | @@ -165,6 +165,8 @@ Since not all datasources have the same configuration settings we only have the | tsdbVersion | string | OpenTSDB | Version | | tsdbResolution | string | OpenTSDB | Resolution | | sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' | +| postgresVersion | number | PostgreSQL | Postgres version as a number (903/904/905/906/1000) meaning v9.3, v9.4, ..., v10 | +| timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension | #### Secure Json Data diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 58046cafae4..a5b7f4264e0 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -16,12 +16,11 @@ weight = 2 When an alert changes state, it sends out notifications. Each alert rule can have multiple notifications. In order to add a notification to an alert rule you first need -to add and configure a `notification` channel (can be email, PagerDuty or other integration). This is done from the Notification Channels page. +to add and configure a `notification` channel (can be email, PagerDuty or other integration). +This is done from the Notification Channels page. ## Notification Channel Setup -{{< imgbox max-width="30%" img="/img/docs/v50/alerts_notifications_menu.png" caption="Alerting Notification Channels" >}} - On the Notification Channels page hit the `New Channel` button to go the page where you can configure and setup a new Notification Channel. @@ -30,7 +29,31 @@ sure it's setup correctly. ### Send on all alerts -When checked, this option will nofity for all alert rules - existing and new. +When checked, this option will notify for all alert rules - existing and new. + +### Send reminders + +> Only available in Grafana v5.3 and above. + +{{< docs-imagebox max-width="600px" img="/img/docs/v53/alerting_notification_reminders.png" class="docs-image--right" caption="Alerting notification reminders setup" >}} + +When this option is checked additional notifications (reminders) will be sent for triggered alerts. You can specify how often reminders +should be sent using number of seconds (s), minutes (m) or hours (h), for example `30s`, `3m`, `5m` or `1h` etc. + +**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval). + +These examples show how often and when reminders are sent for a triggered alert. + +Alert rule evaluation interval | Send reminders every | Reminder sent every (after last alert notification) +---------- | ----------- | ----------- +`30s` | `15s` | ~30 seconds +`1m` | `5m` | ~5 minutes +`5m` | `15m` | ~15 minutes +`6m` | `20m` | ~24 minutes +`1h` | `15m` | ~1 hour +`1h` | `2h` | ~2 hours + +
## Supported Notification Types @@ -132,23 +155,23 @@ Once these two properties are set, you can send the alerts to Kafka for further ### All supported notifiers -Name | Type |Support images ------|------------ | ------ -Slack | `slack` | yes -Pagerduty | `pagerduty` | yes -Email | `email` | yes -Webhook | `webhook` | link -Kafka | `kafka` | no -Hipchat | `hipchat` | yes -VictorOps | `victorops` | yes -Sensu | `sensu` | yes -OpsGenie | `opsgenie` | yes -Threema | `threema` | yes -Pushover | `pushover` | no -Telegram | `telegram` | no -Line | `line` | no -Prometheus Alertmanager | `prometheus-alertmanager` | no -Microsoft Teams | `teams` | yes +Name | Type |Support images | Support reminders +-----|------------ | ------ | ------ | +Slack | `slack` | yes | yes +Pagerduty | `pagerduty` | yes | yes +Email | `email` | yes | yes +Webhook | `webhook` | link | yes +Kafka | `kafka` | no | yes +Hipchat | `hipchat` | yes | yes +VictorOps | `victorops` | yes | yes +Sensu | `sensu` | yes | yes +OpsGenie | `opsgenie` | yes | yes +Threema | `threema` | yes | yes +Pushover | `pushover` | no | yes +Telegram | `telegram` | no | yes +Line | `line` | no | yes +Microsoft Teams | `teams` | yes | yes +Prometheus Alertmanager | `prometheus-alertmanager` | no | no diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index fa7332e7145..488619055e2 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -88,6 +88,11 @@ So as you can see from the above scenario Grafana will not send out notification to fire if the rule already is in state `Alerting`. To improve support for queries that return multiple series we plan to track state **per series** in a future release. +> Starting with Grafana v5.3 you can configure reminders to be sent for triggered alerts. This will send additional notifications +> when an alert continues to fire. If other series (like server2 in the example above) also cause the alert rule to fire they will +> be included in the reminder notification. Depending on what notification channel you're using you may be able to take advantage +> of this feature for identifying new/existing series causing alert to fire. [Read more about notification reminders here](/alerting/notifications/#send-reminders). + ### No Data / Null values Below your conditions you can configure how the rule evaluation engine should handle queries that return no data or only null values. diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 1d195a01349..4dfe6929bc1 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -31,7 +31,9 @@ Name | Description *User* | Database user's login/username *Password* | Database user's password *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. -*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+). +*Version* | This option determines which functions are available in the query builder (only available in Grafana 5.3+). +*TimescaleDB* | TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use `time_bucket` in the `$__timeGroup` macro and display TimescaleDB specific aggregate functions in the query builder (only available in Grafana 5.3+). + ### Database User Permissions (Important!) @@ -292,5 +294,6 @@ datasources: password: "Password!" jsonData: sslmode: "disable" # disable/require/verify-ca/verify-full + postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10 timescaledb: false ``` diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index f724504156f..a27c6ca4c99 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -13,7 +13,35 @@ weight = 1 # Getting started -This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running and have added at least one [Data Source](/features/datasources/). +This guide will help you get started and acquainted with Grafana. It assumes you have a working Grafana server up and running. If not please read the [installation guide](/installation/). + +## Logging in for the first time + +To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port). + +There you will see the login page. Default username is admin and default password is admin. When you log in for the first time you will be asked to change your password. We strongly encourage you to +follow Grafana’s best practices and change the default administrator password. You can later go to user preferences and change your user name. + + +## How to add a data source + +{{< docs-imagebox img="/img/docs/v52/sidemenu-datasource.png" max-width="250px" class="docs-image--right docs-image--no-shadow">}} + +Before you create your first dashboard you need to add your data source. + +First move your cursor to the cog on the side menu which will show you the configuration menu. If the side menu is not visible click the Grafana icon in the upper left corner. The first item on the configuration menu is data sources, click on that and you'll be taken to the data sources page where you can add and edit data sources. You can also simply click the cog. + + +Click Add data source and you will come to the settings page of your new data source. + +{{< docs-imagebox img="/img/docs/v52/add-datasource.png" max-width="700px" class="docs-image--no-shadow">}} + +First, give the data source a Name and then select which Type of data source you'll want to create, see [Supported data sources](/features/datasources/#supported-data-sources/) for more information and how to configure your data source. + + +{{< docs-imagebox img="/img/docs/v52/datasource-settings.png" max-width="700px" class="docs-image--no-shadow">}} + +After you have configuered your data source you are ready to save and test. ## Beginner guides diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index 80b6e283be3..032fd508dd0 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -50,6 +50,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + [ { "id": 1, @@ -86,6 +87,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + { "id": 1, "dashboardId": 1, @@ -146,6 +148,7 @@ JSON Body Schema: ```http HTTP/1.1 200 Content-Type: application/json + { "alertId": 1, "state": "Paused", @@ -177,6 +180,7 @@ JSON Body Schema: ```http HTTP/1.1 200 Content-Type: application/json + { "state": "Paused", "message": "alert paused", @@ -204,14 +208,21 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk HTTP/1.1 200 Content-Type: application/json -{ - "id": 1, - "name": "Team A", - "type": "email", - "isDefault": true, - "created": "2017-01-01 12:45", - "updated": "2017-01-01 12:45" -} +[ + { + "id": 1, + "name": "Team A", + "type": "email", + "isDefault": false, + "sendReminder": false, + "settings": { + "addresses": "carl@grafana.com;dev@grafana.com" + }, + "created": "2018-04-23T14:44:09+02:00", + "updated": "2018-08-20T15:47:49+02:00" + } +] + ``` ## Create alert notification @@ -232,6 +243,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "name": "new alert notification", //Required "type": "email", //Required "isDefault": false, + "sendReminder": false, "settings": { "addresses": "carl@grafana.com;dev@grafana.com" } @@ -243,14 +255,18 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + { "id": 1, "name": "new alert notification", "type": "email", "isDefault": false, - "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } - "created": "2017-01-01 12:34", - "updated": "2017-01-01 12:34" + "sendReminder": false, + "settings": { + "addresses": "carl@grafana.com;dev@grafana.com" + }, + "created": "2018-04-23T14:44:09+02:00", + "updated": "2018-08-20T15:47:49+02:00" } ``` @@ -271,6 +287,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "name": "new alert notification", //Required "type": "email", //Required "isDefault": false, + "sendReminder": true, + "frequency": "15m", "settings": { "addresses: "carl@grafana.com;dev@grafana.com" } @@ -282,12 +300,17 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + { "id": 1, "name": "new alert notification", "type": "email", "isDefault": false, - "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } + "sendReminder": true, + "frequency": "15m", + "settings": { + "addresses": "carl@grafana.com;dev@grafana.com" + }, "created": "2017-01-01 12:34", "updated": "2017-01-01 12:34" } @@ -311,6 +334,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + { "message": "Notification deleted" } diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 4b14829b689..3394dfe16bc 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -266,7 +266,8 @@ The number of days the keep me logged in / remember me cookie lasts. ### secret_key -Used for signing keep me logged in / remember me cookies. +Used for signing some datasource settings like secrets and passwords. Cannot be changed without requiring an update +to datasource settings to re-encode them. ### disable_gravatar diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 4bb245a586e..13fa3440170 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -166,3 +166,8 @@ To configure Grafana add a configuration file named `custom.ini` to the Start Grafana by executing `./bin/grafana-server web`. The `grafana-server` binary needs the working directory to be the root install directory (where the binary and the `public` folder is located). + +## Logging in for the first time + +To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port). +Then follow the instructions [here](/guides/getting_started/). \ No newline at end of file diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index 6bf25ad8232..c71dc105ad4 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -217,3 +217,8 @@ chown -R root:root /etc/grafana && \ chown -R grafana:grafana /var/lib/grafana && \ chown -R grafana:grafana /usr/share/grafana ``` + +## Logging in for the first time + +To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port). +Then follow the instructions [here](/guides/getting_started/). \ No newline at end of file diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md index 12ff4adaab9..fbc00c01737 100644 --- a/docs/sources/installation/mac.md +++ b/docs/sources/installation/mac.md @@ -92,3 +92,7 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server` binary needs the working directory to be the root install directory (where the binary and the `public` folder is located). +## Logging in for the first time + +To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port). +Then follow the instructions [here](/guides/getting_started/). \ No newline at end of file diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 13597b9d921..24c301c5763 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -193,3 +193,7 @@ Start Grafana by executing `./bin/grafana-server web`. The `grafana-server` binary needs the working directory to be the root install directory (where the binary and the `public` folder is located). +## Logging in for the first time + +To run Grafana open your browser and go to http://localhost:3000/. 3000 is the default http port that Grafana listens to if you haven't [configured a different port](/installation/configuration/#http-port). +Then follow the instructions [here](/guides/getting_started/). \ No newline at end of file diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index c72bb4c0921..a476a38c3c5 100644 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -109,3 +109,11 @@ positioning system when you load them in v5. Dashboards saved in v5 will not wor external panel plugins might need to be updated to work properly. For more details on the new panel positioning system, [click here]({{< relref "reference/dashboard.md#panel-size-position" >}}) + +## Upgrading to v5.2 + +One of the database migrations included in this release will update all annotation timestamps from second to millisecond precision. If you have a large amount of annotations the database migration may take a long time to complete which may cause problems if you use systemd to run Grafana. + +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. diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 5dc87984512..572081a1c54 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -38,6 +38,11 @@ service using that tool. Read more about the [configuration options]({{< relref "configuration.md" >}}). +## Logging in for the first time + +To run Grafana open your browser and go to the port you configured above, e.g. http://localhost:8080/. +Then follow the instructions [here](/guides/getting_started/). + ## Building on Windows The Grafana backend includes Sqlite3 which requires GCC to compile. So diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 08673404572..64e67a22bae 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -141,3 +141,8 @@ Please contribute to the Grafana project and submit a pull request! Build new fe **Problem**: On Windows, getting errors about a tool not being installed even though you just installed that tool. **Solution**: It is usually because it got added to the path and you have to restart your command prompt to use it. + +## Logging in for the first time + +To run Grafana open your browser and go to the default port http://localhost:3000 or the port you have configured. +Then follow the instructions [here](/guides/getting_started/). \ No newline at end of file diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md index 9ae2989f6e6..0f138b20a17 100644 --- a/docs/sources/tutorials/ha_setup.md +++ b/docs/sources/tutorials/ha_setup.md @@ -27,7 +27,7 @@ Grafana will now persist all long term data in the database. How to configure th ## User sessions The second thing to consider is how to deal with user sessions and how to configure your load balancer infront of Grafana. -Grafana support two says of storing session data locally on disk or in a database/cache-server. +Grafana supports two ways of storing session data: locally on disk or in a database/cache-server. If you want to store sessions on disk you can use `sticky sessions` in your load balanacer. If you prefer to store session data in a database/cache-server you can use any stateless routing strategy in your load balancer (ex round robin or least connections). diff --git a/latest.json b/latest.json index 8e26289c856..7b36131fea2 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.2.0", - "testing": "5.2.0" + "stable": "5.2.3", + "testing": "5.2.3" } diff --git a/package.json b/package.json index 4c70f1ec345..e41f8412114 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "build": "grunt build", "test": "grunt test", "test:coverage": "grunt test --coverage=true", - "lint": "tslint -c tslint.json --project tsconfig.json --type-check", + "lint": "tslint -c tslint.json --project tsconfig.json", "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", "precommit": "lint-staged && grunt precommit" diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 60013fe2b10..a936d696207 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -192,14 +192,7 @@ func GetAlertNotifications(c *m.ReqContext) Response { result := make([]*dtos.AlertNotification, 0) for _, notification := range query.Result { - result = append(result, &dtos.AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, - }) + result = append(result, dtos.NewAlertNotification(notification)) } return JSON(200, result) @@ -215,7 +208,7 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } - return JSON(200, query.Result) + return JSON(200, dtos.NewAlertNotification(query.Result)) } func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response { @@ -225,7 +218,7 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma return Error(500, "Failed to create alert notification", err) } - return JSON(200, cmd.Result) + return JSON(200, dtos.NewAlertNotification(cmd.Result)) } func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response { @@ -235,7 +228,7 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma return Error(500, "Failed to update alert notification", err) } - return JSON(200, cmd.Result) + return JSON(200, dtos.NewAlertNotification(cmd.Result)) } func DeleteAlertNotification(c *m.ReqContext) Response { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index d30f2697f3f..697d0a35a08 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -1,35 +1,76 @@ package dtos import ( + "fmt" "time" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models" ) type AlertRule struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - Name string `json:"name"` - Message string `json:"message"` - State m.AlertStateType `json:"state"` - NewStateDate time.Time `json:"newStateDate"` - EvalDate time.Time `json:"evalDate"` - EvalData *simplejson.Json `json:"evalData"` - ExecutionError string `json:"executionError"` - Url string `json:"url"` - CanEdit bool `json:"canEdit"` + Id int64 `json:"id"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + Name string `json:"name"` + Message string `json:"message"` + State models.AlertStateType `json:"state"` + NewStateDate time.Time `json:"newStateDate"` + EvalDate time.Time `json:"evalDate"` + EvalData *simplejson.Json `json:"evalData"` + ExecutionError string `json:"executionError"` + Url string `json:"url"` + CanEdit bool `json:"canEdit"` +} + +func formatShort(interval time.Duration) string { + var result string + + hours := interval / time.Hour + if hours > 0 { + result += fmt.Sprintf("%dh", hours) + } + + remaining := interval - (hours * time.Hour) + mins := remaining / time.Minute + if mins > 0 { + result += fmt.Sprintf("%dm", mins) + } + + remaining = remaining - (mins * time.Minute) + seconds := remaining / time.Second + if seconds > 0 { + result += fmt.Sprintf("%ds", seconds) + } + + return result +} + +func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { + return &AlertNotification{ + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: formatShort(notification.Frequency), + SendReminder: notification.SendReminder, + Settings: notification.Settings, + } } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Settings *simplejson.Json `json:"settings"` } type AlertTestCommand struct { @@ -39,7 +80,7 @@ type AlertTestCommand struct { type AlertTestResult struct { Firing bool `json:"firing"` - State m.AlertStateType `json:"state"` + State models.AlertStateType `json:"state"` ConditionEvals string `json:"conditionEvals"` TimeMs string `json:"timeMs"` Error string `json:"error,omitempty"` @@ -59,9 +100,11 @@ type EvalMatch struct { } type NotificationTestCommand struct { - Name string `json:"name"` - Type string `json:"type"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name"` + Type string `json:"type"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + Settings *simplejson.Json `json:"settings"` } type PauseAlertCommand struct { diff --git a/pkg/api/dtos/alerting_test.go b/pkg/api/dtos/alerting_test.go new file mode 100644 index 00000000000..c38f281be9c --- /dev/null +++ b/pkg/api/dtos/alerting_test.go @@ -0,0 +1,35 @@ +package dtos + +import ( + "testing" + "time" +) + +func TestFormatShort(t *testing.T) { + tcs := []struct { + interval time.Duration + expected string + }{ + {interval: time.Hour, expected: "1h"}, + {interval: time.Hour + time.Minute, expected: "1h1m"}, + {interval: (time.Hour * 10) + time.Minute, expected: "10h1m"}, + {interval: (time.Hour * 10) + (time.Minute * 10) + time.Second, expected: "10h10m1s"}, + {interval: time.Minute * 10, expected: "10m"}, + } + + for _, tc := range tcs { + got := formatShort(tc.interval) + if got != tc.expected { + t.Errorf("expected %s got %s interval: %v", tc.expected, got, tc.interval) + } + + parsed, err := time.ParseDuration(tc.expected) + if err != nil { + t.Fatalf("could not parse expected duration") + } + + if parsed != tc.interval { + t.Errorf("expectes the parsed duration to equal the interval. Got %v expected: %v", parsed, tc.interval) + } + } +} diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 9bdb73a5858..5d4969e06af 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -152,7 +152,7 @@ func downloadFile(pluginName, filePath, url string) (err error) { return err } - r, err := zip.NewReader(bytes.NewReader(body), resp.ContentLength) + r, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) if err != nil { return err } diff --git a/pkg/cmd/grafana-cli/utils/grafana_path.go b/pkg/cmd/grafana-cli/utils/grafana_path.go index afb622bbb93..5f5c944f52b 100644 --- a/pkg/cmd/grafana-cli/utils/grafana_path.go +++ b/pkg/cmd/grafana-cli/utils/grafana_path.go @@ -42,6 +42,8 @@ func returnOsDefault(currentOs string) string { return "/usr/local/var/lib/grafana/plugins" case "freebsd": return "/var/db/grafana/plugins" + case "openbsd": + return "/var/grafana/plugins" default: //"linux" return "/var/lib/grafana/plugins" } diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index f00e6bba0fd..f1e298671d7 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -96,13 +96,17 @@ func main() { func listenToSystemSignals(server *GrafanaServerImpl) { signalChan := make(chan os.Signal, 1) - ignoreChan := make(chan os.Signal, 1) + sighupChan := make(chan os.Signal, 1) - signal.Notify(ignoreChan, syscall.SIGHUP) + signal.Notify(sighupChan, syscall.SIGHUP) signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM) - select { - case sig := <-signalChan: - server.Shutdown(fmt.Sprintf("System signal: %s", sig)) + for { + select { + case _ = <-sighupChan: + log.Reload() + case sig := <-signalChan: + server.Shutdown(fmt.Sprintf("System signal: %s", sig)) + } } } diff --git a/pkg/log/file.go b/pkg/log/file.go index d137adbf3de..b8430dc6086 100644 --- a/pkg/log/file.go +++ b/pkg/log/file.go @@ -236,3 +236,20 @@ func (w *FileLogWriter) Close() { func (w *FileLogWriter) Flush() { w.mw.fd.Sync() } + +// Reload file logger +func (w *FileLogWriter) Reload() { + // block Logger's io.Writer + w.mw.Lock() + defer w.mw.Unlock() + + // Close + fd := w.mw.fd + fd.Close() + + // Open again + err := w.StartLogger() + if err != nil { + fmt.Fprintf(os.Stderr, "Reload StartLogger: %s\n", err) + } +} diff --git a/pkg/log/handlers.go b/pkg/log/handlers.go index 14a96fdcdb4..804d8fcbd70 100644 --- a/pkg/log/handlers.go +++ b/pkg/log/handlers.go @@ -3,3 +3,7 @@ package log type DisposableHandler interface { Close() } + +type ReloadableHandler interface { + Reload() +} diff --git a/pkg/log/log.go b/pkg/log/log.go index 0e6874e1b4b..d0e6ea89f27 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -21,10 +21,12 @@ import ( var Root log15.Logger var loggersToClose []DisposableHandler +var loggersToReload []ReloadableHandler var filters map[string]log15.Lvl func init() { loggersToClose = make([]DisposableHandler, 0) + loggersToReload = make([]ReloadableHandler, 0) Root = log15.Root() Root.SetHandler(log15.DiscardHandler()) } @@ -115,6 +117,12 @@ func Close() { loggersToClose = make([]DisposableHandler, 0) } +func Reload() { + for _, logger := range loggersToReload { + logger.Reload() + } +} + func GetLogLevelFor(name string) Lvl { if level, ok := filters[name]; ok { switch level { @@ -230,6 +238,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { fileHandler.Init() loggersToClose = append(loggersToClose, fileHandler) + loggersToReload = append(loggersToReload, fileHandler) handler = fileHandler case "syslog": sysLogHandler := NewSyslog(sec, format) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 87b515f370c..42d33d5ed22 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -1,38 +1,50 @@ package models import ( + "errors" "time" "github.com/grafana/grafana/pkg/components/simplejson" ) +var ( + ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") + ErrJournalingNotFound = errors.New("alert notification journaling not found") +) + type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + SendReminder bool `json:"sendReminder"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings" binding:"Required"` OrgId int64 `json:"-"` Result *AlertNotification @@ -63,3 +75,34 @@ type GetAllAlertNotificationsQuery struct { Result []*AlertNotification } + +type AlertNotificationJournal struct { + Id int64 + OrgId int64 + AlertId int64 + NotifierId int64 + SentAt int64 + Success bool +} + +type RecordNotificationJournalCommand struct { + OrgId int64 + AlertId int64 + NotifierId int64 + SentAt int64 + Success bool +} + +type GetLatestNotificationQuery struct { + OrgId int64 + AlertId int64 + NotifierId int64 + + Result *AlertNotificationJournal +} + +type CleanNotificationJournalCommand struct { + OrgId int64 + AlertId int64 + NotifierId int64 +} diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 18f969ba1b9..46f8b3c769c 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -1,6 +1,9 @@ package alerting -import "time" +import ( + "context" + "time" +) type EvalHandler interface { Eval(evalContext *EvalContext) @@ -15,10 +18,14 @@ type Notifier interface { Notify(evalContext *EvalContext) error GetType() string NeedsImage() bool - ShouldNotify(evalContext *EvalContext) bool + + // ShouldNotify checks this evaluation should send an alert notification + ShouldNotify(ctx context.Context, evalContext *EvalContext) bool GetNotifierId() int64 GetIsDefault() bool + GetSendReminder() bool + GetFrequency() time.Duration } type NotifierSlice []Notifier diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index f4e0a0f434f..7fbd956f4f9 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -1,10 +1,10 @@ package alerting import ( + "context" "errors" "fmt" - - "golang.org/x/sync/errgroup" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" @@ -58,17 +58,47 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { return n.sendNotifications(context, notifiers) } -func (n *notificationService) sendNotifications(context *EvalContext, notifiers []Notifier) error { - g, _ := errgroup.WithContext(context.Ctx) - +func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error { for _, notifier := range notifiers { - not := notifier //avoid updating scope variable in go routine - n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) - metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() - g.Go(func() error { return not.Notify(context) }) + not := notifier + + err := bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { + n.log.Debug("trying to send notification", "id", not.GetNotifierId()) + + // Verify that we can send the notification again + // but this time within the same transaction. + if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) { + return nil + } + + n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + + //send notification + success := not.Notify(evalContext) == nil + + if evalContext.IsTestRun { + return nil + } + + //write result to db. + cmd := &m.RecordNotificationJournalCommand{ + OrgId: evalContext.Rule.OrgId, + AlertId: evalContext.Rule.Id, + NotifierId: not.GetNotifierId(), + SentAt: time.Now().Unix(), + Success: success, + } + + return bus.DispatchCtx(ctx, cmd) + }) + + if err != nil { + n.log.Error("failed to send notification", "id", not.GetNotifierId()) + } } - return g.Wait() + return nil } func (n *notificationService) uploadImage(context *EvalContext) (err error) { @@ -110,7 +140,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { return nil } -func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) { +func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (NotifierSlice, error) { query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} if err := bus.Dispatch(query); err != nil { @@ -123,7 +153,8 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] if err != nil { return nil, err } - if not.ShouldNotify(context) { + + if not.ShouldNotify(evalContext.Ctx, evalContext) { result = append(result, not) } } diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index d449167de13..9826dd1dffb 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -1,6 +1,7 @@ package notifiers import ( + "context" "time" "github.com/grafana/grafana/pkg/bus" @@ -33,7 +34,7 @@ func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, err } return &AlertmanagerNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.prometheus-alertmanager"), }, nil @@ -45,7 +46,7 @@ type AlertmanagerNotifier struct { log log.Logger } -func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool { +func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext) bool { this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState) // Do not notify when we become OK for the first time. diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 868db3aec79..ca011356247 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -1,50 +1,94 @@ package notifiers import ( - "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" + "context" + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" ) type NotifierBase struct { - Name string - Type string - Id int64 - IsDeault bool - UploadImage bool + Name string + Type string + Id int64 + IsDeault bool + UploadImage bool + SendReminder bool + Frequency time.Duration + + log log.Logger } -func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase { +func NewNotifierBase(model *models.AlertNotification) NotifierBase { uploadImage := true - value, exist := model.CheckGet("uploadImage") + value, exist := model.Settings.CheckGet("uploadImage") if exist { uploadImage = value.MustBool() } return NotifierBase{ - Id: id, - Name: name, - IsDeault: isDefault, - Type: notifierType, - UploadImage: uploadImage, + Id: model.Id, + Name: model.Name, + IsDeault: model.IsDefault, + Type: model.Type, + UploadImage: uploadImage, + SendReminder: model.SendReminder, + Frequency: model.Frequency, + log: log.New("alerting.notifier." + model.Name), } } -func defaultShouldNotify(context *alerting.EvalContext) bool { +func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify time.Time) bool { // Only notify on state change. - if context.PrevAlertState == context.Rule.State { + if context.PrevAlertState == context.Rule.State && !sendReminder { return false } + + // Do not notify if interval has not elapsed + if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { + return false + } + + // Do not notify if alert state if OK or pending even on repeated notify + if sendReminder && (context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending) { + return false + } + // Do not notify when we become OK for the first time. - if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) { + if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) { return false } + return true } -func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) +// ShouldNotify checks this evaluation should send an alert notification +func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext) bool { + cmd := &models.GetLatestNotificationQuery{ + OrgId: c.Rule.OrgId, + AlertId: c.Rule.Id, + NotifierId: n.Id, + } + + err := bus.DispatchCtx(ctx, cmd) + if err == models.ErrJournalingNotFound { + return true + } + + if err != nil { + n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) + return false + } + + if !cmd.Result.Success { + return true + } + + return defaultShouldNotify(c, n.SendReminder, n.Frequency, time.Unix(cmd.Result.SentAt, 0)) } func (n *NotifierBase) GetType() string { @@ -62,3 +106,11 @@ func (n *NotifierBase) GetNotifierId() int64 { func (n *NotifierBase) GetIsDefault() bool { return n.IsDeault } + +func (n *NotifierBase) GetSendReminder() bool { + return n.SendReminder +} + +func (n *NotifierBase) GetFrequency() time.Duration { + return n.Frequency +} diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index b7142d144cc..57b82f32466 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -2,7 +2,11 @@ package notifiers import ( "context" + "errors" "testing" + "time" + + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" @@ -10,47 +14,129 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -func TestBaseNotifier(t *testing.T) { - Convey("Base notifier tests", t, func() { - Convey("default constructor for notifiers", func() { - bJson := simplejson.New() +func TestShouldSendAlertNotification(t *testing.T) { + tcs := []struct { + name string + prevState m.AlertStateType + newState m.AlertStateType + expected bool + sendReminder bool + }{ + { + name: "pending -> ok should not trigger an notification", + newState: m.AlertStatePending, + prevState: m.AlertStateOK, + expected: false, + }, + { + name: "ok -> alerting should trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateAlerting, + expected: true, + }, + { + name: "ok -> pending should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStatePending, + expected: false, + }, + { + name: "ok -> ok should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateOK, + expected: false, + sendReminder: false, + }, + { + name: "ok -> alerting should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateAlerting, + expected: true, + sendReminder: true, + }, + { + name: "ok -> ok with reminder should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateOK, + expected: false, + sendReminder: true, + }, + } - Convey("can parse false value", func() { - bJson.Set("uploadImage", false) - - base := NewNotifierBase(1, false, "name", "email", bJson) - So(base.UploadImage, ShouldBeFalse) - }) - - Convey("can parse true value", func() { - bJson.Set("uploadImage", true) - - base := NewNotifierBase(1, false, "name", "email", bJson) - So(base.UploadImage, ShouldBeTrue) - }) - - Convey("default value should be true for backwards compatibility", func() { - base := NewNotifierBase(1, false, "name", "email", bJson) - So(base.UploadImage, ShouldBeTrue) - }) + for _, tc := range tcs { + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + State: tc.newState, }) - Convey("should notify", func() { - Convey("pending -> ok", func() { - context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ - State: m.AlertStatePending, - }) - context.Rule.State = m.AlertStateOK - So(defaultShouldNotify(context), ShouldBeFalse) + evalContext.Rule.State = tc.prevState + if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected { + t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected) + } + } +} + +func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { + Convey("base notifier", t, func() { + bus.ClearBusHandlers() + + notifier := NewNotifierBase(&m.AlertNotification{ + Id: 1, + Name: "name", + Type: "email", + Settings: simplejson.New(), + }) + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{}) + + Convey("should notify if no journaling is found", func() { + bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { + return m.ErrJournalingNotFound }) - Convey("ok -> alerting", func() { - context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ - State: m.AlertStateOK, - }) - context.Rule.State = m.AlertStateAlerting - So(defaultShouldNotify(context), ShouldBeTrue) + if !notifier.ShouldNotify(context.Background(), evalContext) { + t.Errorf("should send notifications when ErrJournalingNotFound is returned") + } + }) + + Convey("should not notify query returns error", func() { + bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { + return errors.New("some kind of error unknown error") }) + + if notifier.ShouldNotify(context.Background(), evalContext) { + t.Errorf("should not send notifications when query returns error") + } + }) + }) +} + +func TestBaseNotifier(t *testing.T) { + Convey("default constructor for notifiers", t, func() { + bJson := simplejson.New() + + model := &m.AlertNotification{ + Id: 1, + Name: "name", + Type: "email", + Settings: bJson, + } + + Convey("can parse false value", func() { + bJson.Set("uploadImage", false) + + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeFalse) + }) + + Convey("can parse true value", func() { + bJson.Set("uploadImage", true) + + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeTrue) + }) + + Convey("default value should be true for backwards compatibility", func() { + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeTrue) }) }) } diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 14eacef5831..738e43af2d2 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -32,7 +32,7 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.dingding"), }, nil diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 3ffa7484870..57d9d438fa2 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -39,7 +39,7 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &DiscordNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), WebhookURL: url, log: log.New("alerting.notifier.discord"), }, nil diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 562ffbe1269..17b88f7d97f 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -52,7 +52,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }) return &EmailNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Addresses: addresses, log: log.New("alerting.notifier.email"), }, nil diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 58e1b7bd71e..1c284ec3d2b 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -59,7 +59,7 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err roomId := model.Settings.Get("roomid").MustString() return &HipChatNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, ApiKey: apikey, RoomId: roomId, diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index 92f6489106b..d8d19fc5dae 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -43,7 +43,7 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &KafkaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Endpoint: endpoint, Topic: topic, log: log.New("alerting.notifier.kafka"), diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 4814662f3a9..9e3888b8f95 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -39,7 +39,7 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &LineNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Token: token, log: log.New("alerting.notifier.line"), }, nil diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index f0f5142cf05..84148a0d99c 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -56,7 +56,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &OpsGenieNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), ApiKey: apiKey, ApiUrl: apiUrl, AutoClose: autoClose, diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 02219b2203d..bf85466388f 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -51,7 +51,7 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &PagerdutyNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Key: key, AutoResolve: autoResolve, log: log.New("alerting.notifier.pagerduty"), diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index cbe9e16801a..55dc02c5f4a 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -99,7 +99,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) return nil, alerting.ValidationError{Reason: "API token not given"} } return &PushoverNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), UserKey: userKey, ApiToken: apiToken, Priority: priority, diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index 9f77801d458..21d5d3d9d9e 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -51,7 +51,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &SensuNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, User: model.Settings.Get("username").MustString(), Source: model.Settings.Get("source").MustString(), diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index c1dadba414d..374b49ea957 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -78,7 +78,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { uploadImage := model.Settings.Get("uploadImage").MustBool(true) return &SlackNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, Recipient: recipient, Mention: mention, diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 4e34e16ab51..09bcf600533 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -33,7 +33,7 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &TeamsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.teams"), }, nil diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index ca24c996914..b03f7ca38c5 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -78,7 +78,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &TelegramNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), BotToken: botToken, ChatID: chatId, UploadImage: uploadImage, diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index e4ffffc9108..28a62fade17 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -106,7 +106,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &ThreemaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), GatewayID: gatewayID, RecipientID: recipientID, APISecret: apiSecret, diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index a753ca3cbf6..3093aec9957 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -51,7 +51,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e } return &VictoropsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), URL: url, AutoResolve: autoResolve, log: log.New("alerting.notifier.victorops"), diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 4c97ed2b75e..4045e496af9 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -47,7 +47,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, User: model.Settings.Get("username").MustString(), Password: model.Settings.Get("password").MustString(), diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index c57b28c7c3e..363d06d1132 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,6 +88,18 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } + if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { + for _, notifierId := range evalContext.Rule.Notifications { + cmd := &m.CleanNotificationJournalCommand{ + AlertId: evalContext.Rule.Id, + NotifierId: notifierId, + OrgId: evalContext.Rule.OrgId, + } + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) + } + } + } handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index 9084ca27353..d47dfaeaae1 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -2,6 +2,7 @@ package rendering import ( "context" + "fmt" "io" "net" "net/http" @@ -20,14 +21,13 @@ var netTransport = &http.Transport{ TLSHandshakeTimeout: 5 * time.Second, } +var netClient = &http.Client{ + Transport: netTransport, +} + func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*RenderResult, error) { filePath := rs.getFilePathForNewImage() - var netClient = &http.Client{ - Timeout: opts.Timeout, - Transport: netTransport, - } - rendererUrl, err := url.Parse(rs.Cfg.RendererUrl) if err != nil { return nil, err @@ -35,10 +35,10 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend queryParams := rendererUrl.Query() queryParams.Add("url", rs.getURL(opts.Path)) - queryParams.Add("renderKey", rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole)) + queryParams.Add("renderKey", rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole)) queryParams.Add("width", strconv.Itoa(opts.Width)) queryParams.Add("height", strconv.Itoa(opts.Height)) - queryParams.Add("domain", rs.getLocalDomain()) + queryParams.Add("domain", rs.domain) queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone)) queryParams.Add("encoding", opts.Encoding) queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds()))) @@ -49,20 +49,48 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend return nil, err } + reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2) + defer cancel() + + req = req.WithContext(reqContext) + // make request to renderer server resp, err := netClient.Do(req) if err != nil { - return nil, err + rs.log.Error("Failed to send request to remote rendering service.", "error", err) + return nil, fmt.Errorf("Failed to send request to remote rendering service. %s", err) } // save response to file defer resp.Body.Close() + + // check for timeout first + if reqContext.Err() == context.DeadlineExceeded { + rs.log.Info("Rendering timed out") + return nil, ErrTimeout + } + + // if we didnt get a 200 response, something went wrong. + if resp.StatusCode != http.StatusOK { + rs.log.Error("Remote rendering request failed", "error", resp.Status) + return nil, fmt.Errorf("Remote rendering request failed. %d: %s", resp.StatusCode, resp.Status) + } + out, err := os.Create(filePath) if err != nil { return nil, err } defer out.Close() - io.Copy(out, resp.Body) + _, err = io.Copy(out, resp.Body) + if err != nil { + // check that we didnt timeout while receiving the response. + if reqContext.Err() == context.DeadlineExceeded { + rs.log.Info("Rendering timed out") + return nil, ErrTimeout + } + rs.log.Error("Remote rendering request failed", "error", err) + return nil, fmt.Errorf("Remote rendering request failed. %s", err) + } return &RenderResult{FilePath: filePath}, err } diff --git a/pkg/services/rendering/phantomjs.go b/pkg/services/rendering/phantomjs.go index 87ccaf6b5d2..1bd7489c153 100644 --- a/pkg/services/rendering/phantomjs.go +++ b/pkg/services/rendering/phantomjs.go @@ -49,7 +49,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) ( fmt.Sprintf("width=%v", opts.Width), fmt.Sprintf("height=%v", opts.Height), fmt.Sprintf("png=%v", pngPath), - fmt.Sprintf("domain=%v", rs.getLocalDomain()), + fmt.Sprintf("domain=%v", rs.domain), fmt.Sprintf("timeout=%v", opts.Timeout.Seconds()), fmt.Sprintf("renderKey=%v", renderKey), } diff --git a/pkg/services/rendering/plugin_mode.go b/pkg/services/rendering/plugin_mode.go index 550779ad7c3..58fef2b095f 100644 --- a/pkg/services/rendering/plugin_mode.go +++ b/pkg/services/rendering/plugin_mode.go @@ -77,10 +77,10 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, opts Opts) (*Re Height: int32(opts.Height), FilePath: pngPath, Timeout: int32(opts.Timeout.Seconds()), - RenderKey: rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole), + RenderKey: rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole), Encoding: opts.Encoding, Timezone: isoTimeOffsetToPosixTz(opts.Timezone), - Domain: rs.getLocalDomain(), + Domain: rs.domain, }) if err != nil { diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 799aecc3e88..ff4a67cc9b6 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -3,6 +3,8 @@ package rendering import ( "context" "fmt" + "net/url" + "os" "path/filepath" plugin "github.com/hashicorp/go-plugin" @@ -27,12 +29,31 @@ type RenderingService struct { grpcPlugin pluginModel.RendererPlugin pluginInfo *plugins.RendererPlugin renderAction renderFunc + domain string Cfg *setting.Cfg `inject:""` } func (rs *RenderingService) Init() error { rs.log = log.New("rendering") + + // ensure ImagesDir exists + err := os.MkdirAll(rs.Cfg.ImagesDir, 0700) + if err != nil { + return err + } + + // set value used for domain attribute of renderKey cookie + if rs.Cfg.RendererUrl != "" { + // RendererCallbackUrl has already been passed, it wont generate an error. + u, _ := url.Parse(rs.Cfg.RendererCallbackUrl) + rs.domain = u.Hostname() + } else if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR { + rs.domain = setting.HttpAddr + } else { + rs.domain = "localhost" + } + return nil } @@ -82,16 +103,17 @@ func (rs *RenderingService) getFilePathForNewImage() string { } func (rs *RenderingService) getURL(path string) string { - // &render=1 signals to the legacy redirect layer to - return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.getLocalDomain(), setting.HttpPort, path) -} + if rs.Cfg.RendererUrl != "" { + // The backend rendering service can potentially be remote. + // So we need to use the root_url to ensure the rendering service + // can reach this Grafana instance. + + // &render=1 signals to the legacy redirect layer to + return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path) -func (rs *RenderingService) getLocalDomain() string { - if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR { - return setting.HttpAddr } - - return "localhost" + // &render=1 signals to the legacy redirect layer to + return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.domain, setting.HttpPort, path) } func (rs *RenderingService) getRenderKey(orgId, userId int64, orgRole models.RoleType) string { diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 651241f7714..8fb1e2212a9 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -2,6 +2,7 @@ package sqlstore import ( "bytes" + "context" "fmt" "strings" "time" @@ -17,6 +18,9 @@ func init() { bus.AddHandler("sql", DeleteAlertNotification) bus.AddHandler("sql", GetAlertNotificationsToSend) bus.AddHandler("sql", GetAllAlertNotifications) + bus.AddHandlerCtx("sql", RecordNotificationJournal) + bus.AddHandlerCtx("sql", GetLatestNotification) + bus.AddHandlerCtx("sql", CleanNotificationJournal) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -53,7 +57,9 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro alert_notification.created, alert_notification.updated, alert_notification.settings, - alert_notification.is_default + alert_notification.is_default, + alert_notification.send_reminder, + alert_notification.frequency FROM alert_notification `) @@ -91,7 +97,9 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS alert_notification.created, alert_notification.updated, alert_notification.settings, - alert_notification.is_default + alert_notification.is_default, + alert_notification.send_reminder, + alert_notification.frequency FROM alert_notification `) @@ -137,17 +145,31 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } - alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - Created: time.Now(), - Updated: time.Now(), - IsDefault: cmd.IsDefault, + var frequency time.Duration + if cmd.SendReminder { + if cmd.Frequency == "" { + return m.ErrNotificationFrequencyNotFound + } + + frequency, err = time.ParseDuration(cmd.Frequency) + if err != nil { + return err + } } - if _, err = sess.Insert(alertNotification); err != nil { + alertNotification := &m.AlertNotification{ + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + SendReminder: cmd.SendReminder, + Frequency: frequency, + Created: time.Now(), + Updated: time.Now(), + IsDefault: cmd.IsDefault, + } + + if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil { return err } @@ -179,16 +201,77 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Name = cmd.Name current.Type = cmd.Type current.IsDefault = cmd.IsDefault + current.SendReminder = cmd.SendReminder - sess.UseBool("is_default") + if current.SendReminder { + if cmd.Frequency == "" { + return m.ErrNotificationFrequencyNotFound + } + + frequency, err := time.ParseDuration(cmd.Frequency) + if err != nil { + return err + } + + current.Frequency = frequency + } + + sess.UseBool("is_default", "send_reminder") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err } else if affected == 0 { - return fmt.Errorf("Could not find alert notification") + return fmt.Errorf("Could not update alert notification") } cmd.Result = ¤t return nil }) } + +func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJournalCommand) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { + journalEntry := &m.AlertNotificationJournal{ + OrgId: cmd.OrgId, + AlertId: cmd.AlertId, + NotifierId: cmd.NotifierId, + SentAt: cmd.SentAt, + Success: cmd.Success, + } + + if _, err := sess.Insert(journalEntry); err != nil { + return err + } + + return nil + }) +} + +func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { + nj := &m.AlertNotificationJournal{} + + _, err := sess.Desc("alert_notification_journal.sent_at"). + Limit(1). + Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj) + + if err != nil { + return err + } + + if nj.AlertId == 0 && nj.Id == 0 && nj.NotifierId == 0 && nj.OrgId == 0 { + return m.ErrJournalingNotFound + } + + cmd.Result = nj + return nil + }) +} + +func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { + sql := "DELETE FROM alert_notification_journal WHERE alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" + _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) + return err + }) +} diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 2dbf9de5ca8..83fb42db9bb 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -1,7 +1,9 @@ package sqlstore import ( + "context" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" @@ -11,7 +13,48 @@ import ( func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Testing Alert notification sql access", t, func() { InitTestDB(t) - var err error + + Convey("Alert notification journal", func() { + var alertId int64 = 5 + var orgId int64 = 5 + var notifierId int64 = 5 + + Convey("Getting last journal should raise error if no one exists", func() { + query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + err := GetLatestNotification(context.Background(), query) + So(err, ShouldEqual, m.ErrJournalingNotFound) + + Convey("shoulbe be able to record two journaling events", func() { + createCmd := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, Success: true, SentAt: 1} + + err := RecordNotificationJournal(context.Background(), createCmd) + So(err, ShouldBeNil) + + createCmd.SentAt += 1000 //increase epoch + + err = RecordNotificationJournal(context.Background(), createCmd) + So(err, ShouldBeNil) + + Convey("get last journaling event", func() { + err := GetLatestNotification(context.Background(), query) + So(err, ShouldBeNil) + So(query.Result.SentAt, ShouldEqual, 1001) + + Convey("be able to clear all journaling for an notifier", func() { + cmd := &m.CleanNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId} + err := CleanNotificationJournal(context.Background(), cmd) + So(err, ShouldBeNil) + + Convey("querying for last junaling should raise error", func() { + query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + err := GetLatestNotification(context.Background(), query) + So(err, ShouldEqual, m.ErrJournalingNotFound) + }) + }) + }) + }) + }) + }) Convey("Alert notifications should be empty", func() { cmd := &m.GetAlertNotificationsQuery{ @@ -24,19 +67,75 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(cmd.Result, ShouldBeNil) }) - Convey("Can save Alert Notification", func() { + Convey("Cannot save alert notifier with send reminder = true", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - Settings: simplejson.New(), + Name: "ops", + Type: "email", + OrgId: 1, + SendReminder: true, + Settings: simplejson.New(), } - err = CreateAlertNotificationCommand(cmd) + Convey("and missing frequency", func() { + err := CreateAlertNotificationCommand(cmd) + So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound) + }) + + Convey("invalid frequency", func() { + cmd.Frequency = "invalid duration" + + err := CreateAlertNotificationCommand(cmd) + So(err.Error(), ShouldEqual, "time: invalid duration invalid duration") + }) + }) + + Convey("Cannot update alert notifier with send reminder = false", func() { + cmd := &m.CreateAlertNotificationCommand{ + Name: "ops update", + Type: "email", + OrgId: 1, + SendReminder: false, + Settings: simplejson.New(), + } + + err := CreateAlertNotificationCommand(cmd) + So(err, ShouldBeNil) + + updateCmd := &m.UpdateAlertNotificationCommand{ + Id: cmd.Result.Id, + SendReminder: true, + } + + Convey("and missing frequency", func() { + err := UpdateAlertNotification(updateCmd) + So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound) + }) + + Convey("invalid frequency", func() { + updateCmd.Frequency = "invalid duration" + + err := UpdateAlertNotification(updateCmd) + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "time: invalid duration invalid duration") + }) + }) + + Convey("Can save Alert Notification", func() { + cmd := &m.CreateAlertNotificationCommand{ + Name: "ops", + Type: "email", + OrgId: 1, + SendReminder: true, + Frequency: "10s", + Settings: simplejson.New(), + } + + err := CreateAlertNotificationCommand(cmd) So(err, ShouldBeNil) So(cmd.Result.Id, ShouldNotEqual, 0) So(cmd.Result.OrgId, ShouldNotEqual, 0) So(cmd.Result.Type, ShouldEqual, "email") + So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) Convey("Cannot save Alert Notification with the same name", func() { err = CreateAlertNotificationCommand(cmd) @@ -45,25 +144,42 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can update alert notification", func() { newCmd := &m.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - Settings: simplejson.New(), - Id: cmd.Result.Id, + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + SendReminder: true, + Frequency: "60s", + Settings: simplejson.New(), + Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) So(newCmd.Result.Name, ShouldEqual, "NewName") + So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second) + }) + + Convey("Can update alert notification to disable sending of reminders", func() { + newCmd := &m.UpdateAlertNotificationCommand{ + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + SendReminder: false, + Settings: simplejson.New(), + Id: cmd.Result.Id, + } + err := UpdateAlertNotification(newCmd) + So(err, ShouldBeNil) + So(newCmd.Result.SendReminder, ShouldBeFalse) }) }) Convey("Can search using an array of ids", func() { - cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()} - cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()} - cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()} - cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()} + cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()} + otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil) diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 2a364d5f464..e27e64c6124 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -65,6 +65,13 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{ Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0", })) + mg.AddMigration("Add column frequency", NewAddColumnMigration(alert_notification, &Column{ + Name: "frequency", Type: DB_BigInt, Nullable: true, + })) + mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{ + Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0", + })) + mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{ @@ -82,4 +89,22 @@ func addAlertMigrations(mg *Migrator) { {Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "settings", Type: DB_Text, Nullable: false}, })) + + notification_journal := Table{ + Name: "alert_notification_journal", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "alert_id", Type: DB_BigInt, Nullable: false}, + {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, + {Name: "sent_at", Type: DB_BigInt, Nullable: false}, + {Name: "success", Type: DB_Bool, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, + }, + } + + mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) + mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index eb61568261d..789622ca0dd 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -197,6 +197,7 @@ type Cfg struct { ImagesDir string PhantomDir string RendererUrl string + RendererCallbackUrl string DisableBruteForceLoginProtection bool TempDataLifetime time.Duration @@ -641,6 +642,18 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { // Rendering renderSec := iniFile.Section("rendering") cfg.RendererUrl = renderSec.Key("server_url").String() + cfg.RendererCallbackUrl = renderSec.Key("callback_url").String() + if cfg.RendererCallbackUrl == "" { + cfg.RendererCallbackUrl = AppUrl + } else { + if cfg.RendererCallbackUrl[len(cfg.RendererCallbackUrl)-1] != '/' { + cfg.RendererCallbackUrl += "/" + } + _, err := url.Parse(cfg.RendererCallbackUrl) + if err != nil { + log.Fatal(4, "Invalid callback_url(%s): %s", cfg.RendererCallbackUrl, err) + } + } cfg.ImagesDir = filepath.Join(DataPath, "png") cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs") cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 9de22c86811..affb3c3e7ca 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -20,6 +20,7 @@ func TestLoadingSettings(t *testing.T) { So(err, ShouldBeNil) So(AdminUser, ShouldEqual, "admin") + So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/") }) Convey("Should be able to override via environment variables", func() { @@ -178,5 +179,15 @@ func TestLoadingSettings(t *testing.T) { So(InstanceName, ShouldEqual, hostname) }) + Convey("Reading callback_url should add trailing slash", func() { + cfg := NewCfg() + cfg.Load(&CommandLineArgs{ + HomePath: "../../", + Args: []string{"cfg:rendering.callback_url=http://myserver/renderer"}, + }) + + So(cfg.RendererCallbackUrl, ShouldEqual, "http://myserver/renderer/") + }) + }) } diff --git a/public/app/app.ts b/public/app/app.ts index d9e31018af9..8e30747072e 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -21,7 +21,7 @@ import _ from 'lodash'; import moment from 'moment'; // add move to lodash for backward compatabiltiy -_.move = function(array, fromIndex, toIndex) { +_.move = (array, fromIndex, toIndex) => { array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); return array; }; @@ -76,9 +76,9 @@ export class GrafanaApp { $provide.decorator('$http', [ '$delegate', '$templateCache', - function($delegate, $templateCache) { + ($delegate, $templateCache) => { const get = $delegate.get; - $delegate.get = function(url, config) { + $delegate.get = (url, config) => { if (url.match(/\.html$/)) { // some template's already exist in the cache if (!$templateCache.get(url)) { @@ -105,9 +105,9 @@ export class GrafanaApp { 'react', ]; - const module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; + const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; - _.each(module_types, type => { + _.each(moduleTypes, type => { const moduleName = 'grafana.' + type; this.useModule(angular.module(moduleName, [])); }); @@ -135,7 +135,7 @@ export class GrafanaApp { this.preBootModules = null; }); }) - .catch(function(err) { + .catch(err => { console.log('Application boot failed:', err); }); } diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 92712709858..16175747a06 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -173,6 +173,12 @@ export class Explore extends React.Component { datasource.init(); } + // Keep queries but reset edit state + const nextQueries = this.state.queries.map(q => ({ + ...q, + edited: false, + })); + this.setState( { datasource, @@ -182,6 +188,7 @@ export class Explore extends React.Component { supportsLogs, supportsTable, datasourceLoading: false, + queries: nextQueries, }, () => datasourceError === null && this.onSubmit() ); diff --git a/public/app/containers/Explore/utils/debounce.ts b/public/app/containers/Explore/utils/debounce.ts index 9f2bd35e116..a7c9450a6c1 100644 --- a/public/app/containers/Explore/utils/debounce.ts +++ b/public/app/containers/Explore/utils/debounce.ts @@ -1,10 +1,10 @@ // Based on underscore.js debounce() export default function debounce(func, wait) { let timeout; - return function() { + return function(this: any) { const context = this; const args = arguments; - const later = function() { + const later = () => { timeout = null; func.apply(context, args); }; diff --git a/public/app/containers/Explore/utils/dom.ts b/public/app/containers/Explore/utils/dom.ts index 6ba21b54c83..381c150e3f4 100644 --- a/public/app/containers/Explore/utils/dom.ts +++ b/public/app/containers/Explore/utils/dom.ts @@ -1,6 +1,6 @@ // Node.closest() polyfill if ('Element' in window && !Element.prototype.closest) { - Element.prototype.closest = function(s) { + Element.prototype.closest = function(this: any, s) { const matches = (this.document || this.ownerDocument).querySelectorAll(s); let el = this; let i; @@ -9,7 +9,8 @@ if ('Element' in window && !Element.prototype.closest) { i = matches.length; // eslint-disable-next-line while (--i >= 0 && matches.item(i) !== el) {} - } while (i < 0 && (el = el.parentElement)); + el = el.parentElement; + } while (i < 0 && el); return el; }; } diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index d0feee75184..2a5743bea96 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -6,6 +6,7 @@ import { NavStore } from 'app/stores/NavStore/NavStore'; import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; interface Props { nav: typeof NavStore.Type; @@ -61,48 +62,81 @@ export class TeamList extends React.Component { ); } + renderTeamList(teams) { + return ( +
+
+
+ +
+ + + +
+ + + + + + + + + {teams.filteredTeams.map(team => this.renderTeamMember(team))} +
+ NameEmailMembers +
+
+
+ ); + } + + renderEmptyList() { + return ( +
+ +
+ ); + } + render() { const { nav, teams } = this.props; + let view; + + if (teams.filteredTeams.length > 0) { + view = this.renderTeamList(teams); + } else { + view = this.renderEmptyList(); + } + return (
-
-
-
- -
- - - -
- - - - - - - - - {teams.filteredTeams.map(team => this.renderTeamMember(team))} -
- NameEmailMembers -
-
-
+ {view}
); } diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index a4439509f8e..18e9d8dbd84 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -2,16 +2,16 @@ import { react2AngularDirective } from 'app/core/utils/react2angular'; import { PasswordStrength } from './components/PasswordStrength'; import PageHeader from './components/PageHeader/PageHeader'; import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; -import LoginBackground from './components/Login/LoginBackground'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; +import { SideMenu } from './components/sidemenu/SideMenu'; import DashboardPermissions from './components/Permissions/DashboardPermissions'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); + react2AngularDirective('sidemenu', SideMenu, []); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); - react2AngularDirective('loginBackground', LoginBackground, []); react2AngularDirective('searchResult', SearchResult, []); react2AngularDirective('tagFilter', TagFilter, [ 'tags', diff --git a/public/app/core/app_events.ts b/public/app/core/app_events.ts index 26dd74bcb00..6af7913167b 100644 --- a/public/app/core/app_events.ts +++ b/public/app/core/app_events.ts @@ -1,4 +1,4 @@ import { Emitter } from './utils/emitter'; -var appEvents = new Emitter(); +const appEvents = new Emitter(); export default appEvents; diff --git a/public/app/core/components/Login/LoginBackground.tsx b/public/app/core/components/Login/LoginBackground.tsx deleted file mode 100644 index 83e228ab6e0..00000000000 --- a/public/app/core/components/Login/LoginBackground.tsx +++ /dev/null @@ -1,1240 +0,0 @@ -import React, { Component } from 'react'; - -const xCount = 50; -const yCount = 50; - -function Cell({ x, y, flipIndex }) { - const index = (y * xCount) + x; - const bgColor1 = getColor(x, y); - return ( -
- ); -} - -function getRandomInt(min, max) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min)) + min; //The maximum is exclusive and the minimum is inclusive -} - -export default class LoginBackground extends Component { - cancelInterval: any; - - constructor(props) { - super(props); - - this.state = { - flipIndex: null, - }; - - this.flipElements = this.flipElements.bind(this); - } - - flipElements() { - const elementIndexToFlip = getRandomInt(0, (xCount * yCount) - 1); - this.setState(prevState => { - return { - ...prevState, - flipIndex: elementIndexToFlip, - }; - }); - } - - componentWillMount() { - this.cancelInterval = setInterval(this.flipElements, 3000); - } - - componentWillUnmount() { - clearInterval(this.cancelInterval); - } - - render() { - console.log('re-render!', this.state.flipIndex); - - return ( -
- {Array.from(Array(yCount)).map((el, y) => { - return ( -
- {Array.from(Array(xCount)).map((el2, x) => { - return ( - - ); - })} -
- ); - })} -
- ); - } -} - -function getColor(x, y) { - const colors = [ - '#14161A', - '#111920', - '#121E27', - '#13212B', - '#122029', - '#101C24', - '#0F1B23', - '#0F1B22', - '#111C24', - '#101A22', - '#101A21', - '#111D25', - '#101E27', - '#101D26', - '#101B23', - '#11191E', - '#131519', - '#131518', - '#101B21', - '#121F29', - '#10232D', - '#11212B', - '#0E1C25', - '#0E1C24', - '#111F29', - '#11222B', - '#101E28', - '#102028', - '#111F2A', - '#11202A', - '#11191F', - '#121417', - '#12191D', - '#101D25', - '#11212C', - '#10242F', - '#0F212B', - '#0F1E27', - '#0F1D26', - '#0F1F29', - '#0F2029', - '#11232E', - '#10212B', - '#10222C', - '#0F202A', - '#112530', - '#10252F', - '#0F242E', - '#10222D', - '#10202A', - '#0F1C24', - '#0F1E28', - '#0F212A', - '#0F222B', - '#14171A', - '#0F1A20', - '#0F1C25', - '#10232E', - '#0E202A', - '#0E1E27', - '#0E1D26', - '#0F202B', - '#11232F', - '#102632', - '#102530', - '#122430', - '#0F1B21', - '#0F212C', - '#0E1F29', - '#112531', - '#0F2734', - '#0F2835', - '#0D1B23', - '#0F1A21', - '#0F1A23', - '#0F1D27', - '#0F222D', - '#102430', - '#102531', - '#10222E', - '#0F232D', - '#0E2633', - '#0E2734', - '#0F2834', - '#0E2835', - '#0F2633', - '#0F2532', - '#0E1A22', - '#0D1C24', - '#0F2735', - '#0F2937', - '#102A38', - '#112938', - '#102A39', - '#0F2A38', - '#102836', - '#0E1B23', - '#0F2938', - '#102A3A', - '#102D3D', - '#0F3040', - '#102D3E', - '#0F2E3E', - '#112C3B', - '#102B3B', - '#102B3A', - '#102D3C', - '#0F2A39', - '#0F2634', - '#0E2029', - '#0E1A21', - '#0F2B39', - '#0F2D3D', - '#0F2F40', - '#0E3142', - '#113445', - '#122431', - '#102E3E', - '#0F3345', - '#0E2F40', - '#0F3143', - '#102C3C', - '#0F2B3A', - '#0F1F28', - '#0F3344', - '#113548', - '#113C51', - '#144258', - '#103A4E', - '#103A4F', - '#103547', - '#10364A', - '#103649', - '#0F3448', - '#102C3A', - '#0F2836', - '#103447', - '#0F384C', - '#123F55', - '#15445A', - '#133F55', - '#103B50', - '#113E54', - '#103446', - '#0F3A4F', - '#0F3548', - '#0D3142', - '#102C3B', - '#0E2937', - '#103D52', - '#0E3544', - '#184C65', - '#154760', - '#14435B', - '#15465F', - '#124159', - '#0F3D53', - '#103C51', - '#0F3447', - '#0E3243', - '#113143', - '#113D53', - '#184B64', - '#184D67', - '#184C66', - '#174A63', - '#15455C', - '#13425A', - '#14445A', - '#10384C', - '#0E3446', - '#10181E', - '#103243', - '#0F384D', - '#14455C', - '#164761', - '#164C66', - '#1D627D', - '#12425A', - '#164A63', - '#14465D', - '#13435A', - '#0A2B38', - '#0F3446', - '#0D2F40', - '#0D2F3F', - '#0F2531', - '#102937', - '#10384B', - '#0F3649', - '#184E68', - '#1A5472', - '#184D68', - '#154A63', - '#19506B', - '#19536F', - '#1A4F69', - '#144760', - '#114058', - '#0E3A4F', - '#0E3547', - '#0C3042', - '#0E1B24', - '#11222C', - '#154C65', - '#1A5776', - '#1B5675', - '#113847', - '#1A5371', - '#194E68', - '#0E2D3D', - '#112D3B', - '#113D52', - '#18516D', - '#1A5979', - '#1B5878', - '#19526E', - '#1A526E', - '#13435B', - '#0F3E55', - '#0B374C', - '#0E3448', - '#0D2E3F', - '#0F2B3B', - '#112E3E', - '#113B50', - '#15465D', - '#1A526F', - '#1E5E81', - '#1D5B7B', - '#1A5777', - '#154456', - '#113949', - '#0D394E', - '#0F3549', - '#0F2C3B', - '#0E2733', - '#112E3D', - '#123D52', - '#10394C', - '#1B5674', - '#1A5370', - '#144861', - '#104058', - '#104159', - '#0E384C', - '#0D2D3D', - '#0E2533', - '#112C3A', - '#1B5979', - '#1B5C7D', - '#1A5675', - '#104057', - '#0F3C51', - '#11425A', - '#0E394D', - '#0C3243', - '#0E2735', - '#112F3E', - '#134158', - '#1D5E7F', - '#1D6083', - '#1C5877', - '#1A5573', - '#184D66', - '#164962', - '#0F3D54', - '#0E3D53', - '#0E3447', - '#0F2A3A', - '#0F2936', - '#101F28', - '#103040', - '#124056', - '#164E69', - '#144B64', - '#164D66', - '#0F3E54', - '#0E3B51', - '#0D3346', - '#0E1F27', - '#124158', - '#164961', - '#0E3C52', - '#19506C', - '#0F2C3C', - '#0E3244', - '#0E2A39', - '#0E2938', - '#113040', - '#134057', - '#1A5471', - '#154B63', - '#1C597A', - '#164760', - '#10374B', - '#0E374C', - '#0E384D', - '#11242F', - '#10394D', - '#18526E', - '#154B65', - '#103F55', - '#0D3345', - '#102532', - '#102029', - '#113142', - '#1B5973', - '#1A516B', - '#1C5979', - '#1C5A7A', - '#184A65', - '#164C65', - '#0D3041', - '#123142', - '#123E54', - '#1B5877', - '#1A5574', - '#1C5878', - '#13435C', - '#0F374B', - '#0C3143', - '#112F40', - '#123C51', - '#174E68', - '#1D5C7D', - '#14465F', - '#0F3F56', - '#0B3041', - '#123243', - '#15435B', - '#19516D', - '#1D5D7E', - '#1C5C7D', - '#184F69', - '#11374B', - '#103E54', - '#0E3143', - '#0F2D3C', - '#11242E', - '#133445', - '#1A5674', - '#1D6184', - '#1F658B', - '#0D3A50', - '#0C374B', - '#154862', - '#164B64', - '#154961', - '#0D384D', - '#102631', - '#113242', - '#134259', - '#185270', - '#1D6386', - '#1E678C', - '#1C5978', - '#0D3549', - '#0F2632', - '#184961', - '#1D5E80', - '#1E6488', - '#1F678D', - '#1E5B7C', - '#164862', - '#19526D', - '#113C52', - '#15455E', - '#0F2F3F', - '#144259', - '#194D67', - '#1D6991', - '#195777', - '#19516C', - '#103F56', - '#144660', - '#0D2E3E', - '#10212A', - '#113141', - '#16455C', - '#1D5B7C', - '#1F6589', - '#1E668C', - '#1E5F81', - '#0F3B50', - '#0D3244', - '#164A64', - '#184E69', - '#0E364A', - '#0E2E3E', - '#10222B', - '#19475E', - '#1B5A7B', - '#1E5D7F', - '#1E678D', - '#1E6184', - '#19506A', - '#1B5370', - '#1B5573', - '#0E3041', - '#122E3E', - '#16455B', - '#195370', - '#1D6489', - '#1D6B93', - '#164A65', - '#154A64', - '#1A5572', - '#1D6082', - '#1F6286', - '#1D6C94', - '#1E709A', - '#174A65', - '#1B526F', - '#1E6589', - '#1D6384', - '#0D3143', - '#0E2F3F', - '#174760', - '#1F6487', - '#1D668C', - '#0D2F41', - '#103B4F', - '#1C5C7E', - '#1F688F', - '#1C5B7C', - '#164D68', - '#1D6285', - '#0D364A', - '#1D5A7A', - '#1E6990', - '#1D6488', - '#18516B', - '#1A506B', - '#0E3B50', - '#0E3548', - '#124259', - '#13455C', - '#14485F', - '#1E5C7D', - '#122D3C', - '#1E6E98', - '#1E6A91', - '#1E6286', - '#1E6C95', - '#1D6990', - '#101F29', - '#174A62', - '#10394E', - '#1D6D96', - '#1E688E', - '#1D6E97', - '#1E6C94', - '#0E394E', - '#112B39', - '#195270', - '#1E668B', - '#1E6386', - '#1D6385', - '#0C3142', - '#1E6083', - '#1E729C', - '#1F709A', - '#1E6F98', - '#1D5F81', - '#1F688D', - '#1C6488', - '#1D6588', - '#1C6A93', - '#1E658B', - '#1F6C95', - '#0D3C52', - '#1C6385', - '#1E5F82', - '#0E3D54', - '#0F3244', - '#18485F', - '#1E6991', - '#1C5B7B', - '#1F6082', - '#0F3346', - '#18536F', - '#114056', - '#1D6B92', - '#1B5776', - '#0F3C52', - '#1E6890', - '#1F688E', - '#0C394E', - '#0F1D25', - '#1F6386', - '#1E688D', - '#1F6488', - '#20668C', - '#1D5978', - '#0F3D52', - '#0F1E26', - '#13465F', - '#0D374C', - '#1B5C7C', - '#0E1A23', - '#0F374A', - '#1B5574', - '#0F394C', - '#0E2A38', - '#102A37', - '#18506B', - '#1E5A7A', - '#0F3245', - '#0E2E3F', - '#1E678E', - '#1C5D7E', - '#1A5A7A', - '#0E2837', - '#102733', - '#0F3B51', - '#15475E', - '#1E6B93', - '#1E648A', - '#194961', - '#0F3A4E', - '#0E1D25', - '#194F69', - '#103345', - '#0F394D', - '#102B39', - '#103E55', - '#1B5572', - '#164861', - '#174861', - '#113B4F', - '#102936', - '#0F3041', - '#174961', - '#113E53', - '#134056', - '#124057', - '#194B63', - '#0E364B', - '#15445B', - '#16475E', - '#102F3F', - '#16485F', - '#0F2E3D', - '#101920', - '#12222C', - '#122C3B', - '#144157', - '#123B50', - '#16465D', - '#184960', - '#112B3A', - '#12232F', - '#132430', - '#113344', - '#11394C', - '#113649', - '#11364A', - '#133F56', - '#121D25', - '#112733', - '#112A38', - '#0F1F2A', - '#113447', - '#113A4E', - '#0F222C', - '#13222B', - '#112836', - '#102F3E', - '#113243', - '#123445', - '#12374B', - '#121E26', - '#122531', - '#11303F', - '#0D1D25', - '#102835', - '#112834', - '#101C23', - '#111C23', - '#12212B', - '#11222D', - '#0E1B22', - '#0E1D27', - '#121C22', - '#12202A', - '#101A20', - '#13191E', - '#111E28', - '#11212D', - '#0F1B24', - '#0F1C23', - '#13181D', - '#15171A', - '#121D23', - '#121F27', - '#111E27', - '#101B22', - '#121F28', - '#111E26', - '#101D24', - '#111C22', - '#12161E', - '#101925', - '#121E2D', - '#112033', - '#111E2F', - '#0F1B29', - '#0F1A28', - '#101B2A', - '#0E1A27', - '#101C2B', - '#111D2D', - '#111D2B', - '#0F1B28', - '#101923', - '#13161D', - '#13161C', - '#0F1A26', - '#101E2F', - '#112235', - '#102031', - '#0F1B2A', - '#112031', - '#102032', - '#101D2E', - '#121F2F', - '#112133', - '#101E30', - '#101F30', - '#102336', - '#101B2C', - '#0F1C2B', - '#111E2E', - '#0F2134', - '#102236', - '#0F2133', - '#101F31', - '#0F2438', - '#102337', - '#102235', - '#102133', - '#11171E', - '#101F2F', - '#102030', - '#102234', - '#102132', - '#12181F', - '#0F1A25', - '#0F2135', - '#0F1F30', - '#0F1C2D', - '#101D2C', - '#0F2033', - '#0E2338', - '#0F2237', - '#0F2236', - '#0B243B', - '#0D2338', - '#0E1A26', - '#0F1D2E', - '#0F2032', - '#0D2339', - '#0B253F', - '#0A253F', - '#0A253E', - '#0C2439', - '#0E1925', - '#0E2135', - '#0F2235', - '#0A243A', - '#08253E', - '#09253E', - '#0A263F', - '#0A243C', - '#0B233B', - '#0E1A28', - '#0D1A26', - '#09253F', - '#0A2743', - '#0B2844', - '#0B2641', - '#0A2744', - '#0A2844', - '#0B2743', - '#092745', - '#0F2337', - '#101D2D', - '#092743', - '#092846', - '#0E2B4C', - '#102E4F', - '#0E2C4D', - '#0B2A49', - '#082947', - '#0D2B4B', - '#0C2A4A', - '#092946', - '#082845', - '#0C2B4B', - '#0F2D4E', - '#103051', - '#133257', - '#0E2D4E', - '#143156', - '#112F51', - '#0B243A', - '#082744', - '#092844', - '#123054', - '#143359', - '#173A64', - '#183F6E', - '#173F6D', - '#153961', - '#163962', - '#133358', - '#15345B', - '#14345A', - '#102F50', - '#0A2948', - '#082844', - '#092641', - '#16375F', - '#193C69', - '#174170', - '#173E6B', - '#163A63', - '#173D69', - '#183D6A', - '#15365E', - '#112E50', - '#0A2A49', - '#082743', - '#0E1927', - '#173C68', - '#13487E', - '#164476', - '#174375', - '#193F6F', - '#173B66', - '#163B65', - '#082A48', - '#0A2641', - '#09243C', - '#174171', - '#14477C', - '#124980', - '#14487F', - '#174374', - '#15467B', - '#184172', - '#17406F', - '#184070', - '#163C67', - '#16355D', - '#123256', - '#0E1B29', - '#0F1923', - '#113052', - '#184274', - '#164579', - '#13477C', - '#193E6D', - '#0A243E', - '#0B233A', - '#0D1A29', - '#0B2742', - '#17365E', - '#163860', - '#124A84', - '#095191', - '#114A83', - '#0D4D8A', - '#0C4D8C', - '#104B85', - '#15477E', - '#174477', - '#183862', - '#0A233A', - '#092947', - '#09243D', - '#173963', - '#194173', - '#085396', - '#085394', - '#114B87', - '#144983', - '#094F8E', - '#075090', - '#0F4C89', - '#215287', - '#0E1A29', - '#184376', - '#0C4D8B', - '#07549A', - '#0A4E8D', - '#0F4C88', - '#0A4E8C', - '#174273', - '#193C6A', - '#0B2948', - '#0B2C4B', - '#0C4E8D', - '#1259A4', - '#0C579E', - '#0D4D8B', - '#095397', - '#085397', - '#085295', - '#144880', - '#173861', - '#15335A', - '#0F2C4D', - '#0C2949', - '#0B4E8D', - '#08559C', - '#07508F', - '#154578', - '#17365F', - '#122F53', - '#111D2C', - '#092A48', - '#08559D', - '#08559E', - '#0C56A1', - '#164271', - '#163E6A', - '#194071', - '#082642', - '#0F1E30', - '#0D2D4D', - '#114C87', - '#0E59A3', - '#135BA6', - '#085498', - '#085497', - '#095192', - '#0E4D8B', - '#0C4E8A', - '#134982', - '#17457B', - '#121F2E', - '#183E6C', - '#153E69', - '#07508E', - '#173F6C', - '#193D6B', - '#112D4F', - '#0A243B', - '#072946', - '#111E2D', - '#0B2740', - '#10497F', - '#17406E', - '#084F8D', - '#104A80', - '#0E2E4F', - '#143358', - '#16365D', - '#0A2742', - '#13477B', - '#154474', - '#104C86', - '#095291', - '#0B4F8E', - '#114A80', - '#095090', - '#075296', - '#163760', - '#2D6DB5', - '#0C2843', - '#0C233A', - '#153A62', - '#14467A', - '#075498', - '#085293', - '#09263F', - '#122030', - '#09559D', - '#0F4B83', - '#08549A', - '#14375D', - '#085499', - '#075499', - '#0A243D', - '#143E68', - '#10497E', - '#074F8E', - '#085496', - '#0C58A3', - '#065499', - '#085190', - '#0A2B4A', - '#104C88', - '#0D4F8E', - '#0F58A2', - '#0B569B', - '#0D58A1', - '#134A81', - '#09559C', - '#0A5293', - '#114B86', - '#0D2C4C', - '#103255', - '#16457A', - '#074F8C', - '#07559C', - '#185DA9', - '#1D61AD', - '#175CA8', - '#16406D', - '#153C65', - '#0E243A', - '#144679', - '#085192', - '#1A5EAC', - '#1D61AE', - '#11497F', - '#12487E', - '#0C243C', - '#123155', - '#0F59A3', - '#1B5FAB', - '#1E61AD', - '#145CA4', - '#0E599F', - '#11497E', - '#094F8D', - '#15345A', - '#134A85', - '#165CA8', - '#2263AF', - '#124466', - '#0A518F', - '#08569D', - '#16416F', - '#0B2B4A', - '#124A83', - '#0C57A2', - '#1E60AD', - '#1E62AE', - '#165DA8', - '#1059A4', - '#15406C', - '#0A4F8E', - '#12365A', - '#0A5191', - '#16355C', - '#1C5EAB', - '#155CA7', - '#085292', - '#174478', - '#153258', - '#111F2F', - '#174272', - '#1159A5', - '#1C5EAC', - '#2F74BB', - '#0C58A2', - '#0D59A3', - '#14477D', - '#132F53', - '#155BA6', - '#195FAA', - '#2366B1', - '#2967B2', - '#14477E', - '#1B5EAB', - '#175DA8', - '#0F4C86', - '#065090', - '#1C5FAC', - '#185CA8', - '#0D58A3', - '#0C4E8C', - '#134981', - '#14416D', - '#0F5AA5', - '#1F63AF', - '#114B88', - '#09508E', - '#0A569D', - '#195DAA', - '#0F1D2F', - '#1059A2', - '#0E599E', - '#2063AF', - '#1F63AE', - '#1A5EAA', - '#0C57A0', - '#195EAA', - '#1A5EA9', - '#0E4E8A', - '#12487D', - '#185DAA', - '#175EAA', - '#0A508E', - '#1559A6', - '#0E58A3', - '#095399', - '#0B4E8B', - '#0B569F', - '#0C57A1', - '#2967B1', - '#2365B0', - '#2163AE', - '#1A5DAA', - '#195EAB', - '#1E5FAC', - '#2564AF', - '#2767B1', - '#2766B1', - '#0D5A9F', - '#2062AE', - '#1F61AD', - '#195FAB', - '#0D4E8D', - '#173760', - '#111D2E', - '#09518F', - '#1A5FAC', - '#135BA7', - '#085291', - '#183761', - '#0B2845', - '#113457', - '#075393', - '#185EA9', - '#2B69B3', - '#2A67B2', - '#2867B1', - '#155DA8', - '#135CA6', - '#135AA5', - '#114980', - '#2566B1', - '#2064AF', - '#2364AF', - '#13365B', - '#154475', - '#08549B', - '#164373', - '#085392', - '#144576', - '#12497E', - '#0E5392', - '#135BA3', - '#0C5395', - '#0C5291', - '#0E579C', - '#0E5290', - '#134C83', - '#2163AC', - '#195CA6', - '#0D4E8C', - '#082945', - '#133256', - '#0E2F50', - '#105AA6', - '#134677', - '#144475', - '#145BA7', - '#154270', - '#1D60AD', - '#09569B', - '#09243E', - '#134A86', - '#0E59A4', - '#0A4E8B', - '#0E4B83', - '#1D5EAC', - '#101C2A', - '#134A84', - '#0E518F', - '#145CA7', - '#0E5699', - '#145BA5', - '#095292', - '#15416E', - '#153D67', - '#153F6B', - '#125AA5', - '#16406E', - '#0E1B27', - '#0D4F8C', - '#0F58A3', - '#114A82', - '#09569C', - '#0C2339', - '#0E1B28', - '#0D59A4', - '#07559D', - '#08569E', - '#095190', - '#0B253E', - '#0C2B49', - '#2264AF', - '#09549A', - '#09569F', - '#163D68', - '#0C263F', - '#143960', - '#183A65', - '#075496', - '#0C579F', - '#085191', - '#102438', - '#075295', - '#082946', - '#102437', - '#0C2642', - '#101C29', - '#0C253E', - '#15355C', - '#0B2E4D', - '#0F3253', - '#154577', - '#16335B', - '#0F1925', - '#0C2742', - '#0B2946', - '#0E2C4B', - '#0E2B48', - '#0E2237', - '#102237', - '#0B253D', - '#0A2946', - '#0C2841', - '#0D2A47', - '#0C2C4A', - '#08253F', - '#08243D', - '#111C2B', - '#0C2844', - '#0C2945', - '#0D243A', - '#122134', - '#0B2642', - '#113154', - '#113255', - '#0A2642', - '#0A2945', - '#0B263F', - '#0D2E4E', - '#0F1E2E', - '#0A2845', - '#0D2439', - '#0F1A29', - '#101C2E', - '#111923', - '#13181F', - '#111D2F', - '#111F30', - '#121E30', - '#121E2E', - '#101B27', - '#101A27', - '#13171F', - ]; - - // let randX = getRandomInt(0, x); - // let randY = getRandomInt(0, y); - // let randIndex = randY * xCount + randX; - - return colors[(y*xCount + x) % colors.length]; -} diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 66aec778d73..50ff55f3083 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -84,7 +84,7 @@ function link(scope, elem, attrs) { // disable depreacation warning codeEditor.$blockScrolling = Infinity; // Padding hacks - (codeEditor.renderer).setScrollMargin(15, 15); + (codeEditor.renderer as any).setScrollMargin(15, 15); codeEditor.renderer.setPadding(10); setThemeMode(); @@ -97,11 +97,11 @@ function link(scope, elem, attrs) { textarea.addClass('gf-form-input'); if (scope.codeEditorFocus) { - setTimeout(function() { + setTimeout(() => { textarea.focus(); - var domEl = textarea[0]; + const domEl = textarea[0]; if (domEl.setSelectionRange) { - var pos = textarea.val().length * 2; + const pos = textarea.val().length * 2; domEl.setSelectionRange(pos, pos); } }, 100); @@ -119,7 +119,7 @@ function link(scope, elem, attrs) { scope.$watch('content', (newValue, oldValue) => { const editorValue = codeEditor.getValue(); if (newValue !== editorValue && newValue !== oldValue) { - scope.$$postDigest(function() { + scope.$$postDigest(() => { setEditorContent(newValue); }); } @@ -152,7 +152,7 @@ function link(scope, elem, attrs) { if (scope.getCompleter()) { // make copy of array as ace seems to share completers array between instances - const anyEditor = codeEditor; + const anyEditor = codeEditor as any; anyEditor.completers = anyEditor.completers.slice(); anyEditor.completers.push(scope.getCompleter()); } diff --git a/public/app/core/components/colorpicker/spectrum_picker.ts b/public/app/core/components/colorpicker/spectrum_picker.ts index 6e93a4f39f4..4576648df83 100644 --- a/public/app/core/components/colorpicker/spectrum_picker.ts +++ b/public/app/core/components/colorpicker/spectrum_picker.ts @@ -13,7 +13,7 @@ export function spectrumPicker() { scope: true, replace: true, template: '', - link: function(scope, element, attrs, ngModel) { + link: (scope, element, attrs, ngModel) => { scope.ngModel = ngModel; scope.onColorChange = color => { ngModel.$setViewValue(color); diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts index 379fd441a19..e1809f3d42c 100644 --- a/public/app/core/components/dashboard_selector.ts +++ b/public/app/core/components/dashboard_selector.ts @@ -1,6 +1,6 @@ import coreModule from 'app/core/core_module'; -var template = ` +const template = ` `; diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 4604e1d7838..6e863e1cb5d 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; import coreModule from '../../core_module'; -function typeaheadMatcher(item) { - var str = this.query; +function typeaheadMatcher(this: any, item) { + let str = this.query; if (str === '') { return true; } @@ -36,7 +36,7 @@ export class FormDropdownCtrl { startOpen: any; debounce: number; - /** @ngInject **/ + /** @ngInject */ constructor(private $scope, $element, private $sce, private templateSrv, private $q) { this.inputElement = $element.find('input').first(); this.linkElement = $element.find('a').first(); diff --git a/public/app/core/components/gf_page.ts b/public/app/core/components/gf_page.ts index ad0770940ec..057a307f205 100644 --- a/public/app/core/components/gf_page.ts +++ b/public/app/core/components/gf_page.ts @@ -31,7 +31,7 @@ export function gfPageDirective() { header: '?gfPageHeader', body: 'gfPageBody', }, - link: function(scope, elem, attrs) { + link: (scope, elem, attrs) => { console.log(scope); }, }; diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 085f0db0a6d..926438ffbc9 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -30,7 +30,7 @@ export class GrafanaCtrl { setBackendSrv(backendSrv); createStore({ backendSrv, datasourceSrv }); - $scope.init = function() { + $scope.init = () => { $scope.contextSrv = contextSrv; $scope.appSubUrl = config.appSubUrl; $scope._ = _; @@ -45,14 +45,14 @@ export class GrafanaCtrl { $rootScope.colors = colors; - $scope.initDashboard = function(dashboardData, viewScope) { + $scope.initDashboard = (dashboardData, viewScope) => { $scope.appEvent('dashboard-fetch-end', dashboardData); $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData); }; $rootScope.onAppEvent = function(name, callback, localScope) { - var unbind = $rootScope.$on(name, callback); - var callerScope = this; + const unbind = $rootScope.$on(name, callback); + let callerScope = this; if (callerScope.$id === 1 && !localScope) { console.log('warning rootScope onAppEvent called without localscope'); } @@ -62,7 +62,7 @@ export class GrafanaCtrl { callerScope.$on('$destroy', unbind); }; - $rootScope.appEvent = function(name, payload) { + $rootScope.appEvent = (name, payload) => { $rootScope.$emit(name, payload); appEvents.emit(name, payload); }; @@ -71,17 +71,43 @@ export class GrafanaCtrl { } } +function setViewModeBodyClass(body, mode, sidemenuOpen: boolean) { + body.removeClass('view-mode--tv'); + body.removeClass('view-mode--kiosk'); + body.removeClass('view-mode--inactive'); + + switch (mode) { + case 'tv': { + body.removeClass('sidemenu-open'); + body.addClass('view-mode--tv'); + break; + } + // 1 & true for legacy states + case 1: + case true: { + body.removeClass('sidemenu-open'); + body.addClass('view-mode--kiosk'); + break; + } + default: { + body.toggleClass('sidemenu-open', sidemenuOpen); + } + } +} + /** @ngInject */ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScope, $location) { return { restrict: 'E', controller: GrafanaCtrl, link: (scope, elem) => { - var sidemenuOpen; - var body = $('body'); + let sidemenuOpen; + const body = $('body'); // see https://github.com/zenorocha/clipboard.js/issues/155 - $.fn.modal.Constructor.prototype.enforceFocus = function() {}; + $.fn.modal.Constructor.prototype.enforceFocus = () => {}; + + $('.preloader').remove(); sidemenuOpen = scope.contextSrv.sidemenu; body.toggleClass('sidemenu-open', sidemenuOpen); @@ -99,9 +125,12 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop body.toggleClass('sidemenu-hidden'); }); - scope.$watch(() => playlistSrv.isPlaying, function(newValue) { - elem.toggleClass('playlist-active', newValue === true); - }); + scope.$watch( + () => playlistSrv.isPlaying, + newValue => { + elem.toggleClass('view-mode--playlist', newValue === true); + } + ); // check if we are in server side render if (document.cookie.indexOf('renderKey') !== -1) { @@ -110,8 +139,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop // tooltip removal fix // manage page classes - var pageClass; - scope.$on('$routeChangeSuccess', function(evt, data) { + let pageClass; + scope.$on('$routeChangeSuccess', (evt, data) => { if (pageClass) { body.removeClass(pageClass); } @@ -129,17 +158,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop $('#tooltip, .tooltip').remove(); // check for kiosk url param - if (data.params.kiosk) { - appEvents.emit('toggle-kiosk-mode'); - } - - // check for 'inactive' url param for clean looks like kiosk, but with title - if (data.params.inactive) { - body.addClass('user-activity-low'); - - // for some reason, with this class it looks cleanest - body.addClass('sidemenu-open'); - } + setViewModeBodyClass(body, data.params.kiosk, sidemenuOpen); // close all drops for (const drop of Drop.drops) { @@ -148,15 +167,37 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop }); // handle kiosk mode - appEvents.on('toggle-kiosk-mode', () => { - body.toggleClass('page-kiosk-mode'); + appEvents.on('toggle-kiosk-mode', options => { + const search = $location.search(); + + if (options && options.exit) { + search.kiosk = 1; + } + + switch (search.kiosk) { + case 'tv': { + search.kiosk = 1; + appEvents.emit('alert-success', ['Press ESC to exit Kiosk mode']); + break; + } + case 1: + case true: { + delete search.kiosk; + break; + } + default: { + search.kiosk = 'tv'; + } + } + + $location.search(search); + setViewModeBodyClass(body, search.kiosk, sidemenuOpen); }); // handle in active view state class - var lastActivity = new Date().getTime(); - var activeUser = true; - var inActiveTimeLimit = 60 * 1000; - var sidemenuHidden = false; + let lastActivity = new Date().getTime(); + let activeUser = true; + const inActiveTimeLimit = 60 * 5000; function checkForInActiveUser() { if (!activeUser) { @@ -169,15 +210,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop if (new Date().getTime() - lastActivity > inActiveTimeLimit) { activeUser = false; - body.addClass('user-activity-low'); - // hide sidemenu - if (sidemenuOpen) { - sidemenuHidden = true; - body.removeClass('sidemenu-open'); - $timeout(function() { - $rootScope.$broadcast('render'); - }, 100); - } + body.addClass('view-mode--inactive'); + body.removeClass('sidemenu-open'); } } @@ -185,17 +219,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop lastActivity = new Date().getTime(); if (!activeUser) { activeUser = true; - body.removeClass('user-activity-low'); - - // restore sidemenu - if (sidemenuHidden) { - sidemenuHidden = false; - body.addClass('sidemenu-open'); - appEvents.emit('toggle-inactive-mode'); - $timeout(function() { - $rootScope.$broadcast('render'); - }, 100); - } + body.removeClass('view-mode--inactive'); + body.toggleClass('sidemenu-open', sidemenuOpen); } } @@ -216,19 +241,19 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop }); // handle document clicks that should hide things - body.click(function(evt) { - var target = $(evt.target); + body.click(evt => { + const target = $(evt.target); if (target.parents().length === 0) { return; } // for stuff that animates, slides out etc, clicking it needs to // hide it right away - var clickAutoHide = target.closest('[data-click-hide]'); + const clickAutoHide = target.closest('[data-click-hide]'); if (clickAutoHide.length) { - var clickAutoHideParent = clickAutoHide.parent(); + const clickAutoHideParent = clickAutoHide.parent(); clickAutoHide.detach(); - setTimeout(function() { + setTimeout(() => { clickAutoHideParent.append(clickAutoHide); }, 100); } @@ -240,14 +265,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop // hide search if (body.find('.search-container').length > 0) { if (target.parents('.search-results-container, .search-field-wrapper').length === 0) { - scope.$apply(function() { + scope.$apply(() => { scope.appEvent('hide-dash-search'); }); } } // hide popovers - var popover = elem.find('.popover'); + const popover = elem.find('.popover'); if (popover.length > 0 && target.parents('.graph-legend').length === 0) { popover.hide(); } diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index ae4feeec701..2ada91b09f1 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -7,7 +7,7 @@ export function infoPopover() { restrict: 'E', template: '', transclude: true, - link: function(scope, elem, attrs, ctrl, transclude) { + link: (scope, elem, attrs, ctrl, transclude) => { const offset = attrs.offset || '0 -10px'; const position = attrs.position || 'right middle'; let classes = 'drop-help drop-hide-out-of-bounds'; @@ -23,7 +23,7 @@ export function infoPopover() { elem.addClass('gf-form-help-icon--' + attrs.mode); } - transclude(function(clone, newScope) { + transclude((clone, newScope) => { const content = document.createElement('div'); content.className = 'markdown-html'; @@ -54,7 +54,7 @@ export function infoPopover() { scope.$applyAsync(() => { const drop = new Drop(dropOptions); - const unbind = scope.$on('$destroy', function() { + const unbind = scope.$on('$destroy', () => { drop.destroy(); unbind(); }); diff --git a/public/app/core/components/json_explorer/helpers.ts b/public/app/core/components/json_explorer/helpers.ts index c445e1b0667..c039d818281 100644 --- a/public/app/core/components/json_explorer/helpers.ts +++ b/public/app/core/components/json_explorer/helpers.ts @@ -12,7 +12,7 @@ function escapeString(str: string): string { * Determines if a value is an object */ export function isObject(value: any): boolean { - var type = typeof value; + const type = typeof value; return !!value && type === 'object'; } @@ -21,7 +21,7 @@ export function isObject(value: any): boolean { * From http://stackoverflow.com/a/332429 * */ -export function getObjectName(object: Object): string { +export function getObjectName(object: object): string { if (object === undefined) { return ''; } @@ -44,7 +44,7 @@ export function getObjectName(object: Object): string { /* * Gets type of an object. Returns "null" for null objects */ -export function getType(object: Object): string { +export function getType(object: object): string { if (object === null) { return 'null'; } @@ -54,8 +54,8 @@ export function getType(object: Object): string { /* * Generates inline preview for a JavaScript object based on a value */ -export function getValuePreview(object: Object, value: string): string { - var type = getType(object); +export function getValuePreview(object: object, value: string): string { + const type = getType(object); if (type === 'null' || type === 'undefined') { return type; @@ -79,15 +79,15 @@ export function getValuePreview(object: Object, value: string): string { /* * Generates inline preview for a JavaScript object */ -export function getPreview(object: string): string { - let value = ''; - if (isObject(object)) { - value = getObjectName(object); - if (Array.isArray(object)) { - value += '[' + object.length + ']'; +let value = ''; +export function getPreview(obj: object): string { + if (isObject(obj)) { + value = getObjectName(obj); + if (Array.isArray(obj)) { + value += '[' + obj.length + ']'; } } else { - value = getValuePreview(object, object); + value = getValuePreview(obj, obj.toString()); } return value; } diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index 790ed442d5c..9a344d3195b 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -14,10 +14,10 @@ const MAX_ANIMATED_TOGGLE_ITEMS = 10; const requestAnimationFrame = window.requestAnimationFrame || - function(cb: () => void) { + ((cb: () => void) => { cb(); return 0; - }; + }); export interface JsonExplorerConfig { animateOpen?: boolean; @@ -279,7 +279,7 @@ export class JsonExplorer { const objectWrapperSpan = createElement('span'); // get constructor name and append it to wrapper span - var constructorName = createElement('span', 'constructor-name', this.constructorName); + const constructorName = createElement('span', 'constructor-name', this.constructorName); objectWrapperSpan.appendChild(constructorName); // if it's an array append the array specific elements like brackets and length diff --git a/public/app/core/components/jsontree/jsontree.ts b/public/app/core/components/jsontree/jsontree.ts index e127d7b14a9..4bcb2f632c2 100644 --- a/public/app/core/components/jsontree/jsontree.ts +++ b/public/app/core/components/jsontree/jsontree.ts @@ -10,8 +10,8 @@ coreModule.directive('jsonTree', [ startExpanded: '@', rootName: '@', }, - link: function(scope, elem) { - var jsonExp = new JsonExplorer(scope.object, 3, { + link: (scope, elem) => { + const jsonExp = new JsonExplorer(scope.object, 3, { animateOpen: true, }); diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index 91a3afea250..b3f3cdc14d1 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -1,7 +1,7 @@ import store from 'app/core/store'; import coreModule from 'app/core/core_module'; -var template = ` +const template = `
- - @@ -42,6 +42,12 @@
+ + `; -/** @ngInject **/ +/** @ngInject */ function dashRepeatOptionDirective(variableSrv) { return { restrict: 'E', @@ -15,7 +15,7 @@ function dashRepeatOptionDirective(variableSrv) { scope: { panel: '=', }, - link: function(scope, element) { + link: (scope, element) => { element.css({ display: 'block', width: '100%' }); scope.variables = variableSrv.variables.map(item => { @@ -36,7 +36,7 @@ function dashRepeatOptionDirective(variableSrv) { scope.panel.repeatDirection = 'h'; } - scope.optionChanged = function() { + scope.optionChanged = () => { if (scope.panel.repeat) { scope.panel.repeatDirection = 'h'; } diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index dabf5f85378..048a51efead 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -179,8 +179,8 @@ export class SettingsCtrl { } deleteDashboard() { - var confirmText = ''; - var text2 = this.dashboard.title; + let confirmText = ''; + let text2 = this.dashboard.title; const alerts = _.sumBy(this.dashboard.panels, panel => { return panel.alert ? 1 : 0; diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index 694329c3102..c00a6d8d57f 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -11,7 +11,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, }; $scope.editor = { index: $scope.tabIndex || 0 }; - $scope.init = function() { + $scope.init = () => { $scope.modeSharePanel = $scope.panel ? true : false; $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }]; @@ -34,8 +34,8 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, $scope.buildUrl(); }; - $scope.buildUrl = function() { - var baseUrl = $location.absUrl(); + $scope.buildUrl = () => { + let baseUrl = $location.absUrl(); const queryStart = baseUrl.indexOf('?'); if (queryStart !== -1) { @@ -72,7 +72,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params); - var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); + let soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/'); soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); delete params.fullscreen; delete params.edit; @@ -90,15 +90,15 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, // This function will try to return the proper full name of the local timezone // Chrome does not handle the timezone offset (but phantomjs does) - $scope.getLocalTimeZone = function() { + $scope.getLocalTimeZone = () => { const utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z')); // Older browser does not the internationalization API - if (!(window).Intl) { + if (!(window as any).Intl) { return utcOffset; } - const dateFormat = (window).Intl.DateTimeFormat(); + const dateFormat = (window as any).Intl.DateTimeFormat(); if (!dateFormat.resolvedOptions) { return utcOffset; } @@ -111,7 +111,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, return '&tz=' + encodeURIComponent(options.timeZone); }; - $scope.getShareUrl = function() { + $scope.getShareUrl = () => { return $scope.shareUrl; }; } diff --git a/public/app/features/dashboard/share_snapshot_ctrl.ts b/public/app/features/dashboard/share_snapshot_ctrl.ts index c470ddb47dd..ec487801948 100644 --- a/public/app/features/dashboard/share_snapshot_ctrl.ts +++ b/public/app/features/dashboard/share_snapshot_ctrl.ts @@ -2,7 +2,7 @@ import angular from 'angular'; import _ from 'lodash'; export class ShareSnapshotCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) { $scope.snapshot = { name: $scope.dashboard.title, @@ -25,8 +25,8 @@ export class ShareSnapshotCtrl { { text: 'Public on the web', value: 3 }, ]; - $scope.init = function() { - backendSrv.get('/api/snapshot/shared-options').then(function(options) { + $scope.init = () => { + backendSrv.get('/api/snapshot/shared-options').then(options => { $scope.externalUrl = options['externalSnapshotURL']; $scope.sharingButtonText = options['externalSnapshotName']; $scope.externalEnabled = options['externalEnabled']; @@ -35,7 +35,7 @@ export class ShareSnapshotCtrl { $scope.apiUrl = '/api/snapshots'; - $scope.createSnapshot = function(external) { + $scope.createSnapshot = external => { $scope.dashboard.snapshot = { timestamp: new Date(), }; @@ -49,12 +49,12 @@ export class ShareSnapshotCtrl { $rootScope.$broadcast('refresh'); - $timeout(function() { + $timeout(() => { $scope.saveSnapshot(external); }, $scope.snapshot.timeoutSeconds * 1000); }; - $scope.saveSnapshot = function(external) { + $scope.saveSnapshot = external => { const dash = $scope.dashboard.getSaveModelClone(); $scope.scrubDashboard(dash); @@ -67,7 +67,7 @@ export class ShareSnapshotCtrl { const postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl; backendSrv.post(postUrl, cmdData).then( - function(results) { + results => { $scope.loading = false; if (external) { @@ -88,17 +88,17 @@ export class ShareSnapshotCtrl { $scope.step = 2; }, - function() { + () => { $scope.loading = false; } ); }; - $scope.getSnapshotUrl = function() { + $scope.getSnapshotUrl = () => { return $scope.snapshotUrl; }; - $scope.scrubDashboard = function(dash) { + $scope.scrubDashboard = dash => { // change title dash.title = $scope.snapshot.name; @@ -106,7 +106,7 @@ export class ShareSnapshotCtrl { dash.time = timeSrv.timeRange(); // remove panel queries & links - _.each(dash.panels, function(panel) { + _.each(dash.panels, panel => { panel.targets = []; panel.links = []; panel.datasource = null; @@ -114,10 +114,10 @@ export class ShareSnapshotCtrl { // remove annotation queries dash.annotations.list = _.chain(dash.annotations.list) - .filter(function(annotation) { + .filter(annotation => { return annotation.enable; }) - .map(function(annotation) { + .map(annotation => { return { name: annotation.name, enable: annotation.enable, @@ -131,7 +131,7 @@ export class ShareSnapshotCtrl { .value(); // remove template queries - _.each(dash.templating.list, function(variable) { + _.each(dash.templating.list, variable => { variable.query = ''; variable.options = variable.current; variable.refresh = false; @@ -149,21 +149,21 @@ export class ShareSnapshotCtrl { // cleanup snapshotData delete $scope.dashboard.snapshot; - $scope.dashboard.forEachPanel(function(panel) { + $scope.dashboard.forEachPanel(panel => { delete panel.snapshotData; }); - _.each($scope.dashboard.annotations.list, function(annotation) { + _.each($scope.dashboard.annotations.list, annotation => { delete annotation.snapshotData; }); }; - $scope.deleteSnapshot = function() { - backendSrv.get($scope.deleteUrl).then(function() { + $scope.deleteSnapshot = () => { + backendSrv.get($scope.deleteUrl).then(() => { $scope.step = 3; }); }; - $scope.saveExternalSnapshotRef = function(cmdData, results) { + $scope.saveExternalSnapshotRef = (cmdData, results) => { // save external in local instance as well cmdData.external = true; cmdData.key = results.key; diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts index fca857184f1..bcde009cb3a 100644 --- a/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.test.ts @@ -1,7 +1,7 @@ import { DashboardImportCtrl } from '../dashboard_import_ctrl'; import config from '../../../core/config'; -describe('DashboardImportCtrl', function() { +describe('DashboardImportCtrl', () => { const ctx: any = {}; let navModelSrv; @@ -26,8 +26,8 @@ describe('DashboardImportCtrl', function() { ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {}); }); - describe('when uploading json', function() { - beforeEach(function() { + describe('when uploading json', () => { + beforeEach(() => { config.datasources = { ds: { type: 'test-db', @@ -46,19 +46,19 @@ describe('DashboardImportCtrl', function() { }); }); - it('should build input model', function() { + it('should build input model', () => { expect(ctx.ctrl.inputs.length).toBe(1); expect(ctx.ctrl.inputs[0].name).toBe('ds'); expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source'); }); - it('should set inputValid to false', function() { + it('should set inputValid to false', () => { expect(ctx.ctrl.inputsValid).toBe(false); }); }); - describe('when specifying grafana.com url', function() { - beforeEach(function() { + describe('when specifying grafana.com url', () => { + beforeEach(() => { ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; // setup api mock backendSrv.get = jest.fn(() => { @@ -69,13 +69,13 @@ describe('DashboardImportCtrl', function() { return ctx.ctrl.checkGnetDashboard(); }); - it('should call gnet api with correct dashboard id', function() { + it('should call gnet api with correct dashboard id', () => { expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); }); }); - describe('when specifying dashboard id', function() { - beforeEach(function() { + describe('when specifying dashboard id', () => { + beforeEach(() => { ctx.ctrl.gnetUrl = '2342'; // setup api mock backendSrv.get = jest.fn(() => { @@ -86,7 +86,7 @@ describe('DashboardImportCtrl', function() { return ctx.ctrl.checkGnetDashboard(); }); - it('should call gnet api with correct dashboard id', function() { + it('should call gnet api with correct dashboard id', () => { expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); }); }); diff --git a/public/app/features/dashboard/specs/dashboard_migration.test.ts b/public/app/features/dashboard/specs/dashboard_migration.test.ts index d07df0e7be2..5f693c9f6d9 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.test.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.test.ts @@ -6,14 +6,14 @@ import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({})); -describe('DashboardModel', function() { - describe('when creating dashboard with old schema', function() { +describe('DashboardModel', () => { + describe('when creating dashboard with old schema', () => { let model; let graph; let singlestat; let table; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ services: { filter: { time: { from: 'now-1d', to: 'now' }, list: [{}] }, @@ -65,52 +65,52 @@ describe('DashboardModel', function() { table = model.panels[2]; }); - it('should have title', function() { + it('should have title', () => { expect(model.title).toBe('No Title'); }); - it('should have panel id', function() { + it('should have panel id', () => { expect(graph.id).toBe(1); }); - it('should move time and filtering list', function() { + it('should move time and filtering list', () => { expect(model.time.from).toBe('now-1d'); expect(model.templating.list[0].allFormat).toBe('glob'); }); - it('graphite panel should change name too graph', function() { + it('graphite panel should change name too graph', () => { expect(graph.type).toBe('graph'); }); - it('single stat panel should have two thresholds', function() { + it('single stat panel should have two thresholds', () => { expect(singlestat.thresholds).toBe('20,30'); }); - it('queries without refId should get it', function() { + it('queries without refId should get it', () => { expect(graph.targets[1].refId).toBe('B'); }); - it('update legend setting', function() { + it('update legend setting', () => { expect(graph.legend.show).toBe(true); }); - it('move aliasYAxis to series override', function() { + it('move aliasYAxis to series override', () => { expect(graph.seriesOverrides[0].alias).toBe('test'); expect(graph.seriesOverrides[0].yaxis).toBe(2); }); - it('should move pulldowns to new schema', function() { + it('should move pulldowns to new schema', () => { expect(model.annotations.list[1].name).toBe('old'); }); - it('table panel should only have two thresholds values', function() { + it('table panel should only have two thresholds values', () => { expect(table.styles[0].thresholds[0]).toBe('20'); expect(table.styles[0].thresholds[1]).toBe('30'); expect(table.styles[1].thresholds[0]).toBe('200'); expect(table.styles[1].thresholds[1]).toBe('300'); }); - it('graph grid to yaxes options', function() { + it('graph grid to yaxes options', () => { expect(graph.yaxes[0].min).toBe(1); expect(graph.yaxes[0].max).toBe(10); expect(graph.yaxes[0].format).toBe('kbyte'); @@ -126,11 +126,11 @@ describe('DashboardModel', function() { expect(graph.y_formats).toBe(undefined); }); - it('dashboard schema version should be set to latest', function() { + it('dashboard schema version should be set to latest', () => { expect(model.schemaVersion).toBe(16); }); - it('graph thresholds should be migrated', function() { + it('graph thresholds should be migrated', () => { expect(graph.thresholds.length).toBe(2); expect(graph.thresholds[0].op).toBe('gt'); expect(graph.thresholds[0].value).toBe(200); @@ -140,16 +140,16 @@ describe('DashboardModel', function() { }); }); - describe('when migrating to the grid layout', function() { + describe('when migrating to the grid layout', () => { let model; - beforeEach(function() { + beforeEach(() => { model = { rows: [], }; }); - it('should create proper grid', function() { + it('should create proper grid', () => { model.rows = [createRow({ collapse: false, height: 8 }, [[6], [6]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -158,7 +158,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should add special "row" panel if row is collapsed', function() { + it('should add special "row" panel if row is collapsed', () => { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -171,7 +171,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should add special "row" panel if row has visible title', function() { + it('should add special "row" panel if row has visible title', () => { model.rows = [ createRow({ showTitle: true, title: 'Row', height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]]), @@ -189,7 +189,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should not add "row" panel if row has not visible title or not collapsed', function() { + it('should not add "row" panel if row has not visible title or not collapsed', () => { model.rows = [ createRow({ collapse: true, height: 8 }, [[12]]), createRow({ height: 8 }, [[12]]), @@ -212,7 +212,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should add all rows if even one collapsed or titled row is present', function() { + it('should add all rows if even one collapsed or titled row is present', () => { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]]), createRow({ height: 8 }, [[12]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -225,7 +225,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should properly place panels with fixed height', function() { + it('should properly place panels with fixed height', () => { model.rows = [ createRow({ height: 6 }, [[6], [6, 3], [6, 3]]), createRow({ height: 6 }, [[4], [4], [4, 3], [4, 3]]), @@ -245,7 +245,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should place panel to the right side of panel having bigger height', function() { + it('should place panel to the right side of panel having bigger height', () => { model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -260,7 +260,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should fill current row if it possible', function() { + it('should fill current row if it possible', () => { model.rows = [createRow({ height: 9 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -276,7 +276,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should fill current row if it possible (2)', function() { + it('should fill current row if it possible (2)', () => { model.rows = [createRow({ height: 8 }, [[4], [2, 3], [4, 6], [2, 3], [2, 3], [8, 3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -292,7 +292,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should fill current row if panel height more than row height', function() { + it('should fill current row if panel height more than row height', () => { model.rows = [createRow({ height: 6 }, [[4], [2, 3], [4, 8], [2, 3], [2, 3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -307,7 +307,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should wrap panels to multiple rows', function() { + it('should wrap panels to multiple rows', () => { model.rows = [createRow({ height: 6 }, [[6], [6], [12], [6], [3], [3]])]; const dashboard = new DashboardModel(model); const panelGridPos = getGridPositions(dashboard); @@ -323,7 +323,7 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); - it('should add repeated row if repeat set', function() { + it('should add repeated row if repeat set', () => { model.rows = [ createRow({ showTitle: true, title: 'Row', height: 8, repeat: 'server' }, [[6]]), createRow({ height: 8 }, [[12]]), @@ -344,7 +344,7 @@ describe('DashboardModel', function() { expect(dashboard.panels[3].repeat).toBeUndefined(); }); - it('should ignore repeated row', function() { + it('should ignore repeated row', () => { model.rows = [ createRow({ showTitle: true, title: 'Row1', height: 8, repeat: 'server' }, [[6]]), createRow( @@ -364,7 +364,7 @@ describe('DashboardModel', function() { expect(dashboard.panels.length).toBe(2); }); - it('minSpan should be twice', function() { + it('minSpan should be twice', () => { model.rows = [createRow({ height: 8 }, [[6]])]; model.rows[0].panels[0] = { minSpan: 12 }; @@ -372,7 +372,7 @@ describe('DashboardModel', function() { expect(dashboard.panels[0].minSpan).toBe(24); }); - it('should assign id', function() { + it('should assign id', () => { model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])]; model.rows[0].panels[0] = {}; diff --git a/public/app/features/dashboard/specs/dashboard_model.test.ts b/public/app/features/dashboard/specs/dashboard_model.test.ts index 24d036a8233..e59d52f2410 100644 --- a/public/app/features/dashboard/specs/dashboard_model.test.ts +++ b/public/app/features/dashboard/specs/dashboard_model.test.ts @@ -4,43 +4,43 @@ import { PanelModel } from '../panel_model'; jest.mock('app/core/services/context_srv', () => ({})); -describe('DashboardModel', function() { - describe('when creating new dashboard model defaults only', function() { +describe('DashboardModel', () => { + describe('when creating new dashboard model defaults only', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({}, {}); }); - it('should have title', function() { + it('should have title', () => { expect(model.title).toBe('No Title'); }); - it('should have meta', function() { + it('should have meta', () => { expect(model.meta.canSave).toBe(true); expect(model.meta.canShare).toBe(true); }); - it('should have default properties', function() { + it('should have default properties', () => { expect(model.panels.length).toBe(0); }); }); - describe('when getting next panel id', function() { + describe('when getting next panel id', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ panels: [{ id: 5 }], }); }); - it('should return max id + 1', function() { + it('should return max id + 1', () => { expect(model.getNextPanelId()).toBe(6); }); }); - describe('getSaveModelClone', function() { + describe('getSaveModelClone', () => { it('should sort keys', () => { const model = new DashboardModel({}); const saveModel = model.getSaveModelClone(); @@ -68,20 +68,20 @@ describe('DashboardModel', function() { }); }); - describe('row and panel manipulation', function() { + describe('row and panel manipulation', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({}); }); - it('adding panel should new up panel model', function() { + it('adding panel should new up panel model', () => { dashboard.addPanel({ type: 'test', title: 'test' }); expect(dashboard.panels[0] instanceof PanelModel).toBe(true); }); - it('duplicate panel should try to add to the right if there is space', function() { + it('duplicate panel should try to add to the right if there is space', () => { const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 } }; dashboard.addPanel(panel); @@ -95,7 +95,7 @@ describe('DashboardModel', function() { }); }); - it('duplicate panel should remove repeat data', function() { + it('duplicate panel should remove repeat data', () => { const panel = { id: 10, gridPos: { x: 0, y: 0, w: 6, h: 2 }, @@ -111,29 +111,29 @@ describe('DashboardModel', function() { }); }); - describe('Given editable false dashboard', function() { + describe('Given editable false dashboard', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ editable: false }); }); - it('Should set meta canEdit and canSave to false', function() { + it('Should set meta canEdit and canSave to false', () => { expect(model.meta.canSave).toBe(false); expect(model.meta.canEdit).toBe(false); }); - it('getSaveModelClone should remove meta', function() { + it('getSaveModelClone should remove meta', () => { const clone = model.getSaveModelClone(); expect(clone.meta).toBe(undefined); }); }); - describe('when loading dashboard with old influxdb query schema', function() { + describe('when loading dashboard with old influxdb query schema', () => { let model; let target; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ panels: [ { @@ -185,7 +185,7 @@ describe('DashboardModel', function() { target = model.panels[0].targets[0]; }); - it('should update query schema', function() { + it('should update query schema', () => { expect(target.fields).toBe(undefined); expect(target.select.length).toBe(2); expect(target.select[0].length).toBe(4); @@ -196,10 +196,10 @@ describe('DashboardModel', function() { }); }); - describe('when creating dashboard model with missing list for annoations or templating', function() { + describe('when creating dashboard model with missing list for annoations or templating', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ annotations: { enable: true, @@ -210,54 +210,54 @@ describe('DashboardModel', function() { }); }); - it('should add empty list', function() { + it('should add empty list', () => { expect(model.annotations.list.length).toBe(1); expect(model.templating.list.length).toBe(0); }); - it('should add builtin annotation query', function() { + it('should add builtin annotation query', () => { expect(model.annotations.list[0].builtIn).toBe(1); expect(model.templating.list.length).toBe(0); }); }); - describe('Formatting epoch timestamp when timezone is set as utc', function() { + describe('Formatting epoch timestamp when timezone is set as utc', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ timezone: 'utc' }); }); - it('Should format timestamp with second resolution by default', function() { + it('Should format timestamp with second resolution by default', () => { expect(dashboard.formatDate(1234567890000)).toBe('2009-02-13 23:31:30'); }); - it('Should format timestamp with second resolution even if second format is passed as parameter', function() { + it('Should format timestamp with second resolution even if second format is passed as parameter', () => { expect(dashboard.formatDate(1234567890007, 'YYYY-MM-DD HH:mm:ss')).toBe('2009-02-13 23:31:30'); }); - it('Should format timestamp with millisecond resolution if format is passed as parameter', function() { + it('Should format timestamp with millisecond resolution if format is passed as parameter', () => { expect(dashboard.formatDate(1234567890007, 'YYYY-MM-DD HH:mm:ss.SSS')).toBe('2009-02-13 23:31:30.007'); }); }); - describe('updateSubmenuVisibility with empty lists', function() { + describe('updateSubmenuVisibility with empty lists', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({}); model.updateSubmenuVisibility(); }); - it('should not enable submmenu', function() { + it('should not enable submmenu', () => { expect(model.meta.submenuEnabled).toBe(false); }); }); - describe('updateSubmenuVisibility with annotation', function() { + describe('updateSubmenuVisibility with annotation', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ annotations: { list: [{}], @@ -266,15 +266,15 @@ describe('DashboardModel', function() { model.updateSubmenuVisibility(); }); - it('should enable submmenu', function() { + it('should enable submmenu', () => { expect(model.meta.submenuEnabled).toBe(true); }); }); - describe('updateSubmenuVisibility with template var', function() { + describe('updateSubmenuVisibility with template var', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ templating: { list: [{}], @@ -283,15 +283,15 @@ describe('DashboardModel', function() { model.updateSubmenuVisibility(); }); - it('should enable submmenu', function() { + it('should enable submmenu', () => { expect(model.meta.submenuEnabled).toBe(true); }); }); - describe('updateSubmenuVisibility with hidden template var', function() { + describe('updateSubmenuVisibility with hidden template var', () => { let model; - beforeEach(function() { + beforeEach(() => { model = new DashboardModel({ templating: { list: [{ hide: 2 }], @@ -300,15 +300,15 @@ describe('DashboardModel', function() { model.updateSubmenuVisibility(); }); - it('should not enable submmenu', function() { + it('should not enable submmenu', () => { expect(model.meta.submenuEnabled).toBe(false); }); }); - describe('updateSubmenuVisibility with hidden annotation toggle', function() { + describe('updateSubmenuVisibility with hidden annotation toggle', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ annotations: { list: [{ hide: true }], @@ -317,15 +317,15 @@ describe('DashboardModel', function() { dashboard.updateSubmenuVisibility(); }); - it('should not enable submmenu', function() { + it('should not enable submmenu', () => { expect(dashboard.meta.submenuEnabled).toBe(false); }); }); - describe('When collapsing row', function() { + describe('When collapsing row', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ panels: [ { id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 2 } }, @@ -338,36 +338,36 @@ describe('DashboardModel', function() { dashboard.toggleRow(dashboard.panels[1]); }); - it('should remove panels and put them inside collapsed row', function() { + it('should remove panels and put them inside collapsed row', () => { expect(dashboard.panels.length).toBe(3); expect(dashboard.panels[1].panels.length).toBe(2); }); - describe('and when removing row and its panels', function() { - beforeEach(function() { + describe('and when removing row and its panels', () => { + beforeEach(() => { dashboard.removeRow(dashboard.panels[1], true); }); - it('should remove row and its panels', function() { + it('should remove row and its panels', () => { expect(dashboard.panels.length).toBe(2); }); }); - describe('and when removing only the row', function() { - beforeEach(function() { + describe('and when removing only the row', () => { + beforeEach(() => { dashboard.removeRow(dashboard.panels[1], false); }); - it('should only remove row', function() { + it('should only remove row', () => { expect(dashboard.panels.length).toBe(4); }); }); }); - describe('When expanding row', function() { + describe('When expanding row', () => { let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ panels: [ { id: 1, type: 'graph', gridPos: { x: 0, y: 0, w: 24, h: 6 } }, @@ -387,16 +387,16 @@ describe('DashboardModel', function() { dashboard.toggleRow(dashboard.panels[1]); }); - it('should add panels back', function() { + it('should add panels back', () => { expect(dashboard.panels.length).toBe(5); }); - it('should add them below row in array', function() { + it('should add them below row in array', () => { expect(dashboard.panels[2].id).toBe(3); expect(dashboard.panels[3].id).toBe(4); }); - it('should position them below row', function() { + it('should position them below row', () => { expect(dashboard.panels[2].gridPos).toMatchObject({ x: 0, y: 7, @@ -405,7 +405,7 @@ describe('DashboardModel', function() { }); }); - it('should move panels below down', function() { + it('should move panels below down', () => { expect(dashboard.panels[4].gridPos).toMatchObject({ x: 0, y: 9, @@ -414,22 +414,22 @@ describe('DashboardModel', function() { }); }); - describe('and when removing row and its panels', function() { - beforeEach(function() { + describe('and when removing row and its panels', () => { + beforeEach(() => { dashboard.removeRow(dashboard.panels[1], true); }); - it('should remove row and its panels', function() { + it('should remove row and its panels', () => { expect(dashboard.panels.length).toBe(2); }); }); - describe('and when removing only the row', function() { - beforeEach(function() { + describe('and when removing only the row', () => { + beforeEach(() => { dashboard.removeRow(dashboard.panels[1], false); }); - it('should only remove row', function() { + it('should only remove row', () => { expect(dashboard.panels.length).toBe(4); }); }); diff --git a/public/app/features/dashboard/specs/history_srv.test.ts b/public/app/features/dashboard/specs/history_srv.test.ts index 5c8578ecf39..1e2bd57a221 100644 --- a/public/app/features/dashboard/specs/history_srv.test.ts +++ b/public/app/features/dashboard/specs/history_srv.test.ts @@ -4,7 +4,7 @@ import { HistorySrv } from '../history/history_srv'; import { DashboardModel } from '../dashboard_model'; jest.mock('app/core/store'); -describe('historySrv', function() { +describe('historySrv', () => { const versionsResponse = versions(); const restoreResponse = restore; @@ -19,35 +19,35 @@ describe('historySrv', function() { const emptyDash = new DashboardModel({}); const historyListOpts = { limit: 10, start: 0 }; - describe('getHistoryList', function() { - it('should return a versions array for the given dashboard id', function() { + describe('getHistoryList', () => { + it('should return a versions array for the given dashboard id', () => { backendSrv.get = jest.fn(() => Promise.resolve(versionsResponse)); historySrv = new HistorySrv(backendSrv); - return historySrv.getHistoryList(dash, historyListOpts).then(function(versions) { + return historySrv.getHistoryList(dash, historyListOpts).then(versions => { expect(versions).toEqual(versionsResponse); }); }); - it('should return an empty array when not given an id', function() { - return historySrv.getHistoryList(emptyDash, historyListOpts).then(function(versions) { + it('should return an empty array when not given an id', () => { + return historySrv.getHistoryList(emptyDash, historyListOpts).then(versions => { expect(versions).toEqual([]); }); }); - it('should return an empty array when not given a dashboard', function() { - return historySrv.getHistoryList(null, historyListOpts).then(function(versions) { + it('should return an empty array when not given a dashboard', () => { + return historySrv.getHistoryList(null, historyListOpts).then(versions => { expect(versions).toEqual([]); }); }); }); describe('restoreDashboard', () => { - it('should return a success response given valid parameters', function() { + it('should return a success response given valid parameters', () => { const version = 6; backendSrv.post = jest.fn(() => Promise.resolve(restoreResponse(version))); historySrv = new HistorySrv(backendSrv); - return historySrv.restoreDashboard(dash, version).then(function(response) { + return historySrv.restoreDashboard(dash, version).then(response => { expect(response).toEqual(restoreResponse(version)); }); }); diff --git a/public/app/features/dashboard/specs/repeat.test.ts b/public/app/features/dashboard/specs/repeat.test.ts index d8c9e3bc2ed..49fb4ea9ee7 100644 --- a/public/app/features/dashboard/specs/repeat.test.ts +++ b/public/app/features/dashboard/specs/repeat.test.ts @@ -4,10 +4,10 @@ import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({})); -describe('given dashboard with panel repeat', function() { - var dashboard; +describe('given dashboard with panel repeat', () => { + let dashboard; - beforeEach(function() { + beforeEach(() => { const dashboardJSON = { panels: [ { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } }, @@ -35,7 +35,7 @@ describe('given dashboard with panel repeat', function() { dashboard.processRepeats(); }); - it('should repeat panels when row is expanding', function() { + it('should repeat panels when row is expanding', () => { expect(dashboard.panels.length).toBe(4); // toggle row @@ -55,10 +55,10 @@ describe('given dashboard with panel repeat', function() { }); }); -describe('given dashboard with panel repeat in horizontal direction', function() { - var dashboard; +describe('given dashboard with panel repeat in horizontal direction', () => { + let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ panels: [ { @@ -89,22 +89,22 @@ describe('given dashboard with panel repeat in horizontal direction', function() dashboard.processRepeats(); }); - it('should repeat panel 3 times', function() { + it('should repeat panel 3 times', () => { expect(dashboard.panels.length).toBe(3); }); - it('should mark panel repeated', function() { + it('should mark panel repeated', () => { expect(dashboard.panels[0].repeat).toBe('apps'); expect(dashboard.panels[1].repeatPanelId).toBe(2); }); - it('should set scopedVars on panels', function() { + it('should set scopedVars on panels', () => { expect(dashboard.panels[0].scopedVars.apps.value).toBe('se1'); expect(dashboard.panels[1].scopedVars.apps.value).toBe('se2'); expect(dashboard.panels[2].scopedVars.apps.value).toBe('se3'); }); - it('should place on first row and adjust width so all fit', function() { + it('should place on first row and adjust width so all fit', () => { expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, @@ -125,23 +125,23 @@ describe('given dashboard with panel repeat in horizontal direction', function() }); }); - describe('After a second iteration', function() { - beforeEach(function() { + describe('After a second iteration', () => { + beforeEach(() => { dashboard.panels[0].fill = 10; dashboard.processRepeats(); }); - it('reused panel should copy properties from source', function() { + it('reused panel should copy properties from source', () => { expect(dashboard.panels[1].fill).toBe(10); }); - it('should have same panel count', function() { + it('should have same panel count', () => { expect(dashboard.panels.length).toBe(3); }); }); - describe('After a second iteration with different variable', function() { - beforeEach(function() { + describe('After a second iteration with different variable', () => { + beforeEach(() => { dashboard.templating.list.push({ name: 'server', current: { text: 'se1, se2, se3', value: ['se1'] }, @@ -151,46 +151,46 @@ describe('given dashboard with panel repeat in horizontal direction', function() dashboard.processRepeats(); }); - it('should remove scopedVars value for last variable', function() { + it('should remove scopedVars value for last variable', () => { expect(dashboard.panels[0].scopedVars.apps).toBe(undefined); }); - it('should have new variable value in scopedVars', function() { + it('should have new variable value in scopedVars', () => { expect(dashboard.panels[0].scopedVars.server.value).toBe('se1'); }); }); - describe('After a second iteration and selected values reduced', function() { - beforeEach(function() { + describe('After a second iteration and selected values reduced', () => { + beforeEach(() => { dashboard.templating.list[0].options[1].selected = false; dashboard.processRepeats(); }); - it('should clean up repeated panel', function() { + it('should clean up repeated panel', () => { expect(dashboard.panels.length).toBe(2); }); }); - describe('After a second iteration and panel repeat is turned off', function() { - beforeEach(function() { + describe('After a second iteration and panel repeat is turned off', () => { + beforeEach(() => { dashboard.panels[0].repeat = null; dashboard.processRepeats(); }); - it('should clean up repeated panel', function() { + it('should clean up repeated panel', () => { expect(dashboard.panels.length).toBe(1); }); - it('should remove scoped vars from reused panel', function() { + it('should remove scoped vars from reused panel', () => { expect(dashboard.panels[0].scopedVars).toBe(undefined); }); }); }); -describe('given dashboard with panel repeat in vertical direction', function() { - var dashboard; +describe('given dashboard with panel repeat in vertical direction', () => { + let dashboard; - beforeEach(function() { + beforeEach(() => { dashboard = new DashboardModel({ panels: [ { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } }, @@ -218,7 +218,7 @@ describe('given dashboard with panel repeat in vertical direction', function() { dashboard.processRepeats(); }); - it('should place on items on top of each other and keep witdh', function() { + it('should place on items on top of each other and keep witdh', () => { expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // first row expect(dashboard.panels[1].gridPos).toMatchObject({ x: 5, y: 1, h: 2, w: 8 }); @@ -271,8 +271,8 @@ describe('given dashboard with row repeat and panel repeat in horizontal directi }); it('should panels in self row', () => { - const panel_types = _.map(dashboard.panels, 'type'); - expect(panel_types).toEqual([ + const panelTypes = _.map(dashboard.panels, 'type'); + expect(panelTypes).toEqual([ 'row', 'graph', 'graph', @@ -290,7 +290,7 @@ describe('given dashboard with row repeat and panel repeat in horizontal directi ]); }); - it('should be placed in their places', function() { + it('should be placed in their places', () => { expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // 1st row expect(dashboard.panels[1].gridPos).toMatchObject({ x: 0, y: 1, h: 2, w: 6 }); @@ -311,10 +311,10 @@ describe('given dashboard with row repeat and panel repeat in horizontal directi }); }); -describe('given dashboard with row repeat', function() { +describe('given dashboard with row repeat', () => { let dashboard, dashboardJSON; - beforeEach(function() { + beforeEach(() => { dashboardJSON = { panels: [ { @@ -349,12 +349,12 @@ describe('given dashboard with row repeat', function() { dashboard.processRepeats(); }); - it('should not repeat only row', function() { - const panel_types = _.map(dashboard.panels, 'type'); - expect(panel_types).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph', 'row', 'graph']); + it('should not repeat only row', () => { + const panelTypes = _.map(dashboard.panels, 'type'); + expect(panelTypes).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph', 'row', 'graph']); }); - it('should set scopedVars for each panel', function() { + it('should set scopedVars for each panel', () => { dashboardJSON.templating.list[0].options[2].selected = true; dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); @@ -375,12 +375,12 @@ describe('given dashboard with row repeat', function() { expect(scopedVars).toEqual(['se1', 'se1', 'se1', 'se2', 'se2', 'se2', 'se3', 'se3', 'se3']); }); - it('should repeat only configured row', function() { + it('should repeat only configured row', () => { expect(dashboard.panels[6].id).toBe(4); expect(dashboard.panels[7].id).toBe(5); }); - it('should repeat only row if it is collapsed', function() { + it('should repeat only row if it is collapsed', () => { dashboardJSON.panels = [ { id: 1, @@ -399,13 +399,13 @@ describe('given dashboard with row repeat', function() { dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); - const panel_types = _.map(dashboard.panels, 'type'); - expect(panel_types).toEqual(['row', 'row', 'row', 'graph']); + const panelTypes = _.map(dashboard.panels, 'type'); + expect(panelTypes).toEqual(['row', 'row', 'row', 'graph']); expect(dashboard.panels[0].panels).toHaveLength(2); expect(dashboard.panels[1].panels).toHaveLength(2); }); - it('should properly repeat multiple rows', function() { + it('should properly repeat multiple rows', () => { dashboardJSON.panels = [ { id: 1, @@ -441,8 +441,8 @@ describe('given dashboard with row repeat', function() { dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); - const panel_types = _.map(dashboard.panels, 'type'); - expect(panel_types).toEqual([ + const panelTypes = _.map(dashboard.panels, 'type'); + expect(panelTypes).toEqual([ 'row', 'graph', 'graph', @@ -469,7 +469,7 @@ describe('given dashboard with row repeat', function() { expect(dashboard.panels[12].scopedVars['hosts'].value).toBe('backend02'); }); - it('should assign unique ids for repeated panels', function() { + it('should assign unique ids for repeated panels', () => { dashboardJSON.panels = [ { id: 1, @@ -488,7 +488,7 @@ describe('given dashboard with row repeat', function() { dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); - const panel_ids = _.flattenDeep( + const panelIds = _.flattenDeep( _.map(dashboard.panels, panel => { let ids = []; if (panel.panels && panel.panels.length) { @@ -498,10 +498,10 @@ describe('given dashboard with row repeat', function() { return ids; }) ); - expect(panel_ids.length).toEqual(_.uniq(panel_ids).length); + expect(panelIds.length).toEqual(_.uniq(panelIds).length); }); - it('should place new panels in proper order', function() { + it('should place new panels in proper order', () => { dashboardJSON.panels = [ { id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 }, repeat: 'apps' }, { id: 2, type: 'graph', gridPos: { x: 0, y: 1, h: 3, w: 12 } }, @@ -511,10 +511,10 @@ describe('given dashboard with row repeat', function() { dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); - const panel_types = _.map(dashboard.panels, 'type'); - expect(panel_types).toEqual(['row', 'graph', 'graph', 'graph', 'row', 'graph', 'graph', 'graph']); - const panel_y_positions = _.map(dashboard.panels, p => p.gridPos.y); - expect(panel_y_positions).toEqual([0, 1, 1, 5, 7, 8, 8, 12]); + const panelTypes = _.map(dashboard.panels, 'type'); + expect(panelTypes).toEqual(['row', 'graph', 'graph', 'graph', 'row', 'graph', 'graph', 'graph']); + const panelYPositions = _.map(dashboard.panels, p => p.gridPos.y); + expect(panelYPositions).toEqual([0, 1, 1, 5, 7, 8, 8, 12]); }); }); @@ -566,8 +566,8 @@ describe('given dashboard with row and panel repeat', () => { }); it('should repeat row and panels for each row', () => { - const panel_types = _.map(dashboard.panels, 'type'); - expect(panel_types).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph']); + const panelTypes = _.map(dashboard.panels, 'type'); + expect(panelTypes).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph']); }); it('should clean up old repeated panels', () => { @@ -592,8 +592,8 @@ describe('given dashboard with row and panel repeat', () => { dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); - const panel_types = _.map(dashboard.panels, 'type'); - expect(panel_types).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph']); + const panelTypes = _.map(dashboard.panels, 'type'); + expect(panelTypes).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph']); }); it('should set scopedVars for each row', () => { @@ -646,7 +646,7 @@ describe('given dashboard with row and panel repeat', () => { }); }); - it('should repeat panels when row is expanding', function() { + it('should repeat panels when row is expanding', () => { dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); diff --git a/public/app/features/dashboard/specs/save_as_modal.test.ts b/public/app/features/dashboard/specs/save_as_modal.test.ts index 29ed694474b..ceb7e49c550 100644 --- a/public/app/features/dashboard/specs/save_as_modal.test.ts +++ b/public/app/features/dashboard/specs/save_as_modal.test.ts @@ -10,11 +10,11 @@ describe('saving dashboard as', () => { }; const mockDashboardSrv = { - getCurrent: function() { + getCurrent: () => { return { id: 5, meta: {}, - getSaveModelClone: function() { + getSaveModelClone: () => { return json; }, }; diff --git a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts index fb1a652a03c..a3ab27a984f 100644 --- a/public/app/features/dashboard/specs/save_provisioned_modal.test.ts +++ b/public/app/features/dashboard/specs/save_provisioned_modal.test.ts @@ -7,11 +7,11 @@ describe('SaveProvisionedDashboardModalCtrl', () => { }; const mockDashboardSrv = { - getCurrent: function() { + getCurrent: () => { return { id: 5, meta: {}, - getSaveModelClone: function() { + getSaveModelClone: () => { return json; }, }; diff --git a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts index 796baf7f522..8a8d94fdddb 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl.test.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl.test.ts @@ -4,7 +4,7 @@ import config from 'app/core/config'; import { LinkSrv } from 'app/features/panellinks/link_srv'; describe('ShareModalCtrl', () => { - const ctx = { + const ctx = { timeSrv: { timeRange: () => { return { from: new Date(1000), to: new Date(2000) }; @@ -26,9 +26,9 @@ describe('ShareModalCtrl', () => { templateSrv: { fillVariableValuesForUrl: () => {}, }, - }; + } as any; - (window).Intl.DateTimeFormat = () => { + (window as any).Intl.DateTimeFormat = () => { return { resolvedOptions: () => { return { timeZone: 'UTC' }; @@ -136,7 +136,7 @@ describe('ShareModalCtrl', () => { ctx.$location.absUrl = () => 'http://server/#!/test'; ctx.scope.options.includeTemplateVars = true; - ctx.templateSrv.fillVariableValuesForUrl = function(params) { + ctx.templateSrv.fillVariableValuesForUrl = params => { params['var-app'] = 'mupp'; params['var-server'] = 'srv-01'; }; diff --git a/public/app/features/dashboard/specs/time_srv.test.ts b/public/app/features/dashboard/specs/time_srv.test.ts index 046ac52c9bf..514e0b90792 100644 --- a/public/app/features/dashboard/specs/time_srv.test.ts +++ b/public/app/features/dashboard/specs/time_srv.test.ts @@ -2,7 +2,7 @@ import { TimeSrv } from '../time_srv'; import '../time_srv'; import moment from 'moment'; -describe('timeSrv', function() { +describe('timeSrv', () => { const rootScope = { $on: jest.fn(), onAppEvent: jest.fn(), @@ -26,20 +26,20 @@ describe('timeSrv', function() { getTimezone: jest.fn(() => 'browser'), }; - beforeEach(function() { + beforeEach(() => { timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); timeSrv.init(_dashboard); }); - describe('timeRange', function() { - it('should return unparsed when parse is false', function() { + describe('timeRange', () => { + it('should return unparsed when parse is false', () => { timeSrv.setTime({ from: 'now', to: 'now-1h' }); const time = timeSrv.timeRange(); expect(time.raw.from).toBe('now'); expect(time.raw.to).toBe('now-1h'); }); - it('should return parsed when parse is true', function() { + it('should return parsed when parse is true', () => { timeSrv.setTime({ from: 'now', to: 'now-1h' }); const time = timeSrv.timeRange(); expect(moment.isMoment(time.from)).toBe(true); @@ -47,8 +47,8 @@ describe('timeSrv', function() { }); }); - describe('init time from url', function() { - it('should handle relative times', function() { + describe('init time from url', () => { + it('should handle relative times', () => { location = { search: jest.fn(() => ({ from: 'now-2d', @@ -63,7 +63,7 @@ describe('timeSrv', function() { expect(time.raw.to).toBe('now'); }); - it('should handle formatted dates', function() { + it('should handle formatted dates', () => { location = { search: jest.fn(() => ({ from: '20140410T052010', @@ -79,7 +79,7 @@ describe('timeSrv', function() { expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime()); }); - it('should handle formatted dates without time', function() { + it('should handle formatted dates without time', () => { location = { search: jest.fn(() => ({ from: '20140410', @@ -95,7 +95,7 @@ describe('timeSrv', function() { expect(time.to.valueOf()).toEqual(new Date('2014-05-20T00:00:00Z').getTime()); }); - it('should handle epochs', function() { + it('should handle epochs', () => { location = { search: jest.fn(() => ({ from: '1410337646373', @@ -111,7 +111,7 @@ describe('timeSrv', function() { expect(time.to.valueOf()).toEqual(1410337665699); }); - it('should handle bad dates', function() { + it('should handle bad dates', () => { location = { search: jest.fn(() => ({ from: '20151126T00010%3C%2Fp%3E%3Cspan%20class', @@ -128,22 +128,22 @@ describe('timeSrv', function() { }); }); - describe('setTime', function() { - it('should return disable refresh if refresh is disabled for any range', function() { + describe('setTime', () => { + it('should return disable refresh if refresh is disabled for any range', () => { _dashboard.refresh = false; timeSrv.setTime({ from: '2011-01-01', to: '2015-01-01' }); expect(_dashboard.refresh).toBe(false); }); - it('should restore refresh for absolute time range', function() { + it('should restore refresh for absolute time range', () => { _dashboard.refresh = '30s'; timeSrv.setTime({ from: '2011-01-01', to: '2015-01-01' }); expect(_dashboard.refresh).toBe('30s'); }); - it('should restore refresh after relative time range is set', function() { + it('should restore refresh after relative time range is set', () => { _dashboard.refresh = '10s'; timeSrv.setTime({ from: moment([2011, 1, 1]), @@ -154,7 +154,7 @@ describe('timeSrv', function() { expect(_dashboard.refresh).toBe('10s'); }); - it('should keep refresh after relative time range is changed and now delay exists', function() { + it('should keep refresh after relative time range is changed and now delay exists', () => { _dashboard.refresh = '10s'; timeSrv.setTime({ from: 'now-1h', to: 'now-10s' }); expect(_dashboard.refresh).toBe('10s'); diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index 9435433848e..4bd78ce776d 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -13,7 +13,7 @@ export class TimeSrv { timeAtLoad: any; private autoRefreshBlocked: boolean; - /** @ngInject **/ + /** @ngInject */ constructor(private $rootScope, private $timeout, private $location, private timer, private contextSrv) { // default time this.time = { from: '6h', to: 'now' }; diff --git a/public/app/features/dashboard/timepicker/input_date.ts b/public/app/features/dashboard/timepicker/input_date.ts index 0e5e176b72b..7de39dfacb2 100644 --- a/public/app/features/dashboard/timepicker/input_date.ts +++ b/public/app/features/dashboard/timepicker/input_date.ts @@ -5,10 +5,10 @@ export function inputDateDirective() { return { restrict: 'A', require: 'ngModel', - link: function($scope, $elem, attrs, ngModel) { + link: ($scope, $elem, attrs, ngModel) => { const format = 'YYYY-MM-DD HH:mm:ss'; - const fromUser = function(text) { + const fromUser = text => { if (text.indexOf('now') !== -1) { if (!dateMath.isValid(text)) { ngModel.$setValidity('error', false); @@ -18,7 +18,7 @@ export function inputDateDirective() { return text; } - var parsed; + let parsed; if ($scope.ctrl.isUtc) { parsed = moment.utc(text, format); } else { @@ -34,7 +34,7 @@ export function inputDateDirective() { return parsed; }; - const toUser = function(currentValue) { + const toUser = currentValue => { if (moment.isMoment(currentValue)) { return currentValue.format(format); } else { diff --git a/public/app/features/dashboard/timepicker/timepicker.html b/public/app/features/dashboard/timepicker/timepicker.html index 07f97604c42..3e38881d480 100644 --- a/public/app/features/dashboard/timepicker/timepicker.html +++ b/public/app/features/dashboard/timepicker/timepicker.html @@ -1,18 +1,8 @@ - - - diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/timepicker/timepicker.ts index 46d45050ecd..c133203cefc 100644 --- a/public/app/features/dashboard/timepicker/timepicker.ts +++ b/public/app/features/dashboard/timepicker/timepicker.ts @@ -23,6 +23,7 @@ export class TimePickerCtrl { isUtc: boolean; firstDayOfWeek: number; isOpen: boolean; + isAbsolute: boolean; /** @ngInject */ constructor(private $scope, private $rootScope, private timeSrv) { @@ -65,6 +66,7 @@ export class TimePickerCtrl { this.tooltip = this.dashboard.formatDate(time.from) + '
to
'; this.tooltip += this.dashboard.formatDate(time.to); this.timeRaw = timeRaw; + this.isAbsolute = moment.isMoment(this.timeRaw.to); } zoom(factor) { @@ -75,7 +77,7 @@ export class TimePickerCtrl { const range = this.timeSrv.timeRange(); const timespan = (range.to.valueOf() - range.from.valueOf()) / 2; - var to, from; + let to, from; if (direction === -1) { to = range.to.valueOf() - timespan; from = range.from.valueOf() - timespan; diff --git a/public/app/features/dashboard/unsaved_changes_srv.ts b/public/app/features/dashboard/unsaved_changes_srv.ts index 0406e6a55d7..f0a8bf40501 100644 --- a/public/app/features/dashboard/unsaved_changes_srv.ts +++ b/public/app/features/dashboard/unsaved_changes_srv.ts @@ -2,7 +2,7 @@ import angular from 'angular'; import { ChangeTracker } from './change_tracker'; /** @ngInject */ -export function unsavedChangesSrv($rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) { +export function unsavedChangesSrv(this: any, $rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) { this.init = function(dashboard, scope) { this.tracker = new ChangeTracker(dashboard, scope, 1000, $location, $window, $timeout, contextSrv, $rootScope); return this.tracker; diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index cd911f51543..974a0c35cd2 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -16,12 +16,12 @@ function uploadDashboardDirective(timer, alertSrv, $location) { scope: { onUpload: '&', }, - link: function(scope) { + link: scope => { function file_selected(evt) { const files = evt.target.files; // FileList object - const readerOnload = function() { - return function(e) { - var dash; + const readerOnload = () => { + return e => { + let dash; try { dash = JSON.parse(e.target.result); } catch (err) { @@ -30,16 +30,21 @@ function uploadDashboardDirective(timer, alertSrv, $location) { return; } - scope.$apply(function() { + scope.$apply(() => { scope.onUpload({ dash: dash }); }); }; }; - for (var i = 0, f; (f = files[i]); i++) { + let i = 0; + let file = files[i]; + + while (file) { const reader = new FileReader(); reader.onload = readerOnload(); - reader.readAsText(f); + reader.readAsText(file); + i += 1; + file = files[i]; } } diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 773ec6ec711..521de4ecbad 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -22,18 +22,18 @@ export class DashboardViewState { self.$scope = $scope; self.dashboard = $scope.dashboard; - $scope.onAppEvent('$routeUpdate', function() { + $scope.onAppEvent('$routeUpdate', () => { const urlState = self.getQueryStringState(); if (self.needsSync(urlState)) { self.update(urlState, true); } }); - $scope.onAppEvent('panel-change-view', function(evt, payload) { + $scope.onAppEvent('panel-change-view', (evt, payload) => { self.update(payload); }); - $scope.onAppEvent('panel-initialized', function(evt, payload) { + $scope.onAppEvent('panel-initialized', (evt, payload) => { self.registerPanel(payload.scope); }); @@ -156,7 +156,7 @@ export class DashboardViewState { } getPanelScope(id) { - return _.find(this.panelScopes, function(panelScope) { + return _.find(this.panelScopes, panelScope => { return panelScope.ctrl.panel.id === id; }); } @@ -176,7 +176,7 @@ export class DashboardViewState { return false; } - this.$timeout(function() { + this.$timeout(() => { if (self.oldTimeRange !== ctrl.range) { self.$rootScope.$broadcast('refresh'); } else { @@ -216,7 +216,7 @@ export class DashboardViewState { } } - const unbind = panelScope.$on('$destroy', function() { + const unbind = panelScope.$on('$destroy', () => { self.panelScopes = _.without(self.panelScopes, panelScope); unbind(); }); @@ -226,7 +226,7 @@ export class DashboardViewState { /** @ngInject */ export function dashboardViewStateSrv($location, $timeout, $rootScope) { return { - create: function($scope) { + create: $scope => { return new DashboardViewState($scope, $location, $timeout, $rootScope); }, }; diff --git a/public/app/features/dashlinks/editor.ts b/public/app/features/dashlinks/editor.ts index aba297603b3..a4ba9ea209a 100644 --- a/public/app/features/dashlinks/editor.ts +++ b/public/app/features/dashlinks/editor.ts @@ -1,7 +1,7 @@ import angular from 'angular'; import _ from 'lodash'; -export var iconMap = { +export let iconMap = { 'external link': 'fa-external-link', dashboard: 'fa-th-large', question: 'fa-question', diff --git a/public/app/features/dashlinks/module.ts b/public/app/features/dashlinks/module.ts index 492ed81a31e..fde41a08d52 100644 --- a/public/app/features/dashlinks/module.ts +++ b/public/app/features/dashlinks/module.ts @@ -10,7 +10,7 @@ function dashLinksContainer() { restrict: 'E', controller: 'DashLinksContainerCtrl', template: '', - link: function() {}, + link: () => {}, }; } @@ -18,9 +18,9 @@ function dashLinksContainer() { function dashLink($compile, $sanitize, linkSrv) { return { restrict: 'E', - link: function(scope, elem) { + link: (scope, elem) => { const link = scope.link; - var template = + let template = '
' + ' { $scope.generatedLinks = _.flatten(results); }); } - $scope.searchDashboards = function(link, limit) { - return backendSrv.search({ tag: link.tags, limit: limit }).then(function(results) { + $scope.searchDashboards = (link, limit) => { + return backendSrv.search({ tag: link.tags, limit: limit }).then(results => { return _.reduce( results, - function(memo, dash) { + (memo, dash) => { // do not add current dashboard if (dash.id !== currentDashId) { memo.push({ @@ -158,9 +158,9 @@ export class DashLinksContainerCtrl { }); }; - $scope.fillDropdown = function(link) { - $scope.searchDashboards(link, 100).then(function(results) { - _.each(results, function(hit) { + $scope.fillDropdown = link => { + $scope.searchDashboards(link, 100).then(results => { + _.each(results, hit => { hit.url = linkSrv.getLinkUrl(hit); }); link.searchHits = results; diff --git a/public/app/features/org/change_password_ctrl.ts b/public/app/features/org/change_password_ctrl.ts index b84cbecfea7..7a4ba0f031a 100644 --- a/public/app/features/org/change_password_ctrl.ts +++ b/public/app/features/org/change_password_ctrl.ts @@ -2,14 +2,14 @@ import angular from 'angular'; import config from 'app/core/config'; export class ChangePasswordCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, backendSrv, $location, navModelSrv) { $scope.command = {}; $scope.authProxyEnabled = config.authProxyEnabled; $scope.ldapEnabled = config.ldapEnabled; $scope.navModel = navModelSrv.getNav('profile', 'change-password', 0); - $scope.changePassword = function() { + $scope.changePassword = () => { if (!$scope.userForm.$valid) { return; } @@ -19,7 +19,7 @@ export class ChangePasswordCtrl { return; } - backendSrv.put('/api/user/password', $scope.command).then(function() { + backendSrv.put('/api/user/password', $scope.command).then(() => { $location.path('profile'); }); }; diff --git a/public/app/features/org/create_team_ctrl.ts b/public/app/features/org/create_team_ctrl.ts index 241e96968a0..d016d85afc0 100644 --- a/public/app/features/org/create_team_ctrl.ts +++ b/public/app/features/org/create_team_ctrl.ts @@ -5,7 +5,7 @@ export default class CreateTeamCtrl { email: string; navModel: any; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, private $location, navModelSrv) { this.navModel = navModelSrv.getNav('cfg', 'teams', 0); } diff --git a/public/app/features/org/new_org_ctrl.ts b/public/app/features/org/new_org_ctrl.ts index 91b16adc113..6a8808abfac 100644 --- a/public/app/features/org/new_org_ctrl.ts +++ b/public/app/features/org/new_org_ctrl.ts @@ -2,14 +2,14 @@ import angular from 'angular'; import config from 'app/core/config'; export class NewOrgCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, $http, backendSrv, navModelSrv) { $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1); $scope.newOrg = { name: '' }; - $scope.createOrg = function() { - backendSrv.post('/api/orgs/', $scope.newOrg).then(function(result) { - backendSrv.post('/api/user/using/' + result.orgId).then(function() { + $scope.createOrg = () => { + backendSrv.post('/api/orgs/', $scope.newOrg).then(result => { + backendSrv.post('/api/user/using/' + result.orgId).then(() => { window.location.href = config.appSubUrl + '/org'; }); }); diff --git a/public/app/features/org/org_api_keys_ctrl.ts b/public/app/features/org/org_api_keys_ctrl.ts index 4ea40b900c7..1ead0a350b9 100644 --- a/public/app/features/org/org_api_keys_ctrl.ts +++ b/public/app/features/org/org_api_keys_ctrl.ts @@ -1,29 +1,29 @@ import angular from 'angular'; export class OrgApiKeysCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, $http, backendSrv, navModelSrv) { $scope.navModel = navModelSrv.getNav('cfg', 'apikeys', 0); $scope.roleTypes = ['Viewer', 'Editor', 'Admin']; $scope.token = { role: 'Viewer' }; - $scope.init = function() { + $scope.init = () => { $scope.getTokens(); }; - $scope.getTokens = function() { - backendSrv.get('/api/auth/keys').then(function(tokens) { + $scope.getTokens = () => { + backendSrv.get('/api/auth/keys').then(tokens => { $scope.tokens = tokens; }); }; - $scope.removeToken = function(id) { + $scope.removeToken = id => { backendSrv.delete('/api/auth/keys/' + id).then($scope.getTokens); }; - $scope.addToken = function() { - backendSrv.post('/api/auth/keys', $scope.token).then(function(result) { + $scope.addToken = () => { + backendSrv.post('/api/auth/keys', $scope.token).then(result => { const modalScope = $scope.$new(true); modalScope.key = result.key; modalScope.rootPath = window.location.origin + $scope.$root.appSubUrl; diff --git a/public/app/features/org/org_details_ctrl.ts b/public/app/features/org/org_details_ctrl.ts index 7bc6ee1336b..1d4a92c6e8b 100644 --- a/public/app/features/org/org_details_ctrl.ts +++ b/public/app/features/org/org_details_ctrl.ts @@ -1,22 +1,22 @@ import angular from 'angular'; export class OrgDetailsCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, $http, backendSrv, contextSrv, navModelSrv) { - $scope.init = function() { + $scope.init = () => { $scope.getOrgInfo(); $scope.navModel = navModelSrv.getNav('cfg', 'org-settings', 0); }; - $scope.getOrgInfo = function() { - backendSrv.get('/api/org').then(function(org) { + $scope.getOrgInfo = () => { + backendSrv.get('/api/org').then(org => { $scope.org = org; $scope.address = org.address; contextSrv.user.orgName = org.name; }); }; - $scope.update = function() { + $scope.update = () => { if (!$scope.orgForm.$valid) { return; } @@ -24,7 +24,7 @@ export class OrgDetailsCtrl { backendSrv.put('/api/org', data).then($scope.getOrgInfo); }; - $scope.updateAddress = function() { + $scope.updateAddress = () => { if (!$scope.addressForm.$valid) { return; } diff --git a/public/app/features/org/prefs_control.ts b/public/app/features/org/prefs_control.ts index 6c6ffdf647d..74dde250eec 100644 --- a/public/app/features/org/prefs_control.ts +++ b/public/app/features/org/prefs_control.ts @@ -14,7 +14,7 @@ export class PrefsControlCtrl { ]; themes: any = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }]; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, private $location) {} $onInit() { diff --git a/public/app/features/org/profile_ctrl.ts b/public/app/features/org/profile_ctrl.ts index 40ee4d908a1..b6d330a3216 100644 --- a/public/app/features/org/profile_ctrl.ts +++ b/public/app/features/org/profile_ctrl.ts @@ -3,7 +3,7 @@ import { coreModule } from 'app/core/core'; export class ProfileCtrl { user: any; - old_theme: any; + oldTheme: any; teams: any = []; orgs: any = []; userForm: any; @@ -12,7 +12,7 @@ export class ProfileCtrl { readonlyLoginFields = config.disableLoginForm; navModel: any; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, private contextSrv, private $location, navModelSrv) { this.getUser(); this.getUserTeams(); @@ -54,7 +54,7 @@ export class ProfileCtrl { this.backendSrv.put('/api/user/', this.user).then(() => { this.contextSrv.user.name = this.user.name || this.user.login; - if (this.old_theme !== this.user.theme) { + if (this.oldTheme !== this.user.theme) { window.location.href = config.appSubUrl + this.$location.path(); } }); diff --git a/public/app/features/org/select_org_ctrl.ts b/public/app/features/org/select_org_ctrl.ts index 199d1f8ac94..cd5166771ac 100644 --- a/public/app/features/org/select_org_ctrl.ts +++ b/public/app/features/org/select_org_ctrl.ts @@ -2,7 +2,7 @@ import angular from 'angular'; import config from 'app/core/config'; export class SelectOrgCtrl { - /** @ngInject **/ + /** @ngInject */ constructor($scope, backendSrv, contextSrv) { contextSrv.sidemenu = false; @@ -14,18 +14,18 @@ export class SelectOrgCtrl { }, }; - $scope.init = function() { + $scope.init = () => { $scope.getUserOrgs(); }; - $scope.getUserOrgs = function() { - backendSrv.get('/api/user/orgs').then(function(orgs) { + $scope.getUserOrgs = () => { + backendSrv.get('/api/user/orgs').then(orgs => { $scope.orgs = orgs; }); }; - $scope.setUsingOrg = function(org) { - backendSrv.post('/api/user/using/' + org.orgId).then(function() { + $scope.setUsingOrg = org => { + backendSrv.post('/api/user/using/' + org.orgId).then(() => { window.location.href = config.appSubUrl + '/'; }); }; diff --git a/public/app/features/org/user_invite_ctrl.ts b/public/app/features/org/user_invite_ctrl.ts index 36dde418b2c..9f3b641035a 100644 --- a/public/app/features/org/user_invite_ctrl.ts +++ b/public/app/features/org/user_invite_ctrl.ts @@ -5,7 +5,7 @@ export class UserInviteCtrl { invite: any; inviteForm: any; - /** @ngInject **/ + /** @ngInject */ constructor(private backendSrv, navModelSrv, private $location) { this.navModel = navModelSrv.getNav('cfg', 'users', 0); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 3d8a4ed3736..5eecf6036d8 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -75,7 +75,7 @@ class MetricsPanelCtrl extends PanelCtrl { // if we have snapshot data use that if (this.panel.snapshotData) { this.updateTimeRange(); - var data = this.panel.snapshotData; + let data = this.panel.snapshotData; // backward compatibility if (!_.isArray(data)) { data = data.data; @@ -155,7 +155,7 @@ class MetricsPanelCtrl extends PanelCtrl { } calculateInterval() { - var intervalOverride = this.panel.interval; + let intervalOverride = this.panel.interval; // if no panel interval check datasource if (intervalOverride) { @@ -164,7 +164,7 @@ class MetricsPanelCtrl extends PanelCtrl { intervalOverride = this.datasource.interval; } - var res = kbn.calculateInterval(this.range, this.resolution, intervalOverride); + const res = kbn.calculateInterval(this.range, this.resolution, intervalOverride); this.interval = res.interval; this.intervalMs = res.intervalMs; } @@ -174,15 +174,15 @@ class MetricsPanelCtrl extends PanelCtrl { // check panel time overrrides if (this.panel.timeFrom) { - var timeFromInterpolated = this.templateSrv.replace(this.panel.timeFrom, this.panel.scopedVars); - var timeFromInfo = rangeUtil.describeTextRange(timeFromInterpolated); + const timeFromInterpolated = this.templateSrv.replace(this.panel.timeFrom, this.panel.scopedVars); + const timeFromInfo = rangeUtil.describeTextRange(timeFromInterpolated); if (timeFromInfo.invalid) { this.timeInfo = 'invalid time override'; return; } if (_.isString(this.range.raw.from)) { - var timeFromDate = dateMath.parse(timeFromInfo.from); + const timeFromDate = dateMath.parse(timeFromInfo.from); this.timeInfo = timeFromInfo.display; this.range.from = timeFromDate; this.range.to = dateMath.parse(timeFromInfo.to); @@ -192,14 +192,14 @@ class MetricsPanelCtrl extends PanelCtrl { } if (this.panel.timeShift) { - var timeShiftInterpolated = this.templateSrv.replace(this.panel.timeShift, this.panel.scopedVars); - var timeShiftInfo = rangeUtil.describeTextRange(timeShiftInterpolated); + const timeShiftInterpolated = this.templateSrv.replace(this.panel.timeShift, this.panel.scopedVars); + const timeShiftInfo = rangeUtil.describeTextRange(timeShiftInterpolated); if (timeShiftInfo.invalid) { this.timeInfo = 'invalid timeshift'; return; } - var timeShift = '-' + timeShiftInterpolated; + const timeShift = '-' + timeShiftInterpolated; this.timeInfo += ' timeshift ' + timeShift; this.range.from = dateMath.parseDateMath(timeShift, this.range.from, false); this.range.to = dateMath.parseDateMath(timeShift, this.range.to, true); @@ -220,12 +220,12 @@ class MetricsPanelCtrl extends PanelCtrl { // make shallow copy of scoped vars, // and add built in variables interval and interval_ms - var scopedVars = Object.assign({}, this.panel.scopedVars, { + const scopedVars = Object.assign({}, this.panel.scopedVars, { __interval: { text: this.interval, value: this.interval }, __interval_ms: { text: this.intervalMs, value: this.intervalMs }, }); - var metricsQuery = { + const metricsQuery = { timezone: this.dashboard.getTimezone(), panelId: this.panel.id, dashboardId: this.dashboard.id, @@ -343,14 +343,14 @@ class MetricsPanelCtrl extends PanelCtrl { } removeQuery(target) { - var index = _.indexOf(this.panel.targets, target); + const index = _.indexOf(this.panel.targets, target); this.panel.targets.splice(index, 1); this.nextRefId = this.dashboard.getNextQueryLetter(this.panel); this.refresh(); } moveQuery(target, direction) { - var index = _.indexOf(this.panel.targets, target); + const index = _.indexOf(this.panel.targets, target); _.move(this.panel.targets, index, index + direction); } } diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 94aa142a6b8..3a1d0abe1c2 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -92,7 +92,7 @@ export class MetricsTabCtrl { this.helpOpen = !this.helpOpen; this.backendSrv.get(`/api/plugins/${this.datasourceInstance.meta.id}/markdown/query_help`).then(res => { - var md = new Remarkable(); + const md = new Remarkable(); this.helpHtml = this.$sce.trustAsHtml(md.render(res)); }); } @@ -110,7 +110,7 @@ export class MetricsTabCtrl { } } -/** @ngInject **/ +/** @ngInject */ export function metricsTabDirective() { 'use strict'; return { diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 6a583b700ef..e2ae5cc78a9 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -43,7 +43,7 @@ export class PanelCtrl { this.events = this.panel.events; this.timing = {}; - var plugin = config.panels[this.panel.type]; + const plugin = config.panels[this.panel.type]; if (plugin) { this.pluginId = plugin.id; this.pluginName = plugin.name; @@ -105,7 +105,7 @@ export class PanelCtrl { this.editModeInitiated = true; this.events.emit('init-edit-mode', null); - var urlTab = (this.$injector.get('$routeParams').tab || '').toLowerCase(); + const urlTab = (this.$injector.get('$routeParams').tab || '').toLowerCase(); if (urlTab) { this.editorTabs.forEach((tab, i) => { if (tab.title.toLowerCase() === urlTab) { @@ -117,16 +117,16 @@ export class PanelCtrl { changeTab(newIndex) { this.editorTabIndex = newIndex; - var route = this.$injector.get('$route'); + const route = this.$injector.get('$route'); route.current.params.tab = this.editorTabs[newIndex].title.toLowerCase(); route.updateParams(); } addEditorTab(title, directiveFn, index?) { - var editorTab = { title, directiveFn }; + const editorTab = { title, directiveFn }; if (_.isString(directiveFn)) { - editorTab.directiveFn = function() { + editorTab.directiveFn = () => { return { templateUrl: directiveFn }; }; } @@ -225,9 +225,9 @@ export class PanelCtrl { calculatePanelHeight() { if (this.fullscreen) { - var docHeight = $(window).height(); - var editHeight = Math.floor(docHeight * 0.4); - var fullscreenHeight = Math.floor(docHeight * 0.8); + const docHeight = $(window).height(); + const editHeight = Math.floor(docHeight * 0.4); + const fullscreenHeight = Math.floor(docHeight * 0.8); this.containerHeight = this.editMode ? editHeight : fullscreenHeight; } else { this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN; @@ -293,7 +293,7 @@ export class PanelCtrl { } sharePanel() { - var shareScope = this.$scope.$new(); + const shareScope = this.$scope.$new(); shareScope.panel = this.panel; shareScope.dashboard = this.dashboard; @@ -317,24 +317,24 @@ export class PanelCtrl { } getInfoContent(options) { - var markdown = this.panel.description; + let markdown = this.panel.description; if (options.mode === 'tooltip') { markdown = this.error || this.panel.description; } - var linkSrv = this.$injector.get('linkSrv'); - var sanitize = this.$injector.get('$sanitize'); - var templateSrv = this.$injector.get('templateSrv'); - var interpolatedMarkdown = templateSrv.replace(markdown, this.panel.scopedVars); - var html = '
'; + const linkSrv = this.$injector.get('linkSrv'); + const sanitize = this.$injector.get('$sanitize'); + const templateSrv = this.$injector.get('templateSrv'); + const interpolatedMarkdown = templateSrv.replace(markdown, this.panel.scopedVars); + let html = '
'; html += new Remarkable().render(interpolatedMarkdown); if (this.panel.links && this.panel.links.length > 0) { html += '