diff --git a/.circleci/config.yml b/.circleci/config.yml index 465be85d508..3aedd49c935 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,12 +88,9 @@ jobs: test-frontend: docker: - - image: circleci/node:6.11.4 + - image: circleci/node:8 steps: - checkout - - run: - name: install yarn - command: 'sudo npm install -g yarn --quiet' - restore_cache: key: dependency-cache-{{ checksum "yarn.lock" }} - run: diff --git a/CHANGELOG.md b/CHANGELOG.md index 19daed035c4..b3ba9076774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,27 @@ * **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) +* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps) -# 5.2.0 (unreleased) +# 5.2.1 (2018-06-29) + +### Minor + +* **Auth Proxy**: Important security fix for whitelist of IP address feature [#12444](https://github.com/grafana/grafana/pull/12444) +* **UI**: Fix - Grafana footer overlapping page [#12430](https://github.com/grafana/grafana/issues/12430) +* **Logging**: Errors should be reported before crashing [#12438](https://github.com/grafana/grafana/issues/12438) + +# 5.2.0-stable (2018-06-27) ### Minor * **Plugins**: Handle errors correctly when loading datasource plugin [#12383](https://github.com/grafana/grafana/pull/12383) thx [@rozetko](https://github.com/rozetko) * **Render**: Enhance error message if phantomjs executable is not found [#11868](https://github.com/grafana/grafana/issues/11868) +* **Dashboard**: Set correct text in drop down when variable is present in url [#11968](https://github.com/grafana/grafana/issues/11968) + +### 5.2.0-beta3 fixes + +* **LDAP**: Handle "dn" ldap attribute more gracefully [#12385](https://github.com/grafana/grafana/pull/12385), reverts [#10970](https://github.com/grafana/grafana/pull/10970) # 5.2.0-beta3 (2018-06-21) @@ -56,6 +70,7 @@ ### New Features * **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95) +* **Build**: Crosscompile and packages Grafana on arm, windows, linux and darwin [#11920](https://github.com/grafana/grafana/pull/11920), thx [@fg2it](https://github.com/fg2it) * **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882) * **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541) @@ -91,6 +106,10 @@ * **Dashboard list panel**: Search dashboards by folder [#11525](https://github.com/grafana/grafana/issues/11525) * **Sidenav**: Always show server admin link in sidenav if grafana admin [#11657](https://github.com/grafana/grafana/issues/11657) +# 5.1.5 (2018-06-27) + +* **Docker**: Config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170) + # 5.1.4 (2018-06-19) * **Permissions**: Important security fix for API keys with viewer role [#12343](https://github.com/grafana/grafana/issues/12343) @@ -1318,7 +1337,7 @@ Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated **New features** - [Issue #1623](https://github.com/grafana/grafana/issues/1623). Share Dashboard: Dashboard snapshot sharing (dash and data snapshot), save to local or save to public snapshot dashboard snapshots.raintank.io site - [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site -- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is promted with a warning if he really wants to overwrite the other's changes +- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is prompted with a warning if he really wants to overwrite the other's changes - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views - [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, useful when you want to ignore last minute because it contains incomplete data - [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift diff --git a/docs/sources/guides/whats-new-in-v5-2.md b/docs/sources/guides/whats-new-in-v5-2.md index 554f8f073d8..e084f8618e4 100644 --- a/docs/sources/guides/whats-new-in-v5-2.md +++ b/docs/sources/guides/whats-new-in-v5-2.md @@ -14,14 +14,14 @@ weight = -8 Grafana v5.2 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements. -* [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here! -* [Cross platform build support]({{< relref "#cross-platform-build-support" >}}) enables native builds of Grafana for many more platforms! -* [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets -* [Security]({{< relref "#security" >}}) make your Grafana instance more secure -* [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements -* [InfluxDB]({{< relref "#influxdb" >}}) with support for a new function -* [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord -* [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements +- [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here! +- [Native builds for ARM]({{< relref "#native-builds-for-arm" >}}) native builds of Grafana for many more platforms! +- [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets +- [Security]({{< relref "#security" >}}) make your Grafana instance more secure +- [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements +- [InfluxDB]({{< relref "#influxdb" >}}) now supports the `mode` function +- [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord +- [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements ## Elasticsearch alerting @@ -32,16 +32,18 @@ the most requested features by our community and now it's finally here. Please t
-## Cross platform build support +## Native builds for ARM -Grafana v5.2 brings an improved build pipeline with cross platform support. This enables native builds of Grafana for ARMv7 (x32), ARM64 (x64), -MacOS/Darwin (x64) and Windows (x64) in both stable and nightly builds. +Grafana v5.2 brings an improved build pipeline with cross-platform support. This enables native builds of Grafana for ARMv7 (x32) and ARM64 (x64). +We've been longing for native ARM build support for ages. With the help from our amazing community this is now finally available. +Please try it out and let us know what you think. -We've been longing for native ARM build support for a long time. With the help from our amazing community this is now finally available. +Another great addition with the improved build pipeline is that binaries for MacOS/Darwin (x64) and Windows (x64) are now automatically built and +published for both stable and nightly builds. ## Improved Docker image -The Grafana docker image now includes support for Docker secrets which enables you to supply Grafana with configuration through files. More +The Grafana docker image adds support for Docker secrets which enables you to supply Grafana with configuration through files. More information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets). ## Security @@ -49,18 +51,18 @@ information in the [Installing using Docker documentation](/installation/docker/ {{< docs-imagebox img="/img/docs/v52/login_change_password.png" max-width="800px" class="docs-image--right" >}} Starting from Grafana v5.2, when you login with the administrator account using the default password you'll be presented with a form to change the password. -By this we hope to encourage users to follow Grafana's best practices and change the default administrator password. +We hope this encourages users to follow Grafana's best practices and change the default administrator password.
## Prometheus The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate* -functions get consistent results, and thus avoid graphs jumping around on reload. +functions get consistent results, and thus avoids graphs jumping around on reload. ## InfluxDB -The InfluxDB datasource now includes support for the *mode* function which allows to return the most frequent value in a list of field values. +The InfluxDB datasource now includes support for the *mode* function which returns the most frequent value in a list of field values. ## Alerting @@ -72,9 +74,9 @@ By popular demand Grafana now includes support for an alert notification channel {{< docs-imagebox img="/img/docs/v52/dashboard_save_modal.png" max-width="800px" class="docs-image--right" >}} -Starting from Grafana v5.2 a modified time range or variable are no longer saved by default. To save a modified -time range or variable you'll need to actively select that when saving a dashboard, see screenshot. -This should hopefully make it easier to have sane defaults of time and variables in dashboards and make it more explicit +Starting from Grafana v5.2, a modified time range or variable are no longer saved by default. To save a modified +time range or variable, you'll need to actively select that when saving a dashboard, see screenshot. +This should hopefully make it easier to have sane defaults for time and variables in dashboards and make it more explicit when you actually want to overwrite those settings.
@@ -83,13 +85,13 @@ when you actually want to overwrite those settings. {{< docs-imagebox img="/img/docs/v52/dashboard_import.png" max-width="800px" class="docs-image--right" >}} -Grafana v5.2 adds support for specifying an existing folder or create a new one when importing a dashboard, a long awaited feature since -Grafana v5.0 introduced support for dashboard folders and permissions. The import dashboard page have also got some general improvements +Grafana v5.2 adds support for specifying an existing folder or creating a new one when importing a dashboard - a long-awaited feature since +Grafana v5.0 introduced support for dashboard folders and permissions. The import dashboard page has also got some general improvements and should now make it more clear if a possible import will overwrite an existing dashboard, or not. -This release also adds some improvements for those users only having editor or admin permissions in certain folders. Now the links to -*Create Dashboard* and *Import Dashboard* is available in side navigation, dashboard search and manage dashboards/folder page for a -user that has editor role in an organization or edit permission in at least one folder. +This release also adds some improvements for those users only having editor or admin permissions in certain folders. The links to +*Create Dashboard* and *Import Dashboard* are now available in the side navigation, in dashboard search and on the manage dashboards/folder page for a +user that has editor role in an organization or the edit permission in at least one folder.
diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index 166a5a4fdb9..8ff40b5ef04 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -44,6 +44,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk The `Authorization` header value should be `Bearer `. +The API Token can also be passed as a Basic authorization password with the special username `api_key`: + +curl example: +```bash +?curl http://api_key:eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk@localhost:3000/api/org +{"id":1,"name":"Main Org."} +``` + # Auth HTTP resources / actions ## Api Keys diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md index 7ee1f737799..fb318ecf58e 100644 --- a/docs/sources/http_api/folder.md +++ b/docs/sources/http_api/folder.md @@ -19,6 +19,10 @@ The unique identifier (uid) of a folder can be used for uniquely identify folder The uid can have a maximum length of 40 characters. +## A note about the General folder + +The General folder (id=0) is special and is not part of the Folder API which means +that you cannot use this API for retrieving information about the General folder. ## Get all folders @@ -273,14 +277,14 @@ Status Codes: ## Get folder by id -`GET /api/folders/:id` +`GET /api/folders/id/:id` Will return the folder identified by id. **Example Request**: ```http -GET /api/folders/1 HTTP/1.1 +GET /api/folders/id/1 HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md index 1ebde0878e7..c55107d42f8 100644 --- a/docs/sources/http_api/org.md +++ b/docs/sources/http_api/org.md @@ -12,7 +12,13 @@ parent = "http_api" # Organisation API -## Get current Organisation +The Organisation HTTP API is divided in two resources, `/api/org` (current organisation) +and `/api/orgs` (admin organisations). One big difference between these are that +the admin of all organisations API only works with basic authentication, see [Admin Organisations API](#admin-organisations-api) for more information. + +## Current Organisation API + +### Get current Organisation `GET /api/org/` @@ -37,135 +43,7 @@ Content-Type: application/json } ``` -## Get Organisation by Id - -`GET /api/orgs/:orgId` - -**Example Request**: - -```http -GET /api/orgs/1 HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` -Note: The api will only work when you pass the admin name and password -to the request http url, like http://admin:admin@localhost:3000/api/orgs/1 - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "id":1, - "name":"Main Org.", - "address":{ - "address1":"", - "address2":"", - "city":"", - "zipCode":"", - "state":"", - "country":"" - } -} -``` -## Get Organisation by Name - -`GET /api/orgs/name/:orgName` - -**Example Request**: - -```http -GET /api/orgs/name/Main%20Org%2E HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk -``` -Note: The api will only work when you pass the admin name and password -to the request http url, like http://admin:admin@localhost:3000/api/orgs/name/Main%20Org%2E - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "id":1, - "name":"Main Org.", - "address":{ - "address1":"", - "address2":"", - "city":"", - "zipCode":"", - "state":"", - "country":"" - } -} -``` - -## Create Organisation - -`POST /api/orgs` - -**Example Request**: - -```http -POST /api/orgs HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "name":"New Org." -} -``` -Note: The api will work in the following two ways -1) Need to set GF_USERS_ALLOW_ORG_CREATE=true -2) Set the config users.allow_org_create to true in ini file - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "orgId":"1", - "message":"Organization created" -} -``` - - -## Update current Organisation - -`PUT /api/org` - -**Example Request**: - -```http -PUT /api/org HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "name":"Main Org." -} -``` - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{"message":"Organization updated"} -``` - -## Get all users within the actual organisation +### Get all users within the current organisation `GET /api/org/users` @@ -195,36 +73,7 @@ Content-Type: application/json ] ``` -## Add a new user to the actual organisation - -`POST /api/org/users` - -Adds a global user to the actual organisation. - -**Example Request**: - -```http -POST /api/org/users HTTP/1.1 -Accept: application/json -Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - -{ - "role": "Admin", - "loginOrEmail": "admin" -} -``` - -**Example Response**: - -```http -HTTP/1.1 200 -Content-Type: application/json - -{"message":"User added to organization"} -``` - -## Updates the given user +### Updates the given user `PATCH /api/org/users/:userId` @@ -250,7 +99,7 @@ Content-Type: application/json {"message":"Organization user updated"} ``` -## Delete user in actual organisation +### Delete user in current organisation `DELETE /api/org/users/:userId` @@ -272,19 +121,181 @@ Content-Type: application/json {"message":"User removed from organization"} ``` -# Organisations +### Update current Organisation -## Search all Organisations +`PUT /api/org` + +**Example Request**: + +```http +PUT /api/org HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "name":"Main Org." +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Organization updated"} +``` + +### Add a new user to the current organisation + +`POST /api/org/users` + +Adds a global user to the current organisation. + +**Example Request**: + +```http +POST /api/org/users HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "role": "Admin", + "loginOrEmail": "admin" +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"User added to organization"} +``` + +## Admin Organisations API + +The Admin Organisations HTTP API does not currently work with an API Token. API Tokens are currently +only linked to an organization and an organization role. They cannot be given the permission of server +admin, only users can be given that permission. So in order to use these API calls you will have to +use Basic Auth and the Grafana user must have the Grafana Admin permission (The default admin user +is called `admin` and has permission to use this API). + +### Get Organisation by Id + +`GET /api/orgs/:orgId` + +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + +**Example Request**: + +```http +GET /api/orgs/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "id":1, + "name":"Main Org.", + "address":{ + "address1":"", + "address2":"", + "city":"", + "zipCode":"", + "state":"", + "country":"" + } +} +``` +### Get Organisation by Name + +`GET /api/orgs/name/:orgName` + +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + +**Example Request**: + +```http +GET /api/orgs/name/Main%20Org%2E HTTP/1.1 +Accept: application/json +Content-Type: application/json +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "id":1, + "name":"Main Org.", + "address":{ + "address1":"", + "address2":"", + "city":"", + "zipCode":"", + "state":"", + "country":"" + } +} +``` + +### Create Organisation + +`POST /api/orgs` + +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + +**Example Request**: + +```http +POST /api/orgs HTTP/1.1 +Accept: application/json +Content-Type: application/json + +{ + "name":"New Org." +} +``` +Note: The api will work in the following two ways +1) Need to set GF_USERS_ALLOW_ORG_CREATE=true +2) Set the config users.allow_org_create to true in ini file + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "orgId":"1", + "message":"Organization created" +} +``` + +### Search all Organisations `GET /api/orgs` +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + **Example Request**: ```http GET /api/orgs HTTP/1.1 Accept: application/json Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` Note: The api will only work when you pass the admin name and password to the request http url, like http://admin:admin@localhost:3000/api/orgs @@ -303,11 +314,12 @@ Content-Type: application/json ] ``` -## Update Organisation +### Update Organisation `PUT /api/orgs/:orgId` Update Organisation, fields *Address 1*, *Address 2*, *City* are not implemented yet. +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). **Example Request**: @@ -315,7 +327,6 @@ Update Organisation, fields *Address 1*, *Address 2*, *City* are not implemented PUT /api/orgs/1 HTTP/1.1 Accept: application/json Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk { "name":"Main Org 2." @@ -331,16 +342,17 @@ Content-Type: application/json {"message":"Organization updated"} ``` -## Delete Organisation +### Delete Organisation `DELETE /api/orgs/:orgId` +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + **Example Request**: ```http DELETE /api/orgs/1 HTTP/1.1 Accept: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` **Example Response**: @@ -352,17 +364,18 @@ Content-Type: application/json {"message":"Organization deleted"} ``` -## Get Users in Organisation +### Get Users in Organisation `GET /api/orgs/:orgId/users` +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + **Example Request**: ```http GET /api/orgs/1/users HTTP/1.1 Accept: application/json Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` Note: The api will only work when you pass the admin name and password to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users @@ -384,25 +397,24 @@ Content-Type: application/json ] ``` -## Add User in Organisation +### Add User in Organisation `POST /api/orgs/:orgId/users` +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + **Example Request**: ```http POST /api/orgs/1/users HTTP/1.1 Accept: application/json Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk { "loginOrEmail":"user", "role":"Viewer" } ``` -Note: The api will only work when you pass the admin name and password -to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users **Example Response**: @@ -413,17 +425,18 @@ Content-Type: application/json {"message":"User added to organization"} ``` -## Update Users in Organisation +### Update Users in Organisation `PATCH /api/orgs/:orgId/users/:userId` +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + **Example Request**: ```http PATCH /api/orgs/1/users/2 HTTP/1.1 Accept: application/json Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk { "role":"Admin" @@ -439,17 +452,18 @@ Content-Type: application/json {"message":"Organization user updated"} ``` -## Delete User in Organisation +### Delete User in Organisation `DELETE /api/orgs/:orgId/users/:userId` +Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api). + **Example Request**: ```http DELETE /api/orgs/1/users/2 HTTP/1.1 Accept: application/json Content-Type: application/json -Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` **Example Response**: diff --git a/docs/sources/index.md b/docs/sources/index.md index 3c59b9baba0..da977b73e0c 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]

