Browse Source

Merge branch 'master' of https://github.com/grafana/grafana into macros/sql

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

+ 1 - 4
.circleci/config.yml

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

+ 19 - 1
CHANGELOG.md

@@ -7,13 +7,26 @@
 
 * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
+* **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
 
-# 5.2.0 (unreleased)
+# 5.2.1 (unreleased)
+
+### Minor
+
+* **UI**: Fix - Grafana footer overlapping page [#12430](https://github.com/grafana/grafana/issues/12430)
+* **Auth Proxy**: Revert of "Whitelist proxy IP address instead of client IP address" introduced in 5.2.0-beta2 [#12444](https://github.com/grafana/grafana/pull/12444)
+
+# 5.2.0-stable (2018-06-27)
 
 ### Minor
 
 * **Plugins**: Handle errors correctly when loading datasource plugin [#12383](https://github.com/grafana/grafana/pull/12383) thx [@rozetko](https://github.com/rozetko)
 * **Render**: Enhance error message if phantomjs executable is not found [#11868](https://github.com/grafana/grafana/issues/11868)
+* **Dashboard**: Set correct text in drop down when variable is present in url [#11968](https://github.com/grafana/grafana/issues/11968)
+
+### 5.2.0-beta3 fixes
+
+* **LDAP**: Handle "dn" ldap attribute more gracefully [#12385](https://github.com/grafana/grafana/pull/12385), reverts [#10970](https://github.com/grafana/grafana/pull/10970)
 
 # 5.2.0-beta3 (2018-06-21)
 
@@ -56,6 +69,7 @@
 ### New Features
 
 * **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95)
+* **Build**: Crosscompile and packages Grafana on arm, windows, linux and darwin [#11920](https://github.com/grafana/grafana/pull/11920), thx [@fg2it](https://github.com/fg2it)
 * **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882)
 * **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541)
 
@@ -91,6 +105,10 @@
 * **Dashboard list panel**: Search dashboards by folder [#11525](https://github.com/grafana/grafana/issues/11525)
 * **Sidenav**: Always show server admin link in sidenav if grafana admin [#11657](https://github.com/grafana/grafana/issues/11657)
 
+# 5.1.5 (2018-06-27)
+
+* **Docker**: Config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170)
+
 # 5.1.4 (2018-06-19)
 
 * **Permissions**: Important security fix for API keys with viewer role [#12343](https://github.com/grafana/grafana/issues/12343)

+ 5 - 12
ROADMAP.md

@@ -1,28 +1,21 @@
-# Roadmap (2018-05-06)
+# Roadmap (2018-06-26)
 
 This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. 
 But it will give you an idea of our current vision and plan. 
   
 ### Short term (1-2 months)
-
-  - Elasticsearch alerting
-  - Crossplatform builds
-  - Backend service refactorings
-  - Explore UI 
-  - First login registration view
-  
-### Mid term (2-4 months)
   - Multi-Stat panel
+  - Metrics & Log Explore UI 
+ 
+### Mid term (2-4 months)  
   - React Panels 
+  - Change visualization (panel type) on the fly. 
   - Templating Query Editor UI Plugin hook
   
 ### Long term (4 - 8 months)
 
 - Alerting improvements (silence, per series tracking, etc)
 - Progress on React migration
-- Change visualization (panel type) on the fly. 
-- Multi stat panel (vertical version of singlestat with bars/graph mode with big number etc) 
-- Repeat panel by query results 
 
 ### In a distant future far far away
 

+ 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.
 
-* [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here!
-* [Cross platform build support]({{< relref "#cross-platform-build-support" >}}) enables native builds of Grafana for many more platforms!
-* [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets
-* [Security]({{< relref "#security" >}}) make your Grafana instance more secure
-* [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements
-* [InfluxDB]({{< relref "#influxdb" >}}) with support for a new function
-* [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord
-* [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements
+- [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here!
+- [Native builds for ARM]({{< relref "#native-builds-for-arm" >}}) native builds of Grafana for many more platforms!
+- [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets
+- [Security]({{< relref "#security" >}}) make your Grafana instance more secure
+- [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements
+- [InfluxDB]({{< relref "#influxdb" >}}) now supports the `mode` function
+- [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord
+- [Dashboards & Panels]({{< relref "#dashboards-panels" >}}) with save & import enhancements
 
 ## Elasticsearch alerting
 
@@ -32,16 +32,18 @@ the most requested features by our community and now it's finally here. Please t
 
 <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
 
-The Grafana docker image now includes support for Docker secrets which enables you to supply Grafana with configuration through files. More
+The Grafana docker image adds support for Docker secrets which enables you to supply Grafana with configuration through files. More
 information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets).
 
 ## Security
@@ -49,18 +51,18 @@ information in the [Installing using Docker documentation](/installation/docker/
 {{< docs-imagebox img="/img/docs/v52/login_change_password.png" max-width="800px" class="docs-image--right" >}}
 
 Starting from Grafana v5.2, when you login with the administrator account using the default password you'll be presented with a form to change the password.
-By this we hope to encourage users to follow Grafana's best practices and change the default administrator password.
+We hope this encourages users to follow Grafana's best practices and change the default administrator password.
 
 <div class="clearfix"></div>
 
 ## Prometheus
 
 The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate*
-functions get consistent results, and thus avoid graphs jumping around on reload.
+functions get consistent results, and thus avoids graphs jumping around on reload.
 
 ## InfluxDB
 
-The InfluxDB datasource now includes support for the *mode* function which allows to return the most frequent value in a list of field values.
+The InfluxDB datasource now includes support for the *mode* function which returns the most frequent value in a list of field values.
 
 ## Alerting
 
@@ -72,9 +74,9 @@ By popular demand Grafana now includes support for an alert notification channel
 
 {{< docs-imagebox img="/img/docs/v52/dashboard_save_modal.png" max-width="800px" class="docs-image--right" >}}
 
-Starting from Grafana v5.2 a modified time range or variable are no longer saved by default. To save a modified
-time range or variable you'll need to actively select that when saving a dashboard, see screenshot.
-This should hopefully make it easier to have sane defaults of time and variables in dashboards and make it more explicit
+Starting from Grafana v5.2, a modified time range or variable are no longer saved by default. To save a modified
+time range or variable, you'll need to actively select that when saving a dashboard, see screenshot.
+This should hopefully make it easier to have sane defaults for time and variables in dashboards and make it more explicit
 when you actually want to overwrite those settings.
 
 <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" >}}
 
-Grafana v5.2 adds support for specifying an existing folder or create a new one when importing a dashboard, a long awaited feature since
-Grafana v5.0 introduced support for dashboard folders and permissions. The import dashboard page have also got some general improvements
+Grafana v5.2 adds support for specifying an existing folder or creating a new one when importing a dashboard - a long-awaited feature since
+Grafana v5.0 introduced support for dashboard folders and permissions. The import dashboard page has also got some general improvements
 and should now make it more clear if a possible import will overwrite an existing dashboard, or not.
 
-This release also adds some improvements for those users only having editor or admin permissions in certain folders. Now the links to
-*Create Dashboard* and *Import Dashboard* is available in side navigation, dashboard search and manage dashboards/folder page for a
-user that has editor role in an organization or edit permission in at least one folder.
+This release also adds some improvements for those users only having editor or admin permissions in certain folders. The links to
+*Create Dashboard* and *Import Dashboard* are now available in the side navigation, in dashboard search and on the manage dashboards/folder page for a
+user that has editor role in an organization or the edit permission in at least one folder.
 
 <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 API Token can also be passed as a Basic authorization password with the special username `api_key`:
+
+curl example:
+```bash
+?curl http://api_key:eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk@localhost:3000/api/org
+{"id":1,"name":"Main Org."}
+```
+
 # Auth HTTP resources / actions
 
 ## Api Keys

+ 3 - 3
docs/sources/index.md

@@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]
         <h4>Provisioning</h4>
         <p>A guide to help you automate your Grafana setup & configuration.</p>
     </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 href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
         <h4>Screencasts</h4>

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

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

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

@@ -11,6 +11,8 @@ weight = 4
 
 # Installing on Mac
 
+## Install using homebrew
+
 Installation can be done using [homebrew](http://brew.sh/)
 
 Install latest stable:
@@ -75,3 +77,18 @@ If you want to manually install a plugin place it here: `/usr/local/var/lib/graf
 
 The default sqlite database is located at `/usr/local/var/lib/grafana`
 
+## Installing from binary tar file
+
+Download [the latest `.tar.gz` file](https://grafana.com/get) and
+extract it.  This will extract into a folder named after the version you
+downloaded. This folder contains all files required to run Grafana.  There are
+no init scripts or install scripts in this package.
+
+To configure Grafana add a configuration file named `custom.ini` to the
+`conf` folder and override any of the settings defined in
+`conf/defaults.ini`.
+
+Start Grafana by executing `./bin/grafana-server web`. The `grafana-server`
+binary needs the working directory to be the root install directory (where the
+binary and the `public` folder is located).
+

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

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

+ 1 - 1
docs/sources/installation/troubleshooting.md

@@ -21,7 +21,7 @@ the data source response.
 
 To check this you should use Query Inspector (new in Grafana v4.5). The query Inspector shows query requests and responses.
 
-For more on the query insector read [this guide here](https://community.grafana.com/t/using-grafanas-query-inspector-to-troubleshoot-issues/2630). For
+For more on the query inspector read [this guide here](https://community.grafana.com/t/using-grafanas-query-inspector-to-troubleshoot-issues/2630). For
 older versions of Grafana read the [how troubleshoot metric query issue](https://community.grafana.com/t/how-to-troubleshoot-metric-query-issues/50/2) article.
 
 ## Logging

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

@@ -12,11 +12,7 @@ weight = 3
 
 Description | Download
 ------------ | -------------
-Latest stable package for Windows | [grafana-5.1.4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4.windows-x64.zip)
-
-<!--
-Latest 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
 installation.

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

+ 3 - 3
pkg/api/annotations.go

@@ -272,9 +272,9 @@ func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) {
 		return false, nil
 	}
 
-	if dashboardID > 0 {
-		guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
-		if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
+	if dashboardID != 0 {
+		guard := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
+		if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
 			return false, err
 		}
 	}

+ 2 - 4
pkg/api/routing/route_register.go

@@ -42,7 +42,7 @@ type RouteRegister interface {
 
 	// Register iterates over all routes added to the RouteRegister
 	// and add them to the `Router` pass as an parameter.
-	Register(Router) *macaron.Router
+	Register(Router)
 }
 
 type RegisterNamedMiddleware func(name string) macaron.Handler
@@ -101,7 +101,7 @@ func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handle
 	rr.groups = append(rr.groups, group)
 }
 
-func (rr *routeRegister) Register(router Router) *macaron.Router {
+func (rr *routeRegister) Register(router Router) {
 	for _, r := range rr.routes {
 		// GET requests have to be added to macaron routing using Get()
 		// Otherwise HEAD requests will not be allowed.
@@ -116,8 +116,6 @@ func (rr *routeRegister) Register(router Router) *macaron.Router {
 	for _, g := range rr.groups {
 		g.Register(router)
 	}
-
-	return &macaron.Router{}
 }
 
 func (rr *routeRegister) route(pattern, method string, handlers ...macaron.Handler) {

+ 2 - 9
pkg/login/ldap.go

@@ -308,9 +308,6 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
 			} else {
 				filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
 			}
-			if a.server.GroupSearchFilterUserAttribute == "dn" {
-				filter_replace = searchResult.Entries[0].DN
-			}
 
 			filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
 
@@ -334,11 +331,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
 
 			if len(groupSearchResult.Entries) > 0 {
 				for i := range groupSearchResult.Entries {
-					if a.server.Attr.MemberOf == "dn" {
-						memberOf = append(memberOf, groupSearchResult.Entries[i].DN)
-					} else {
-						memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
-					}
+					memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i))
 				}
 				break
 			}
@@ -356,7 +349,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
 }
 
 func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
-	if name == "DN" {
+	if strings.ToLower(name) == "dn" {
 		return result.Entries[n].DN
 	}
 	for _, attr := range result.Entries[n].Attributes {

+ 6 - 0
pkg/middleware/auth.go

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

+ 8 - 12
pkg/middleware/auth_proxy.go

@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"fmt"
+	"net"
 	"net/mail"
 	"reflect"
 	"strings"
@@ -28,7 +29,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 	}
 
 	// if auth proxy ip(s) defined, check if request comes from one of those
-	if err := checkAuthenticationProxy(ctx.RemoteAddr(), proxyHeaderValue); err != nil {
+	if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil {
 		ctx.Handle(407, "Proxy authentication required", err)
 		return true
 	}
@@ -196,23 +197,18 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
 		return nil
 	}
 
-	// Multiple ip addresses? Right-most IP address is the IP address of the most recent proxy
-	if strings.Contains(remoteAddr, ",") {
-		sourceIPs := strings.Split(remoteAddr, ",")
-		remoteAddr = strings.TrimSpace(sourceIPs[len(sourceIPs)-1])
-	}
-
-	remoteAddr = strings.TrimPrefix(remoteAddr, "[")
-	remoteAddr = strings.TrimSuffix(remoteAddr, "]")
-
 	proxies := strings.Split(setting.AuthProxyWhitelist, ",")
+	sourceIP, _, err := net.SplitHostPort(remoteAddr)
+	if err != nil {
+		return err
+	}
 
 	// Compare allowed IP addresses to actual address
 	for _, proxyIP := range proxies {
-		if remoteAddr == strings.TrimSpace(proxyIP) {
+		if sourceIP == strings.TrimSpace(proxyIP) {
 			return nil
 		}
 	}
 
-	return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, remoteAddr)
+	return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
 }

+ 24 - 57
pkg/middleware/middleware_test.go

@@ -82,7 +82,7 @@ func TestMiddlewareContext(t *testing.T) {
 
 			setting.BasicAuthEnabled = true
 			authHeader := util.GetBasicAuthHeader("myUser", "myPass")
-			sc.fakeReq("GET", "/").withAuthoriziationHeader(authHeader).exec()
+			sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
 
 			Convey("Should init middleware context with user", func() {
 				So(sc.context.IsSignedIn, ShouldEqual, true)
@@ -128,6 +128,28 @@ func TestMiddlewareContext(t *testing.T) {
 			})
 		})
 
+		middlewareScenario("Valid api key via Basic auth", func(sc *scenarioContext) {
+			keyhash := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
+
+			bus.AddHandler("test", func(query *m.GetApiKeyByNameQuery) error {
+				query.Result = &m.ApiKey{OrgId: 12, Role: m.ROLE_EDITOR, Key: keyhash}
+				return nil
+			})
+
+			authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9")
+			sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
+
+			Convey("Should return 200", func() {
+				So(sc.resp.Code, ShouldEqual, 200)
+			})
+
+			Convey("Should init middleware context", func() {
+				So(sc.context.IsSignedIn, ShouldEqual, true)
+				So(sc.context.OrgId, ShouldEqual, 12)
+				So(sc.context.OrgRole, ShouldEqual, m.ROLE_EDITOR)
+			})
+		})
+
 		middlewareScenario("UserId in session", func(sc *scenarioContext) {
 
 			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
@@ -293,61 +315,6 @@ func TestMiddlewareContext(t *testing.T) {
 			})
 		})
 
-		middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is not trusted", func(sc *scenarioContext) {
-			setting.AuthProxyEnabled = true
-			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
-			setting.AuthProxyHeaderProperty = "username"
-			setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
-
-			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
-				query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
-				return nil
-			})
-
-			bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
-				cmd.Result = &m.User{Id: 33}
-				return nil
-			})
-
-			sc.fakeReq("GET", "/")
-			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
-			sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.1, 192.168.1.2")
-			sc.exec()
-
-			Convey("should return 407 status code", func() {
-				So(sc.resp.Code, ShouldEqual, 407)
-				So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.1.2 is not from the authentication proxy")
-			})
-		})
-
-		middlewareScenario("When auth_proxy is enabled and request has X-Forwarded-For that is trusted", func(sc *scenarioContext) {
-			setting.AuthProxyEnabled = true
-			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
-			setting.AuthProxyHeaderProperty = "username"
-			setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
-
-			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
-				query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
-				return nil
-			})
-
-			bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
-				cmd.Result = &m.User{Id: 33}
-				return nil
-			})
-
-			sc.fakeReq("GET", "/")
-			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
-			sc.req.Header.Add("X-Forwarded-For", "client-ip, 192.168.1.2, 192.168.1.1")
-			sc.exec()
-
-			Convey("Should init context with user info", func() {
-				So(sc.context.IsSignedIn, ShouldBeTrue)
-				So(sc.context.UserId, ShouldEqual, 33)
-				So(sc.context.OrgId, ShouldEqual, 4)
-			})
-		})
-
 		middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
@@ -473,7 +440,7 @@ func (sc *scenarioContext) withInvalidApiKey() *scenarioContext {
 	return sc
 }
 
-func (sc *scenarioContext) withAuthoriziationHeader(authHeader string) *scenarioContext {
+func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext {
 	sc.authHeader = authHeader
 	return sc
 }

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

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

+ 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 helpers from 'test/specs/helpers';
 import 'app/features/dashboard/time_srv';
+import { AnnotationsSrv } from '../annotations_srv';
 
 describe('AnnotationsSrv', function() {
-  var ctx = new helpers.ServiceTestContext();
+  let $rootScope = {
+    onAppEvent: jest.fn(),
+  };
+  let $q;
+  let datasourceSrv;
+  let backendSrv;
+  let timeSrv;
 
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(ctx.createService('timeSrv'));
-  beforeEach(() => {
-    ctx.createService('annotationsSrv');
-  });
+  let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv);
 
   describe('When translating the query result', () => {
     const annotationSource = {
@@ -30,11 +30,11 @@ describe('AnnotationsSrv', function() {
     let translatedAnnotations;
 
     beforeEach(() => {
-      translatedAnnotations = ctx.service.translateQueryResult(annotationSource, annotations);
+      translatedAnnotations = annotationsSrv.translateQueryResult(annotationSource, annotations);
     });
 
     it('should set defaults', () => {
-      expect(translatedAnnotations[0].source).to.eql(annotationSource);
+      expect(translatedAnnotations[0].source).toEqual(annotationSource);
     });
   });
 });

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

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

@@ -1,160 +1,158 @@
 import _ from 'lodash';
 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);

+ 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>
         <label class="gf-form-label width-6">Font size</label>
         <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>
@@ -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>
       <label class="gf-form-label width-6">Font size</label>
       <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 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-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
     </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 max-width-21">
         <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);
   }
 
-  canChangeFontSize() {
-    return this.panel.gauge.show;
+  canModifyText() {
+    return !this.panel.gauge.show;
   }
 
   setColoring(options) {
@@ -405,10 +405,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     elem = elem.find('.singlestat-panel');
 
     function applyColoringThresholds(value, valueString) {
-      if (!panel.colorValue) {
-        return valueString;
-      }
-
       var color = getColorForValue(data, value);
       if (color) {
         return '<span style="color:' + color + '">' + valueString + '</span>';
@@ -426,15 +422,24 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       var body = '<div class="singlestat-panel-value-container">';
 
       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);
       }
 
-      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);
 
       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);
       }
 

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

@@ -25,7 +25,7 @@
     display: inline-block;
     padding-right: 2px;
     &::after {
-      content: " | ";
+      content: ' | ';
       padding-left: 2px;
     }
   }
@@ -33,14 +33,23 @@
   li:last-child {
     &::after {
       padding-left: 0;
-      content: "";
+      content: '';
     }
   }
 }
 
 .login-page {
   .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 {
-    font-family: "FontAwesome";
-    content: "\f096"; // square-o
+    font-family: 'FontAwesome';
+    content: '\f096'; // square-o
     color: $text-color-weak;
     transition: transform 0.4s;
     backface-visibility: hidden;
@@ -73,11 +73,11 @@
   }
 
   input + label::after {
-    content: "\f046"; // check-square-o
+    content: '\f046'; // check-square-o
     color: $orange;
     text-shadow: $text-shadow-strong;
 
-    font-family: "FontAwesome";
+    font-family: 'FontAwesome';
     transition: transform 0.4s;
     transform: rotateY(180deg);
     backface-visibility: hidden;
@@ -154,7 +154,8 @@ gf-form-switch[disabled] {
   .gf-form-switch input + label {
     cursor: default;
     pointer-events: none !important;
-    &::before {
+    &::before,
+    &::after {
       color: $text-color-faint;
       text-shadow: none;
     }

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

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

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

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

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

@@ -1,37 +1,29 @@
 '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 {
     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: {
-          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 { CheckerPlugin } = require('awesome-typescript-loader');
+const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
 
 module.exports = {
   target: 'web',
@@ -61,6 +61,8 @@ module.exports = {
     ]
   },
   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 ExtractTextPlugin = require("extract-text-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, {
   devtool: "cheap-module-source-map",
+  mode: 'development',
 
   entry: {
     app: './public/app/index.ts',
     dark: './public/sass/grafana.dark.scss',
     light: './public/sass/grafana.light.scss',
-    vendor: require('./dependencies'),
   },
 
   output: {
@@ -48,15 +45,13 @@ module.exports = merge(common, {
         test: /\.tsx?$/,
         exclude: /node_modules/,
         use: {
-          loader: 'awesome-typescript-loader',
+          loader: 'ts-loader',
           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=&.]+)?$/,
         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: [
     new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
-    extractSass,
+    new MiniCssExtractPlugin({
+      filename: "grafana.[name].css"
+    }),
     new HtmlWebpackPlugin({
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       template: path.resolve(__dirname, '../../public/views/index.template.html'),
@@ -80,9 +96,6 @@ module.exports = merge(common, {
         'NODE_ENV': JSON.stringify('development')
       }
     }),
-    new webpack.optimize.CommonsChunkPlugin({
-      names: ['vendor', 'manifest'],
-    }),
     // new BundleAnalyzerPlugin({
     //   analyzerPort: 8889
     // })

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

@@ -42,20 +42,23 @@ module.exports = merge(common, {
       {
         test: /\.tsx?$/,
         exclude: /node_modules/,
-        use: {
-          loader: 'awesome-typescript-loader',
+        use: [{
+          loader: 'babel-loader',
           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$/,

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

@@ -1,21 +1,22 @@
 'use strict';
 
 const merge = require('webpack-merge');
-const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 const common = require('./webpack.common.js');
 const webpack = require('webpack');
 const path = require('path');
 const ngAnnotatePlugin = require('ng-annotate-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, {
+  mode: 'production',
   devtool: "source-map",
 
   entry: {
     dark: './public/sass/grafana.dark.scss',
     light: './public/sass/grafana.light.scss',
-    vendor: require('./dependencies'),
   },
 
   module: {
@@ -35,49 +36,49 @@ module.exports = merge(common, {
       {
         test: /\.tsx?$/,
         exclude: /node_modules/,
-        use: [
-          {
-            loader: 'awesome-typescript-loader',
-            options: {
-              errorsAsWarnings: false,
-            },
+        use: {
+          loader: 'ts-loader',
+          options: {
+            transpileOnly: true
           },
-        ]
+        },
       },
       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: [
-    new ExtractTextPlugin({
-      filename: 'grafana.[name].css',
+    new MiniCssExtractPlugin({
+      filename: "grafana.[name].css"
     }),
     new ngAnnotatePlugin(),
-    new UglifyJSPlugin({
-      sourceMap: true,
-    }),
-    new webpack.DefinePlugin({
-      'process.env': {
-        'NODE_ENV': JSON.stringify('production')
-      }
-    }),
     new HtmlWebpackPlugin({
       filename: path.resolve(__dirname, '../../public/views/index.html'),
       template: path.resolve(__dirname, '../../public/views/index.template.html'),
       inject: 'body',
-      chunks: ['manifest', 'vendor', 'app'],
-    }),
-    new webpack.optimize.CommonsChunkPlugin({
-      names: ['vendor', 'manifest'],
+      chunks: ['vendor', 'app'],
     }),
     function () {
       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');
 
 config = merge(common, {
+  mode: 'development',
   devtool: 'cheap-module-source-map',
+
   externals: {
     'react/addons': true,
     'react/lib/ExecutionEnvironment': true,
     'react/lib/ReactContext': true,
   },
+
   module: {
     rules: [
       {
         test: /\.tsx?$/,
         exclude: /node_modules/,
-        use: [
-          { loader: "awesome-typescript-loader" }
-        ]
+        use: {
+          loader: 'ts-loader',
+          options: {
+            transpileOnly: true,
+          },
+        },
       },
-    ]
+    ],
   },
+
   plugins: [
     new webpack.SourceMapDevToolPlugin({
       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;

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