Browse Source

Merge branch 'master' into provisioning_datasources_examples

Marcus Efraimsson 7 years ago
parent
commit
a6a45afc2b
86 changed files with 1506 additions and 950 deletions
  1. 14 3
      CHANGELOG.md
  2. 1 0
      PLUGIN_DEV.md
  3. 1 1
      appveyor.yml
  4. 1 0
      docker/blocks/openldap/Dockerfile
  5. 7 3
      docker/blocks/openldap/entrypoint.sh
  6. 13 0
      docker/blocks/openldap/notes.md
  7. 10 0
      docker/blocks/openldap/prepopulate/admin.ldif
  8. 5 0
      docker/blocks/openldap/prepopulate/adminsgroup.ldif
  9. 10 0
      docker/blocks/openldap/prepopulate/editor.ldif
  10. 5 0
      docker/blocks/openldap/prepopulate/usersgroup.ldif
  11. 9 0
      docker/blocks/openldap/prepopulate/viewer.ldif
  12. 34 0
      docs/sources/features/datasources/cloudwatch.md
  13. 4 4
      docs/sources/http_api/annotations.md
  14. 5 5
      docs/sources/installation/debian.md
  15. 7 7
      docs/sources/installation/rpm.md
  16. 1 1
      docs/sources/installation/upgrading.md
  17. 1 2
      docs/sources/installation/windows.md
  18. 5 5
      docs/sources/reference/dashboard.md
  19. 5 5
      package.json
  20. 1 1
      pkg/cmd/grafana-server/server.go
  21. 4 1
      pkg/services/alerting/notifiers/dingding.go
  22. 1 1
      pkg/services/sqlstore/annotation.go
  23. 19 6
      pkg/setting/setting.go
  24. 7 0
      pkg/setting/setting_test.go
  25. 21 10
      public/app/core/components/ScrollBar/ScrollBar.tsx
  26. 1 0
      public/app/core/components/grafana_app.ts
  27. 41 0
      public/app/core/components/scroll/page_scroll.ts
  28. 46 6
      public/app/core/components/scroll/scroll.ts
  29. 2 0
      public/app/core/components/search/search.html
  30. 1 1
      public/app/core/components/search/search_results.html
  31. 2 0
      public/app/core/core.ts
  32. 0 36
      public/app/core/directives/dash_class.js
  33. 31 0
      public/app/core/directives/dash_class.ts
  34. 0 246
      public/app/core/directives/metric_segment.js
  35. 263 0
      public/app/core/directives/metric_segment.ts
  36. 0 13
      public/app/core/services/all.js
  37. 10 0
      public/app/core/services/all.ts
  38. 9 1
      public/app/core/services/keybindingSrv.ts
  39. 0 111
      public/app/core/services/segment_srv.js
  40. 111 0
      public/app/core/services/segment_srv.ts
  41. 0 4
      public/app/features/alerting/threshold_mapper.ts
  42. 0 15
      public/app/features/all.js
  43. 13 0
      public/app/features/all.ts
  44. 1 1
      public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
  45. 10 4
      public/app/features/dashboard/timepicker/timepicker.ts
  46. 2 2
      public/app/features/dashboard/unsaved_changes_srv.ts
  47. 2 1
      public/app/features/dashboard/view_state_srv.ts
  48. 0 9
      public/app/features/panel/all.js
  49. 7 0
      public/app/features/panel/all.ts
  50. 30 4
      public/app/features/panel/panel_directive.ts
  51. 0 7
      public/app/features/playlist/all.js
  52. 5 0
      public/app/features/playlist/all.ts
  53. 0 39
      public/app/features/playlist/playlist_routes.js
  54. 34 0
      public/app/features/playlist/playlist_routes.ts
  55. 33 12
      public/app/features/plugins/partials/ds_http_settings.html
  56. 2 0
      public/app/features/templating/editor_ctrl.ts
  57. 4 0
      public/app/features/templating/query_variable.ts
  58. 35 6
      public/app/features/templating/specs/query_variable.jest.ts
  59. 12 12
      public/app/partials/dashboard.html
  60. 1 0
      public/app/plugins/datasource/graphite/add_graphite_func.ts
  61. 1 0
      public/app/plugins/datasource/graphite/func_editor.ts
  62. 1 1
      public/app/plugins/datasource/mssql/partials/annotations.editor.html
  63. 1 1
      public/app/plugins/datasource/mssql/partials/query.editor.html
  64. 7 3
      public/app/plugins/datasource/prometheus/datasource.ts
  65. 1 1
      public/app/plugins/datasource/prometheus/partials/query.editor.html
  66. 38 1
      public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
  67. 16 14
      public/app/plugins/panel/dashlist/module.html
  68. 0 292
      public/app/plugins/panel/graph/graph_tooltip.js
  69. 289 0
      public/app/plugins/panel/graph/graph_tooltip.ts
  70. 42 16
      public/app/plugins/panel/graph/legend.ts
  71. 3 2
      public/app/plugins/panel/graph/specs/tooltip_specs.ts
  72. 3 1
      public/app/plugins/panel/graph/template.ts
  73. 3 3
      public/app/plugins/panel/table/column_options.html
  74. 1 2
      public/sass/_variables.light.scss
  75. 8 1
      public/sass/components/_panel_add_panel.scss
  76. 30 1
      public/sass/components/_panel_graph.scss
  77. 123 1
      public/sass/components/_scrollbar.scss
  78. 12 2
      public/sass/components/_search.scss
  79. 3 0
      public/sass/components/_sidemenu.scss
  80. 1 1
      public/sass/components/_tabbed_view.scss
  81. 8 0
      public/sass/layout/_page.scss
  82. 2 1
      public/sass/pages/_alerting.scss
  83. 7 1
      public/sass/pages/_login.scss
  84. 2 2
      public/views/index.template.html
  85. 1 14
      scripts/circle-test-backend.sh
  86. 4 4
      yarn.lock

+ 14 - 3
CHANGELOG.md

