Browse Source

Merge remote-tracking branch 'upstream/master' into postgres-query-builder

Sven Klemm 7 years ago
parent
commit
d26cacccd9
49 changed files with 1491 additions and 1023 deletions
  1. 1 4
      .circleci/config.yml
  2. 21 2
      CHANGELOG.md
  3. 26 24
      docs/sources/guides/whats-new-in-v5-2.md
  4. 8 0
      docs/sources/http_api/auth.md
  5. 6 2
      docs/sources/http_api/folder.md
  6. 119 105
      docs/sources/http_api/org.md
  7. 3 3
      docs/sources/index.md
  8. 9 9
      docs/sources/installation/debian.md
  9. 17 0
      docs/sources/installation/mac.md
  10. 22 15
      docs/sources/installation/rpm.md
  11. 3 5
      docs/sources/installation/windows.md
  12. 20 30
      docs/sources/reference/scripting.md
  13. 2 1
      docs/versions.json
  14. 2 2
      karma.conf.js
  15. 2 2
      latest.json
  16. 21 14
      package.json
  17. 3 3
      pkg/api/annotations.go
  18. 2 1
      pkg/cmd/grafana-server/main.go
  19. 2 2
      pkg/cmd/grafana-server/server.go
  20. 6 0
      pkg/middleware/auth.go
  21. 8 12
      pkg/middleware/auth_proxy.go
  22. 24 57
      pkg/middleware/middleware_test.go
  23. 19 5
      pkg/registry/registry.go
  24. 4 1
      pkg/services/alerting/notifier.go
  25. 2 2
      public/app/core/directives/value_select_dropdown.ts
  26. 159 0
      public/app/core/specs/value_select_dropdown.jest.ts
  27. 0 171
      public/app/core/specs/value_select_dropdown_specs.ts
  28. 11 11
      public/app/features/annotations/specs/annotations_srv.jest.ts
  29. 67 0
      public/app/features/dashboard/specs/viewstate_srv.jest.ts
  30. 0 65
      public/app/features/dashboard/specs/viewstate_srv_specs.ts
  31. 34 0
      public/app/features/templating/specs/variable_srv_init_specs.ts
  32. 18 1
      public/app/features/templating/variable_srv.ts
  33. 1 1
      public/app/partials/login.html
  34. 139 141
      public/app/plugins/panel/graph/series_overrides_ctrl.ts
  35. 42 0
      public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts
  36. 0 55
      public/app/plugins/panel/graph/specs/series_override_ctrl_specs.ts
  37. 6 2
      public/app/plugins/panel/singlestat/editor.html
  38. 14 9
      public/app/plugins/panel/singlestat/module.ts
  39. 13 4
      public/sass/components/_footer.scss
  40. 6 5
      public/sass/components/_switch.scss
  41. 16 13
      public/sass/pages/_login.scss
  42. 5 2
      scripts/grunt/options/karma.js
  43. 21 29
      scripts/webpack/sass.rule.js
  44. 4 2
      scripts/webpack/webpack.common.js
  45. 29 16
      scripts/webpack/webpack.dev.js
  46. 15 12
      scripts/webpack/webpack.hot.js
  47. 30 29
      scripts/webpack/webpack.prod.js
  48. 13 6
      scripts/webpack/webpack.test.js
  49. 496 148
      yarn.lock

+ 1 - 4
.circleci/config.yml

@@ -88,12 +88,9 @@ jobs:
 
 
   test-frontend:
   test-frontend:
     docker:
     docker:
-      - image: circleci/node:6.11.4
+      - image: circleci/node:8
     steps:
     steps:
       - checkout
       - checkout
-      - run:
-          name: install yarn
-          command: 'sudo npm install -g yarn --quiet'
       - restore_cache:
       - restore_cache:
           key: dependency-cache-{{ checksum "yarn.lock" }}
           key: dependency-cache-{{ checksum "yarn.lock" }}
       - run:
       - run:

+ 21 - 2
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)
 * **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**: 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
 ### Minor
 
 
 * **Plugins**: Handle errors correctly when loading datasource plugin [#12383](https://github.com/grafana/grafana/pull/12383) thx [@rozetko](https://github.com/rozetko)
 * **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)
 * **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)
 # 5.2.0-beta3 (2018-06-21)
 
 