Provisioning

A guide to help you automate your Grafana setup & configuration.

- }}" class="nav-cards__item nav-cards__item--guide"> -

What's new in v5.0

-

Article on all the new cool features and enhancements in v5.0

+
}}" class="nav-cards__item nav-cards__item--guide"> +

What's new in v5.2

+

Article on all the new cool features and enhancements in v5.2

}}" class="nav-cards__item nav-cards__item--guide">

Screencasts

diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 2c847b10471..4bb245a586e 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,10 +15,9 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [grafana_5.1.4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb) - +Stable for Debian-based Linux | [x86-64](https://grafana.com/grafana/download?platform=linux) +Stable for Debian-based Linux | [ARM64](https://grafana.com/grafana/download?platform=arm) +Stable for Debian-based Linux | [ARMv7](https://grafana.com/grafana/download?platform=arm) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. @@ -27,17 +26,18 @@ installation. ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb +wget sudo apt-get install -y adduser libfontconfig sudo dpkg -i grafana_5.1.4_amd64.deb ``` - +sudo dpkg -i grafana_5.1.4_amd64.deb +``` ## APT Repository diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md index b1d4f18f699..12ff4adaab9 100644 --- a/docs/sources/installation/mac.md +++ b/docs/sources/installation/mac.md @@ -11,6 +11,8 @@ weight = 4 # Installing on Mac +## Install using homebrew + Installation can be done using [homebrew](http://brew.sh/) Install latest stable: @@ -75,3 +77,18 @@ If you want to manually install a plugin place it here: `/usr/local/var/lib/graf The default sqlite database is located at `/usr/local/var/lib/grafana` +## Installing from binary tar file + +Download [the latest `.tar.gz` file](https://grafana.com/get) and +extract it. This will extract into a folder named after the version you +downloaded. This folder contains all files required to run Grafana. There are +no init scripts or install scripts in this package. + +To configure Grafana add a configuration file named `custom.ini` to the +`conf` folder and override any of the settings defined in +`conf/defaults.ini`. + +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). + diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 91a1c239a08..13597b9d921 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -15,42 +15,49 @@ weight = 2 Description | Download ------------ | ------------- -Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.4 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm) - +Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [x86-64](https://grafana.com/grafana/download?platform=linux) +Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [ARM64](https://grafana.com/grafana/download?platform=arm) +Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [ARMv7](https://grafana.com/grafana/download?platform=arm) -Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing -installation. +Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. ## Install Stable You can install Grafana using Yum directly. +```bash +$ sudo yum install +``` + +Example: + ```bash $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm ``` - +$ wget +``` -Or install manually using `rpm`. - -#### On CentOS / Fedora / Redhat: +Example: ```bash $ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm -$ sudo yum install initscripts fontconfig -$ sudo rpm -Uvh grafana-5.1.4-1.x86_64.rpm ``` -#### On OpenSuse: +### On CentOS / Fedora / Redhat: ```bash -$ sudo rpm -i --nodeps grafana-5.1.4-1.x86_64.rpm +$ sudo yum install initscripts fontconfig +$ sudo rpm -Uvh +``` + +### On OpenSuse: + +```bash +$ sudo rpm -i --nodeps ``` ## Install via YUM Repository diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index ccd5641cd11..5dc87984512 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -12,17 +12,15 @@ weight = 3 Description | Download ------------ | ------------- -Latest stable package for Windows | [grafana-5.1.4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4.windows-x64.zip) - - +Latest stable package for Windows | [x64](https://grafana.com/grafana/download?platform=windows) Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing installation. ## Configure +**Important:** After you've downloaded the zip file and before extracting it, make sure to open properties for that file (right-click Properties) and check the `unblock` checkbox and `Ok`. + The zip file contains a folder with the current Grafana version. Extract this folder to anywhere you want Grafana to run from. Go into the `conf` directory and copy `sample.ini` to `custom.ini`. You should edit diff --git a/docs/sources/reference/scripting.md b/docs/sources/reference/scripting.md index 551805b567a..7f218765d39 100644 --- a/docs/sources/reference/scripting.md +++ b/docs/sources/reference/scripting.md @@ -21,42 +21,32 @@ If you open scripted.js you can see how it reads url parameters from ARGS variab ## Example ```javascript -var rows = 1; var seriesName = 'argName'; -if(!_.isUndefined(ARGS.rows)) { - rows = parseInt(ARGS.rows, 10); -} - if(!_.isUndefined(ARGS.name)) { seriesName = ARGS.name; } -for (var i = 0; i < rows; i++) { - - dashboard.rows.push({ - title: 'Scripted Graph ' + i, - height: '300px', - panels: [ - { - title: 'Events', - type: 'graph', - span: 12, - fill: 1, - linewidth: 2, - targets: [ - { - 'target': "randomWalk('" + seriesName + "')" - }, - { - 'target': "randomWalk('random walk2')" - } - ], - } - ] - }); - -} +dashboard.panels.push({ + title: 'Events', + type: 'graph', + fill: 1, + linewidth: 2, + gridPos: { + h: 10, + w: 24, + x: 0, + y: 10, + }, + targets: [ + { + 'target': "randomWalk('" + seriesName + "')" + }, + { + 'target': "randomWalk('random walk2')" + } + ] +}); return dashboard; ``` diff --git a/docs/versions.json b/docs/versions.json index 61e471938f2..caefbe198d6 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,5 +1,6 @@ [ - { "version": "v5.1", "path": "/", "archived": false, "current": true }, + { "version": "v5.2", "path": "/", "archived": false, "current": true }, + { "version": "v5.1", "path": "/v5.1", "archived": true }, { "version": "v5.0", "path": "/v5.0", "archived": true }, { "version": "v4.6", "path": "/v4.6", "archived": true }, { "version": "v4.5", "path": "/v4.5", "archived": true }, diff --git a/karma.conf.js b/karma.conf.js index 3f006af08b6..352e8e4e027 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -19,8 +19,8 @@ module.exports = function(config) { }, webpack: webpackTestConfig, - webpackServer: { - noInfo: true, // please don't spam the console when running in karma! + webpackMiddleware: { + stats: 'minimal', }, // list of files to exclude diff --git a/latest.json b/latest.json index d8804f98441..8e26289c856 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.1.3", - "testing": "5.1.3" + "stable": "5.2.0", + "testing": "5.2.0" } diff --git a/package.json b/package.json index c00494d452e..4f2220abbae 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.2.0-pre1", + "version": "5.3.0-pre1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" @@ -16,11 +16,11 @@ "@types/node": "^8.0.31", "@types/react": "^16.0.25", "@types/react-dom": "^16.0.3", - "angular-mocks": "^1.6.6", + "angular-mocks": "1.6.6", "autoprefixer": "^6.4.0", - "awesome-typescript-loader": "^4.0.0", "axios": "^0.17.1", "babel-core": "^6.26.0", + "babel-loader": "^7.1.4", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-preset-es2015": "^6.24.1", "clean-webpack-plugin": "^0.1.19", @@ -32,8 +32,9 @@ "es6-shim": "^0.35.3", "expect.js": "~0.2.0", "expose-loader": "^0.7.3", - "extract-text-webpack-plugin": "^3.0.0", + "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^1.1.11", + "fork-ts-checker-webpack-plugin": "^0.4.1", "gaze": "^1.1.2", "glob": "~7.0.0", "grunt": "1.0.1", @@ -56,7 +57,7 @@ "grunt-webpack": "^3.0.2", "html-loader": "^0.5.1", "html-webpack-harddisk-plugin": "^0.2.0", - "html-webpack-plugin": "^2.30.1", + "html-webpack-plugin": "^3.2.0", "husky": "^0.14.3", "jest": "^22.0.4", "jshint-stylish": "~2.2.1", @@ -67,7 +68,7 @@ "karma-phantomjs-launcher": "1.0.4", "karma-sinon": "^1.0.5", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.4", + "karma-webpack": "^3.0.0", "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", "mobx-react-devtools": "^4.2.15", @@ -89,21 +90,24 @@ "style-loader": "^0.21.0", "systemjs": "0.20.19", "systemjs-plugin-css": "^0.1.36", + "ts-loader": "^4.3.0", "ts-jest": "^22.4.6", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", "typescript": "^2.6.2", - "webpack": "^3.10.0", + "webpack": "^4.8.0", "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", - "webpack-dev-server": "2.11.1", + "fork-ts-checker-webpack-plugin": "^0.4.2", + "webpack-cli": "^2.1.4", + "webpack-dev-server": "^3.1.0", "webpack-merge": "^4.1.0", "zone.js": "^0.7.2" }, "scripts": { - "dev": "webpack --progress --colors --config scripts/webpack/webpack.dev.js", - "start": "webpack-dev-server --progress --colors --config scripts/webpack/webpack.hot.js", - "watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", + "dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js", + "start": "webpack-dev-server --progress --colors --mode development --config scripts/webpack/webpack.hot.js", + "watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js", "build": "grunt build", "test": "grunt test", "test:coverage": "grunt test --coverage=true", @@ -135,8 +139,8 @@ "license": "Apache-2.0", "dependencies": { "angular": "1.6.6", - "angular-bindonce": "^0.3.1", - "angular-native-dragdrop": "^1.2.2", + "angular-bindonce": "0.3.1", + "angular-native-dragdrop": "1.2.2", "angular-route": "1.6.6", "angular-sanitize": "1.6.6", "babel-polyfill": "^6.26.0", @@ -151,12 +155,14 @@ "immutable": "^3.8.2", "jquery": "^3.2.1", "lodash": "^4.17.4", + "mini-css-extract-plugin": "^0.4.0", "mobx": "^3.4.1", "mobx-react": "^4.3.5", "mobx-state-tree": "^1.3.1", "moment": "^2.18.1", "mousetrap": "^1.6.0", "mousetrap-global-bind": "^1.1.0", + "optimize-css-assets-webpack-plugin": "^4.0.2", "prismjs": "^1.6.0", "prop-types": "^15.6.0", "react": "^16.2.0", @@ -175,7 +181,8 @@ "slate-react": "^0.12.4", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop/tarball/master", - "tinycolor2": "^1.4.1" + "tinycolor2": "^1.4.1", + "uglifyjs-webpack-plugin": "^1.2.7" }, "resolutions": { "caniuse-db": "1.0.30000772" diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index a44108537cb..55c9c954940 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -272,9 +272,9 @@ func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) { return false, nil } - if dashboardID > 0 { - guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser) - if canEdit, err := guardian.CanEdit(); err != nil || !canEdit { + if dashboardID != 0 { + guard := guardian.New(dashboardID, c.OrgId, c.SignedInUser) + if canEdit, err := guard.CanEdit(); err != nil || !canEdit { return false, err } } diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 976c027d749..7b90cf8b1e8 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -88,10 +88,11 @@ func main() { err := server.Run() + code := server.Exit(err) trace.Stop() log.Close() - server.Exit(err) + os.Exit(code) } func listenToSystemSignals(server *GrafanaServerImpl) { diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 6444528f7f0..8794d7d8338 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -175,7 +175,7 @@ func (g *GrafanaServerImpl) Shutdown(reason string) { g.childRoutines.Wait() } -func (g *GrafanaServerImpl) Exit(reason error) { +func (g *GrafanaServerImpl) Exit(reason error) int { // default exit code is 1 code := 1 @@ -185,7 +185,7 @@ func (g *GrafanaServerImpl) Exit(reason error) { } g.log.Error("Server shutdown", "reason", reason) - os.Exit(code) + return code } func (g *GrafanaServerImpl) writePIDFile() { diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 37e79c01071..5faee1e3fa7 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -9,6 +9,7 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) type AuthOptions struct { @@ -34,6 +35,11 @@ func getApiKey(c *m.ReqContext) string { return key } + username, password, err := util.DecodeBasicAuthHeader(header) + if err == nil && username == "api_key" { + return password + } + return "" } diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index eff532b0da2..144a0ae3a69 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -2,6 +2,7 @@ package middleware import ( "fmt" + "net" "net/mail" "reflect" "strings" @@ -28,7 +29,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { } // if auth proxy ip(s) defined, check if request comes from one of those - if err := checkAuthenticationProxy(ctx.RemoteAddr(), proxyHeaderValue); err != nil { + if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil { ctx.Handle(407, "Proxy authentication required", err) return true } @@ -196,23 +197,18 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error return nil } - // Multiple ip addresses? Right-most IP address is the IP address of the most recent proxy - if strings.Contains(remoteAddr, ",") { - sourceIPs := strings.Split(remoteAddr, ",") - remoteAddr = strings.TrimSpace(sourceIPs[len(sourceIPs)-1]) - } - - remoteAddr = strings.TrimPrefix(remoteAddr, "[") - remoteAddr = strings.TrimSuffix(remoteAddr, "]") - proxies := strings.Split(setting.AuthProxyWhitelist, ",") + sourceIP, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + return err + } // Compare allowed IP addresses to actual address for _, proxyIP := range proxies { - if remoteAddr == strings.TrimSpace(proxyIP) { + if sourceIP == strings.TrimSpace(proxyIP) { return nil } } - return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, remoteAddr) + return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP) } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 0b50358ad73..87c23a7b49a 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -82,7 +82,7 @@ func TestMiddlewareContext(t *testing.T) { setting.BasicAuthEnabled = true authHeader := util.GetBasicAuthHeader("myUser", "myPass") - sc.fakeReq("GET", "/").withAuthoriziationHeader(authHeader).exec() + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() Convey("Should init middleware context with user", func() { So(sc.context.IsSignedIn, ShouldEqual, true) @@ -128,6 +128,28 @@ func TestMiddlewareContext(t *testing.T) { }) }) + middlewareScenario("Valid api key via Basic auth", func(sc *scenarioContext) { + keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd") + + bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error { + query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash} + return nil + }) + + authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9") + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec() + + Convey("Should return 200", func() { + So(sc.resp.Code, ShouldEqual, 200) + }) + + Convey("Should init middleware context", func() { + So(sc.context.IsSignedIn, ShouldEqual, true) + So(sc.context.OrgId, ShouldEqual, 12) + So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR) + }) + }) + middlewareScenario("UserId in session", func(sc *scenarioContext) { sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) { @@ -293,61 +315,6 @@ func TestMiddlewareContext(t *testing.T) { }) }) - middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is not trusted", func(sc *scenarioContext) { - setting.AuthProxyEnabled = true - setting.AuthProxyHeaderName = "X-WEBAUTH-USER" - setting.AuthProxyHeaderProperty = "username" - setting.AuthProxyWhitelist = "192.168.1.1, 2001::23" - - bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { - query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} - return nil - }) - - bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { - cmd.Result = &m.User{Id: 33} - return nil - }) - - sc.fakeReq("GET", "/") - sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") - sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.1, 192.168.1.2") - sc.exec() - - Convey("should return 407 status code", func() { - So(sc.resp.Code, ShouldEqual, 407) - So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.1.2 is not from the authentication proxy") - }) - }) - - middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is trusted", func(sc *scenarioContext) { - setting.AuthProxyEnabled = true - setting.AuthProxyHeaderName = "X-WEBAUTH-USER" - setting.AuthProxyHeaderProperty = "username" - setting.AuthProxyWhitelist = "192.168.1.1, 2001::23" - - bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { - query.Result = &m.SignedInUser{OrgId: 4, UserId: 33} - return nil - }) - - bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { - cmd.Result = &m.User{Id: 33} - return nil - }) - - sc.fakeReq("GET", "/") - sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") - sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.2, 192.168.1.1") - sc.exec() - - Convey("Should init context with user info", func() { - So(sc.context.IsSignedIn, ShouldBeTrue) - So(sc.context.UserId, ShouldEqual, 33) - So(sc.context.OrgId, ShouldEqual, 4) - }) - }) - middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) { setting.AuthProxyEnabled = true setting.AuthProxyHeaderName = "X-WEBAUTH-USER" @@ -473,7 +440,7 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext { return sc } -func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext { +func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext { sc.authHeader = authHeader return sc } diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index e597a06f15f..6bbc302c593 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -34,23 +34,37 @@ func GetServices() []*Descriptor { return services } +// Service interface is the lowest common shape that services +// are expected to forfill to be started within Grafana. type Service interface { + + // Init is called by Grafana main process which gives the service + // the possibility do some initial work before its started. Things + // like adding routes, bus handlers should be done in the Init function Init() error } -// Useful for alerting service +// CanBeDisabled allows the services to decide if it should +// be started or not by itself. This is useful for services +// that might not always be started, ex alerting. +// This will be called after `Init()`. type CanBeDisabled interface { + + // IsDisabled should return a bool saying if it can be started or not. IsDisabled() bool } +// BackgroundService should be implemented for services that have +// long running tasks in the background. type BackgroundService interface { + + // Run starts the background process of the service after `Init` have been called + // on all services. The `context.Context` passed into the function should be used + // to subscribe to ctx.Done() so the service can be notified when Grafana shuts down. Run(ctx context.Context) error } -type HasInitPriority interface { - GetInitPriority() Priority -} - +// IsDisabled takes an service and return true if its disabled func IsDisabled(srv Service) bool { canBeDisabled, ok := srv.(CanBeDisabled) return ok && canBeDisabled.IsDisabled() diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 2ea68cf5085..07212746f7e 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -104,7 +104,10 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { return err } - n.log.Info("uploaded", "url", context.ImagePublicUrl) + if context.ImagePublicUrl != "" { + n.log.Info("uploaded screenshot of alert to external image store", "url", context.ImagePublicUrl) + } + return nil } diff --git a/public/app/core/directives/value_select_dropdown.ts b/public/app/core/directives/value_select_dropdown.ts index d6c6c3af5c5..d384904c2d8 100644 --- a/public/app/core/directives/value_select_dropdown.ts +++ b/public/app/core/directives/value_select_dropdown.ts @@ -93,7 +93,7 @@ export class ValueSelectDropdownCtrl { tagValuesPromise = this.$q.when(tag.values); } - tagValuesPromise.then(values => { + return tagValuesPromise.then(values => { tag.values = values; tag.valuesText = values.join(' + '); _.each(this.options, option => { @@ -132,7 +132,7 @@ export class ValueSelectDropdownCtrl { this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length; } - selectValue(option, event, commitChange, excludeOthers) { + selectValue(option, event, commitChange?, excludeOthers?) { if (!option) { return; } diff --git a/public/app/core/specs/value_select_dropdown.jest.ts b/public/app/core/specs/value_select_dropdown.jest.ts new file mode 100644 index 00000000000..3cc310435b7 --- /dev/null +++ b/public/app/core/specs/value_select_dropdown.jest.ts @@ -0,0 +1,159 @@ +import 'app/core/directives/value_select_dropdown'; +import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown'; +import q from 'q'; + +describe('SelectDropdownCtrl', () => { + let tagValuesMap: any = {}; + + ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn(); + let ctrl; + + describe('Given simple variable', () => { + beforeEach(() => { + ctrl = new ValueSelectDropdownCtrl(q); + ctrl.variable = { + current: { text: 'hej', value: 'hej' }, + getValuesForTag: key => { + return Promise.resolve(tagValuesMap[key]); + }, + }; + ctrl.init(); + }); + + it('Should init labelText and linkText', () => { + expect(ctrl.linkText).toBe('hej'); + }); + }); + + describe('Given variable with tags and dropdown is opened', () => { + beforeEach(() => { + ctrl = new ValueSelectDropdownCtrl(q); + ctrl.variable = { + current: { text: 'server-1', value: 'server-1' }, + options: [ + { text: 'server-1', value: 'server-1', selected: true }, + { text: 'server-2', value: 'server-2' }, + { text: 'server-3', value: 'server-3' }, + ], + tags: ['key1', 'key2', 'key3'], + getValuesForTag: key => { + return Promise.resolve(tagValuesMap[key]); + }, + multi: true, + }; + tagValuesMap.key1 = ['server-1', 'server-3']; + tagValuesMap.key2 = ['server-2', 'server-3']; + tagValuesMap.key3 = ['server-1', 'server-2', 'server-3']; + ctrl.init(); + ctrl.show(); + }); + + it('should init tags model', () => { + expect(ctrl.tags.length).toBe(3); + expect(ctrl.tags[0].text).toBe('key1'); + }); + + it('should init options model', () => { + expect(ctrl.options.length).toBe(3); + }); + + it('should init selected values array', () => { + expect(ctrl.selectedValues.length).toBe(1); + }); + + it('should set linkText', () => { + expect(ctrl.linkText).toBe('server-1'); + }); + + describe('after adititional value is selected', () => { + beforeEach(() => { + ctrl.selectValue(ctrl.options[2], {}); + ctrl.commitChanges(); + }); + + it('should update link text', () => { + expect(ctrl.linkText).toBe('server-1 + server-3'); + }); + }); + + describe('When tag is selected', () => { + beforeEach(async () => { + await ctrl.selectTag(ctrl.tags[0]); + ctrl.commitChanges(); + }); + + it('should select tag', () => { + expect(ctrl.selectedTags.length).toBe(1); + }); + + it('should select values', () => { + expect(ctrl.options[0].selected).toBe(true); + expect(ctrl.options[2].selected).toBe(true); + }); + + it('link text should not include tag values', () => { + expect(ctrl.linkText).toBe(''); + }); + + describe('and then dropdown is opened and closed without changes', () => { + beforeEach(() => { + ctrl.show(); + ctrl.commitChanges(); + }); + + it('should still have selected tag', () => { + expect(ctrl.selectedTags.length).toBe(1); + }); + }); + + describe('and then unselected', () => { + beforeEach(async () => { + await ctrl.selectTag(ctrl.tags[0]); + }); + + it('should deselect tag', () => { + expect(ctrl.selectedTags.length).toBe(0); + }); + }); + + describe('and then value is unselected', () => { + beforeEach(() => { + ctrl.selectValue(ctrl.options[0], {}); + }); + + it('should deselect tag', () => { + expect(ctrl.selectedTags.length).toBe(0); + }); + }); + }); + }); + + describe('Given variable with selected tags', () => { + beforeEach(() => { + ctrl = new ValueSelectDropdownCtrl(q); + ctrl.variable = { + current: { + text: 'server-1', + value: 'server-1', + tags: [{ text: 'key1', selected: true }], + }, + options: [ + { text: 'server-1', value: 'server-1' }, + { text: 'server-2', value: 'server-2' }, + { text: 'server-3', value: 'server-3' }, + ], + tags: ['key1', 'key2', 'key3'], + getValuesForTag: key => { + return Promise.resolve(tagValuesMap[key]); + }, + multi: true, + }; + ctrl.init(); + ctrl.show(); + }); + + it('should set tag as selected', () => { + expect(ctrl.tags[0].selected).toBe(true); + }); + }); +}); diff --git a/public/app/core/specs/value_select_dropdown_specs.ts b/public/app/core/specs/value_select_dropdown_specs.ts deleted file mode 100644 index 8f6408fb389..00000000000 --- a/public/app/core/specs/value_select_dropdown_specs.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks, sinon } from 'test/lib/common'; -import 'app/core/directives/value_select_dropdown'; - -describe('SelectDropdownCtrl', function() { - var scope; - var ctrl; - var tagValuesMap: any = {}; - var rootScope; - var q; - - beforeEach(angularMocks.module('grafana.core')); - beforeEach( - angularMocks.inject(function($controller, $rootScope, $q, $httpBackend) { - rootScope = $rootScope; - q = $q; - scope = $rootScope.$new(); - ctrl = $controller('ValueSelectDropdownCtrl', { $scope: scope }); - ctrl.onUpdated = sinon.spy(); - $httpBackend.when('GET', /\.html$/).respond(''); - }) - ); - - describe('Given simple variable', function() { - beforeEach(function() { - ctrl.variable = { - current: { text: 'hej', value: 'hej' }, - getValuesForTag: function(key) { - return q.when(tagValuesMap[key]); - }, - }; - ctrl.init(); - }); - - it('Should init labelText and linkText', function() { - expect(ctrl.linkText).to.be('hej'); - }); - }); - - describe('Given variable with tags and dropdown is opened', function() { - beforeEach(function() { - ctrl.variable = { - current: { text: 'server-1', value: 'server-1' }, - options: [ - { text: 'server-1', value: 'server-1', selected: true }, - { text: 'server-2', value: 'server-2' }, - { text: 'server-3', value: 'server-3' }, - ], - tags: ['key1', 'key2', 'key3'], - getValuesForTag: function(key) { - return q.when(tagValuesMap[key]); - }, - multi: true, - }; - tagValuesMap.key1 = ['server-1', 'server-3']; - tagValuesMap.key2 = ['server-2', 'server-3']; - tagValuesMap.key3 = ['server-1', 'server-2', 'server-3']; - ctrl.init(); - ctrl.show(); - }); - - it('should init tags model', function() { - expect(ctrl.tags.length).to.be(3); - expect(ctrl.tags[0].text).to.be('key1'); - }); - - it('should init options model', function() { - expect(ctrl.options.length).to.be(3); - }); - - it('should init selected values array', function() { - expect(ctrl.selectedValues.length).to.be(1); - }); - - it('should set linkText', function() { - expect(ctrl.linkText).to.be('server-1'); - }); - - describe('after adititional value is selected', function() { - beforeEach(function() { - ctrl.selectValue(ctrl.options[2], {}); - ctrl.commitChanges(); - }); - - it('should update link text', function() { - expect(ctrl.linkText).to.be('server-1 + server-3'); - }); - }); - - describe('When tag is selected', function() { - beforeEach(function() { - ctrl.selectTag(ctrl.tags[0]); - rootScope.$digest(); - ctrl.commitChanges(); - }); - - it('should select tag', function() { - expect(ctrl.selectedTags.length).to.be(1); - }); - - it('should select values', function() { - expect(ctrl.options[0].selected).to.be(true); - expect(ctrl.options[2].selected).to.be(true); - }); - - it('link text should not include tag values', function() { - expect(ctrl.linkText).to.be(''); - }); - - describe('and then dropdown is opened and closed without changes', function() { - beforeEach(function() { - ctrl.show(); - ctrl.commitChanges(); - rootScope.$digest(); - }); - - it('should still have selected tag', function() { - expect(ctrl.selectedTags.length).to.be(1); - }); - }); - - describe('and then unselected', function() { - beforeEach(function() { - ctrl.selectTag(ctrl.tags[0]); - rootScope.$digest(); - }); - - it('should deselect tag', function() { - expect(ctrl.selectedTags.length).to.be(0); - }); - }); - - describe('and then value is unselected', function() { - beforeEach(function() { - ctrl.selectValue(ctrl.options[0], {}); - }); - - it('should deselect tag', function() { - expect(ctrl.selectedTags.length).to.be(0); - }); - }); - }); - }); - - describe('Given variable with selected tags', function() { - beforeEach(function() { - ctrl.variable = { - current: { - text: 'server-1', - value: 'server-1', - tags: [{ text: 'key1', selected: true }], - }, - options: [ - { text: 'server-1', value: 'server-1' }, - { text: 'server-2', value: 'server-2' }, - { text: 'server-3', value: 'server-3' }, - ], - tags: ['key1', 'key2', 'key3'], - getValuesForTag: function(key) { - return q.when(tagValuesMap[key]); - }, - multi: true, - }; - ctrl.init(); - ctrl.show(); - }); - - it('should set tag as selected', function() { - expect(ctrl.tags[0].selected).to.be(true); - }); - }); -}); diff --git a/public/app/features/annotations/specs/annotations_srv_specs.ts b/public/app/features/annotations/specs/annotations_srv.jest.ts similarity index 52% rename from public/app/features/annotations/specs/annotations_srv_specs.ts rename to public/app/features/annotations/specs/annotations_srv.jest.ts index 932fcf9415c..7db7b6c9f05 100644 --- a/public/app/features/annotations/specs/annotations_srv_specs.ts +++ b/public/app/features/annotations/specs/annotations_srv.jest.ts @@ -1,17 +1,17 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; import '../annotations_srv'; -import helpers from 'test/specs/helpers'; import 'app/features/dashboard/time_srv'; +import { AnnotationsSrv } from '../annotations_srv'; describe('AnnotationsSrv', function() { - var ctx = new helpers.ServiceTestContext(); + let $rootScope = { + onAppEvent: jest.fn(), + }; + let $q; + let datasourceSrv; + let backendSrv; + let timeSrv; - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach(ctx.createService('timeSrv')); - beforeEach(() => { - ctx.createService('annotationsSrv'); - }); + let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); describe('When translating the query result', () => { const annotationSource = { @@ -30,11 +30,11 @@ describe('AnnotationsSrv', function() { let translatedAnnotations; beforeEach(() => { - translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations); + translatedAnnotations = annotationsSrv.translateQueryResult(annotationSource, annotations); }); it('should set defaults', () => { - expect(translatedAnnotations[0].source).to.eql(annotationSource); + expect(translatedAnnotations[0].source).toEqual(annotationSource); }); }); }); diff --git a/public/app/features/dashboard/specs/viewstate_srv.jest.ts b/public/app/features/dashboard/specs/viewstate_srv.jest.ts new file mode 100644 index 00000000000..08166c6f2bd --- /dev/null +++ b/public/app/features/dashboard/specs/viewstate_srv.jest.ts @@ -0,0 +1,67 @@ +//import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; +import 'app/features/dashboard/view_state_srv'; +import config from 'app/core/config'; +import { DashboardViewState } from '../view_state_srv'; + +describe('when updating view state', () => { + let location = { + replace: jest.fn(), + search: jest.fn(), + }; + + let $scope = { + onAppEvent: jest.fn(() => {}), + dashboard: { + meta: {}, + panels: [], + }, + }; + + let $rootScope = {}; + let viewState; + + beforeEach(() => { + config.bootData = { + user: { + orgId: 1, + }, + }; + }); + + describe('to fullscreen true and edit true', () => { + beforeEach(() => { + location.search = jest.fn(() => { + return { fullscreen: true, edit: true, panelId: 1 }; + }); + viewState = new DashboardViewState($scope, location, {}, $rootScope); + }); + + it('should update querystring and view state', () => { + var updateState = { fullscreen: true, edit: true, panelId: 1 }; + + viewState.update(updateState); + + expect(location.search).toHaveBeenCalledWith({ + edit: true, + editview: null, + fullscreen: true, + orgId: 1, + panelId: 1, + }); + expect(viewState.dashboard.meta.fullscreen).toBe(true); + expect(viewState.state.fullscreen).toBe(true); + }); + }); + + describe('to fullscreen false', () => { + beforeEach(() => { + viewState = new DashboardViewState($scope, location, {}, $rootScope); + }); + it('should remove params from query string', () => { + viewState.update({ fullscreen: true, panelId: 1, edit: true }); + viewState.update({ fullscreen: false }); + expect(viewState.dashboard.meta.fullscreen).toBe(false); + expect(viewState.state.fullscreen).toBe(null); + }); + }); +}); diff --git a/public/app/features/dashboard/specs/viewstate_srv_specs.ts b/public/app/features/dashboard/specs/viewstate_srv_specs.ts deleted file mode 100644 index d34b15b9113..00000000000 --- a/public/app/features/dashboard/specs/viewstate_srv_specs.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; -import 'app/features/dashboard/view_state_srv'; -import config from 'app/core/config'; - -describe('when updating view state', function() { - var viewState, location; - var timeSrv = {}; - var templateSrv = {}; - var contextSrv = { - user: { - orgId: 19, - }, - }; - beforeEach(function() { - config.bootData = { - user: { - orgId: 1, - }, - }; - }); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.module(function($provide) { - $provide.value('timeSrv', timeSrv); - $provide.value('templateSrv', templateSrv); - $provide.value('contextSrv', contextSrv); - }) - ); - - beforeEach( - angularMocks.inject(function(dashboardViewStateSrv, $location, $rootScope) { - $rootScope.onAppEvent = function() {}; - $rootScope.dashboard = { - meta: {}, - panels: [], - }; - viewState = dashboardViewStateSrv.create($rootScope); - location = $location; - }) - ); - - describe('to fullscreen true and edit true', function() { - it('should update querystring and view state', function() { - var updateState = { fullscreen: true, edit: true, panelId: 1 }; - viewState.update(updateState); - expect(location.search()).to.eql({ - fullscreen: true, - edit: true, - panelId: 1, - orgId: 1, - }); - expect(viewState.dashboard.meta.fullscreen).to.be(true); - expect(viewState.state.fullscreen).to.be(true); - }); - }); - - describe('to fullscreen false', function() { - it('should remove params from query string', function() { - viewState.update({ fullscreen: true, panelId: 1, edit: true }); - viewState.update({ fullscreen: false }); - expect(viewState.dashboard.meta.fullscreen).to.be(false); - expect(viewState.state.fullscreen).to.be(null); - }); - }); -}); diff --git a/public/app/features/templating/specs/variable_srv_init_specs.ts b/public/app/features/templating/specs/variable_srv_init_specs.ts index cb98d1d7736..11639c6aa8f 100644 --- a/public/app/features/templating/specs/variable_srv_init_specs.ts +++ b/public/app/features/templating/specs/variable_srv_init_specs.ts @@ -179,4 +179,38 @@ describe('VariableSrv init', function() { expect(variable.options[2].selected).to.be(false); }); }); + + describeInitScenario('when template variable is present in url multiple times using key/values', scenario => { + scenario.setup(() => { + scenario.variables = [ + { + name: 'apps', + type: 'query', + multi: true, + current: { text: 'Val1', value: 'val1' }, + options: [ + { text: 'Val1', value: 'val1' }, + { text: 'Val2', value: 'val2' }, + { text: 'Val3', value: 'val3', selected: true }, + ], + }, + ]; + scenario.urlParams['var-apps'] = ['val2', 'val1']; + }); + + it('should update current value', function() { + var variable = ctx.variableSrv.variables[0]; + expect(variable.current.value.length).to.be(2); + expect(variable.current.value[0]).to.be('val2'); + expect(variable.current.value[1]).to.be('val1'); + expect(variable.current.text).to.be('Val2 + Val1'); + expect(variable.options[0].selected).to.be(true); + expect(variable.options[1].selected).to.be(true); + }); + + it('should set options that are not in value to selected false', function() { + var variable = ctx.variableSrv.variables[0]; + expect(variable.options[2].selected).to.be(false); + }); + }); }); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index fb882516e85..8a096dd9ad2 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -209,7 +209,24 @@ export class VariableSrv { return op.text === urlValue || op.value === urlValue; }); - option = option || { text: urlValue, value: urlValue }; + let defaultText = urlValue; + let defaultValue = urlValue; + + if (!option && _.isArray(urlValue)) { + defaultText = []; + + for (let n = 0; n < urlValue.length; n++) { + let t = _.find(variable.options, op => { + return op.value === urlValue[n]; + }); + + if (t) { + defaultText.push(t.text); + } + } + } + + option = option || { text: defaultText, value: defaultValue }; return variable.setValue(option); }); } diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 8be9e777b9f..1919759334b 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -89,7 +89,7 @@
Skip - If you skip you will be promted to change password next time you login. + If you skip you will be prompted to change password next time you login.