@@ -13,6 +13,8 @@
 * **Prometheus**: Show template variable candidate in query editor [#9210](https://github.com/grafana/grafana/issues/9210), thx [@mtanda](https://github.com/mtanda)
 * **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
 * **Alerting**: Add support for retries on alert queries [#5855](https://github.com/grafana/grafana/issues/5855), thx [@Thib17](https://github.com/Thib17)
+* **Table**: Table plugin value mappings [#7119](https://github.com/grafana/grafana/issues/7119), thx [infernix](https://github.com/infernix)
+* **IE11**: IE 11 compatibility [#11165](https://github.com/grafana/grafana/issues/11165)
 
 ### Minor
 * **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
@@ -25,9 +27,18 @@
 * **Shortcuts**: Add shortcut for duplicate panel [#11102](https://github.com/grafana/grafana/issues/11102)
 * **AuthProxy**: Support IPv6 in Auth proxy white list [#11330](https://github.com/grafana/grafana/pull/11330), thx [@corny](https://github.com/corny)
 * **SMTP**: Don't connect to STMP server using TLS unless configured. [#7189](https://github.com/grafana/grafana/issues/7189)
-
-# 5.0.4 (unreleased)
-* **Dashboard** Fixed bug where collapsed panels could not be directly linked to/renderer [#11114](https://github.com/grafana/grafana/issues/11114) & [#11086](https://github.com/grafana/grafana/issues/11086)
+* **Prometheus**: Escape backslash in labels correctly. [#10555](https://github.com/grafana/grafana/issues/10555), thx [@roidelapluie](https://github.com/roidelapluie)
+* **Variables**: Case-insensitive sorting for template values [#11128](https://github.com/grafana/grafana/issues/11128) thx [@cross](https://github.com/cross)
+* **Annotations (native)**: Change default limit from 10 to 100 when querying api [#11569](https://github.com/grafana/grafana/issues/11569), thx [@flopp999](https://github.com/flopp999)
+
+# 5.0.4 (2018-03-28)
+
+* **Docker** Can't start Grafana on Kubernetes 1.7.14, 1.8.9, or 1.9.4 [#140 in grafana-docker repo](https://github.com/grafana/grafana-docker/issues/140) thx [@suquant](https://github.com/suquant)
+* **Dashboard** Fixed bug where collapsed panels could not be directly linked to/renderer [#11114](https://github.com/grafana/grafana/issues/11114) & [#11086](https://github.com/grafana/grafana/issues/11086) & [#11296](https://github.com/grafana/grafana/issues/11296)
+* **Dashboard** Provisioning dashboard with alert rules should create alerts [#11247](https://github.com/grafana/grafana/issues/11247)
+* **Snapshots** For snapshots, the Graph panel renders the legend incorrectly on right hand side [#11318](https://github.com/grafana/grafana/issues/11318)
+* **Alerting** Link back to Grafana returns wrong URL if root_path contains sub-path components [#11403](https://github.com/grafana/grafana/issues/11403)
+* **Alerting** Incorrect default value for upload images setting for alert notifiers [#11413](https://github.com/grafana/grafana/pull/11413)
 
 # 5.0.3 (2018-03-16)
 * **Mysql**: Mysql panic occurring occasionally upon Grafana dashboard access (a bigger patch than the one in 5.0.2) [#11155](https://github.com/grafana/grafana/issues/11155)

+ 1 - 0
PLUGIN_DEV.md

@@ -9,6 +9,7 @@ upgrading Grafana please check here before creating an issue.
 - [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource)
 - [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource)
 - [Plugin development guide](http://docs.grafana.org/plugins/developing/development/)
+- [Webpack Grafana plugin template project](https://github.com/CorpGlory/grafana-plugin-template-webpack)
 
 ## Changes in v4.6
 

+ 1 - 1
appveyor.yml

@@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
 environment:
   nodejs_version: "6"
   GOPATH: c:\gopath
-  GOVERSION: 1.9.2
+  GOVERSION: 1.10
 
 install:
   - rmdir c:\go /s /q

+ 1 - 0
docker/blocks/openldap/Dockerfile

@@ -17,6 +17,7 @@ EXPOSE 389
 VOLUME ["/etc/ldap", "/var/lib/ldap"]
 
 COPY modules/ /etc/ldap.dist/modules
+COPY prepopulate/ /etc/ldap.dist/prepopulate
 
 COPY entrypoint.sh /entrypoint.sh
 

+ 7 - 3
docker/blocks/openldap/entrypoint.sh

@@ -65,7 +65,7 @@ EOF
     fi
 
     if [[ -n "$SLAPD_ADDITIONAL_SCHEMAS" ]]; then
-        IFS=","; declare -a schemas=($SLAPD_ADDITIONAL_SCHEMAS)
+        IFS=","; declare -a schemas=($SLAPD_ADDITIONAL_SCHEMAS); unset IFS
 
         for schema in "${schemas[@]}"; do
             slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/schema/${schema}.ldif" >/dev/null 2>&1
@@ -73,14 +73,18 @@ EOF
     fi
 
     if [[ -n "$SLAPD_ADDITIONAL_MODULES" ]]; then
-        IFS=","; declare -a modules=($SLAPD_ADDITIONAL_MODULES)
+        IFS=","; declare -a modules=($SLAPD_ADDITIONAL_MODULES); unset IFS
 
         for module in "${modules[@]}"; do
              slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
         done
     fi
 
-    chown -R openldap:openldap /etc/ldap/slapd.d/
+    for file in `ls /etc/ldap/prepopulate/*.ldif`; do
+        slapadd -F /etc/ldap/slapd.d -l "$file"
+    done
+
+    chown -R openldap:openldap /etc/ldap/slapd.d/ /var/lib/ldap/ /var/run/slapd/
 else
     slapd_configs_in_env=`env | grep 'SLAPD_'`
 

+ 13 - 0
docker/blocks/openldap/notes.md

@@ -0,0 +1,13 @@
+# Notes on OpenLdap Docker Block
+
+Any ldif files added to the prepopulate subdirectory will be automatically imported into the OpenLdap database. 
+
+The ldif files add three users, `ldapviewer`, `ldapeditor` and `ldapadmin`. Two groups, `admins` and `users`, are added that correspond with the group mappings in the default conf/ldap.toml. `ldapadmin` is a member of `admins` and `ldapeditor` is a member of `users`.
+
+Note that users that are added here need to specify a `memberOf` attribute manually as well as the `member` attribute for the group. The `memberOf` module usually does this automatically (if you add a group in Apache Directory Studio for example) but this does not work in the entrypoint script as it uses the `slapadd` command to add entries before the server has started and before the `memberOf` module is loaded.
+
+After adding ldif files to `prepopulate`:
+
+1. Remove your current docker image: `docker rm docker_openldap_1`
+2. Build: `docker-compose build`
+3. `docker-compose up`

+ 10 - 0
docker/blocks/openldap/prepopulate/admin.ldif

@@ -0,0 +1,10 @@
+dn: cn=ldapadmin,dc=grafana,dc=org
+mail: ldapadmin@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldapadmin
+cn: ldapadmin
+memberOf: cn=admins,dc=grafana,dc=org

+ 5 - 0
docker/blocks/openldap/prepopulate/adminsgroup.ldif

@@ -0,0 +1,5 @@
+dn: cn=admins,dc=grafana,dc=org
+cn: admins
+member: cn=ldapadmin,dc=grafana,dc=org
+objectClass: groupOfNames
+objectClass: top

+ 10 - 0
docker/blocks/openldap/prepopulate/editor.ldif

@@ -0,0 +1,10 @@
+dn: cn=ldapeditor,dc=grafana,dc=org
+mail: ldapeditor@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldapeditor
+cn: ldapeditor
+memberOf: cn=users,dc=grafana,dc=org

+ 5 - 0
docker/blocks/openldap/prepopulate/usersgroup.ldif

@@ -0,0 +1,5 @@
+dn: cn=users,dc=grafana,dc=org
+cn: users
+member: cn=ldapeditor,dc=grafana,dc=org
+objectClass: groupOfNames
+objectClass: top

+ 9 - 0
docker/blocks/openldap/prepopulate/viewer.ldif

@@ -0,0 +1,9 @@
+dn: cn=ldapviewer,dc=grafana,dc=org
+mail: ldapviewer@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldapviewer
+cn: ldapviewer

+ 34 - 0
docs/sources/features/datasources/cloudwatch.md

@@ -43,6 +43,40 @@ server is running on AWS you can use IAM Roles and authentication will be handle
 
 Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
 
+## IAM Policies
+
+Grafana needs permissions granted via IAM to be able to read CloudWatch metrics
+and EC2 tags/instances. You can attach these permissions to IAM roles and
+utilize Grafana's built-in support for assuming roles.
+
+Here is a minimal policy example:
+
+```json
+{
+    "Version": "2012-10-17",
+    "Statement": [
+        {
+            "Sid": "AllowReadingMetricsFromCloudWatch",
+            "Effect": "Allow",
+            "Action": [
+                "cloudwatch:ListMetrics",
+                "cloudwatch:GetMetricStatistics"
+            ],
+            "Resource": "*"
+        },
+        {
+            "Sid": "AllowReadingTagsFromEC2",
+            "Effect": "Allow",
+            "Action": [
+                "ec2:DescribeTags",
+                "ec2:DescribeInstances"
+            ],
+            "Resource": "*"
+        }
+    ]
+}
+```
+
 ### AWS credentials file
 
 Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.

+ 4 - 4
docs/sources/http_api/annotations.md

@@ -180,14 +180,14 @@ Content-Type: application/json
 
 ## Delete Annotation By Id
 
-`DELETE /api/annotation/:id`
+`DELETE /api/annotations/:id`
 
 Deletes the annotation that matches the specified id.
 
 **Example Request**:
 
 ```http
-DELETE /api/annotation/1 HTTP/1.1
+DELETE /api/annotations/1 HTTP/1.1
 Accept: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
@@ -204,14 +204,14 @@ Content-Type: application/json
 
 ## Delete Annotation By RegionId
 
-`DELETE /api/annotation/region/:id`
+`DELETE /api/annotations/region/:id`
 
 Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id.
 
 **Example Request**:
 
 ```http
-DELETE /api/annotation/region/1 HTTP/1.1
+DELETE /api/annotations/region/1 HTTP/1.1
 Accept: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk

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

@@ -15,7 +15,7 @@ weight = 1
 
 Description | Download
 ------------ | -------------
-Stable for Debian-based Linux | [grafana_5.0.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.3_amd64.deb)
+Stable for Debian-based Linux | [grafana_5.0.4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb)
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
@@ -24,9 +24,9 @@ installation.
 
 
 ```bash
-wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.3_amd64.deb
+wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb
 sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_5.0.3_amd64.deb
+sudo dpkg -i grafana_5.0.4_amd64.deb
 ```
 
 ## APT Repository
@@ -34,7 +34,7 @@ sudo dpkg -i grafana_5.0.3_amd64.deb
 Add the following line to your `/etc/apt/sources.list` file.
 
 ```bash
-deb https://packagecloud.io/grafana/stable/debian/ jessie main
+deb https://packagecloud.io/grafana/stable/debian/ stretch main
 ```
 
 Use the above line even if you are on Ubuntu or another Debian version.
@@ -42,7 +42,7 @@ There is also a testing repository if you want beta or release
 candidates.
 
 ```bash
-deb https://packagecloud.io/grafana/testing/debian/ jessie main
+deb https://packagecloud.io/grafana/testing/debian/ stretch main
 ```
 
 Then add the [Package Cloud](https://packagecloud.io/grafana) key. This

+ 7 - 7
docs/sources/installation/rpm.md

@@ -15,7 +15,7 @@ weight = 2
 
 Description | Download
 ------------ | -------------
-Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm)
+Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.4 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm)
 
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
@@ -26,7 +26,7 @@ installation.
 You can install Grafana using Yum directly.
 
 ```bash
-$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm
+$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm
 ```
 
 Or install manually using `rpm`.
@@ -34,15 +34,15 @@ Or install manually using `rpm`.
 #### On CentOS / Fedora / Redhat:
 
 ```bash
-$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3-1.x86_64.rpm
+$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm
 $ sudo yum install initscripts fontconfig
-$ sudo rpm -Uvh grafana-5.0.3-1.x86_64.rpm
+$ sudo rpm -Uvh grafana-5.0.4-1.x86_64.rpm
 ```
 
 #### On OpenSuse:
 
 ```bash
-$ sudo rpm -i --nodeps grafana-5.0.3-1.x86_64.rpm
+$ sudo rpm -i --nodeps grafana-5.0.4-1.x86_64.rpm
 ```
 
 ## Install via YUM Repository
@@ -52,7 +52,7 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
 ```bash
 [grafana]
 name=grafana
-baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch
+baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch
 repo_gpgcheck=1
 enabled=1
 gpgcheck=1
@@ -64,7 +64,7 @@ sslcacert=/etc/pki/tls/certs/ca-bundle.crt
 There is also a testing repository if you want beta or release candidates.
 
 ```bash
-baseurl=https://packagecloud.io/grafana/testing/el/6/$basearch
+baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch
 ```
 
 Then install Grafana via the `yum` command.

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

@@ -23,7 +23,7 @@ Before upgrading it can be a good idea to backup your Grafana database. This wil
 
 #### sqlite
 
-If you use sqlite you only need to make a backup of you `grafana.db` file. This is usually located at `/var/lib/grafana/grafana.db` on unix system.
+If you use sqlite you only need to make a backup of your `grafana.db` file. This is usually located at `/var/lib/grafana/grafana.db` on unix system.
 If you are unsure what database you use and where it is stored check you grafana configuration file. If you
 installed grafana to custom location using a binary tar/zip it is usally in `<grafana_install_dir>/data`.
 

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

@@ -8,12 +8,11 @@ parent = "installation"
 weight = 3
 +++
 
-
 # Installing on Windows
 
 Description | Download
 ------------ | -------------
-Latest stable package for Windows | [grafana-5.0.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.3.windows-x64.zip)
+Latest stable package for Windows | [grafana-5.0.4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4.windows-x64.zip)
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.

+ 5 - 5
docs/sources/reference/dashboard.md

@@ -71,13 +71,13 @@ Each field in the dashboard JSON is explained below with its usage:
 | **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
 | **templating** | templating metadata, see [templating section](#templating) for details |
 | **annotations** | annotations metadata, see [annotations section](#annotations) for details |
-| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to the said schema |
+| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema |
 | **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
 | **panels** | panels array, see below for detail. |
 
 ## Panels
 
-Panels are the building blocks a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depends on the panel type. Following is an example of panel JSON of a text panel.
+Panels are the building blocks of a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depend on the panel type. Following is an example of panel JSON of a text panel.
 
 ```json
 "panels": [
@@ -105,7 +105,7 @@ The gridPos property describes the panel size and position in grid coordinates.
 - `x` The x position, in same unit as `w`.
 - `y` The y position, in same unit as `h`.
 
-The grid has a negative gravity that moves panels up if there i empty space above a panel.
+The grid has a negative gravity that moves panels up if there is empty space above a panel.
 
 ### timepicker
 
@@ -161,7 +161,7 @@ Usage of the fields is explained below:
 
 ### templating
 
-`templating` fields contains array of template variables with their saved values along with some other metadata, for example:
+The `templating` field contains an array of template variables with their saved values along with some other metadata, for example:
 
 ```json
  "templating": {
@@ -236,7 +236,7 @@ Usage of the above mentioned fields in the templating section is explained below
 | Name | Usage |
 | ---- | ----- |
 | **enable** | whether templating is enabled or not |
-| **list** | an array of objects representing, each representing one template variable |
+| **list** | an array of objects each representing one template variable |
 | **allFormat** | format to use while fetching all values from datasource, eg: `wildcard`, `glob`, `regex`, `pipe`, etc. |
 | **current** | shows current selected variable text/value on the dashboard |
 | **datasource** | shows datasource for the variables |

+ 5 - 5
package.json

@@ -104,10 +104,10 @@
     "test": "grunt test",
     "test:coverage": "grunt test --coverage=true",
     "lint": "tslint -c tslint.json --project tsconfig.json --type-check",
-    "karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
-    "jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
-    "api-tests": "node ./node_modules/jest-cli/bin/jest.js --notify --watch --config=tests/api/jest.js",
-    "precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
+    "karma": "grunt karma:dev",
+    "jest": "jest --notify --watch",
+    "api-tests": "jest --notify --watch --config=tests/api/jest.js",
+    "precommit": "lint-staged && grunt precommit"
   },
   "lint-staged": {
     "*.{ts,tsx}": [
@@ -136,6 +136,7 @@
     "angular-route": "^1.6.6",
     "angular-sanitize": "^1.6.6",
     "babel-polyfill": "^6.26.0",
+    "baron": "^3.0.3",
     "brace": "^0.10.0",
     "classnames": "^2.2.5",
     "clipboard": "^1.7.1",
@@ -151,7 +152,6 @@
     "moment": "^2.18.1",
     "mousetrap": "^1.6.0",
     "mousetrap-global-bind": "^1.1.0",
-    "perfect-scrollbar": "^1.2.0",
     "prop-types": "^15.6.0",
     "react": "^16.2.0",
     "react-dom": "^16.2.0",

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

@@ -111,7 +111,7 @@ func (g *GrafanaServerImpl) initLogging() {
 	})
 
 	if err != nil {
-		g.log.Error(err.Error())
+		fmt.Fprintf(os.Stderr, "Failed to start grafana. error: %s\n", err.Error())
 		os.Exit(1)
 	}
 

+ 4 - 1
pkg/services/alerting/notifiers/dingding.go

@@ -72,7 +72,10 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
 		this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
 	}
 
-	body, _ := bodyJSON.MarshalJSON()
+	body, err := bodyJSON.MarshalJSON()
+	if err != nil {
+		return err
+	}
 
 	cmd := &m.SendWebhookSync{
 		Url:  this.Url,

+ 1 - 1
pkg/services/sqlstore/annotation.go

@@ -202,7 +202,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
 	}
 
 	if query.Limit == 0 {
-		query.Limit = 10
+		query.Limit = 100
 	}
 
 	sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))

+ 19 - 6
pkg/setting/setting.go

@@ -223,7 +223,7 @@ func shouldRedactURLKey(s string) bool {
 	return strings.Contains(uppercased, "DATABASE_URL")
 }
 
-func applyEnvVariableOverrides() {
+func applyEnvVariableOverrides() error {
 	appliedEnvOverrides = make([]string, 0)
 	for _, section := range Cfg.Sections() {
 		for _, key := range section.Keys() {
@@ -238,7 +238,10 @@ func applyEnvVariableOverrides() {
 					envValue = "*********"
 				}
 				if shouldRedactURLKey(envKey) {
-					u, _ := url.Parse(envValue)
+					u, err := url.Parse(envValue)
+					if err != nil {
+						return fmt.Errorf("could not parse environment variable. key: %s, value: %s. error: %v", envKey, envValue, err)
+					}
 					ui := u.User
 					if ui != nil {
 						_, exists := ui.Password()
@@ -252,6 +255,8 @@ func applyEnvVariableOverrides() {
 			}
 		}
 	}
+
+	return nil
 }
 
 func applyCommandLineDefaultProperties(props map[string]string) {
@@ -377,7 +382,7 @@ func loadSpecifedConfigFile(configFile string) error {
 	return nil
 }
 
-func loadConfiguration(args *CommandLineArgs) {
+func loadConfiguration(args *CommandLineArgs) error {
 	var err error
 
 	// load config defaults
@@ -395,7 +400,7 @@ func loadConfiguration(args *CommandLineArgs) {
 	if err != nil {
 		fmt.Println(fmt.Sprintf("Failed to parse defaults.ini, %v", err))
 		os.Exit(1)
-		return
+		return err
 	}
 
 	Cfg.BlockMode = false
@@ -413,7 +418,10 @@ func loadConfiguration(args *CommandLineArgs) {
 	}
 
 	// apply environment overrides
-	applyEnvVariableOverrides()
+	err = applyEnvVariableOverrides()
+	if err != nil {
+		return err
+	}
 
 	// apply command line overrides
 	applyCommandLineProperties(commandLineProps)
@@ -424,6 +432,8 @@ func loadConfiguration(args *CommandLineArgs) {
 	// update data path and logging config
 	DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath)
 	initLogging()
+
+	return err
 }
 
 func pathExists(path string) bool {
@@ -471,7 +481,10 @@ func validateStaticRootPath() error {
 
 func NewConfigContext(args *CommandLineArgs) error {
 	setHomePath(args)
-	loadConfiguration(args)
+	err := loadConfiguration(args)
+	if err != nil {
+		return err
+	}
 
 	Env = Cfg.Section("").Key("app_mode").MustString("development")
 	InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")

+ 7 - 0
pkg/setting/setting_test.go

@@ -37,6 +37,13 @@ func TestLoadingSettings(t *testing.T) {
 			So(appliedEnvOverrides, ShouldContain, "GF_SECURITY_ADMIN_PASSWORD=*********")
 		})
 
+		Convey("Should return an error when url is invalid", func() {
+			os.Setenv("GF_DATABASE_URL", "postgres.%31://grafana:secret@postgres:5432/grafana")
+			err := NewConfigContext(&CommandLineArgs{HomePath: "../../"})
+
+			So(err, ShouldNotBeNil)
+		})
+
 		Convey("Should replace password in URL when url environment is defined", func() {
 			os.Setenv("GF_DATABASE_URL", "mysql://user:secret@localhost:3306/database")
 			NewConfigContext(&CommandLineArgs{HomePath: "../../"})

+ 21 - 10
public/app/core/components/ScrollBar/ScrollBar.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import PerfectScrollbar from 'perfect-scrollbar';
+import baron from 'baron';
 
 export interface Props {
   children: any;
@@ -8,31 +8,36 @@ export interface Props {
 
 export default class ScrollBar extends React.Component<Props, any> {
   private container: any;
-  private ps: PerfectScrollbar;
+  private scrollbar: baron;
 
   constructor(props) {
     super(props);
   }
 
   componentDidMount() {
-    this.ps = new PerfectScrollbar(this.container, {
-      wheelPropagation: true,
+    this.scrollbar = baron({
+      root: this.container.parentElement,
+      scroller: this.container,
+      bar: '.baron__bar',
+      barOnCls: '_scrollbar',
+      scrollingCls: '_scrolling',
+      track: '.baron__track',
     });
   }
 
   componentDidUpdate() {
-    this.ps.update();
+    this.scrollbar.update();
   }
 
   componentWillUnmount() {
-    this.ps.destroy();
+    this.scrollbar.dispose();
   }
 
   // methods can be invoked by outside
   setScrollTop(top) {
     if (this.container) {
       this.container.scrollTop = top;
-      this.ps.update();
+      this.scrollbar.update();
 
       return true;
     }
@@ -42,7 +47,7 @@ export default class ScrollBar extends React.Component<Props, any> {
   setScrollLeft(left) {
     if (this.container) {
       this.container.scrollLeft = left;
-      this.ps.update();
+      this.scrollbar.update();
 
       return true;
     }
@@ -55,8 +60,14 @@ export default class ScrollBar extends React.Component<Props, any> {
 
   render() {
     return (
-      <div className={this.props.className} ref={this.handleRef}>
-        {this.props.children}
+      <div className="baron baron__root baron__clipper">
+        <div className={this.props.className + ' baron__scroller'} ref={this.handleRef}>
+          {this.props.children}
+        </div>
+
+        <div className="baron__track">
+          <div className="baron__bar" />
+        </div>
       </div>
     );
   }

+ 1 - 0
public/app/core/components/grafana_app.ts

@@ -167,6 +167,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
           if (sidemenuHidden) {
             sidemenuHidden = false;
             body.addClass('sidemenu-open');
+            appEvents.emit('toggle-inactive-mode');
             $timeout(function() {
               $rootScope.$broadcast('render');
             }, 100);

+ 41 - 0
public/app/core/components/scroll/page_scroll.ts

@@ -0,0 +1,41 @@
+import coreModule from 'app/core/core_module';
+import appEvents from 'app/core/app_events';
+
+export function pageScrollbar() {
+  return {
+    restrict: 'A',
+    link: function(scope, elem, attrs) {
+      let lastPos = 0;
+
+      appEvents.on(
+        'dash-scroll',
+        evt => {
+          if (evt.restore) {
+            elem[0].scrollTop = lastPos;
+            return;
+          }
+
+          lastPos = elem[0].scrollTop;
+
+          if (evt.animate) {
+            elem.animate({ scrollTop: evt.pos }, 500);
+          } else {
+            elem[0].scrollTop = evt.pos;
+          }
+        },
+        scope
+      );
+
+      scope.$on('$routeChangeSuccess', () => {
+        lastPos = 0;
+        elem[0].scrollTop = 0;
+        elem[0].focus();
+      });
+
+      elem[0].tabIndex = -1;
+      elem[0].focus();
+    },
+  };
+}
+
+coreModule.directive('pageScrollbar', pageScrollbar);

+ 46 - 6
public/app/core/components/scroll/scroll.ts

@@ -1,15 +1,44 @@
-import PerfectScrollbar from 'perfect-scrollbar';
+import $ from 'jquery';
+import baron from 'baron';
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
 
+const scrollBarHTML = `
+<div class="baron__track">
+  <div class="baron__bar"></div>
+</div>
+`;
+
+const scrollRootClass = 'baron baron__root';
+const scrollerClass = 'baron__scroller';
+
 export function geminiScrollbar() {
   return {
     restrict: 'A',
     link: function(scope, elem, attrs) {
-      let scrollbar = new PerfectScrollbar(elem[0], {
-        wheelPropagation: true,
-        wheelSpeed: 3,
-      });
+      let scrollRoot = elem.parent();
+      let scroller = elem;
+
+      if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
+        scrollRoot = scroller;
+      }
+
+      scrollRoot.addClass(scrollRootClass);
+      $(scrollBarHTML).appendTo(scrollRoot);
+      elem.addClass(scrollerClass);
+
+      let scrollParams = {
+        root: scrollRoot[0],
+        scroller: scroller[0],
+        bar: '.baron__bar',
+        barOnCls: '_scrollbar',
+        scrollingCls: '_scrolling',
+        track: '.baron__track',
+        direction: 'v',
+      };
+
+      let scrollbar = baron(scrollParams);
+
       let lastPos = 0;
 
       appEvents.on(
@@ -31,13 +60,24 @@ export function geminiScrollbar() {
         scope
       );
 
+      // force updating dashboard width
+      appEvents.on('toggle-sidemenu', forceUpdate, scope);
+      appEvents.on('toggle-sidemenu-hidden', forceUpdate, scope);
+      appEvents.on('toggle-view-mode', forceUpdate, scope);
+      appEvents.on('toggle-kiosk-mode', forceUpdate, scope);
+      appEvents.on('toggle-inactive-mode', forceUpdate, scope);
+
+      function forceUpdate() {
+        scrollbar.scroll();
+      }
+
       scope.$on('$routeChangeSuccess', () => {
         lastPos = 0;
         elem[0].scrollTop = 0;
       });
 
       scope.$on('$destroy', () => {
-        scrollbar.destroy();
+        scrollbar.dispose();
       });
     },
   };

+ 2 - 0
public/app/core/components/search/search.html

@@ -19,6 +19,7 @@
 
 	<div class="search-dropdown">
     <div class="search-dropdown__col_1">
+      <div class="search-results-scroller">
         <div class="search-results-container" grafana-scrollbar>
           <h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
           <dashboard-search-results
@@ -27,6 +28,7 @@
             on-folder-expanding="ctrl.folderExpanding()"
             on-folder-expanded="ctrl.folderExpanded($folder)" />
         </div>
+      </div>
     </div>
 
     <div class="search-dropdown__col_2">

+ 1 - 1
public/app/core/components/search/search_results.html

@@ -20,7 +20,7 @@
   <div class="search-section__header" ng-show="section.hideHeader"></div>
 
   <div ng-if="section.expanded">
-    <a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
+    <a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
       <div ng-click="ctrl.toggleSelection(item, $event)">
         <gf-form-switch
            ng-show="ctrl.editable"

+ 2 - 0
public/app/core/core.ts

@@ -47,6 +47,7 @@ import { NavModelSrv, NavModel } from './nav_model_srv';
 import { userPicker } from './components/user_picker';
 import { teamPicker } from './components/team_picker';
 import { geminiScrollbar } from './components/scroll/scroll';
+import { pageScrollbar } from './components/scroll/page_scroll';
 import { gfPageDirective } from './components/gf_page';
 import { orgSwitcher } from './components/org_switcher';
 import { profiler } from './profiler';
@@ -85,6 +86,7 @@ export {
   userPicker,
   teamPicker,
   geminiScrollbar,
+  pageScrollbar,
   gfPageDirective,
   orgSwitcher,
   manageDashboardsDirective,

+ 0 - 36
public/app/core/directives/dash_class.js

@@ -1,36 +0,0 @@
-define([
-  'lodash',
-  'jquery',
-  '../core_module',
-],
-function (_, $, coreModule) {
-  'use strict';
-
-  coreModule.default.directive('dashClass', function() {
-    return {
-      link: function($scope, elem) {
-
-        $scope.onAppEvent('panel-fullscreen-enter', function() {
-          elem.toggleClass('panel-in-fullscreen', true);
-        });
-
-        $scope.onAppEvent('panel-fullscreen-exit', function() {
-          elem.toggleClass('panel-in-fullscreen', false);
-        });
-
-        $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
-          if (newValue) {
-            elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
-            setTimeout(function() {
-              elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
-            }, 10);
-          } else {
-            elem.removeClass('dashboard-page--settings-opening');
-            elem.removeClass('dashboard-page--settings-open');
-          }
-        });
-      }
-    };
-  });
-
-});

+ 31 - 0
public/app/core/directives/dash_class.ts

@@ -0,0 +1,31 @@
+import _ from 'lodash';
+import coreModule from '../core_module';
+
+/** @ngInject */
+export function dashClass() {
+  return {
+    link: function($scope, elem) {
+      $scope.onAppEvent('panel-fullscreen-enter', function() {
+        elem.toggleClass('panel-in-fullscreen', true);
+      });
+
+      $scope.onAppEvent('panel-fullscreen-exit', function() {
+        elem.toggleClass('panel-in-fullscreen', false);
+      });
+
+      $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
+        if (newValue) {
+          elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
+          setTimeout(function() {
+            elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
+          }, 10);
+        } else {
+          elem.removeClass('dashboard-page--settings-opening');
+          elem.removeClass('dashboard-page--settings-open');
+        }
+      });
+    },
+  };
+}
+
+coreModule.directive('dashClass', dashClass);

+ 0 - 246
public/app/core/directives/metric_segment.js

@@ -1,246 +0,0 @@
-define([
-  'lodash',
-  'jquery',
-  '../core_module',
-],
-function (_, $, coreModule) {
-  'use strict';
-
-  coreModule.default.directive('metricSegment', function($compile, $sce) {
-    var inputTemplate = '<input type="text" data-provide="typeahead" ' +
-      ' class="gf-form-input input-medium"' +
-      ' spellcheck="false" style="display:none"></input>';
-
-    var linkTemplate = '<a class="gf-form-label" ng-class="segment.cssClass" ' +
-      'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
-
-    var selectTemplate = '<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
-      'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
-
-    return {
-      scope: {
-        segment: "=",
-        getOptions: "&",
-        onChange: "&",
-        debounce: "@",
-      },
-      link: function($scope, elem) {
-        var $input = $(inputTemplate);
-        var segment = $scope.segment;
-        var $button = $(segment.selectMode ? selectTemplate : linkTemplate);
-        var options = null;
-        var cancelBlur = null;
-        var linkMode = true;
-        var debounceLookup = $scope.debounce;
-
-        $input.appendTo(elem);
-        $button.appendTo(elem);
-
-        $scope.updateVariableValue = function(value) {
-          if (value === '' || segment.value === value) {
-            return;
-          }
-
-          value = _.unescape(value);
-
-          $scope.$apply(function() {
-            var selected = _.find($scope.altSegments, {value: value});
-            if (selected) {
-              segment.value = selected.value;
-              segment.html = selected.html || selected.value;
-              segment.fake = false;
-              segment.expandable = selected.expandable;
-
-              if (selected.type) {
-                segment.type = selected.type;
-              }
-            }
-            else if (segment.custom !== 'false') {
-              segment.value = value;
-              segment.html = $sce.trustAsHtml(value);
-              segment.expandable = true;
-              segment.fake = false;
-            }
-
-            $scope.onChange();
-          });
-        };
-
-        $scope.switchToLink = function(fromClick) {
-          if (linkMode && !fromClick) { return; }
-
-          clearTimeout(cancelBlur);
-          cancelBlur = null;
-          linkMode = true;
-          $input.hide();
-          $button.show();
-          $scope.updateVariableValue($input.val());
-        };
-
-        $scope.inputBlur = function() {
-          // happens long before the click event on the typeahead options
-          // need to have long delay because the blur
-          cancelBlur = setTimeout($scope.switchToLink, 200);
-        };
-
-        $scope.source = function(query, callback) {
-          $scope.$apply(function() {
-            $scope.getOptions({ $query: query }).then(function(altSegments) {
-              $scope.altSegments = altSegments;
-              options = _.map($scope.altSegments, function(alt) {
-                return _.escape(alt.value);
-              });
-
-              // add custom values
-              if (segment.custom !== 'false') {
-                if (!segment.fake && _.indexOf(options, segment.value) === -1) {
-                  options.unshift(segment.value);
-                }
-              }
-
-              callback(options);
-            });
-          });
-        };
-
-        $scope.updater = function(value) {
-          if (value === segment.value) {
-            clearTimeout(cancelBlur);
-            $input.focus();
-            return value;
-          }
-
-          $input.val(value);
-          $scope.switchToLink(true);
-
-          return value;
-        };
-
-        $scope.matcher = function(item) {
-          var str = this.query;
-          if (str[0] === '/') { str = str.substring(1); }
-          if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
-          try {
-            return item.toLowerCase().match(str.toLowerCase());
-          } catch(e) {
-            return false;
-          }
-        };
-
-        $input.attr('data-provide', 'typeahead');
-        $input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater, matcher: $scope.matcher });
-
-        var typeahead = $input.data('typeahead');
-        typeahead.lookup = function () {
-          this.query = this.$element.val() || '';
-          var items = this.source(this.query, $.proxy(this.process, this));
-          return items ? this.process(items) : items;
-        };
-
-        if (debounceLookup) {
-          typeahead.lookup = _.debounce(typeahead.lookup, 500, {leading: true});
-        }
-
-        $button.keydown(function(evt) {
-          // trigger typeahead on down arrow or enter key
-          if (evt.keyCode === 40 || evt.keyCode === 13) {
-            $button.click();
-          }
-        });
-
-        $button.click(function() {
-          options = null;
-          $input.css('width', (Math.max($button.width(), 80) + 16) + 'px');
-
-          $button.hide();
-          $input.show();
-          $input.focus();
-
-          linkMode = false;
-
-          var typeahead = $input.data('typeahead');
-          if (typeahead) {
-            $input.val('');
-            typeahead.lookup();
-          }
-        });
-
-        $input.blur($scope.inputBlur);
-
-        $compile(elem.contents())($scope);
-      }
-    };
-  });
-
-  coreModule.default.directive('metricSegmentModel', function(uiSegmentSrv, $q) {
-    return {
-      template: '<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>',
-      restrict: 'E',
-      scope: {
-        property: "=",
-        options: "=",
-        getOptions: "&",
-        onChange: "&",
-      },
-      link: {
-        pre: function postLink($scope, elem, attrs) {
-          var cachedOptions;
-
-          $scope.valueToSegment = function(value) {
-            var option = _.find($scope.options, {value: value});
-            var segment = {
-              cssClass: attrs.cssClass,
-              custom: attrs.custom,
-              value: option ? option.text : value,
-              selectMode: attrs.selectMode,
-            };
-
-            return uiSegmentSrv.newSegment(segment);
-          };
-
-          $scope.getOptionsInternal = function() {
-            if ($scope.options) {
-              cachedOptions = $scope.options;
-              return $q.when(_.map($scope.options, function(option) {
-                return {value: option.text};
-              }));
-            } else {
-              return $scope.getOptions().then(function(options) {
-                cachedOptions = options;
-                return  _.map(options, function(option) {
-                  if (option.html) {
-                    return option;
-                  }
-                  return {value: option.text};
-                });
-              });
-            }
-          };
-
-          $scope.onSegmentChange = function() {
-            if (cachedOptions) {
-              var option = _.find(cachedOptions, {text: $scope.segment.value});
-              if (option && option.value !== $scope.property) {
-                $scope.property = option.value;
-              } else if (attrs.custom !== 'false') {
-                $scope.property = $scope.segment.value;
-              }
-            } else {
-              $scope.property = $scope.segment.value;
-            }
-
-            // needs to call this after digest so
-            // property is synced with outerscope
-            $scope.$$postDigest(function() {
-              $scope.$apply(function() {
-                $scope.onChange();
-              });
-            });
-          };
-
-          $scope.segment = $scope.valueToSegment($scope.property);
-        }
-      }
-    };
-  });
-});

+ 263 - 0
public/app/core/directives/metric_segment.ts

@@ -0,0 +1,263 @@
+import _ from 'lodash';
+import $ from 'jquery';
+import coreModule from '../core_module';
+
+/** @ngInject */
+export function metricSegment($compile, $sce) {
+  let inputTemplate =
+    '<input type="text" data-provide="typeahead" ' +
+    ' class="gf-form-input input-medium"' +
+    ' spellcheck="false" style="display:none"></input>';
+
+  let linkTemplate =
+    '<a class="gf-form-label" ng-class="segment.cssClass" ' +
+    'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
+
+  let selectTemplate =
+    '<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
+    'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
+
+  return {
+    scope: {
+      segment: '=',
+      getOptions: '&',
+      onChange: '&',
+      debounce: '@',
+    },
+    link: function($scope, elem) {
+      let $input = $(inputTemplate);
+      let segment = $scope.segment;
+      let $button = $(segment.selectMode ? selectTemplate : linkTemplate);
+      let options = null;
+      let cancelBlur = null;
+      let linkMode = true;
+      let debounceLookup = $scope.debounce;
+
+      $input.appendTo(elem);
+      $button.appendTo(elem);
+
+      $scope.updateVariableValue = function(value) {
+        if (value === '' || segment.value === value) {
+          return;
+        }
+
+        value = _.unescape(value);
+
+        $scope.$apply(function() {
+          let selected = _.find($scope.altSegments, { value: value });
+          if (selected) {
+            segment.value = selected.value;
+            segment.html = selected.html || selected.value;
+            segment.fake = false;
+            segment.expandable = selected.expandable;
+
+            if (selected.type) {
+              segment.type = selected.type;
+            }
+          } else if (segment.custom !== 'false') {
+            segment.value = value;
+            segment.html = $sce.trustAsHtml(value);
+            segment.expandable = true;
+            segment.fake = false;
+          }
+
+          $scope.onChange();
+        });
+      };
+
+      $scope.switchToLink = function(fromClick) {
+        if (linkMode && !fromClick) {
+          return;
+        }
+
+        clearTimeout(cancelBlur);
+        cancelBlur = null;
+        linkMode = true;
+        $input.hide();
+        $button.show();
+        $scope.updateVariableValue($input.val());
+      };
+
+      $scope.inputBlur = function() {
+        // happens long before the click event on the typeahead options
+        // need to have long delay because the blur
+        cancelBlur = setTimeout($scope.switchToLink, 200);
+      };
+
+      $scope.source = function(query, callback) {
+        $scope.$apply(function() {
+          $scope.getOptions({ $query: query }).then(function(altSegments) {
+            $scope.altSegments = altSegments;
+            options = _.map($scope.altSegments, function(alt) {
+              return _.escape(alt.value);
+            });
+
+            // add custom values
+            if (segment.custom !== 'false') {
+              if (!segment.fake && _.indexOf(options, segment.value) === -1) {
+                options.unshift(segment.value);
+              }
+            }
+
+            callback(options);
+          });
+        });
+      };
+
+      $scope.updater = function(value) {
+        if (value === segment.value) {
+          clearTimeout(cancelBlur);
+          $input.focus();
+          return value;
+        }
+
+        $input.val(value);
+        $scope.switchToLink(true);
+
+        return value;
+      };
+
+      $scope.matcher = function(item) {
+        let str = this.query;
+        if (str[0] === '/') {
+          str = str.substring(1);
+        }
+        if (str[str.length - 1] === '/') {
+          str = str.substring(0, str.length - 1);
+        }
+        try {
+          return item.toLowerCase().match(str.toLowerCase());
+        } catch (e) {
+          return false;
+        }
+      };
+
+      $input.attr('data-provide', 'typeahead');
+      $input.typeahead({
+        source: $scope.source,
+        minLength: 0,
+        items: 10000,
+        updater: $scope.updater,
+        matcher: $scope.matcher,
+      });
+
+      let typeahead = $input.data('typeahead');
+      typeahead.lookup = function() {
+        this.query = this.$element.val() || '';
+        let items = this.source(this.query, $.proxy(this.process, this));
+        return items ? this.process(items) : items;
+      };
+
+      if (debounceLookup) {
+        typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
+      }
+
+      $button.keydown(function(evt) {
+        // trigger typeahead on down arrow or enter key
+        if (evt.keyCode === 40 || evt.keyCode === 13) {
+          $button.click();
+        }
+      });
+
+      $button.click(function() {
+        options = null;
+        $input.css('width', Math.max($button.width(), 80) + 16 + 'px');
+
+        $button.hide();
+        $input.show();
+        $input.focus();
+
+        linkMode = false;
+
+        let typeahead = $input.data('typeahead');
+        if (typeahead) {
+          $input.val('');
+          typeahead.lookup();
+        }
+      });
+
+      $input.blur($scope.inputBlur);
+
+      $compile(elem.contents())($scope);
+    },
+  };
+}
+
+/** @ngInject */
+export function metricSegmentModel(uiSegmentSrv, $q) {
+  return {
+    template:
+      '<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>',
+    restrict: 'E',
+    scope: {
+      property: '=',
+      options: '=',
+      getOptions: '&',
+      onChange: '&',
+    },
+    link: {
+      pre: function postLink($scope, elem, attrs) {
+        let cachedOptions;
+
+        $scope.valueToSegment = function(value) {
+          let option = _.find($scope.options, { value: value });
+          let segment = {
+            cssClass: attrs.cssClass,
+            custom: attrs.custom,
+            value: option ? option.text : value,
+            selectMode: attrs.selectMode,
+          };
+
+          return uiSegmentSrv.newSegment(segment);
+        };
+
+        $scope.getOptionsInternal = function() {
+          if ($scope.options) {
+            cachedOptions = $scope.options;
+            return $q.when(
+              _.map($scope.options, function(option) {
+                return { value: option.text };
+              })
+            );
+          } else {
+            return $scope.getOptions().then(function(options) {
+              cachedOptions = options;
+              return _.map(options, function(option) {
+                if (option.html) {
+                  return option;
+                }
+                return { value: option.text };
+              });
+            });
+          }
+        };
+
+        $scope.onSegmentChange = function() {
+          if (cachedOptions) {
+            let option = _.find(cachedOptions, { text: $scope.segment.value });
+            if (option && option.value !== $scope.property) {
+              $scope.property = option.value;
+            } else if (attrs.custom !== 'false') {
+              $scope.property = $scope.segment.value;
+            }
+          } else {
+            $scope.property = $scope.segment.value;
+          }
+
+          // needs to call this after digest so
+          // property is synced with outerscope
+          $scope.$$postDigest(function() {
+            $scope.$apply(function() {
+              $scope.onChange();
+            });
+          });
+        };
+
+        $scope.segment = $scope.valueToSegment($scope.property);
+      },
+    },
+  };
+}
+
+coreModule.directive('metricSegment', metricSegment);
+coreModule.directive('metricSegmentModel', metricSegmentModel);

+ 0 - 13
public/app/core/services/all.js

@@ -1,13 +0,0 @@
-define([
-  './alert_srv',
-  './util_srv',
-  './context_srv',
-  './timer',
-  './analytics',
-  './popover_srv',
-  './segment_srv',
-  './backend_srv',
-  './dynamic_directive_srv',
-  './bridge_srv'
-],
-function () {});

+ 10 - 0
public/app/core/services/all.ts

@@ -0,0 +1,10 @@
+import './alert_srv';
+import './util_srv';
+import './context_srv';
+import './timer';
+import './analytics';
+import './popover_srv';
+import './segment_srv';
+import './backend_srv';
+import './dynamic_directive_srv';
+import './bridge_srv';

+ 9 - 1
public/app/core/services/keybindingSrv.ts

@@ -10,6 +10,7 @@ import 'mousetrap-global-bind';
 export class KeybindingSrv {
   helpModal: boolean;
   modalOpen = false;
+  timepickerOpen = false;
 
   /** @ngInject */
   constructor(private $rootScope, private $location) {
@@ -22,6 +23,8 @@ export class KeybindingSrv {
 
     this.setupGlobal();
     appEvents.on('show-modal', () => (this.modalOpen = true));
+    $rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true));
+    $rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false));
   }
 
   setupGlobal() {
@@ -73,7 +76,12 @@ export class KeybindingSrv {
     appEvents.emit('hide-modal');
 
     if (!this.modalOpen) {
-      this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
+      if (this.timepickerOpen) {
+        this.$rootScope.appEvent('closeTimepicker');
+        this.timepickerOpen = false;
+      } else {
+        this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
+      }
     } else {
       this.modalOpen = false;
     }

+ 0 - 111
public/app/core/services/segment_srv.js

@@ -1,111 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  '../core_module',
-],
-function (angular, _, coreModule) {
-  'use strict';
-
-  coreModule.default.service('uiSegmentSrv', function($sce, templateSrv) {
-    var self = this;
-
-    function MetricSegment(options) {
-      if (options === '*' || options.value === '*') {
-        this.value = '*';
-        this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
-        this.type = options.type;
-        this.expandable = true;
-        return;
-      }
-
-      if (_.isString(options)) {
-        this.value = options;
-        this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
-        return;
-      }
-
-      // temp hack to work around legacy inconsistency in segment model
-      this.text = options.value;
-
-      this.cssClass = options.cssClass;
-      this.custom = options.custom;
-      this.type = options.type;
-      this.fake = options.fake;
-      this.value = options.value;
-      this.selectMode = options.selectMode;
-      this.type = options.type;
-      this.expandable = options.expandable;
-      this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
-    }
-
-    this.getSegmentForValue = function(value, fallbackText) {
-      if (value) {
-        return this.newSegment(value);
-      } else {
-        return this.newSegment({value: fallbackText, fake: true});
-      }
-    };
-
-    this.newSelectMeasurement = function() {
-      return new MetricSegment({value: 'select measurement', fake: true});
-    };
-
-    this.newFake = function(text, type, cssClass) {
-      return new MetricSegment({value: text, fake: true, type: type, cssClass: cssClass});
-    };
-
-    this.newSegment = function(options) {
-      return new MetricSegment(options);
-    };
-
-    this.newKey = function(key) {
-      return new MetricSegment({value: key, type: 'key', cssClass: 'query-segment-key' });
-    };
-
-    this.newKeyValue = function(value) {
-      return new MetricSegment({value: value, type: 'value', cssClass: 'query-segment-value' });
-    };
-
-    this.newCondition = function(condition) {
-      return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
-    };
-
-    this.newOperator = function(op) {
-      return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
-    };
-
-    this.newOperators = function(ops) {
-      return _.map(ops, function(op) {
-        return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
-      });
-    };
-
-    this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
-      return function(results) {
-        var segments = _.map(results, function(segment) {
-          return self.newSegment({value: segment.text, expandable: segment.expandable});
-        });
-
-        if (addTemplateVars) {
-          _.each(templateSrv.variables, function(variable) {
-            if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
-              segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
-            }
-          });
-        }
-
-        return segments;
-      };
-    };
-
-    this.newSelectMetric = function() {
-      return new MetricSegment({value: 'select metric', fake: true});
-    };
-
-    this.newPlusButton = function() {
-      return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
-    };
-
-  });
-
-});

+ 111 - 0
public/app/core/services/segment_srv.ts

@@ -0,0 +1,111 @@
+import _ from 'lodash';
+import coreModule from '../core_module';
+
+/** @ngInject */
+export function uiSegmentSrv($sce, templateSrv) {
+  let self = this;
+
+  function MetricSegment(options) {
+    if (options === '*' || options.value === '*') {
+      this.value = '*';
+      this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
+      this.type = options.type;
+      this.expandable = true;
+      return;
+    }
+
+    if (_.isString(options)) {
+      this.value = options;
+      this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
+      return;
+    }
+
+    // temp hack to work around legacy inconsistency in segment model
+    this.text = options.value;
+
+    this.cssClass = options.cssClass;
+    this.custom = options.custom;
+    this.type = options.type;
+    this.fake = options.fake;
+    this.value = options.value;
+    this.selectMode = options.selectMode;
+    this.type = options.type;
+    this.expandable = options.expandable;
+    this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
+  }
+
+  this.getSegmentForValue = function(value, fallbackText) {
+    if (value) {
+      return this.newSegment(value);
+    } else {
+      return this.newSegment({ value: fallbackText, fake: true });
+    }
+  };
+
+  this.newSelectMeasurement = function() {
+    return new MetricSegment({ value: 'select measurement', fake: true });
+  };
+
+  this.newFake = function(text, type, cssClass) {
+    return new MetricSegment({ value: text, fake: true, type: type, cssClass: cssClass });
+  };
+
+  this.newSegment = function(options) {
+    return new MetricSegment(options);
+  };
+
+  this.newKey = function(key) {
+    return new MetricSegment({ value: key, type: 'key', cssClass: 'query-segment-key' });
+  };
+
+  this.newKeyValue = function(value) {
+    return new MetricSegment({ value: value, type: 'value', cssClass: 'query-segment-value' });
+  };
+
+  this.newCondition = function(condition) {
+    return new MetricSegment({ value: condition, type: 'condition', cssClass: 'query-keyword' });
+  };
+
+  this.newOperator = function(op) {
+    return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
+  };
+
+  this.newOperators = function(ops) {
+    return _.map(ops, function(op) {
+      return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
+    });
+  };
+
+  this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
+    return function(results) {
+      let segments = _.map(results, function(segment) {
+        return self.newSegment({ value: segment.text, expandable: segment.expandable });
+      });
+
+      if (addTemplateVars) {
+        _.each(templateSrv.variables, function(variable) {
+          if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
+            segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
+          }
+        });
+      }
+
+      return segments;
+    };
+  };
+
+  this.newSelectMetric = function() {
+    return new MetricSegment({ value: 'select metric', fake: true });
+  };
+
+  this.newPlusButton = function() {
+    return new MetricSegment({
+      fake: true,
+      html: '<i class="fa fa-plus "></i>',
+      type: 'plus-button',
+      cssClass: 'query-part',
+    });
+  };
+}
+
+coreModule.service('uiSegmentSrv', uiSegmentSrv);

+ 0 - 4
public/app/features/alerting/threshold_mapper.ts

@@ -1,9 +1,5 @@
 export class ThresholdMapper {
   static alertToGraphThresholds(panel) {
-    if (panel.type !== 'graph') {
-      return false;
-    }
-
     for (var i = 0; i < panel.alert.conditions.length; i++) {
       let condition = panel.alert.conditions[i];
       if (condition.type !== 'query') {

+ 0 - 15
public/app/features/all.js

@@ -1,15 +0,0 @@
-define([
-  './panellinks/module',
-  './dashlinks/module',
-  './annotations/all',
-  './templating/all',
-  './plugins/all',
-  './dashboard/all',
-  './playlist/all',
-  './snapshot/all',
-  './panel/all',
-  './org/all',
-  './admin/admin',
-  './alerting/all',
-  './styleguide/styleguide',
-], function () {});

+ 13 - 0
public/app/features/all.ts

@@ -0,0 +1,13 @@
+import './panellinks/module';
+import './dashlinks/module';
+import './annotations/all';
+import './templating/all';
+import './plugins/all';
+import './dashboard/all';
+import './playlist/all';
+import './snapshot/all';
+import './panel/all';
+import './org/all';
+import './admin/admin';
+import './alerting/all';
+import './styleguide/styleguide';

+ 1 - 1
public/app/features/dashboard/dashgrid/AddPanelPanel.tsx

@@ -103,7 +103,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
 
   render() {
     return (
-      <div className="panel-container">
+      <div className="panel-container add-panel-container">
         <div className="add-panel">
           <div className="add-panel__header">
             <i className="gicon gicon-add-panel" />

+ 10 - 4
public/app/features/dashboard/timepicker/timepicker.ts

@@ -22,7 +22,6 @@ export class TimePickerCtrl {
   refresh: any;
   isUtc: boolean;
   firstDayOfWeek: number;
-  closeDropdown: any;
   isOpen: boolean;
 
   /** @ngInject */
@@ -32,6 +31,7 @@ export class TimePickerCtrl {
     $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
     $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
     $rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
+    $rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
 
     // init options
     this.panel = this.dashboard.timepicker;
@@ -96,7 +96,7 @@ export class TimePickerCtrl {
 
   openDropdown() {
     if (this.isOpen) {
-      this.isOpen = false;
+      this.closeDropdown();
       return;
     }
 
@@ -112,6 +112,12 @@ export class TimePickerCtrl {
 
     this.refresh.options.unshift({ text: 'off' });
     this.isOpen = true;
+    this.$rootScope.appEvent('timepickerOpen');
+  }
+
+  closeDropdown() {
+    this.isOpen = false;
+    this.$rootScope.appEvent('timepickerClosed');
   }
 
   applyCustom() {
@@ -120,7 +126,7 @@ export class TimePickerCtrl {
     }
 
     this.timeSrv.setTime(this.editTimeRaw);
-    this.isOpen = false;
+    this.closeDropdown();
   }
 
   absoluteFromChanged() {
@@ -143,7 +149,7 @@ export class TimePickerCtrl {
     }
 
     this.timeSrv.setTime(range);
-    this.isOpen = false;
+    this.closeDropdown();
   }
 }
 

+ 2 - 2
public/app/features/dashboard/unsaved_changes_srv.ts

@@ -35,12 +35,12 @@ export class Tracker {
 
     $window.onbeforeunload = () => {
       if (this.ignoreChanges()) {
-        return null;
+        return undefined;
       }
       if (this.hasChanges()) {
         return 'There are unsaved changes to this dashboard';
       }
-      return null;
+      return undefined;
     };
 
     scope.$on('$locationChangeStart', (event, next) => {

+ 2 - 1
public/app/features/dashboard/view_state_srv.ts

@@ -196,9 +196,10 @@ export class DashboardViewState {
     this.oldTimeRange = ctrl.range;
     this.fullscreenPanel = panelScope;
 
+    // Firefox doesn't return scrollTop postion properly if 'dash-scroll' is emitted after setViewMode()
+    this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
     this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
     this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id });
-    this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
   }
 
   registerPanel(panelScope) {

+ 0 - 9
public/app/features/panel/all.js

@@ -1,9 +0,0 @@
-define([
-  './panel_header',
-  './panel_directive',
-  './solo_panel_ctrl',
-  './query_ctrl',
-  './panel_editor_tab',
-  './query_editor_row',
-  './query_troubleshooter',
-], function () {});

+ 7 - 0
public/app/features/panel/all.ts

@@ -0,0 +1,7 @@
+import './panel_header';
+import './panel_directive';
+import './solo_panel_ctrl';
+import './query_ctrl';
+import './panel_editor_tab';
+import './query_editor_row';
+import './query_troubleshooter';

+ 30 - 4
public/app/features/panel/panel_directive.ts

@@ -1,6 +1,7 @@
 import angular from 'angular';
+import $ from 'jquery';
 import Drop from 'tether-drop';
-import PerfectScrollbar from 'perfect-scrollbar';
+import baron from 'baron';
 
 var module = angular.module('grafana.directives');
 
@@ -86,6 +87,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
 
       function panelHeightUpdated() {
         panelContent.css({ height: ctrl.height + 'px' });
+      }
+
+      function resizeScrollableContent() {
         if (panelScrollbar) {
           panelScrollbar.update();
         }
@@ -100,9 +104,30 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
       // update scrollbar after mounting
       ctrl.events.on('component-did-mount', () => {
         if (ctrl.__proto__.constructor.scrollable) {
-          panelScrollbar = new PerfectScrollbar(panelContent[0], {
-            wheelPropagation: true,
+          const scrollRootClass = 'baron baron__root baron__clipper panel-content--scrollable';
+          const scrollerClass = 'baron__scroller';
+          const scrollBarHTML = `
+            <div class="baron__track">
+              <div class="baron__bar"></div>
+            </div>
+          `;
+
+          let scrollRoot = panelContent;
+          let scroller = panelContent.find(':first').find(':first');
+
+          scrollRoot.addClass(scrollRootClass);
+          $(scrollBarHTML).appendTo(scrollRoot);
+          scroller.addClass(scrollerClass);
+
+          panelScrollbar = baron({
+            root: scrollRoot[0],
+            scroller: scroller[0],
+            bar: '.baron__bar',
+            barOnCls: '_scrollbar',
+            scrollingCls: '_scrolling',
           });
+
+          panelScrollbar.scroll();
         }
       });
 
@@ -110,6 +135,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
         ctrl.calculatePanelHeight();
         panelHeightUpdated();
         $timeout(() => {
+          resizeScrollableContent();
           ctrl.render();
         });
       });
@@ -199,7 +225,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
         }
 
         if (panelScrollbar) {
-          panelScrollbar.update();
+          panelScrollbar.dispose();
         }
       });
     },

+ 0 - 7
public/app/features/playlist/all.js

@@ -1,7 +0,0 @@
-define([
-  './playlists_ctrl',
-  './playlist_search',
-  './playlist_srv',
-  './playlist_edit_ctrl',
-  './playlist_routes'
-], function () {});

+ 5 - 0
public/app/features/playlist/all.ts

@@ -0,0 +1,5 @@
+import './playlists_ctrl';
+import './playlist_search';
+import './playlist_srv';
+import './playlist_edit_ctrl';
+import './playlist_routes';

+ 0 - 39
public/app/features/playlist/playlist_routes.js

@@ -1,39 +0,0 @@
-define([
-  'angular',
-  'lodash'
-],
-function (angular) {
-  'use strict';
-
-  var module = angular.module('grafana.routes');
-
-  module.config(function($routeProvider) {
-    $routeProvider
-      .when('/playlists', {
-        templateUrl: 'public/app/features/playlist/partials/playlists.html',
-        controllerAs: 'ctrl',
-        controller : 'PlaylistsCtrl'
-      })
-      .when('/playlists/create', {
-        templateUrl: 'public/app/features/playlist/partials/playlist.html',
-        controllerAs: 'ctrl',
-        controller : 'PlaylistEditCtrl'
-      })
-      .when('/playlists/edit/:id', {
-        templateUrl: 'public/app/features/playlist/partials/playlist.html',
-        controllerAs: 'ctrl',
-        controller : 'PlaylistEditCtrl'
-      })
-      .when('/playlists/play/:id', {
-        templateUrl: 'public/app/features/playlist/partials/playlists.html',
-        controllerAs: 'ctrl',
-        controller : 'PlaylistsCtrl',
-        resolve: {
-          init: function(playlistSrv, $route) {
-            var playlistId = $route.current.params.id;
-            playlistSrv.start(playlistId);
-          }
-        }
-      });
-  });
-});

+ 34 - 0
public/app/features/playlist/playlist_routes.ts

@@ -0,0 +1,34 @@
+import angular from 'angular';
+
+/** @ngInject */
+function grafanaRoutes($routeProvider) {
+  $routeProvider
+    .when('/playlists', {
+      templateUrl: 'public/app/features/playlist/partials/playlists.html',
+      controllerAs: 'ctrl',
+      controller: 'PlaylistsCtrl',
+    })
+    .when('/playlists/create', {
+      templateUrl: 'public/app/features/playlist/partials/playlist.html',
+      controllerAs: 'ctrl',
+      controller: 'PlaylistEditCtrl',
+    })
+    .when('/playlists/edit/:id', {
+      templateUrl: 'public/app/features/playlist/partials/playlist.html',
+      controllerAs: 'ctrl',
+      controller: 'PlaylistEditCtrl',
+    })
+    .when('/playlists/play/:id', {
+      templateUrl: 'public/app/features/playlist/partials/playlists.html',
+      controllerAs: 'ctrl',
+      controller: 'PlaylistsCtrl',
+      resolve: {
+        init: function(playlistSrv, $route) {
+          let playlistId = $route.current.params.id;
+          playlistSrv.start(playlistId);
+        },
+      },
+    });
+}
+
+angular.module('grafana.routes').config(grafanaRoutes);

+ 33 - 12
public/app/features/plugins/partials/ds_http_settings.html

@@ -1,5 +1,3 @@
-
-
 <div class="gf-form-group">
   <h3 class="page-heading">HTTP</h3>
   <div class="gf-form-group">
@@ -13,12 +11,12 @@
         <info-popover mode="right-absolute">
           <p>Specify a complete HTTP URL (for example http://your_server:8080)</p>
           <span ng-show="current.access === 'direct'">
-            Your access method is <em>Direct</em>, this means the URL
+            Your access method is <em>Browser</em>, this means the URL
             needs to be accessible from the browser.
           </span>
           <span ng-show="current.access === 'proxy'">
-            Your access method is currently <em>Proxy</em>, this means the URL
-            needs to be accessible from the grafana backend.
+            Your access method is <em>Server</em>, this means the URL
+            needs to be accessible from the grafana backend/server.
           </span>
         </info-popover>
       </div>
@@ -27,14 +25,38 @@
     <div class="gf-form-inline">
       <div class="gf-form max-width-30">
         <span class="gf-form-label width-7">Access</span>
-        <div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
-          <select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
-          <info-popover mode="right-absolute">
-            Direct = URL is used directly from browser<br>
-            Proxy = Grafana backend will proxy the request
-          </info-popover>
+        <div class="gf-form-select-wrapper max-width-24">
+          <select class="gf-form-input" ng-model="current.access" ng-options="f.key as f.value for f in [{key: 'proxy', value: 'Server (Default)'}, { key: 'direct', value: 'Browser'}]"></select>
         </div>
       </div>
+      <div class="gf-form">
+        <label class="gf-form-label query-keyword pointer" ng-click="ctrl.showAccessHelp = !ctrl.showAccessHelp">
+          Help&nbsp;
+          <i class="fa fa-caret-down" ng-show="ctrl.showAccessHelp"></i>
+          <i class="fa fa-caret-right" ng-hide="ctrl.showAccessHelp">&nbsp;</i>
+        </label>
+      </div>
+    </div>
+
+    <div class="alert alert-info" ng-show="ctrl.showAccessHelp">
+      <div class="alert-body">
+        <p>
+          Access mode controls how requests to the data source will be handled.
+          <strong><i>Server</i></strong> should be the preferred way if nothing else stated.
+        </p>
+        <div class="alert-title">Server access mode (Default):</div>
+        <p>
+          All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to the data source
+          and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements.
+          The URL needs to be accessible from the grafana backend/server if you select this access mode.
+        </p>
+        <div class="alert-title">Browser access mode:</div>
+        <p>
+          All requests will be made from the browser directly to the data source and may be subject to
+          Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this
+          access mode.
+        </p>
+      </div>
     </div>
   </div>
 
@@ -135,4 +157,3 @@
     </div>
   </div>
 </div>
-

+ 2 - 0
public/app/features/templating/editor_ctrl.ts

@@ -23,6 +23,8 @@ export class VariableEditorCtrl {
       { value: 2, text: 'Alphabetical (desc)' },
       { value: 3, text: 'Numerical (asc)' },
       { value: 4, text: 'Numerical (desc)' },
+      { value: 5, text: 'Alphabetical (case-insensitive, asc)' },
+      { value: 6, text: 'Alphabetical (case-insensitive, desc)' },
     ];
 
     $scope.hideOptions = [{ value: 0, text: '' }, { value: 1, text: 'Label' }, { value: 2, text: 'Variable' }];

+ 4 - 0
public/app/features/templating/query_variable.ts

@@ -197,6 +197,10 @@ export class QueryVariable implements Variable {
           return parseInt(matches[1], 10);
         }
       });
+    } else if (sortType === 3) {
+      options = _.sortBy(options, opt => {
+        return _.toLower(opt.text);
+      });
     }
 
     if (reverseSort) {

+ 35 - 6
public/app/features/templating/specs/query_variable.jest.ts

@@ -40,11 +40,11 @@ describe('QueryVariable', () => {
   });
 
   describe('can convert and sort metric names', () => {
-    var variable = new QueryVariable({}, null, null, null, null);
-    variable.sort = 3; // Numerical (asc)
+    const variable = new QueryVariable({}, null, null, null, null);
+    let input;
 
-    describe('can sort a mixed array of metric variables', () => {
-      var input = [
+    beforeEach(() => {
+      input = [
         { text: '0', value: '0' },
         { text: '1', value: '1' },
         { text: null, value: 3 },
@@ -58,11 +58,18 @@ describe('QueryVariable', () => {
         { text: '', value: undefined },
         { text: undefined, value: '' },
       ];
+    });
+
+    describe('can sort a mixed array of metric variables in numeric order', () => {
+      let result;
+
+      beforeEach(() => {
+        variable.sort = 3; // Numerical (asc)
+        result = variable.metricNamesToVariableValues(input);
+      });
 
-      var result = variable.metricNamesToVariableValues(input);
       it('should return in same order', () => {
         var i = 0;
-
         expect(result.length).toBe(11);
         expect(result[i++].text).toBe('');
         expect(result[i++].text).toBe('0');
@@ -73,5 +80,27 @@ describe('QueryVariable', () => {
         expect(result[i++].text).toBe('6');
       });
     });
+
+    describe('can sort a mixed array of metric variables in alphabetical order', () => {
+      let result;
+
+      beforeEach(() => {
+        variable.sort = 5; // Alphabetical CI (asc)
+        result = variable.metricNamesToVariableValues(input);
+      });
+
+      it('should return in same order', () => {
+        var i = 0;
+        console.log(result);
+        expect(result.length).toBe(11);
+        expect(result[i++].text).toBe('');
+        expect(result[i++].text).toBe('0');
+        expect(result[i++].text).toBe('1');
+        expect(result[i++].text).toBe('10');
+        expect(result[i++].text).toBe('3');
+        expect(result[i++].text).toBe('4');
+        expect(result[i++].text).toBe('5');
+      });
+    });
   });
 });

+ 12 - 12
public/app/partials/dashboard.html

@@ -1,18 +1,18 @@
 <div dash-class ng-if="ctrl.dashboard">
 	<dashnav dashboard="ctrl.dashboard"></dashnav>
 
-	<div class="scroll-canvas scroll-canvas--dashboard" grafana-scrollbar>
-		<dashboard-settings dashboard="ctrl.dashboard"
-											  ng-if="ctrl.dashboardViewState.state.editview"
-												class="dashboard-settings">
-		</dashboard-settings>
+	<div class="scroll-canvas scroll-canvas--dashboard" page-scrollbar>
+    <dashboard-settings dashboard="ctrl.dashboard"
+                        ng-if="ctrl.dashboardViewState.state.editview"
+                        class="dashboard-settings">
+    </dashboard-settings>
 
-		<div class="dashboard-container">
-			<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
-			</dashboard-submenu>
+    <div class="dashboard-container">
+      <dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
+      </dashboard-submenu>
 
-			<dashboard-grid get-panel-container="ctrl.getPanelContainer">
-			</dashboard-grid>
-		</div>
-	</div>
+      <dashboard-grid get-panel-container="ctrl.getPanelContainer">
+      </dashboard-grid>
+    </div>
+  </div>
 </div>

+ 1 - 0
public/app/plugins/datasource/graphite/add_graphite_func.ts

@@ -4,6 +4,7 @@ import $ from 'jquery';
 import rst2html from 'rst2html';
 import Drop from 'tether-drop';
 
+/** @ngInject */
 export function graphiteAddFunc($compile) {
   const inputTemplate =
     '<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';

+ 1 - 0
public/app/plugins/datasource/graphite/func_editor.ts

@@ -3,6 +3,7 @@ import _ from 'lodash';
 import $ from 'jquery';
 import rst2html from 'rst2html';
 
+/** @ngInject */
 export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
   const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
   const paramTemplate =

+ 1 - 1
public/app/plugins/datasource/mssql/partials/annotations.editor.html

@@ -28,7 +28,7 @@ An annotation is an event that is overlayed on top of graphs. The query can have
 Macros:
 - $__time(column) -&gt; column AS time
 - $__timeEpoch(column) -&gt; DATEDIFF(second, '1970-01-01', column) AS time
-- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
+- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &lt;= DATEADD(s, 18446744066914187038, '1970-01-01')
 - $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
 
 Or build your own conditionals using these macros which just return the values:

+ 1 - 1
public/app/plugins/datasource/mssql/partials/query.editor.html

@@ -49,7 +49,7 @@ Table:
 Macros:
 - $__time(column) -&gt; column AS time
 - $__timeEpoch(column) -&gt; DATEDIFF(second, '1970-01-01', column) AS time
-- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
+- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &lt;= DATEADD(s, 18446744066914187038, '1970-01-01')
 - $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
 - $__timeGroup(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.
 

+ 7 - 3
public/app/plugins/datasource/prometheus/datasource.ts

@@ -6,8 +6,12 @@ import * as dateMath from 'app/core/utils/datemath';
 import PrometheusMetricFindQuery from './metric_find_query';
 import { ResultTransformer } from './result_transformer';
 
-function prometheusSpecialRegexEscape(value) {
-  return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
+export function prometheusRegularEscape(value) {
+  return value.replace(/'/g, "\\\\'");
+}
+
+export function prometheusSpecialRegexEscape(value) {
+  return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
 }
 
 export class PrometheusDatasource {
@@ -80,7 +84,7 @@ export class PrometheusDatasource {
   interpolateQueryExpr(value, variable, defaultFormatFn) {
     // if no multi or include all do not regexEscape
     if (!variable.multi && !variable.includeAll) {
-      return value;
+      return prometheusRegularEscape(value);
     }
 
     if (typeof value === 'string') {

+ 1 - 1
public/app/plugins/datasource/prometheus/partials/query.editor.html

@@ -14,7 +14,7 @@
         data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
       </input>
       <info-popover mode="right-absolute">
-        Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for
+        Controls the name of the time series, using name or pattern. For example <span ng-non-bindable>{{hostname}}</span> will be replaced with label value for
         the label hostname.
       </info-popover>
     </div>

+ 38 - 1
public/app/plugins/datasource/prometheus/specs/datasource.jest.ts

@@ -1,7 +1,7 @@
 import _ from 'lodash';
 import moment from 'moment';
 import q from 'q';
-import { PrometheusDatasource } from '../datasource';
+import { PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
 
 describe('PrometheusDatasource', () => {
   let ctx: any = {};
@@ -101,4 +101,41 @@ describe('PrometheusDatasource', () => {
       });
     });
   });
+
+  describe('Prometheus regular escaping', function() {
+    it('should not escape simple string', function() {
+      expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
+    });
+    it("should escape '", function() {
+      expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
+    });
+    it('should escape multiple characters', function() {
+      expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
+    });
+  });
+
+  describe('Prometheus regexes escaping', function() {
+    it('should not escape simple string', function() {
+      expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
+    });
+    it('should escape $^*+?.()\\', function() {
+      expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass");
+      expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass');
+      expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass');
+      expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass');
+      expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass');
+      expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass');
+      expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass');
+      expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass');
+      expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass');
+      expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass');
+      expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass');
+      expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass');
+      expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass');
+      expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass');
+    });
+    it('should escape multiple special characters', function() {
+      expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
+    });
+  });
 });

+ 16 - 14
public/app/plugins/panel/dashlist/module.html

@@ -1,17 +1,19 @@
-<div class="dashlist" ng-repeat="group in ctrl.groups">
-  <div class="dashlist-section" ng-if="group.show">
-    <h6 class="dashlist-section-header" ng-show="ctrl.panel.headings">
-      {{group.header}}
-    </h6>
-    <div class="dashlist-item" ng-repeat="dash in group.list">
-      <a class="dashlist-link dashlist-link-{{dash.type}}" href="{{dash.url}}">
-        <span class="dashlist-title">
-          {{dash.title}}
-        </span>
-        <span class="dashlist-star" ng-click="ctrl.starDashboard(dash, $event)">
-          <i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
-        </span>
-      </a>
+<div>
+  <div class="dashlist" ng-repeat="group in ctrl.groups">
+    <div class="dashlist-section" ng-if="group.show">
+      <h6 class="dashlist-section-header" ng-show="ctrl.panel.headings">
+        {{group.header}}
+      </h6>
+      <div class="dashlist-item" ng-repeat="dash in group.list">
+        <a class="dashlist-link dashlist-link-{{dash.type}}" href="{{dash.url}}">
+          <span class="dashlist-title">
+            {{dash.title}}
+          </span>
+          <span class="dashlist-star" ng-click="ctrl.starDashboard(dash, $event)">
+            <i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
+          </span>
+        </a>
+      </div>
     </div>
   </div>
 </div>

+ 0 - 292
public/app/plugins/panel/graph/graph_tooltip.js

@@ -1,292 +0,0 @@
-define([
-  'jquery',
-  'app/core/core',
-],
-function ($, core) {
-  'use strict';
-
-  var appEvents = core.appEvents;
-
-  function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
-    var self = this;
-    var ctrl = scope.ctrl;
-    var panel = ctrl.panel;
-
-    var $tooltip = $('<div class="graph-tooltip">');
-
-    this.destroy = function() {
-      $tooltip.remove();
-    };
-
-    this.findHoverIndexFromDataPoints = function(posX, series, last) {
-      var ps = series.datapoints.pointsize;
-      var initial = last*ps;
-      var len = series.datapoints.points.length;
-      for (var j = initial; j < len; j += ps) {
-        // Special case of a non stepped line, highlight the very last point just before a null point
-        if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
-            //normal case
-            || series.datapoints.points[j] > posX) {
-          return Math.max(j - ps,  0)/ps;
-        }
-      }
-      return j/ps - 1;
-    };
-
-    this.findHoverIndexFromData = function(posX, series) {
-      var lower = 0;
-      var upper = series.data.length - 1;
-      var middle;
-      while (true) {
-        if (lower > upper) {
-          return Math.max(upper, 0);
-        }
-        middle = Math.floor((lower + upper) / 2);
-        if (series.data[middle][0] === posX) {
-          return middle;
-        } else if (series.data[middle][0] < posX) {
-          lower = middle + 1;
-        } else {
-          upper = middle - 1;
-        }
-      }
-    };
-
-    this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
-      if (xMode === 'time') {
-        innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml;
-      }
-      $tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
-    };
-
-    this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
-      var value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
-      // 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
-      var results = [[],[],[]];
-
-      //now we know the current X (j) position for X and Y values
-      var last_value = 0; //needed for stacked values
-
-      var minDistance, minTime;
-
-      for (i = 0; i < seriesList.length; i++) {
-        series = seriesList[i];
-
-        if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
-          // Init value so that it does not brake series sorting
-          results[0].push({ hidden: true, value: 0 });
-          continue;
-        }
-
-        if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
-          // Init value so that it does not brake series sorting
-          results[0].push({ hidden: true, value: 0 });
-          continue;
-        }
-
-        hoverIndex = this.findHoverIndexFromData(pos.x, series);
-        hoverDistance = pos.x - series.data[hoverIndex][0];
-        pointTime = series.data[hoverIndex][0];
-
-        // Take the closest point before the cursor, or if it does not exist, the closest after
-        if (! minDistance
-            || (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0))
-            || (hoverDistance < 0 && hoverDistance > minDistance)) {
-          minDistance = hoverDistance;
-          minTime = pointTime;
-        }
-
-        if (series.stack) {
-          if (panel.tooltip.value_type === 'individual') {
-            value = series.data[hoverIndex][1];
-          } else if (!series.stack) {
-            value = series.data[hoverIndex][1];
-          } else {
-            last_value += series.data[hoverIndex][1];
-            value = last_value;
-          }
-        } else {
-          value = series.data[hoverIndex][1];
-        }
-
-        // Highlighting multiple Points depending on the plot type
-        if (series.lines.steps || series.stack) {
-          // stacked and steppedLine plots can have series with different length.
-          // Stacked series can increase its length on each new stacked serie if null points found,
-          // to speed the index search we begin always on the last found hoverIndex.
-          hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
-        }
-
-        // Be sure we have a yaxis so that it does not brake series sorting
-        yaxis = 0;
-        if (series.yaxis) {
-          yaxis = series.yaxis.n;
-        }
-
-        results[yaxis].push({
-          value: value,
-          hoverIndex: hoverIndex,
-          color: series.color,
-          label: series.aliasEscaped,
-          time: pointTime,
-          distance: hoverDistance,
-          index: i
-        });
-      }
-
-      // Contat the 3 sub-arrays
-      results = results[0].concat(results[1],results[2]);
-
-      // Time of the point closer to pointer
-      results.time = minTime;
-
-      return results;
-    };
-
-    elem.mouseleave(function () {
-      if (panel.tooltip.shared) {
-        var plot = elem.data().plot;
-        if (plot) {
-          $tooltip.detach();
-          plot.unhighlight();
-        }
-      }
-      appEvents.emit('graph-hover-clear');
-    });
-
-    elem.bind("plothover", function (event, pos, item) {
-      self.show(pos, item);
-
-      // broadcast to other graph panels that we are hovering!
-      pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
-      appEvents.emit('graph-hover', {pos: pos, panel: panel});
-    });
-
-    elem.bind("plotclick", function (event, pos, item) {
-      appEvents.emit('graph-click', {pos: pos, panel: panel, item: item});
-    });
-
-    this.clear = function(plot) {
-      $tooltip.detach();
-      plot.clearCrosshair();
-      plot.unhighlight();
-    };
-
-    this.show = function(pos, item) {
-      var plot = elem.data().plot;
-      var plotData = plot.getData();
-      var xAxes = plot.getXAxes();
-      var xMode = xAxes[0].options.mode;
-      var seriesList = getSeriesFn();
-      var allSeriesMode = panel.tooltip.shared;
-      var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
-
-      // if panelRelY is defined another panel wants us to show a tooltip
-      // get pageX from position on x axis and pageY from relative position in original panel
-      if (pos.panelRelY) {
-        var pointOffset = plot.pointOffset({x: pos.x});
-        if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
-          self.clear(plot);
-          return;
-        }
-        pos.pageX = elem.offset().left + pointOffset.left;
-        pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
-        var isVisible = pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
-        if (!isVisible) {
-          self.clear(plot);
-          return;
-        }
-        plot.setCrosshair(pos);
-        allSeriesMode = true;
-
-        if (dashboard.sharedCrosshairModeOnly()) {
-          // if only crosshair mode we are done
-          return;
-        }
-      }
-
-      if (seriesList.length === 0) {
-        return;
-      }
-
-      if (seriesList[0].hasMsResolution) {
-        tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
-      } else {
-        tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
-      }
-
-      if (allSeriesMode) {
-        plot.unhighlight();
-
-        var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
-
-        seriesHtml = '';
-
-        absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
-
-        // Dynamically reorder the hovercard for the current time point if the
-        // option is enabled.
-        if (panel.tooltip.sort === 2) {
-          seriesHoverInfo.sort(function(a, b) {
-            return b.value - a.value;
-          });
-        } else if (panel.tooltip.sort === 1) {
-          seriesHoverInfo.sort(function(a, b) {
-            return a.value - b.value;
-          });
-        }
-
-        for (i = 0; i < seriesHoverInfo.length; i++) {
-          hoverInfo = seriesHoverInfo[i];
-
-          if (hoverInfo.hidden) {
-            continue;
-          }
-
-          var highlightClass = '';
-          if (item && hoverInfo.index === item.seriesIndex) {
-            highlightClass = 'graph-tooltip-list-item--highlight';
-          }
-
-          series = seriesList[hoverInfo.index];
-
-          value = series.formatValue(hoverInfo.value);
-
-          seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
-          seriesHtml += '<i class="fa fa-minus" style="color:' + hoverInfo.color +';"></i> ' + hoverInfo.label + ':</div>';
-          seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
-          plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
-        }
-
-        self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
-      }
-      // single series tooltip
-      else if (item) {
-        series = seriesList[item.seriesIndex];
-        group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
-        group += '<i class="fa fa-minus" style="color:' + item.series.color +';"></i> ' + series.aliasEscaped + ':</div>';
-
-        if (panel.stack && panel.tooltip.value_type === 'individual') {
-          value = item.datapoint[1] - item.datapoint[2];
-        }
-        else {
-          value = item.datapoint[1];
-        }
-
-        value = series.formatValue(value);
-
-        absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
-
-        group += '<div class="graph-tooltip-value">' + value + '</div>';
-
-        self.renderAndShow(absoluteTime, group, pos, xMode);
-      }
-      // no hit
-      else {
-        $tooltip.detach();
-      }
-    };
-  }
-
-  return GraphTooltip;
-});

+ 289 - 0
public/app/plugins/panel/graph/graph_tooltip.ts

@@ -0,0 +1,289 @@
+import $ from 'jquery';
+import { appEvents } from 'app/core/core';
+
+export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
+  let self = this;
+  let ctrl = scope.ctrl;
+  let panel = ctrl.panel;
+
+  let $tooltip = $('<div class="graph-tooltip">');
+
+  this.destroy = function() {
+    $tooltip.remove();
+  };
+
+  this.findHoverIndexFromDataPoints = function(posX, series, last) {
+    let ps = series.datapoints.pointsize;
+    let initial = last * ps;
+    let len = series.datapoints.points.length;
+    let j;
+    for (j = initial; j < len; j += ps) {
+      // Special case of a non stepped line, highlight the very last point just before a null point
+      if (
+        (!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) ||
+        //normal case
+        series.datapoints.points[j] > posX
+      ) {
+        return Math.max(j - ps, 0) / ps;
+      }
+    }
+    return j / ps - 1;
+  };
+
+  this.findHoverIndexFromData = function(posX, series) {
+    let lower = 0;
+    let upper = series.data.length - 1;
+    let middle;
+    while (true) {
+      if (lower > upper) {
+        return Math.max(upper, 0);
+      }
+      middle = Math.floor((lower + upper) / 2);
+      if (series.data[middle][0] === posX) {
+        return middle;
+      } else if (series.data[middle][0] < posX) {
+        lower = middle + 1;
+      } else {
+        upper = middle - 1;
+      }
+    }
+  };
+
+  this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
+    if (xMode === 'time') {
+      innerHtml = '<div class="graph-tooltip-time">' + absoluteTime + '</div>' + innerHtml;
+    }
+    $tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
+  };
+
+  this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
+    let value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
+    // 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
+    let results: any = [[], [], []];
+
+    //now we know the current X (j) position for X and Y values
+    let last_value = 0; //needed for stacked values
+
+    let minDistance, minTime;
+
+    for (i = 0; i < seriesList.length; i++) {
+      series = seriesList[i];
+
+      if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
+        // Init value so that it does not brake series sorting
+        results[0].push({ hidden: true, value: 0 });
+        continue;
+      }
+
+      if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
+        // Init value so that it does not brake series sorting
+        results[0].push({ hidden: true, value: 0 });
+        continue;
+      }
+
+      hoverIndex = this.findHoverIndexFromData(pos.x, series);
+      hoverDistance = pos.x - series.data[hoverIndex][0];
+      pointTime = series.data[hoverIndex][0];
+
+      // Take the closest point before the cursor, or if it does not exist, the closest after
+      if (
+        !minDistance ||
+        (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) ||
+        (hoverDistance < 0 && hoverDistance > minDistance)
+      ) {
+        minDistance = hoverDistance;
+        minTime = pointTime;
+      }
+
+      if (series.stack) {
+        if (panel.tooltip.value_type === 'individual') {
+          value = series.data[hoverIndex][1];
+        } else if (!series.stack) {
+          value = series.data[hoverIndex][1];
+        } else {
+          last_value += series.data[hoverIndex][1];
+          value = last_value;
+        }
+      } else {
+        value = series.data[hoverIndex][1];
+      }
+
+      // Highlighting multiple Points depending on the plot type
+      if (series.lines.steps || series.stack) {
+        // stacked and steppedLine plots can have series with different length.
+        // Stacked series can increase its length on each new stacked serie if null points found,
+        // to speed the index search we begin always on the last found hoverIndex.
+        hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
+      }
+
+      // Be sure we have a yaxis so that it does not brake series sorting
+      yaxis = 0;
+      if (series.yaxis) {
+        yaxis = series.yaxis.n;
+      }
+
+      results[yaxis].push({
+        value: value,
+        hoverIndex: hoverIndex,
+        color: series.color,
+        label: series.aliasEscaped,
+        time: pointTime,
+        distance: hoverDistance,
+        index: i,
+      });
+    }
+
+    // Contat the 3 sub-arrays
+    results = results[0].concat(results[1], results[2]);
+
+    // Time of the point closer to pointer
+    results.time = minTime;
+
+    return results;
+  };
+
+  elem.mouseleave(function() {
+    if (panel.tooltip.shared) {
+      let plot = elem.data().plot;
+      if (plot) {
+        $tooltip.detach();
+        plot.unhighlight();
+      }
+    }
+    appEvents.emit('graph-hover-clear');
+  });
+
+  elem.bind('plothover', function(event, pos, item) {
+    self.show(pos, item);
+
+    // broadcast to other graph panels that we are hovering!
+    pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
+    appEvents.emit('graph-hover', { pos: pos, panel: panel });
+  });
+
+  elem.bind('plotclick', function(event, pos, item) {
+    appEvents.emit('graph-click', { pos: pos, panel: panel, item: item });
+  });
+
+  this.clear = function(plot) {
+    $tooltip.detach();
+    plot.clearCrosshair();
+    plot.unhighlight();
+  };
+
+  this.show = function(pos, item) {
+    let plot = elem.data().plot;
+    let plotData = plot.getData();
+    let xAxes = plot.getXAxes();
+    let xMode = xAxes[0].options.mode;
+    let seriesList = getSeriesFn();
+    let allSeriesMode = panel.tooltip.shared;
+    let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
+
+    // if panelRelY is defined another panel wants us to show a tooltip
+    // get pageX from position on x axis and pageY from relative position in original panel
+    if (pos.panelRelY) {
+      let pointOffset = plot.pointOffset({ x: pos.x });
+      if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
+        self.clear(plot);
+        return;
+      }
+      pos.pageX = elem.offset().left + pointOffset.left;
+      pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
+      let isVisible =
+        pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
+      if (!isVisible) {
+        self.clear(plot);
+        return;
+      }
+      plot.setCrosshair(pos);
+      allSeriesMode = true;
+
+      if (dashboard.sharedCrosshairModeOnly()) {
+        // if only crosshair mode we are done
+        return;
+      }
+    }
+
+    if (seriesList.length === 0) {
+      return;
+    }
+
+    if (seriesList[0].hasMsResolution) {
+      tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
+    } else {
+      tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
+    }
+
+    if (allSeriesMode) {
+      plot.unhighlight();
+
+      let seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
+
+      seriesHtml = '';
+
+      absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
+
+      // Dynamically reorder the hovercard for the current time point if the
+      // option is enabled.
+      if (panel.tooltip.sort === 2) {
+        seriesHoverInfo.sort(function(a, b) {
+          return b.value - a.value;
+        });
+      } else if (panel.tooltip.sort === 1) {
+        seriesHoverInfo.sort(function(a, b) {
+          return a.value - b.value;
+        });
+      }
+
+      for (i = 0; i < seriesHoverInfo.length; i++) {
+        hoverInfo = seriesHoverInfo[i];
+
+        if (hoverInfo.hidden) {
+          continue;
+        }
+
+        let highlightClass = '';
+        if (item && hoverInfo.index === item.seriesIndex) {
+          highlightClass = 'graph-tooltip-list-item--highlight';
+        }
+
+        series = seriesList[hoverInfo.index];
+
+        value = series.formatValue(hoverInfo.value);
+
+        seriesHtml +=
+          '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
+        seriesHtml +=
+          '<i class="fa fa-minus" style="color:' + hoverInfo.color + ';"></i> ' + hoverInfo.label + ':</div>';
+        seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
+        plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
+      }
+
+      self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
+    } else if (item) {
+      // single series tooltip
+      series = seriesList[item.seriesIndex];
+      group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
+      group +=
+        '<i class="fa fa-minus" style="color:' + item.series.color + ';"></i> ' + series.aliasEscaped + ':</div>';
+
+      if (panel.stack && panel.tooltip.value_type === 'individual') {
+        value = item.datapoint[1] - item.datapoint[2];
+      } else {
+        value = item.datapoint[1];
+      }
+
+      value = series.formatValue(value);
+
+      absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
+
+      group += '<div class="graph-tooltip-value">' + value + '</div>';
+
+      self.renderAndShow(absoluteTime, group, pos, xMode);
+    } else {
+      // no hit
+      $tooltip.detach();
+    }
+  };
+}

+ 42 - 16
public/app/plugins/panel/graph/legend.ts

@@ -1,7 +1,7 @@
 import angular from 'angular';
 import _ from 'lodash';
 import $ from 'jquery';
-import PerfectScrollbar from 'perfect-scrollbar';
+import baron from 'baron';
 
 var module = angular.module('grafana.directives');
 
@@ -16,11 +16,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
       var i;
       var legendScrollbar;
       const legendRightDefaultWidth = 10;
+      let legendElem = elem.parent();
 
       scope.$on('$destroy', function() {
-        if (legendScrollbar) {
-          legendScrollbar.destroy();
-        }
+        destroyScrollbar();
       });
 
       ctrl.events.on('render-legend', () => {
@@ -112,7 +111,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
       }
 
       function render() {
-        let legendWidth = elem.width();
+        let legendWidth = legendElem.width();
         if (!ctrl.panel.legend.show) {
           elem.empty();
           firstRender = true;
@@ -131,8 +130,11 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
         elem.empty();
 
         // Set min-width if side style and there is a value, otherwise remove the CSS propery
-        var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
-        elem.css('min-width', width);
+        // Set width so it works with IE11
+        var width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
+        var ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : '';
+        legendElem.css('min-width', width);
+        legendElem.css('width', ieWidth);
 
         elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
 
@@ -238,8 +240,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
           tbodyElem.append(tableHeaderElem);
           tbodyElem.append(seriesElements);
           elem.append(tbodyElem);
+          tbodyElem.wrap('<div class="graph-legend-scroll"></div>');
         } else {
-          elem.append(seriesElements);
+          elem.append('<div class="graph-legend-scroll"></div>');
+          elem.find('.graph-legend-scroll').append(seriesElements);
         }
 
         if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) {
@@ -250,23 +254,45 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
       }
 
       function addScrollbar() {
-        const scrollbarOptions = {
-          // Number of pixels the content height can surpass the container height without enabling the scroll bar.
-          scrollYMarginOffset: 2,
-          suppressScrollX: true,
-          wheelPropagation: true,
+        const scrollRootClass = 'baron baron__root';
+        const scrollerClass = 'baron__scroller';
+        const scrollBarHTML = `
+          <div class="baron__track">
+            <div class="baron__bar"></div>
+          </div>
+        `;
+
+        let scrollRoot = elem;
+        let scroller = elem.find('.graph-legend-scroll');
+
+        // clear existing scroll bar track to prevent duplication
+        scrollRoot.find('.baron__track').remove();
+
+        scrollRoot.addClass(scrollRootClass);
+        $(scrollBarHTML).appendTo(scrollRoot);
+        scroller.addClass(scrollerClass);
+
+        let scrollbarParams = {
+          root: scrollRoot[0],
+          scroller: scroller[0],
+          bar: '.baron__bar',
+          track: '.baron__track',
+          barOnCls: '_scrollbar',
+          scrollingCls: '_scrolling',
         };
 
         if (!legendScrollbar) {
-          legendScrollbar = new PerfectScrollbar(elem[0], scrollbarOptions);
+          legendScrollbar = baron(scrollbarParams);
         } else {
-          legendScrollbar.update();
+          destroyScrollbar();
+          legendScrollbar = baron(scrollbarParams);
         }
+        legendScrollbar.scroll();
       }
 
       function destroyScrollbar() {
         if (legendScrollbar) {
-          legendScrollbar.destroy();
+          legendScrollbar.dispose();
           legendScrollbar = undefined;
         }
       }

+ 3 - 2
public/app/plugins/panel/graph/specs/tooltip_specs.ts

@@ -11,6 +11,7 @@ var scope = {
 
 var elem = $('<div></div>');
 var dashboard = {};
+var getSeriesFn;
 
 function describeSharedTooltip(desc, fn) {
   var ctx: any = {};
@@ -30,7 +31,7 @@ function describeSharedTooltip(desc, fn) {
   describe(desc, function() {
     beforeEach(function() {
       ctx.setupFn();
-      var tooltip = new GraphTooltip(elem, dashboard, scope);
+      var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
       ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
     });
 
@@ -39,7 +40,7 @@ function describeSharedTooltip(desc, fn) {
 }
 
 describe('findHoverIndexFromData', function() {
-  var tooltip = new GraphTooltip(elem, dashboard, scope);
+  var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
   var series = {
     data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]],
   };

+ 3 - 1
public/app/plugins/panel/graph/template.ts

@@ -3,7 +3,9 @@ var template = `
   <div class="graph-panel__chart" grafana-graph ng-dblclick="ctrl.zoomOut()">
   </div>
 
-  <div class="graph-legend" graph-legend></div>
+  <div class="graph-legend">
+    <div class="graph-legend-content" graph-legend></div>
+  </div>
 </div>
 `;
 

+ 3 - 3
public/app/plugins/panel/table/column_options.html

@@ -163,10 +163,10 @@
           <span>
             Use special variables to specify cell values:
             <br>
-            <em>$__cell</em> refers to current cell value
+            <em>${__cell}</em> refers to current cell value
             <br>
-            <em>$__cell_n</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
-            <em>$__cell_1</em> refers to second column's value.
+            <em>${__cell_n}</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
+            <em>${__cell_1}</em> refers to second column's value.
           </span>
         </info-popover>
       </div>

+ 1 - 2
public/sass/_variables.light.scss

@@ -59,9 +59,8 @@ $critical: #ec2128;
 $body-bg: $gray-7;
 $page-bg: $gray-7;
 $body-color: $gray-1;
-//$text-color: $dark-4;
 $text-color: $gray-1;
-$text-color-strong: $white;
+$text-color-strong: $dark-2;
 $text-color-weak: $gray-2;
 $text-color-faint: $gray-4;
 $text-color-emphasis: $dark-5;

+ 8 - 1
public/sass/components/_panel_add_panel.scss

@@ -1,5 +1,13 @@
+.add-panel-container {
+  height: 100%;
+}
+
 .add-panel {
   height: 100%;
+
+  .baron__root {
+    height: calc(100% - 43px);
+  }
 }
 
 .add-panel__header {
@@ -39,7 +47,6 @@
   flex-direction: row;
   flex-wrap: wrap;
   overflow: auto;
-  height: calc(100% - 43px);
   align-content: flex-start;
   justify-content: space-around;
   position: relative;

+ 30 - 1
public/sass/components/_panel_graph.scss

@@ -49,6 +49,7 @@
 }
 
 .graph-legend {
+  display: flex;
   flex: 0 1 auto;
   max-height: 30%;
   margin: 0;
@@ -56,11 +57,27 @@
   padding-top: 6px;
   position: relative;
 
+  // fix for Firefox (white stripe on the right of scrollbar)
+  width: calc(100% - 1px);
+
   .popover-content {
     padding: 0;
   }
 }
 
+.graph-legend-content {
+  position: relative;
+
+  // fix for Firefox (white stripe on the right of scrollbar)
+  width: calc(100% - 1px);
+}
+
+.graph-legend-scroll {
+  position: relative;
+  overflow: auto !important;
+  padding: 1px;
+}
+
 .graph-legend-icon {
   position: relative;
   padding-right: 4px;
@@ -115,8 +132,20 @@
 // fix for phantomjs
 .body--phantomjs {
   .graph-panel--legend-right {
+    .graph-legend {
+      display: inline-block;
+    }
+
+    .graph-panel__chart {
+      display: flex;
+    }
+
     .graph-legend-table {
       display: table;
+
+      .graph-legend-scroll {
+        display: table;
+      }
     }
   }
 }
@@ -124,9 +153,9 @@
 .graph-legend-table {
   tbody {
     display: block;
+    position: relative;
     overflow-y: auto;
     overflow-x: hidden;
-    height: 100%;
     padding-bottom: 1px;
     padding-right: 5px;
     padding-left: 5px;

+ 123 - 1
public/sass/components/_scrollbar.scss

@@ -9,6 +9,11 @@
   -ms-touch-action: auto;
 }
 
+// ._scrollbar {
+//   overflow-x: hidden !important;
+//   overflow-y: auto;
+// }
+
 /*
  * Scrollbar rail styles
  */
@@ -101,7 +106,7 @@
   opacity: 0.9;
 }
 
-// Srollbars
+// Scrollbars
 //
 
 ::-webkit-scrollbar {
@@ -172,3 +177,120 @@
   border-top: 1px solid $scrollbarBorder;
   border-left: 1px solid $scrollbarBorder;
 }
+
+// Baron styles
+
+.baron {
+  // display: inline-block; // this brakes phantomjs rendering (width becomes 0)
+  overflow: hidden;
+}
+
+// Fix for side menu on mobile devices
+.main-view.baron {
+  width: unset;
+}
+
+.baron__clipper {
+  position: relative;
+  overflow: hidden;
+}
+
+.baron__scroller {
+  overflow-y: scroll;
+  -ms-overflow-style: none;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  margin: 0;
+  border: 0;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  -webkit-overflow-scrolling: touch;
+  /* remove line to customize scrollbar in iOs */
+}
+
+.baron__scroller::-webkit-scrollbar {
+  width: 0;
+  height: 0;
+}
+
+.baron__track {
+  display: none;
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.baron._scrollbar .baron__track {
+  display: block;
+}
+
+.baron__free {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  right: 0;
+}
+
+.baron__bar {
+  display: none;
+  position: absolute;
+  right: 0;
+  z-index: 1;
+  // width: 10px;
+  background: #999;
+
+  // height: 15px;
+  width: 15px;
+  transition: background-color 0.2s linear, opacity 0.2s linear;
+  opacity: 0;
+}
+
+.baron._scrollbar .baron__bar {
+  display: block;
+
+  @include gradient-vertical($scrollbarBackground, $scrollbarBackground2);
+  border-radius: 6px;
+  width: 6px;
+  /* there must be 'right' for ps__thumb-y */
+  right: 0px;
+  /* please don't change 'position' */
+  position: absolute;
+
+  // background-color: transparent;
+  // opacity: 0.6;
+
+  &:hover,
+  &:focus {
+    // background-color: transparent;
+    opacity: 0.9;
+  }
+}
+
+.panel-hover-highlight .baron__track .baron__bar {
+  opacity: 0.6;
+}
+
+.baron._scrolling > .baron__track .baron__bar {
+  opacity: 0.9;
+}
+
+// fix for phantomjs
+.body--phantomjs .baron__track .baron__bar {
+  opacity: 0 !important;
+}
+
+.baron__control {
+  display: none;
+}
+
+.baron.panel-content--scrollable {
+  // Width needs to be set to prevent content width issues
+  // Set to less than 100% for fixing Firefox issue (white stripe on the right of scrollbar)
+  width: calc(100% - 2px);
+
+  .baron__scroller {
+    padding-top: 1px;
+  }
+}

+ 12 - 2
public/sass/components/_search.scss

@@ -31,7 +31,6 @@
     //padding: 0.5rem 1.5rem 0.5rem 0;
     padding: 1rem 1rem 0.75rem 1rem;
     height: 51px;
-    line-height: 51px;
     box-sizing: border-box;
     outline: none;
     background: $side-menu-bg;
@@ -61,6 +60,10 @@
   display: flex;
   flex-direction: column;
   flex-grow: 1;
+
+  .search-item--indent {
+    margin-left: 14px;
+  }
 }
 
 .search-dropdown__col_2 {
@@ -99,14 +102,21 @@
   }
 }
 
+.search-results-scroller {
+  display: flex;
+  position: relative;
+}
+
 .search-results-container {
-  height: 100%;
   display: block;
   padding: $spacer;
   position: relative;
   flex-grow: 10;
   margin-bottom: 1rem;
 
+  // Fix for search scroller in mobile view
+  height: unset;
+
   .label-tag {
     margin-left: 6px;
     font-size: 11px;

+ 3 - 0
public/sass/components/_sidemenu.scss

@@ -123,6 +123,8 @@
     position: relative;
     opacity: 0.7;
     font-size: 130%;
+    height: 22px;
+    width: 22px;
   }
 
   .fa {
@@ -178,6 +180,7 @@ li.sidemenu-org-switcher {
   padding: 0.4rem 1rem 0.4rem 0.65rem;
   min-height: $navbarHeight;
   position: relative;
+  height: $navbarHeight - 1px;
 
   &:hover {
     background: $navbarButtonBackgroundHighlight;

+ 1 - 1
public/sass/components/_tabbed_view.scss

@@ -43,7 +43,7 @@
     font-size: 120%;
   }
   &:hover {
-    color: $white;
+    color: $text-color-strong;
   }
 }
 

+ 8 - 0
public/sass/layout/_page.scss

@@ -28,12 +28,20 @@
   width: 100%;
   overflow: auto;
   height: 100%;
+  -webkit-overflow-scrolling: touch;
 
   &--dashboard {
     height: calc(100% - 56px);
   }
 }
 
+// fix for phantomjs
+.body--phantomjs {
+  .scroll-canvas {
+    overflow: hidden;
+  }
+}
+
 .page-body {
   padding-top: $spacer*2;
   min-height: 500px;

+ 2 - 1
public/sass/pages/_alerting.scss

@@ -108,7 +108,8 @@
   justify-content: center;
   align-items: center;
   width: 40px;
-  padding: 0 28px 0 16px;
+  //margin-right: 8px;
+  padding: 0 4px 0 2px;
   .icon-gf,
   .fa {
     font-size: 200%;

+ 7 - 1
public/sass/pages/_login.scss

@@ -3,6 +3,7 @@ $login-border: #8daac5;
 .login {
   background-position: center;
   min-height: 85vh;
+  height: 80vh;
   background-repeat: no-repeat;
   min-width: 100%;
   margin-left: 0;
@@ -290,9 +291,14 @@ select:-webkit-autofill:focus {
 }
 
 @include media-breakpoint-up(md) {
+  .login-content {
+    flex: 1 0 100%;
+  }
+
   .login-branding {
     width: 45%;
     padding: 2rem 4rem;
+    flex-grow: 1;
 
     .logo-icon {
       width: 130px;
@@ -371,7 +377,7 @@ select:-webkit-autofill:focus {
       left: 0;
       right: 0;
       height: 100%;
-      content: "";
+      content: '';
       display: block;
     }
 

+ 2 - 2
public/views/index.template.html

@@ -16,7 +16,7 @@
   <link rel="icon" type="image/png" href="public/img/fav32.png">
   <link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
   <link rel="apple-touch-icon" href="public/img/fav32.png">
-  
+
 </head>
 
 <body ng-cloak class="theme-[[ .Theme ]]">
@@ -40,7 +40,7 @@
     </div>
 
     <div class="main-view">
-      <div class="scroll-canvas" grafana-scrollbar>
+      <div class="scroll-canvas" page-scrollbar>
         <div ng-view></div>
 
         <footer class="footer">

+ 1 - 14
scripts/circle-test-backend.sh

@@ -20,17 +20,4 @@ echo "building backend with install to cache pkgs"
 exit_if_fail time go install ./pkg/cmd/grafana-server
 
 echo "running go test"
-
-set -e
-echo "" > coverage.txt
-
-time for d in $(go list ./pkg/...); do
-  exit_if_fail go test -coverprofile=profile.out -covermode=atomic $d
-  if [ -f profile.out ]; then
-    cat profile.out >> coverage.txt
-    rm profile.out
-  fi
-done
-
-echo "Publishing go code coverage"
-bash <(curl -s https://codecov.io/bash) -cF go
+go test ./pkg/...

+ 4 - 4
yarn.lock

@@ -1162,6 +1162,10 @@ balanced-match@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
 
+baron@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/baron/-/baron-3.0.3.tgz#0f0a08a567062882e130a0ecfd41a46d52103f4a"
+
 base64-arraybuffer@0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
@@ -7503,10 +7507,6 @@ pend@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
 
-perfect-scrollbar@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/perfect-scrollbar/-/perfect-scrollbar-1.2.0.tgz#ad23a2529c17f4535f21d1486f8bc3046e31a9d2"
-
 performance-now@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"