@@ -56,6 +70,7 @@
 ### New Features
 ### New Features
 
 
 * **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95)
 * **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)
 * **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)
 * **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)
 * **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)
 * **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)
 # 5.1.4 (2018-06-19)
 
 
 * **Permissions**: Important security fix for API keys with viewer role [#12343](https://github.com/grafana/grafana/issues/12343)
 * **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**
 **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 #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 #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 #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 #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
 - [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

+ 26 - 24
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.
 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
 ## Elasticsearch alerting
 
 
@@ -32,16 +32,18 @@ the most requested features by our community and now it's finally here. Please t
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 
-## 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
 ## 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).
 information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets).
 
 
 ## Security
 ## 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" >}}
 {{< 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.
 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.
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 
 ## Prometheus
 ## Prometheus
 
 
 The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate*
 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
 ## 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
 ## 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" >}}
 {{< 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.
 when you actually want to overwrite those settings.
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
@@ -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" >}}
 {{< 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.
 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.
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 

+ 8 - 0
docs/sources/http_api/auth.md

@@ -44,6 +44,14 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 
 The `Authorization` header value should be `Bearer <your api key>`.
 The `Authorization` header value should be `Bearer <your api key>`.
 
 
+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
 # Auth HTTP resources / actions
 
 
 ## Api Keys
 ## Api Keys

+ 6 - 2
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.
 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
 ## Get all folders
 
 
@@ -273,14 +277,14 @@ Status Codes:
 
 
 ## Get folder by id
 ## Get folder by id
 
 
-`GET /api/folders/:id`
+`GET /api/folders/id/:id`
 
 
 Will return the folder identified by id.
 Will return the folder identified by id.
 
 
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
-GET /api/folders/1 HTTP/1.1
+GET /api/folders/id/1 HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk

+ 119 - 105
docs/sources/http_api/org.md

@@ -12,7 +12,13 @@ parent = "http_api"
 
 
 # Organisation 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/`
 `GET /api/org/`
 
 
@@ -37,20 +43,18 @@ Content-Type: application/json
 }
 }
 ```
 ```
 
 
-## Get Organisation by Id
+### Get all users within the current organisation
 
 
-`GET /api/orgs/:orgId`
+`GET /api/org/users`
 
 
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
-GET /api/orgs/1 HTTP/1.1
+GET /api/org/users HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 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**:
 **Example Response**:
 
 
@@ -58,33 +62,33 @@ to the request http url, like http://admin:admin@localhost:3000/api/orgs/1
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{
-  "id":1,
-  "name":"Main Org.",
-  "address":{
-    "address1":"",
-    "address2":"",
-    "city":"",
-    "zipCode":"",
-    "state":"",
-    "country":""
+[
+  {
+    "orgId":1,
+    "userId":1,
+    "email":"admin@mygraf.com",
+    "login":"admin",
+    "role":"Admin"
   }
   }
-}
+]
 ```
 ```
-## Get Organisation by Name
 
 
-`GET /api/orgs/name/:orgName`
+### Updates the given user
+
+`PATCH /api/org/users/:userId`
 
 
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
-GET /api/orgs/name/Main%20Org%2E HTTP/1.1
+PATCH /api/org/users/1 HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+{
+  "role": "Viewer",
+}
 ```
 ```
-Note: The api will only work when you pass the admin name and password
-to the request http url, like http://admin:admin@localhost:3000/api/orgs/name/Main%20Org%2E
 
 
 **Example Response**:
 **Example Response**:
 
 
@@ -92,39 +96,21 @@ to the request http url, like http://admin:admin@localhost:3000/api/orgs/name/Ma
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{
-  "id":1,
-  "name":"Main Org.",
-  "address":{
-    "address1":"",
-    "address2":"",
-    "city":"",
-    "zipCode":"",
-    "state":"",
-    "country":""
-  }
-}
+{"message":"Organization user updated"}
 ```
 ```
 
 
-## Create Organisation
+### Delete user in current organisation
 
 
-`POST /api/orgs`
+`DELETE /api/org/users/:userId`
 
 
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
-POST /api/orgs HTTP/1.1
+DELETE /api/org/users/1 HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 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**:
 **Example Response**:
 
 
@@ -132,14 +118,10 @@ Note: The api will work in the following two ways
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{
-  "orgId":"1",
-  "message":"Organization created"
-}
+{"message":"User removed from organization"}
 ```
 ```
 
 
-
-## Update current Organisation
+### Update current Organisation
 
 
 `PUT /api/org`
 `PUT /api/org`
 
 
@@ -165,17 +147,24 @@ Content-Type: application/json
 {"message":"Organization updated"}
 {"message":"Organization updated"}
 ```
 ```
 
 
-## Get all users within the actual organisation
+### Add a new user to the current organisation
 
 
-`GET /api/org/users`
+`POST /api/org/users`
+
+Adds a global user to the current organisation.
 
 
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
-GET /api/org/users HTTP/1.1
+POST /api/org/users HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+
+{
+  "role": "Admin",
+  "loginOrEmail": "admin"
+}
 ```
 ```
 
 
 **Example Response**:
 **Example Response**:
@@ -184,35 +173,29 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-[
-  {
-    "orgId":1,
-    "userId":1,
-    "email":"admin@mygraf.com",
-    "login":"admin",
-    "role":"Admin"
-  }
-]
+{"message":"User added to organization"}
 ```
 ```
 
 
-## Add a new user to the actual organisation
+## Admin Organisations API
 
 
-`POST /api/org/users`
+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`
 
 
-Adds a global user to the actual organisation.
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
-POST /api/org/users HTTP/1.1
+GET /api/orgs/1 HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
-
-{
-  "role": "Admin",
-  "loginOrEmail": "admin"
-}
 ```
 ```
 
 
 **Example Response**:
 **Example Response**:
@@ -221,24 +204,31 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{"message":"User added to organization"}
+{
+  "id":1,
+  "name":"Main Org.",
+  "address":{
+    "address1":"",
+    "address2":"",
+    "city":"",
+    "zipCode":"",
+    "state":"",
+    "country":""
+  }
+}
 ```
 ```
+### Get Organisation by Name
 
 
-## Updates the given user
+`GET /api/orgs/name/:orgName`
 
 
-`PATCH /api/org/users/:userId`
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
-PATCH /api/org/users/1 HTTP/1.1
+GET /api/orgs/name/Main%20Org%2E HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
-
-{
-  "role": "Viewer",
-}
 ```
 ```
 
 
 **Example Response**:
 **Example Response**:
@@ -247,21 +237,40 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{"message":"Organization user updated"}
+{
+  "id":1,
+  "name":"Main Org.",
+  "address":{
+    "address1":"",
+    "address2":"",
+    "city":"",
+    "zipCode":"",
+    "state":"",
+    "country":""
+  }
+}
 ```
 ```
 
 
-## Delete user in actual organisation
+### Create Organisation
 
 
-`DELETE /api/org/users/:userId`
+`POST /api/orgs`
+
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
-DELETE /api/org/users/1 HTTP/1.1
+POST /api/orgs HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: 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**:
 **Example Response**:
 
 
@@ -269,22 +278,24 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{"message":"User removed from organization"}
+{
+  "orgId":"1",
+  "message":"Organization created"
+}
 ```
 ```
 
 
-# Organisations
-
-## Search all Organisations
+### Search all Organisations
 
 
 `GET /api/orgs`
 `GET /api/orgs`
 
 
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
 GET /api/orgs HTTP/1.1
 GET /api/orgs HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 ```
 ```
 Note: The api will only work when you pass the admin name and password
 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
 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`
 `PUT /api/orgs/:orgId`
 
 
 Update Organisation, fields *Address 1*, *Address 2*, *City* are not implemented yet.
 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**:
 **Example Request**:
 
 
@@ -315,7 +327,6 @@ Update Organisation, fields *Address 1*, *Address 2*, *City* are not implemented
 PUT /api/orgs/1 HTTP/1.1
 PUT /api/orgs/1 HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 
 {
 {
   "name":"Main Org 2."
   "name":"Main Org 2."
@@ -331,16 +342,17 @@ Content-Type: application/json
 {"message":"Organization updated"}
 {"message":"Organization updated"}
 ```
 ```
 
 
-## Delete Organisation
+### Delete Organisation
 
 
 `DELETE /api/orgs/:orgId`
 `DELETE /api/orgs/:orgId`
 
 
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
 DELETE /api/orgs/1 HTTP/1.1
 DELETE /api/orgs/1 HTTP/1.1
 Accept: application/json
 Accept: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 ```
 ```
 
 
 **Example Response**:
 **Example Response**:
@@ -352,17 +364,18 @@ Content-Type: application/json
 {"message":"Organization deleted"}
 {"message":"Organization deleted"}
 ```
 ```
 
 
-## Get Users in Organisation
+### Get Users in Organisation
 
 
 `GET /api/orgs/:orgId/users`
 `GET /api/orgs/:orgId/users`
 
 
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
 GET /api/orgs/1/users HTTP/1.1
 GET /api/orgs/1/users HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 ```
 ```
 Note: The api will only work when you pass the admin name and password
 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
 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`
 `POST /api/orgs/:orgId/users`
 
 
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
 POST /api/orgs/1/users HTTP/1.1
 POST /api/orgs/1/users HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 
 {
 {
   "loginOrEmail":"user",
   "loginOrEmail":"user",
   "role":"Viewer"
   "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**:
 **Example Response**:
 
 
@@ -413,17 +425,18 @@ Content-Type: application/json
 {"message":"User added to organization"}
 {"message":"User added to organization"}
 ```
 ```
 
 
-## Update Users in Organisation
+### Update Users in Organisation
 
 
 `PATCH /api/orgs/:orgId/users/:userId`
 `PATCH /api/orgs/:orgId/users/:userId`
 
 
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
 PATCH /api/orgs/1/users/2 HTTP/1.1
 PATCH /api/orgs/1/users/2 HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 
 {
 {
   "role":"Admin"
   "role":"Admin"
@@ -439,17 +452,18 @@ Content-Type: application/json
 {"message":"Organization user updated"}
 {"message":"Organization user updated"}
 ```
 ```
 
 
-## Delete User in Organisation
+### Delete User in Organisation
 
 
 `DELETE /api/orgs/:orgId/users/:userId`
 `DELETE /api/orgs/:orgId/users/:userId`
 
 
+Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+
 **Example Request**:
 **Example Request**:
 
 
 ```http
 ```http
 DELETE /api/orgs/1/users/2 HTTP/1.1
 DELETE /api/orgs/1/users/2 HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 ```
 ```
 
 
 **Example Response**:
 **Example Response**:

+ 3 - 3
docs/sources/index.md

@@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]
         <h4>Provisioning</h4>
         <h4>Provisioning</h4>
         <p>A guide to help you automate your Grafana setup & configuration.</p>
         <p>A guide to help you automate your Grafana setup & configuration.</p>
     </a>
     </a>
-    <a href="{{< relref "guides/whats-new-in-v5.md" >}}" class="nav-cards__item nav-cards__item--guide">
-        <h4>What's new in v5.0</h4>
-        <p>Article on all the new cool features and enhancements in v5.0</p>
+    <a href="{{< relref "guides/whats-new-in-v5-2.md" >}}" class="nav-cards__item nav-cards__item--guide">
+        <h4>What's new in v5.2</h4>
+        <p>Article on all the new cool features and enhancements in v5.2</p>
     </a>
     </a>
     <a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
     <a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
         <h4>Screencasts</h4>
         <h4>Screencasts</h4>

+ 9 - 9
docs/sources/installation/debian.md

@@ -15,10 +15,9 @@ weight = 1
 
 
 Description | Download
 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)
-<!--
-Beta for Debian-based Linux | [grafana_5.1.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_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
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
 installation.
@@ -27,17 +26,18 @@ installation.
 
 
 
 
 ```bash
 ```bash
-wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb
+wget <debian package url>
 sudo apt-get install -y adduser libfontconfig
 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
 ```
 ```
 
 
-<!-- ## Install Latest Beta
+Example:
+
 ```bash
 ```bash
-wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_amd64.deb
+wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb
 sudo apt-get install -y adduser libfontconfig
 sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_5.1.0-beta1_amd64.deb
-``` -->
+sudo dpkg -i grafana_5.1.4_amd64.deb
+```
 
 
 ## APT Repository
 ## APT Repository
 
 

+ 17 - 0
docs/sources/installation/mac.md

@@ -11,6 +11,8 @@ weight = 4
 
 
 # Installing on Mac
 # Installing on Mac
 
 
+## Install using homebrew
+
 Installation can be done using [homebrew](http://brew.sh/)
 Installation can be done using [homebrew](http://brew.sh/)
 
 
 Install latest stable:
 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`
 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).
+

+ 22 - 15
docs/sources/installation/rpm.md

@@ -15,42 +15,49 @@ weight = 2
 
 
 Description | Download
 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)
-<!--
-Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.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
 ## Install Stable
 
 
 You can install Grafana using Yum directly.
 You can install Grafana using Yum directly.
 
 
 ```bash
 ```bash
-$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
+$ sudo yum install <rpm package url>
 ```
 ```
 
 
-<!-- ## Install Beta
+Example:
 
 
 ```bash
 ```bash
-$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.x86_64.rpm
-``` -->
+$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
+```
 
 
-Or install manually using `rpm`.
+Or install manually using `rpm`. First execute
+
+```bash
+$ wget <rpm package url>
+```
 
 
-#### On CentOS / Fedora / Redhat:
+Example:
 
 
 ```bash
 ```bash
 $ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
 $ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
+```
+
+### On CentOS / Fedora / Redhat:
+
+```bash
 $ sudo yum install initscripts fontconfig
 $ sudo yum install initscripts fontconfig
-$ sudo rpm -Uvh grafana-5.1.4-1.x86_64.rpm
+$ sudo rpm -Uvh <local rpm package>
 ```
 ```
 
 
-#### On OpenSuse:
+### On OpenSuse:
 
 
 ```bash
 ```bash
-$ sudo rpm -i --nodeps grafana-5.1.4-1.x86_64.rpm
+$ sudo rpm -i --nodeps <local rpm package>
 ```
 ```
 
 
 ## Install via YUM Repository
 ## Install via YUM Repository

+ 3 - 5
docs/sources/installation/windows.md

@@ -12,17 +12,15 @@ weight = 3
 
 
 Description | Download
 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 beta package for Windows | [grafana.5.1.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.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
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
 installation.
 
 
 ## Configure
 ## 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
 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
 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
 `conf` directory and copy `sample.ini` to `custom.ini`. You should edit

+ 20 - 30
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
 ## Example
 
 
 ```javascript
 ```javascript
-var rows = 1;
 var seriesName = 'argName';
 var seriesName = 'argName';
 
 
-if(!_.isUndefined(ARGS.rows)) {
-  rows = parseInt(ARGS.rows, 10);
-}
-
 if(!_.isUndefined(ARGS.name)) {
 if(!_.isUndefined(ARGS.name)) {
   seriesName = 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;
 return dashboard;
 ```
 ```

+ 2 - 1
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": "v5.0", "path": "/v5.0", "archived": true },
   { "version": "v4.6", "path": "/v4.6", "archived": true },
   { "version": "v4.6", "path": "/v4.6", "archived": true },
   { "version": "v4.5", "path": "/v4.5", "archived": true },
   { "version": "v4.5", "path": "/v4.5", "archived": true },

+ 2 - 2
karma.conf.js

@@ -19,8 +19,8 @@ module.exports = function(config) {
     },
     },
 
 
     webpack: webpackTestConfig,
     webpack: webpackTestConfig,
-    webpackServer: {
-      noInfo: true, // please don't spam the console when running in karma!
+    webpackMiddleware: {
+      stats: 'minimal',
     },
     },
 
 
     // list of files to exclude
     // list of files to exclude

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
 {
-  "stable": "5.1.3",
-  "testing": "5.1.3"
+  "stable": "5.2.0",
+  "testing": "5.2.0"
 }
 }

+ 21 - 14
package.json

@@ -4,7 +4,7 @@
     "company": "Grafana Labs"
     "company": "Grafana Labs"
   },
   },
   "name": "grafana",
   "name": "grafana",
-  "version": "5.2.0-pre1",
+  "version": "5.3.0-pre1",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
     "url": "http://github.com/grafana/grafana.git"
@@ -16,11 +16,11 @@
     "@types/node": "^8.0.31",
     "@types/node": "^8.0.31",
     "@types/react": "^16.0.25",
     "@types/react": "^16.0.25",
     "@types/react-dom": "^16.0.3",
     "@types/react-dom": "^16.0.3",
-    "angular-mocks": "^1.6.6",
+    "angular-mocks": "1.6.6",
     "autoprefixer": "^6.4.0",
     "autoprefixer": "^6.4.0",
-    "awesome-typescript-loader": "^4.0.0",
     "axios": "^0.17.1",
     "axios": "^0.17.1",
     "babel-core": "^6.26.0",
     "babel-core": "^6.26.0",
+    "babel-loader": "^7.1.4",
     "babel-plugin-syntax-dynamic-import": "^6.18.0",
     "babel-plugin-syntax-dynamic-import": "^6.18.0",
     "babel-preset-es2015": "^6.24.1",
     "babel-preset-es2015": "^6.24.1",
     "clean-webpack-plugin": "^0.1.19",
     "clean-webpack-plugin": "^0.1.19",
@@ -32,8 +32,9 @@
     "es6-shim": "^0.35.3",
     "es6-shim": "^0.35.3",
     "expect.js": "~0.2.0",
     "expect.js": "~0.2.0",
     "expose-loader": "^0.7.3",
     "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",
     "file-loader": "^1.1.11",
+    "fork-ts-checker-webpack-plugin": "^0.4.1",
     "gaze": "^1.1.2",
     "gaze": "^1.1.2",
     "glob": "~7.0.0",
     "glob": "~7.0.0",
     "grunt": "1.0.1",
     "grunt": "1.0.1",
@@ -56,7 +57,7 @@
     "grunt-webpack": "^3.0.2",
     "grunt-webpack": "^3.0.2",
     "html-loader": "^0.5.1",
     "html-loader": "^0.5.1",
     "html-webpack-harddisk-plugin": "^0.2.0",
     "html-webpack-harddisk-plugin": "^0.2.0",
-    "html-webpack-plugin": "^2.30.1",
+    "html-webpack-plugin": "^3.2.0",
     "husky": "^0.14.3",
     "husky": "^0.14.3",
     "jest": "^22.0.4",
     "jest": "^22.0.4",
     "jshint-stylish": "~2.2.1",
     "jshint-stylish": "~2.2.1",
@@ -67,7 +68,7 @@
     "karma-phantomjs-launcher": "1.0.4",
     "karma-phantomjs-launcher": "1.0.4",
     "karma-sinon": "^1.0.5",
     "karma-sinon": "^1.0.5",
     "karma-sourcemap-loader": "^0.3.7",
     "karma-sourcemap-loader": "^0.3.7",
-    "karma-webpack": "^2.0.4",
+    "karma-webpack": "^3.0.0",
     "lint-staged": "^6.0.0",
     "lint-staged": "^6.0.0",
     "load-grunt-tasks": "3.5.2",
     "load-grunt-tasks": "3.5.2",
     "mobx-react-devtools": "^4.2.15",
     "mobx-react-devtools": "^4.2.15",
@@ -89,21 +90,24 @@
     "style-loader": "^0.21.0",
     "style-loader": "^0.21.0",
     "systemjs": "0.20.19",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
     "systemjs-plugin-css": "^0.1.36",
+    "ts-loader": "^4.3.0",
     "ts-jest": "^22.4.6",
     "ts-jest": "^22.4.6",
     "tslint": "^5.8.0",
     "tslint": "^5.8.0",
     "tslint-loader": "^3.5.3",
     "tslint-loader": "^3.5.3",
     "typescript": "^2.6.2",
     "typescript": "^2.6.2",
-    "webpack": "^3.10.0",
+    "webpack": "^4.8.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-cleanup-plugin": "^0.5.1",
     "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",
     "webpack-merge": "^4.1.0",
     "zone.js": "^0.7.2"
     "zone.js": "^0.7.2"
   },
   },
   "scripts": {
   "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",
     "build": "grunt build",
     "test": "grunt test",
     "test": "grunt test",
     "test:coverage": "grunt test --coverage=true",
     "test:coverage": "grunt test --coverage=true",
@@ -135,8 +139,8 @@
   "license": "Apache-2.0",
   "license": "Apache-2.0",
   "dependencies": {
   "dependencies": {
     "angular": "1.6.6",
     "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-route": "1.6.6",
     "angular-sanitize": "1.6.6",
     "angular-sanitize": "1.6.6",
     "babel-polyfill": "^6.26.0",
     "babel-polyfill": "^6.26.0",
@@ -151,12 +155,14 @@
     "immutable": "^3.8.2",
     "immutable": "^3.8.2",
     "jquery": "^3.2.1",
     "jquery": "^3.2.1",
     "lodash": "^4.17.4",
     "lodash": "^4.17.4",
+    "mini-css-extract-plugin": "^0.4.0",
     "mobx": "^3.4.1",
     "mobx": "^3.4.1",
     "mobx-react": "^4.3.5",
     "mobx-react": "^4.3.5",
     "mobx-state-tree": "^1.3.1",
     "mobx-state-tree": "^1.3.1",
     "moment": "^2.18.1",
     "moment": "^2.18.1",
     "mousetrap": "^1.6.0",
     "mousetrap": "^1.6.0",
     "mousetrap-global-bind": "^1.1.0",
     "mousetrap-global-bind": "^1.1.0",
+    "optimize-css-assets-webpack-plugin": "^4.0.2",
     "prismjs": "^1.6.0",
     "prismjs": "^1.6.0",
     "prop-types": "^15.6.0",
     "prop-types": "^15.6.0",
     "react": "^16.2.0",
     "react": "^16.2.0",
@@ -175,7 +181,8 @@
     "slate-react": "^0.12.4",
     "slate-react": "^0.12.4",
     "tether": "^1.4.0",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
-    "tinycolor2": "^1.4.1"
+    "tinycolor2": "^1.4.1",
+    "uglifyjs-webpack-plugin": "^1.2.7"
   },
   },
   "resolutions": {
   "resolutions": {
     "caniuse-db": "1.0.30000772"
     "caniuse-db": "1.0.30000772"

+ 3 - 3
pkg/api/annotations.go

@@ -272,9 +272,9 @@ func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) {
 		return false, nil
 		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
 			return false, err
 		}
 		}
 	}
 	}

+ 2 - 1
pkg/cmd/grafana-server/main.go

@@ -88,10 +88,11 @@ func main() {
 
 
 	err := server.Run()
 	err := server.Run()
 
 
+	code := server.Exit(err)
 	trace.Stop()
 	trace.Stop()
 	log.Close()
 	log.Close()
 
 
-	server.Exit(err)
+	os.Exit(code)
 }
 }
 
 
 func listenToSystemSignals(server *GrafanaServerImpl) {
 func listenToSystemSignals(server *GrafanaServerImpl) {

+ 2 - 2
pkg/cmd/grafana-server/server.go

@@ -175,7 +175,7 @@ func (g *GrafanaServerImpl) Shutdown(reason string) {
 	g.childRoutines.Wait()
 	g.childRoutines.Wait()
 }
 }
 
 
-func (g *GrafanaServerImpl) Exit(reason error) {
+func (g *GrafanaServerImpl) Exit(reason error) int {
 	// default exit code is 1
 	// default exit code is 1
 	code := 1
 	code := 1
 
 
@@ -185,7 +185,7 @@ func (g *GrafanaServerImpl) Exit(reason error) {
 	}
 	}
 
 
 	g.log.Error("Server shutdown", "reason", reason)
 	g.log.Error("Server shutdown", "reason", reason)
-	os.Exit(code)
+	return code
 }
 }
 
 
 func (g *GrafanaServerImpl) writePIDFile() {
 func (g *GrafanaServerImpl) writePIDFile() {

+ 6 - 0
pkg/middleware/auth.go

@@ -9,6 +9,7 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
 type AuthOptions struct {
 type AuthOptions struct {
@@ -34,6 +35,11 @@ func getApiKey(c *m.ReqContext) string {
 		return key
 		return key
 	}
 	}
 
 
+	username, password, err := util.DecodeBasicAuthHeader(header)
+	if err == nil && username == "api_key" {
+		return password
+	}
+
 	return ""
 	return ""
 }
 }
 
 

+ 8 - 12
pkg/middleware/auth_proxy.go

@@ -2,6 +2,7 @@ package middleware
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"net"
 	"net/mail"
 	"net/mail"
 	"reflect"
 	"reflect"
 	"strings"
 	"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 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)
 		ctx.Handle(407, "Proxy authentication required", err)
 		return true
 		return true
 	}
 	}
@@ -196,23 +197,18 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
 		return nil
 		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, ",")
 	proxies := strings.Split(setting.AuthProxyWhitelist, ",")
+	sourceIP, _, err := net.SplitHostPort(remoteAddr)
+	if err != nil {
+		return err
+	}
 
 
 	// Compare allowed IP addresses to actual address
 	// Compare allowed IP addresses to actual address
 	for _, proxyIP := range proxies {
 	for _, proxyIP := range proxies {
-		if remoteAddr == strings.TrimSpace(proxyIP) {
+		if sourceIP == strings.TrimSpace(proxyIP) {
 			return nil
 			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)
 }
 }

+ 24 - 57
pkg/middleware/middleware_test.go

@@ -82,7 +82,7 @@ func TestMiddlewareContext(t *testing.T) {
 
 
 			setting.BasicAuthEnabled = true
 			setting.BasicAuthEnabled = true
 			authHeader := util.GetBasicAuthHeader("myUser", "myPass")
 			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() {
 			Convey("Should init middleware context with user", func() {
 				So(sc.context.IsSignedIn, ShouldEqual, true)
 				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) {
 		middlewareScenario("UserId in session", func(sc *scenarioContext) {
 
 
 			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
 			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) {
 		middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
@@ -473,7 +440,7 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext {
 	return sc
 	return sc
 }
 }
 
 
-func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext {
+func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext {
 	sc.authHeader = authHeader
 	sc.authHeader = authHeader
 	return sc
 	return sc
 }
 }

+ 19 - 5
pkg/registry/registry.go

@@ -34,23 +34,37 @@ func GetServices() []*Descriptor {
 	return services
 	return services
 }
 }
 
 
+// Service interface is the lowest common shape that services
+// are expected to forfill to be started within Grafana.
 type Service interface {
 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
 	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 {
 type CanBeDisabled interface {
+
+	// IsDisabled should return a bool saying if it can be started or not.
 	IsDisabled() bool
 	IsDisabled() bool
 }
 }
 
 
+// BackgroundService should be implemented for services that have
+// long running tasks in the background.
 type BackgroundService interface {
 type BackgroundService interface {
-	Run(ctx context.Context) error
-}
 
 
-type HasInitPriority interface {
-	GetInitPriority() Priority
+	// 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
 }
 }
 
 
+// IsDisabled takes an service and return true if its disabled
 func IsDisabled(srv Service) bool {
 func IsDisabled(srv Service) bool {
 	canBeDisabled, ok := srv.(CanBeDisabled)
 	canBeDisabled, ok := srv.(CanBeDisabled)
 	return ok && canBeDisabled.IsDisabled()
 	return ok && canBeDisabled.IsDisabled()

+ 4 - 1
pkg/services/alerting/notifier.go

@@ -104,7 +104,10 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
 		return err
 		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
 	return nil
 }
 }
 
 

+ 2 - 2
public/app/core/directives/value_select_dropdown.ts

@@ -93,7 +93,7 @@ export class ValueSelectDropdownCtrl {
       tagValuesPromise = this.$q.when(tag.values);
       tagValuesPromise = this.$q.when(tag.values);
     }
     }
 
 
-    tagValuesPromise.then(values => {
+    return tagValuesPromise.then(values => {
       tag.values = values;
       tag.values = values;
       tag.valuesText = values.join(' + ');
       tag.valuesText = values.join(' + ');
       _.each(this.options, option => {
       _.each(this.options, option => {
@@ -132,7 +132,7 @@ export class ValueSelectDropdownCtrl {
     this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length;
     this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length;
   }
   }
 
 
-  selectValue(option, event, commitChange, excludeOthers) {
+  selectValue(option, event, commitChange?, excludeOthers?) {
     if (!option) {
     if (!option) {
       return;
       return;
     }
     }

+ 159 - 0
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);
+    });
+  });
+});

+ 0 - 171
public/app/core/specs/value_select_dropdown_specs.ts

@@ -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);
-    });
-  });
-});

+ 11 - 11
public/app/features/annotations/specs/annotations_srv_specs.ts → 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 '../annotations_srv';
-import helpers from 'test/specs/helpers';
 import 'app/features/dashboard/time_srv';
 import 'app/features/dashboard/time_srv';
+import { AnnotationsSrv } from '../annotations_srv';
 
 
 describe('AnnotationsSrv', function() {
 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', () => {
   describe('When translating the query result', () => {
     const annotationSource = {
     const annotationSource = {
@@ -30,11 +30,11 @@ describe('AnnotationsSrv', function() {
     let translatedAnnotations;
     let translatedAnnotations;
 
 
     beforeEach(() => {
     beforeEach(() => {
-      translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations);
+      translatedAnnotations = annotationsSrv.translateQueryResult(annotationSource, annotations);
     });
     });
 
 
     it('should set defaults', () => {
     it('should set defaults', () => {
-      expect(translatedAnnotations[0].source).to.eql(annotationSource);
+      expect(translatedAnnotations[0].source).toEqual(annotationSource);
     });
     });
   });
   });
 });
 });

+ 67 - 0
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);
+    });
+  });
+});

+ 0 - 65
public/app/features/dashboard/specs/viewstate_srv_specs.ts

@@ -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);
-    });
-  });
-});

+ 34 - 0
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);
       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);
+    });
+  });
 });
 });

+ 18 - 1
public/app/features/templating/variable_srv.ts

@@ -209,7 +209,24 @@ export class VariableSrv {
         return op.text === urlValue || op.value === urlValue;
         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);
       return variable.setValue(option);
     });
     });
   }
   }

+ 1 - 1
public/app/partials/login.html

@@ -89,7 +89,7 @@
             <a class="btn btn-link" ng-click="skip();">
             <a class="btn btn-link" ng-click="skip();">
               Skip
               Skip
               <info-popover mode="no-padding">
               <info-popover mode="no-padding">
-                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.
               </info-popover>
               </info-popover>
             </a>
             </a>
             <button type="submit" class="btn btn-large p-x-2" ng-click="changePassword();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-success': loginForm.$valid}">
             <button type="submit" class="btn btn-large p-x-2" ng-click="changePassword();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-success': loginForm.$valid}">

+ 139 - 141
public/app/plugins/panel/graph/series_overrides_ctrl.ts

@@ -1,160 +1,158 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import angular from 'angular';
 import angular from 'angular';
 
 
-export class SeriesOverridesCtrl {
-  /** @ngInject */
-  constructor($scope, $element, popoverSrv) {
-    $scope.overrideMenu = [];
-    $scope.currentOverrides = [];
-    $scope.override = $scope.override || {};
-
-    $scope.addOverrideOption = function(name, propertyName, values) {
-      var option = {
-        text: name,
-        propertyName: propertyName,
-        index: $scope.overrideMenu.lenght,
-        values: values,
-        submenu: _.map(values, function(value) {
-          return { text: String(value), value: value };
-        }),
-      };
+/** @ngInject */
+export function SeriesOverridesCtrl($scope, $element, popoverSrv) {
+  $scope.overrideMenu = [];
+  $scope.currentOverrides = [];
+  $scope.override = $scope.override || {};
 
 
-      $scope.overrideMenu.push(option);
+  $scope.addOverrideOption = function(name, propertyName, values) {
+    var option = {
+      text: name,
+      propertyName: propertyName,
+      index: $scope.overrideMenu.lenght,
+      values: values,
+      submenu: _.map(values, function(value) {
+        return { text: String(value), value: value };
+      }),
     };
     };
 
 
-    $scope.setOverride = function(item, subItem) {
-      // handle color overrides
-      if (item.propertyName === 'color') {
-        $scope.openColorSelector($scope.override['color']);
-        return;
-      }
+    $scope.overrideMenu.push(option);
+  };
 
 
-      $scope.override[item.propertyName] = subItem.value;
+  $scope.setOverride = function(item, subItem) {
+    // handle color overrides
+    if (item.propertyName === 'color') {
+      $scope.openColorSelector($scope.override['color']);
+      return;
+    }
 
 
-      // automatically disable lines for this series and the fill below to series
-      // can be removed by the user if they still want lines
-      if (item.propertyName === 'fillBelowTo') {
-        $scope.override['lines'] = false;
-        $scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false });
-      }
+    $scope.override[item.propertyName] = subItem.value;
 
 
-      $scope.updateCurrentOverrides();
-      $scope.ctrl.render();
-    };
+    // automatically disable lines for this series and the fill below to series
+    // can be removed by the user if they still want lines
+    if (item.propertyName === 'fillBelowTo') {
+      $scope.override['lines'] = false;
+      $scope.ctrl.addSeriesOverride({ alias: subItem.value, lines: false });
+    }
 
 
-    $scope.colorSelected = function(color) {
-      $scope.override['color'] = color;
-      $scope.updateCurrentOverrides();
-      $scope.ctrl.render();
-    };
+    $scope.updateCurrentOverrides();
+    $scope.ctrl.render();
+  };
 
 
-    $scope.openColorSelector = function(color) {
-      var fakeSeries = { color: color };
-      popoverSrv.show({
-        element: $element.find('.dropdown')[0],
-        position: 'top center',
-        openOn: 'click',
-        template: '<series-color-picker series="series" onColorChange="colorSelected" />',
-        model: {
-          autoClose: true,
-          colorSelected: $scope.colorSelected,
-          series: fakeSeries,
-        },
-        onClose: function() {
-          $scope.ctrl.render();
-        },
-      });
-    };
+  $scope.colorSelected = function(color) {
+    $scope.override['color'] = color;
+    $scope.updateCurrentOverrides();
+    $scope.ctrl.render();
+  };
 
 
-    $scope.removeOverride = function(option) {
-      delete $scope.override[option.propertyName];
-      $scope.updateCurrentOverrides();
-      $scope.ctrl.refresh();
-    };
+  $scope.openColorSelector = function(color) {
+    var fakeSeries = { color: color };
+    popoverSrv.show({
+      element: $element.find('.dropdown')[0],
+      position: 'top center',
+      openOn: 'click',
+      template: '<series-color-picker series="series" onColorChange="colorSelected" />',
+      model: {
+        autoClose: true,
+        colorSelected: $scope.colorSelected,
+        series: fakeSeries,
+      },
+      onClose: function() {
+        $scope.ctrl.render();
+      },
+    });
+  };
 
 
-    $scope.getSeriesNames = function() {
-      return _.map($scope.ctrl.seriesList, function(series) {
-        return series.alias;
-      });
-    };
+  $scope.removeOverride = function(option) {
+    delete $scope.override[option.propertyName];
+    $scope.updateCurrentOverrides();
+    $scope.ctrl.refresh();
+  };
+
+  $scope.getSeriesNames = function() {
+    return _.map($scope.ctrl.seriesList, function(series) {
+      return series.alias;
+    });
+  };
 
 
-    $scope.updateCurrentOverrides = function() {
-      $scope.currentOverrides = [];
-      _.each($scope.overrideMenu, function(option) {
-        var value = $scope.override[option.propertyName];
-        if (_.isUndefined(value)) {
-          return;
-        }
-        $scope.currentOverrides.push({
-          name: option.text,
-          propertyName: option.propertyName,
-          value: String(value),
-        });
+  $scope.updateCurrentOverrides = function() {
+    $scope.currentOverrides = [];
+    _.each($scope.overrideMenu, function(option) {
+      var value = $scope.override[option.propertyName];
+      if (_.isUndefined(value)) {
+        return;
+      }
+      $scope.currentOverrides.push({
+        name: option.text,
+        propertyName: option.propertyName,
+        value: String(value),
       });
       });
-    };
+    });
+  };
 
 
-    $scope.addOverrideOption('Bars', 'bars', [true, false]);
-    $scope.addOverrideOption('Lines', 'lines', [true, false]);
-    $scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
-    $scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
-    $scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']);
-    $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
-    $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
-    $scope.addOverrideOption('Dashes', 'dashes', [true, false]);
-    $scope.addOverrideOption('Dash Length', 'dashLength', [
-      1,
-      2,
-      3,
-      4,
-      5,
-      6,
-      7,
-      8,
-      9,
-      10,
-      11,
-      12,
-      13,
-      14,
-      15,
-      16,
-      17,
-      18,
-      19,
-      20,
-    ]);
-    $scope.addOverrideOption('Dash Space', 'spaceLength', [
-      1,
-      2,
-      3,
-      4,
-      5,
-      6,
-      7,
-      8,
-      9,
-      10,
-      11,
-      12,
-      13,
-      14,
-      15,
-      16,
-      17,
-      18,
-      19,
-      20,
-    ]);
-    $scope.addOverrideOption('Points', 'points', [true, false]);
-    $scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]);
-    $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']);
-    $scope.addOverrideOption('Color', 'color', ['change']);
-    $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
-    $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]);
-    $scope.addOverrideOption('Transform', 'transform', ['negative-Y']);
-    $scope.addOverrideOption('Legend', 'legend', [true, false]);
-    $scope.updateCurrentOverrides();
-  }
+  $scope.addOverrideOption('Bars', 'bars', [true, false]);
+  $scope.addOverrideOption('Lines', 'lines', [true, false]);
+  $scope.addOverrideOption('Line fill', 'fill', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
+  $scope.addOverrideOption('Line width', 'linewidth', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
+  $scope.addOverrideOption('Null point mode', 'nullPointMode', ['connected', 'null', 'null as zero']);
+  $scope.addOverrideOption('Fill below to', 'fillBelowTo', $scope.getSeriesNames());
+  $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]);
+  $scope.addOverrideOption('Dashes', 'dashes', [true, false]);
+  $scope.addOverrideOption('Dash Length', 'dashLength', [
+    1,
+    2,
+    3,
+    4,
+    5,
+    6,
+    7,
+    8,
+    9,
+    10,
+    11,
+    12,
+    13,
+    14,
+    15,
+    16,
+    17,
+    18,
+    19,
+    20,
+  ]);
+  $scope.addOverrideOption('Dash Space', 'spaceLength', [
+    1,
+    2,
+    3,
+    4,
+    5,
+    6,
+    7,
+    8,
+    9,
+    10,
+    11,
+    12,
+    13,
+    14,
+    15,
+    16,
+    17,
+    18,
+    19,
+    20,
+  ]);
+  $scope.addOverrideOption('Points', 'points', [true, false]);
+  $scope.addOverrideOption('Points Radius', 'pointradius', [1, 2, 3, 4, 5]);
+  $scope.addOverrideOption('Stack', 'stack', [true, false, 'A', 'B', 'C', 'D']);
+  $scope.addOverrideOption('Color', 'color', ['change']);
+  $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]);
+  $scope.addOverrideOption('Z-index', 'zindex', [-3, -2, -1, 0, 1, 2, 3]);
+  $scope.addOverrideOption('Transform', 'transform', ['negative-Y']);
+  $scope.addOverrideOption('Legend', 'legend', [true, false]);
+  $scope.updateCurrentOverrides();
 }
 }
 
 
 angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl);
 angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl);

+ 42 - 0
public/app/plugins/panel/graph/specs/series_override_ctrl.jest.ts

@@ -0,0 +1,42 @@
+import '../series_overrides_ctrl';
+import { SeriesOverridesCtrl } from '../series_overrides_ctrl';
+
+describe('SeriesOverridesCtrl', () => {
+  let popoverSrv = {};
+  let $scope;
+
+  beforeEach(() => {
+    $scope = {
+      ctrl: {
+        refresh: jest.fn(),
+        render: jest.fn(),
+        seriesList: [],
+      },
+      render: jest.fn(() => {}),
+    };
+    SeriesOverridesCtrl($scope, {}, popoverSrv);
+  });
+
+  describe('When setting an override', () => {
+    beforeEach(() => {
+      $scope.setOverride({ propertyName: 'lines' }, { value: true });
+    });
+
+    it('should set override property', () => {
+      expect($scope.override.lines).toBe(true);
+    });
+
+    it('should update view model', () => {
+      expect($scope.currentOverrides[0].name).toBe('Lines');
+      expect($scope.currentOverrides[0].value).toBe('true');
+    });
+  });
+
+  describe('When removing overide', () => {
+    it('click should include option and value index', () => {
+      $scope.setOverride(1, 0);
+      $scope.removeOverride({ propertyName: 'lines' });
+      expect($scope.currentOverrides.length).toBe(0);
+    });
+  });
+});

+ 0 - 55
public/app/plugins/panel/graph/specs/series_override_ctrl_specs.ts

@@ -1,55 +0,0 @@
-import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common';
-import '../series_overrides_ctrl';
-import helpers from 'test/specs/helpers';
-
-describe('SeriesOverridesCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
-  var popoverSrv = {};
-
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-
-  beforeEach(
-    ctx.providePhase({
-      popoverSrv: popoverSrv,
-    })
-  );
-
-  beforeEach(
-    angularMocks.inject(function($rootScope, $controller) {
-      ctx.scope = $rootScope.$new();
-      ctx.scope.ctrl = {
-        refresh: sinon.spy(),
-        render: sinon.spy(),
-        seriesList: [],
-      };
-      ctx.scope.render = function() {};
-      ctx.controller = $controller('SeriesOverridesCtrl', {
-        $scope: ctx.scope,
-      });
-    })
-  );
-
-  describe('When setting an override', function() {
-    beforeEach(function() {
-      ctx.scope.setOverride({ propertyName: 'lines' }, { value: true });
-    });
-
-    it('should set override property', function() {
-      expect(ctx.scope.override.lines).to.be(true);
-    });
-
-    it('should update view model', function() {
-      expect(ctx.scope.currentOverrides[0].name).to.be('Lines');
-      expect(ctx.scope.currentOverrides[0].value).to.be('true');
-    });
-  });
-
-  describe('When removing overide', function() {
-    it('click should include option and value index', function() {
-      ctx.scope.setOverride(1, 0);
-      ctx.scope.removeOverride({ propertyName: 'lines' });
-      expect(ctx.scope.currentOverrides.length).to.be(0);
-    });
-  });
-});

+ 6 - 2
public/app/plugins/panel/singlestat/editor.html

@@ -29,7 +29,7 @@
         <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
         <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
         <label class="gf-form-label width-6">Font size</label>
         <label class="gf-form-label width-6">Font size</label>
         <div class="gf-form-select-wrapper">
         <div class="gf-form-select-wrapper">
-          <select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="ctrl.canChangeFontSize()"></select>
+          <select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -39,7 +39,7 @@
       <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
       <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
       <label class="gf-form-label width-6">Font size</label>
       <label class="gf-form-label width-6">Font size</label>
       <div class="gf-form-select-wrapper">
       <div class="gf-form-select-wrapper">
-        <select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="ctrl.canChangeFontSize()"></select>
+        <select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></select>
       </div>
       </div>
     </div>
     </div>
     <div class="gf-form">
     <div class="gf-form">
@@ -58,6 +58,10 @@
       <gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
       <gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
       <gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
       <gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
     </div>
     </div>
+    <div class="gf-form-inline">
+      <gf-form-switch class="gf-form" label-class="width-6" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
+      <gf-form-switch class="gf-form" label-class="width-6" label="Postfix" checked="ctrl.panel.colorPostfix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
+    </div>
     <div class="gf-form-inline">
     <div class="gf-form-inline">
       <div class="gf-form max-width-21">
       <div class="gf-form max-width-21">
         <label class="gf-form-label width-8">Thresholds
         <label class="gf-form-label width-8">Thresholds

+ 14 - 9
public/app/plugins/panel/singlestat/module.ts

@@ -198,8 +198,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     this.setValueMapping(data);
     this.setValueMapping(data);
   }
   }
 
 
-  canChangeFontSize() {
-    return this.panel.gauge.show;
+  canModifyText() {
+    return !this.panel.gauge.show;
   }
   }
 
 
   setColoring(options) {
   setColoring(options) {
@@ -405,10 +405,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     elem = elem.find('.singlestat-panel');
     elem = elem.find('.singlestat-panel');
 
 
     function applyColoringThresholds(value, valueString) {
     function applyColoringThresholds(value, valueString) {
-      if (!panel.colorValue) {
-        return valueString;
-      }
-
       var color = getColorForValue(data, value);
       var color = getColorForValue(data, value);
       if (color) {
       if (color) {
         return '<span style="color:' + color + '">' + valueString + '</span>';
         return '<span style="color:' + color + '">' + valueString + '</span>';
@@ -426,15 +422,24 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       var body = '<div class="singlestat-panel-value-container">';
       var body = '<div class="singlestat-panel-value-container">';
 
 
       if (panel.prefix) {
       if (panel.prefix) {
-        var prefix = applyColoringThresholds(data.value, panel.prefix);
+        var prefix = panel.prefix;
+        if (panel.colorPrefix) {
+          prefix = applyColoringThresholds(data.value, panel.prefix);
+        }
         body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix);
         body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix);
       }
       }
 
 
-      var value = applyColoringThresholds(data.value, data.valueFormatted);
+      var value = data.valueFormatted;
+      if (panel.colorValue) {
+        value = applyColoringThresholds(data.value, value);
+      }
       body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
       body += getSpan('singlestat-panel-value', panel.valueFontSize, value);
 
 
       if (panel.postfix) {
       if (panel.postfix) {
-        var postfix = applyColoringThresholds(data.value, panel.postfix);
+        var postfix = panel.postfix;
+        if (panel.colorPostfix) {
+          postfix = applyColoringThresholds(data.value, panel.postfix);
+        }
         body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix);
         body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix);
       }
       }
 
 

+ 13 - 4
public/sass/components/_footer.scss

@@ -25,7 +25,7 @@
     display: inline-block;
     display: inline-block;
     padding-right: 2px;
     padding-right: 2px;
     &::after {
     &::after {
-      content: " | ";
+      content: ' | ';
       padding-left: 2px;
       padding-left: 2px;
     }
     }
   }
   }
@@ -33,14 +33,23 @@
   li:last-child {
   li:last-child {
     &::after {
     &::after {
       padding-left: 0;
       padding-left: 0;
-      content: "";
+      content: '';
     }
     }
   }
   }
 }
 }
 
 
 .login-page {
 .login-page {
   .footer {
   .footer {
-    position: absolute;
-    bottom: $spacer;
+    padding: 1rem 0 1rem 0;
+  }
+}
+
+@include media-breakpoint-up(md) {
+  .login-page {
+    .footer {
+      bottom: $spacer;
+      position: absolute;
+      padding: 5rem 0 1rem 0;
+    }
   }
   }
 }
 }

+ 6 - 5
public/sass/components/_switch.scss

@@ -64,8 +64,8 @@
   }
   }
 
 
   input + label::before {
   input + label::before {
-    font-family: "FontAwesome";
-    content: "\f096"; // square-o
+    font-family: 'FontAwesome';
+    content: '\f096'; // square-o
     color: $text-color-weak;
     color: $text-color-weak;
     transition: transform 0.4s;
     transition: transform 0.4s;
     backface-visibility: hidden;
     backface-visibility: hidden;
@@ -73,11 +73,11 @@
   }
   }
 
 
   input + label::after {
   input + label::after {
-    content: "\f046"; // check-square-o
+    content: '\f046'; // check-square-o
     color: $orange;
     color: $orange;
     text-shadow: $text-shadow-strong;
     text-shadow: $text-shadow-strong;
 
 
-    font-family: "FontAwesome";
+    font-family: 'FontAwesome';
     transition: transform 0.4s;
     transition: transform 0.4s;
     transform: rotateY(180deg);
     transform: rotateY(180deg);
     backface-visibility: hidden;
     backface-visibility: hidden;
@@ -154,7 +154,8 @@ gf-form-switch[disabled] {
   .gf-form-switch input + label {
   .gf-form-switch input + label {
     cursor: default;
     cursor: default;
     pointer-events: none !important;
     pointer-events: none !important;
-    &::before {
+    &::before,
+    &::after {
       color: $text-color-faint;
       color: $text-color-faint;
       text-shadow: none;
       text-shadow: none;
     }
     }

+ 16 - 13
public/sass/pages/_login.scss

@@ -1,9 +1,8 @@
 $login-border: #8daac5;
 $login-border: #8daac5;
 
 
 .login {
 .login {
-  background-position: center;
   min-height: 85vh;
   min-height: 85vh;
-  height: 80vh;
+  background-position: center;
   background-repeat: no-repeat;
   background-repeat: no-repeat;
   min-width: 100%;
   min-width: 100%;
   margin-left: 0;
   margin-left: 0;
@@ -95,7 +94,7 @@ select:-webkit-autofill:focus {
   position: relative;
   position: relative;
   justify-content: center;
   justify-content: center;
   z-index: 1;
   z-index: 1;
-  height: 320px;
+  min-height: 320px;
 }
 }
 
 
 .login-branding {
 .login-branding {
@@ -106,6 +105,7 @@ select:-webkit-autofill:focus {
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
   flex-grow: 0;
   flex-grow: 0;
+  padding-top: 2rem;
 
 
   .logo-icon {
   .logo-icon {
     width: 70px;
     width: 70px;
@@ -127,7 +127,7 @@ select:-webkit-autofill:focus {
 
 
 .login-inner-box {
 .login-inner-box {
   text-align: center;
   text-align: center;
-  padding: 2rem 4rem;
+  padding: 2rem;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
@@ -243,7 +243,7 @@ select:-webkit-autofill:focus {
   justify-content: space-between;
   justify-content: space-between;
 
 
   .login-divider-line {
   .login-divider-line {
-    width: 110px;
+    width: 100px;
     height: 10px;
     height: 10px;
     border-bottom: 1px solid $login-border;
     border-bottom: 1px solid $login-border;
 
 
@@ -323,7 +323,10 @@ select:-webkit-autofill:focus {
     width: 35%;
     width: 35%;
     padding: 4rem 2rem;
     padding: 4rem 2rem;
     border-right: 1px solid $login-border;
     border-right: 1px solid $login-border;
-    justify-content: flex-start;
+
+    .logo-icon {
+      width: 80px;
+    }
   }
   }
 
 
   .login-inner-box {
   .login-inner-box {
@@ -331,14 +334,18 @@ select:-webkit-autofill:focus {
     padding: 1rem 2rem;
     padding: 1rem 2rem;
   }
   }
 
 
-  .login-branding {
-    .logo-icon {
-      width: 80px;
+  .login-divider {
+    .login-divider-line {
+      width: 110px;
     }
     }
   }
   }
 }
 }
 
 
 @include media-breakpoint-up(md) {
 @include media-breakpoint-up(md) {
+  .login {
+    min-height: 100vh;
+  }
+
   .login-content {
   .login-content {
     flex: 1 0 100%;
     flex: 1 0 100%;
   }
   }
@@ -373,10 +380,6 @@ select:-webkit-autofill:focus {
 }
 }
 
 
 @include media-breakpoint-up(lg) {
 @include media-breakpoint-up(lg) {
-  .login {
-    min-height: 100vh;
-  }
-
   .login-form-input {
   .login-form-input {
     min-width: 300px;
     min-width: 300px;
   }
   }

+ 5 - 2
scripts/grunt/options/karma.js

@@ -1,4 +1,4 @@
-module.exports = function(config) {
+module.exports = function (config) {
   'use strict';
   'use strict';
 
 
   return {
   return {
@@ -10,7 +10,10 @@ module.exports = function(config) {
     debug: {
     debug: {
       configFile: 'karma.conf.js',
       configFile: 'karma.conf.js',
       singleRun: false,
       singleRun: false,
-      browsers: ['Chrome']
+      browsers: ['Chrome'],
+      mime: {
+        'text/x-typescript': ['ts', 'tsx']
+      },
     },
     },
 
 
     test: {
     test: {

+ 21 - 29
scripts/webpack/sass.rule.js

@@ -1,37 +1,29 @@
 'use strict';
 'use strict';
 
 
-const ExtractTextPlugin = require("extract-text-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 
 
-module.exports = function (options, extractSass) {
+module.exports = function(options) {
   return {
   return {
     test: /\.scss$/,
     test: /\.scss$/,
-    use: (extractSass || ExtractTextPlugin).extract({
-      use: [
-        {
-          loader: 'css-loader',
-          options: {
-            importLoaders: 2,
-            url: options.preserveUrl,
-            sourceMap: options.sourceMap,
-            minimize: options.minimize,
-          }
-        },
-        {
-          loader: 'postcss-loader',
-          options: {
-            sourceMap: options.sourceMap,
-            config: { path: __dirname + '/postcss.config.js' }
-          }
+    use: [
+      MiniCssExtractPlugin.loader,
+      {
+        loader: 'css-loader',
+        options: {
+          importLoaders: 2,
+          url: options.preserveUrl,
+          sourceMap: options.sourceMap,
+          minimize: options.minimize,
         },
         },
-        { loader: 'sass-loader', options: { sourceMap: options.sourceMap } }
-      ],
-      fallback: [{
-        loader: 'style-loader',
+      },
+      {
+        loader: 'postcss-loader',
         options: {
         options: {
-          sourceMap: true
-        }
-      }]
-    })
+          sourceMap: options.sourceMap,
+          config: { path: __dirname + '/postcss.config.js' },
+        },
+      },
+      { loader: 'sass-loader', options: { sourceMap: options.sourceMap } },
+    ],
   };
   };
-}
-
+};

+ 4 - 2
scripts/webpack/webpack.common.js

@@ -1,5 +1,5 @@
 const path = require('path');
 const path = require('path');
-const { CheckerPlugin } = require('awesome-typescript-loader');
+const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
 
 
 module.exports = {
 module.exports = {
   target: 'web',
   target: 'web',
@@ -61,6 +61,8 @@ module.exports = {
     ]
     ]
   },
   },
   plugins: [
   plugins: [
-    new CheckerPlugin(),
+    new ForkTsCheckerWebpackPlugin({
+      checkSyntacticErrors: true,
+    }),
   ]
   ]
 };
 };

+ 29 - 16
scripts/webpack/webpack.dev.js

@@ -7,20 +7,17 @@ const webpack = require('webpack');
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const ExtractTextPlugin = require("extract-text-webpack-plugin");
 const ExtractTextPlugin = require("extract-text-webpack-plugin");
 const CleanWebpackPlugin = require('clean-webpack-plugin');
 const CleanWebpackPlugin = require('clean-webpack-plugin');
-const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
-
-const extractSass = new ExtractTextPlugin({
-  filename: "grafana.[name].css"
-});
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
 
 module.exports = merge(common, {
 module.exports = merge(common, {
   devtool: "cheap-module-source-map",
   devtool: "cheap-module-source-map",
+  mode: 'development',
 
 
   entry: {
   entry: {
     app: './public/app/index.ts',
     app: './public/app/index.ts',
     dark: './public/sass/grafana.dark.scss',
     dark: './public/sass/grafana.dark.scss',
     light: './public/sass/grafana.light.scss',
     light: './public/sass/grafana.light.scss',
-    vendor: require('./dependencies'),
   },
   },
 
 
   output: {
   output: {
@@ -48,15 +45,13 @@ module.exports = merge(common, {
         test: /\.tsx?$/,
         test: /\.tsx?$/,
         exclude: /node_modules/,
         exclude: /node_modules/,
         use: {
         use: {
-          loader: 'awesome-typescript-loader',
+          loader: 'ts-loader',
           options: {
           options: {
-            useCache: true,
+            transpileOnly: true
           },
           },
-        }
+        },
       },
       },
-      require('./sass.rule.js')({
-        sourceMap: true, minimize: false, preserveUrl: false
-      }, extractSass),
+      require('./sass.rule.js')({ sourceMap: false, minimize: false, preserveUrl: false }),
       {
       {
         test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
         test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
         loader: 'file-loader'
         loader: 'file-loader'
@@ -64,9 +59,30 @@ module.exports = merge(common, {
     ]
     ]
   },
   },
 
 
+  optimization: {
+    splitChunks: {
+      cacheGroups: {
+        manifest: {
+          chunks: "initial",
+          test: "vendor",
+          name: "vendor",
+          enforce: true
+        },
+        vendor: {
+          chunks: "initial",
+          test: "vendor",
+          name: "vendor",
+          enforce: true
+        }
+      }
+    }
+  },
+
   plugins: [
   plugins: [
     new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
     new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
-    extractSass,
+    new MiniCssExtractPlugin({
+      filename: "grafana.[name].css"
+    }),
     new HtmlWebpackPlugin({
     new HtmlWebpackPlugin({
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       template: path.resolve(__dirname, '../../public/views/index.template.html'),
       template: path.resolve(__dirname, '../../public/views/index.template.html'),
@@ -80,9 +96,6 @@ module.exports = merge(common, {
         'NODE_ENV': JSON.stringify('development')
         'NODE_ENV': JSON.stringify('development')
       }
       }
     }),
     }),
-    new webpack.optimize.CommonsChunkPlugin({
-      names: ['vendor', 'manifest'],
-    }),
     // new BundleAnalyzerPlugin({
     // new BundleAnalyzerPlugin({
     //   analyzerPort: 8889
     //   analyzerPort: 8889
     // })
     // })

+ 15 - 12
scripts/webpack/webpack.hot.js

@@ -42,20 +42,23 @@ module.exports = merge(common, {
       {
       {
         test: /\.tsx?$/,
         test: /\.tsx?$/,
         exclude: /node_modules/,
         exclude: /node_modules/,
-        use: {
-          loader: 'awesome-typescript-loader',
+        use: [{
+          loader: 'babel-loader',
           options: {
           options: {
-            useCache: true,
-            useBabel: true,
-            babelOptions: {
-              babelrc: false,
-              plugins: [
-                'syntax-dynamic-import',
-                'react-hot-loader/babel'
-              ]
-            }
+            cacheDirectory: true,
+            babelrc: false,
+            plugins: [
+              'syntax-dynamic-import',
+              'react-hot-loader/babel'
+            ]
+          }
+        },
+        {
+          loader: 'ts-loader',
+          options: {
+            transpileOnly: true
           },
           },
-        }
+        }],
       },
       },
       {
       {
         test: /\.scss$/,
         test: /\.scss$/,

+ 30 - 29
scripts/webpack/webpack.prod.js

@@ -1,21 +1,22 @@
 'use strict';
 'use strict';
 
 
 const merge = require('webpack-merge');
 const merge = require('webpack-merge');
-const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 const common = require('./webpack.common.js');
 const common = require('./webpack.common.js');
 const webpack = require('webpack');
 const webpack = require('webpack');
 const path = require('path');
 const path = require('path');
 const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
 const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
 const HtmlWebpackPlugin = require("html-webpack-plugin");
 const HtmlWebpackPlugin = require("html-webpack-plugin");
-const ExtractTextPlugin = require("extract-text-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
 
 
 module.exports = merge(common, {
 module.exports = merge(common, {
+  mode: 'production',
   devtool: "source-map",
   devtool: "source-map",
 
 
   entry: {
   entry: {
     dark: './public/sass/grafana.dark.scss',
     dark: './public/sass/grafana.dark.scss',
     light: './public/sass/grafana.light.scss',
     light: './public/sass/grafana.light.scss',
-    vendor: require('./dependencies'),
   },
   },
 
 
   module: {
   module: {
@@ -35,49 +36,49 @@ module.exports = merge(common, {
       {
       {
         test: /\.tsx?$/,
         test: /\.tsx?$/,
         exclude: /node_modules/,
         exclude: /node_modules/,
-        use: [
-          {
-            loader: 'awesome-typescript-loader',
-            options: {
-              errorsAsWarnings: false,
-            },
+        use: {
+          loader: 'ts-loader',
+          options: {
+            transpileOnly: true
           },
           },
-        ]
+        },
       },
       },
       require('./sass.rule.js')({
       require('./sass.rule.js')({
-        sourceMap: false, minimize: true, preserveUrl: false
+        sourceMap: false, minimize: false, preserveUrl: false
       })
       })
     ]
     ]
   },
   },
 
 
-  devServer: {
-    noInfo: true,
-    stats: {
-      chunks: false,
+  optimization: {
+    splitChunks: {
+      cacheGroups: {
+        commons: {
+          test: /[\\/]node_modules[\\/].*[jt]sx?$/,
+          name: "vendor",
+          chunks: "all"
+        }
+      }
     },
     },
+    minimizer: [
+      new UglifyJsPlugin({
+        cache: true,
+        parallel: true,
+        sourceMap: true
+      }),
+      new OptimizeCSSAssetsPlugin({})
+    ]
   },
   },
 
 
   plugins: [
   plugins: [
-    new ExtractTextPlugin({
-      filename: 'grafana.[name].css',
+    new MiniCssExtractPlugin({
+      filename: "grafana.[name].css"
     }),
     }),
     new ngAnnotatePlugin(),
     new ngAnnotatePlugin(),
-    new UglifyJSPlugin({
-      sourceMap: true,
-    }),
-    new webpack.DefinePlugin({
-      'process.env': {
-        'NODE_ENV': JSON.stringify('production')
-      }
-    }),
     new HtmlWebpackPlugin({
     new HtmlWebpackPlugin({
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       template: path.resolve(__dirname, '../../public/views/index.template.html'),
       template: path.resolve(__dirname, '../../public/views/index.template.html'),
       inject: 'body',
       inject: 'body',
-      chunks: ['manifest', 'vendor', 'app'],
-    }),
-    new webpack.optimize.CommonsChunkPlugin({
-      names: ['vendor', 'manifest'],
+      chunks: ['vendor', 'app'],
     }),
     }),
     function () {
     function () {
       this.plugin("done", function (stats) {
       this.plugin("done", function (stats) {

+ 13 - 6
scripts/webpack/webpack.test.js

@@ -3,29 +3,36 @@ const merge = require('webpack-merge');
 const common = require('./webpack.common.js');
 const common = require('./webpack.common.js');
 
 
 config = merge(common, {
 config = merge(common, {
+  mode: 'development',
   devtool: 'cheap-module-source-map',
   devtool: 'cheap-module-source-map',
+
   externals: {
   externals: {
     'react/addons': true,
     'react/addons': true,
     'react/lib/ExecutionEnvironment': true,
     'react/lib/ExecutionEnvironment': true,
     'react/lib/ReactContext': true,
     'react/lib/ReactContext': true,
   },
   },
+
   module: {
   module: {
     rules: [
     rules: [
       {
       {
         test: /\.tsx?$/,
         test: /\.tsx?$/,
         exclude: /node_modules/,
         exclude: /node_modules/,
-        use: [
-          { loader: "awesome-typescript-loader" }
-        ]
+        use: {
+          loader: 'ts-loader',
+          options: {
+            transpileOnly: true,
+          },
+        },
       },
       },
-    ]
+    ],
   },
   },
+
   plugins: [
   plugins: [
     new webpack.SourceMapDevToolPlugin({
     new webpack.SourceMapDevToolPlugin({
       filename: null, // if no value is provided the sourcemap is inlined
       filename: null, // if no value is provided the sourcemap is inlined
-      test: /\.(ts|js)($|\?)/i // process .js and .ts files only
+      test: /\.(ts|js)($|\?)/i, // process .js and .ts files only
     }),
     }),
-  ]
+  ],
 });
 });
 
 
 module.exports = config;
 module.exports = config;

File diff suppressed because it is too large
+ 496 - 148
yarn.lock


Some files were not shown because too many files changed in this diff