Przeglądaj źródła

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

Sven Klemm 7 lat temu
rodzic
commit
1c0a188272
39 zmienionych plików z 653 dodań i 127 usunięć
  1. 2 1
      CHANGELOG.md
  2. 1 1
      appveyor.yml
  3. 1 0
      docs/sources/administration/provisioning.md
  4. 68 0
      docs/sources/features/datasources/cloudwatch.md
  5. 20 0
      docs/sources/features/datasources/elasticsearch.md
  6. 18 0
      docs/sources/features/datasources/graphite.md
  7. 19 0
      docs/sources/features/datasources/influxdb.md
  8. 18 0
      docs/sources/features/datasources/mysql.md
  9. 19 0
      docs/sources/features/datasources/opentsdb.md
  10. 22 0
      docs/sources/features/datasources/postgres.md
  11. 17 1
      docs/sources/features/datasources/prometheus.md
  12. 1 1
      package.json
  13. 0 6
      pkg/api/pluginproxy/ds_proxy.go
  14. 4 1
      pkg/services/alerting/notifiers/dingding.go
  15. 1 1
      pkg/services/sqlstore/annotation.go
  16. 21 10
      public/app/core/components/ScrollBar/ScrollBar.tsx
  17. 1 0
      public/app/core/components/grafana_app.ts
  18. 41 0
      public/app/core/components/scroll/page_scroll.ts
  19. 46 6
      public/app/core/components/scroll/scroll.ts
  20. 2 0
      public/app/core/components/search/search.html
  21. 2 0
      public/app/core/core.ts
  22. 1 1
      public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
  23. 2 1
      public/app/features/dashboard/view_state_srv.ts
  24. 30 4
      public/app/features/panel/panel_directive.ts
  25. 3 1
      public/app/features/templating/query_variable.ts
  26. 12 12
      public/app/partials/dashboard.html
  27. 25 25
      public/app/plugins/datasource/influxdb/datasource.ts
  28. 14 0
      public/app/plugins/datasource/influxdb/partials/config.html
  29. 16 14
      public/app/plugins/panel/dashlist/module.html
  30. 39 16
      public/app/plugins/panel/graph/legend.ts
  31. 3 1
      public/app/plugins/panel/graph/template.ts
  32. 8 1
      public/sass/components/_panel_add_panel.scss
  33. 30 1
      public/sass/components/_panel_graph.scss
  34. 123 1
      public/sass/components/_scrollbar.scss
  35. 8 1
      public/sass/components/_search.scss
  36. 8 0
      public/sass/layout/_page.scss
  37. 2 2
      public/views/index.template.html
  38. 1 14
      scripts/circle-test-backend.sh
  39. 4 4
      yarn.lock

+ 2 - 1
CHANGELOG.md

@@ -28,7 +28,8 @@
 * **AuthProxy**: Support IPv6 in Auth proxy white list [#11330](https://github.com/grafana/grafana/pull/11330), thx [@corny](https://github.com/corny)
 * **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)
 * **SMTP**: Don't connect to STMP server using TLS unless configured. [#7189](https://github.com/grafana/grafana/issues/7189)
 * **Prometheus**: Escape backslash in labels correctly. [#10555](https://github.com/grafana/grafana/issues/10555), thx [@roidelapluie](https://github.com/roidelapluie)
 * **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)
+* **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)
 # 5.0.4 (2018-03-28)
 
 

+ 1 - 1
appveyor.yml

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

+ 1 - 0
docs/sources/administration/provisioning.md

@@ -138,6 +138,7 @@ datasources:
 ```
 ```
 
 
 #### Custom Settings per Datasource
 #### Custom Settings per Datasource
+Please refer to each datasource documentation for specific provisioning examples.
 
 
 | Datasource | Misc |
 | Datasource | Misc |
 | ---- | ---- |
 | ---- | ---- |

+ 68 - 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)
 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
 ### AWS credentials file
 
 
 Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.
 Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.
@@ -173,3 +207,37 @@ Amazon provides 1 million CloudWatch API requests each month at no additional ch
 it costs $0.01 per 1,000 GetMetricStatistics or ListMetrics requests. For each query Grafana will
 it costs $0.01 per 1,000 GetMetricStatistics or ListMetrics requests. For each query Grafana will
 issue a GetMetricStatistics request and every time you pick a dimension in the query editor
 issue a GetMetricStatistics request and every time you pick a dimension in the query editor
 Grafana will issue a ListMetrics request.
 Grafana will issue a ListMetrics request.
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+Using a credentials file
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Cloudwatch
+    type: cloudwatch
+    jsonData:
+      authType: credentials
+      defaultRegion: eu-west-2
+```
+
+Using `accessKey` and `secretKey`
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Cloudwatch
+    type: cloudwatch
+    jsonData:
+      authType: keys
+      defaultRegion: eu-west-2
+    secureJsonData:
+      accessKey: "<your access key>"
+      secretKey: "<your secret key>"
+```

+ 20 - 0
docs/sources/features/datasources/elasticsearch.md

@@ -137,3 +137,23 @@ Query | You can leave the search query blank or specify a lucene query
 Time | The name of the time field, needs to be date field.
 Time | The name of the time field, needs to be date field.
 Text | Event description field.
 Text | Event description field.
 Tags | Optional field name to use for event tags (can be an array or a CSV string).
 Tags | Optional field name to use for event tags (can be an array or a CSV string).
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Elastic
+    type: elasticsearch
+    access: proxy
+    database: "[metrics-]YYYY.MM.DD"
+    url: http://localhost:9200
+    jsonData:
+      interval: Daily
+      timeField: "@timestamp"
+```

+ 18 - 0
docs/sources/features/datasources/graphite.md

@@ -120,3 +120,21 @@ queries via the Dashboard menu / Annotations view.
 
 
 Graphite supports two ways to query annotations. A regular metric query, for this you use the `Graphite query` textbox. A Graphite events query, use the `Graphite event tags` textbox,
 Graphite supports two ways to query annotations. A regular metric query, for this you use the `Graphite query` textbox. A Graphite events query, use the `Graphite event tags` textbox,
 specify a tag or wildcard (leave empty should also work)
 specify a tag or wildcard (leave empty should also work)
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Graphite
+    type: graphite
+    access: proxy
+    url: http://localhost:8080
+    jsonData:
+      graphiteVersion: "1.1"
+```

+ 19 - 0
docs/sources/features/datasources/influxdb.md

@@ -174,3 +174,22 @@ SELECT title, description from events WHERE $timeFilter order asc
 For InfluxDB you need to enter a query like in the above example. You need to have the ```where $timeFilter```
 For InfluxDB you need to enter a query like in the above example. You need to have the ```where $timeFilter```
 part. If you only select one column you will not need to enter anything in the column mapping fields. The
 part. If you only select one column you will not need to enter anything in the column mapping fields. The
 Tags field can be a comma separated string.
 Tags field can be a comma separated string.
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: InfluxDB
+    type: influxdb
+    access: proxy
+    database: site
+    user: grafana
+    password: grafana
+    url: http://localhost:8086
+```

+ 18 - 0
docs/sources/features/datasources/mysql.md

@@ -225,3 +225,21 @@ tags | Optional field name to use for event tags as a comma separated string.
 ## Alerting
 ## Alerting
 
 
 Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule conditions.
 Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule conditions.
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: MySQL
+    type: mysql
+    url: localhost:3306
+    database: grafana
+    user: grafana
+    password: password
+```

+ 19 - 0
docs/sources/features/datasources/opentsdb.md

@@ -88,3 +88,22 @@ Query | Description
 *tag_values(cpu, hostanme, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname
 *tag_values(cpu, hostanme, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname
 
 
 For details on OpenTSDB metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html)
 For details on OpenTSDB metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html)
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: OpenTsdb
+    type: opentsdb
+    access: proxy
+    url: http://localhost:4242
+    jsonData:
+      tsdbResolution: 1
+      tsdbVersion: 1
+```

+ 22 - 0
docs/sources/features/datasources/postgres.md

@@ -217,3 +217,25 @@ tags | Optional field name to use for event tags as a comma separated string.
 
 
 Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule
 Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule
 conditions.
 conditions.
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Postgres
+    type: postgres
+    url: localhost:5432
+    database: grafana
+    user: grafana
+    secureJsonData:
+      password: "Password!"
+    jsonData:
+      sslmode: "disable" # disable/require/verify-ca/verify-full
+
+```

+ 17 - 1
docs/sources/features/datasources/prometheus.md

@@ -34,7 +34,7 @@ Name | Description
 *Basic Auth* | Enable basic authentication to the Prometheus data source.
 *Basic Auth* | Enable basic authentication to the Prometheus data source.
 *User* | Name of your Prometheus user
 *User* | Name of your Prometheus user
 *Password* | Database user's password
 *Password* | Database user's password
-*Scrape interval* | This will be used as a lower limit for the Prometheus step query parameter. Default value is 15s. 
+*Scrape interval* | This will be used as a lower limit for the Prometheus step query parameter. Default value is 15s.
 
 
 ## Query editor
 ## Query editor
 
 
@@ -100,3 +100,19 @@ The step option is useful to limit the number of events returned from your query
 ## Getting Grafana metrics into Prometheus
 ## Getting Grafana metrics into Prometheus
 
 
 Since 4.6.0 Grafana exposes metrics for Prometheus on the `/metrics` endpoint. We also bundle a dashboard within Grafana so you can get started viewing your metrics faster. You can import the bundled dashboard by going to the data source edit page and click the dashboard tab. There you can find a dashboard for Grafana and one for Prometheus. Import and start viewing all the metrics!
 Since 4.6.0 Grafana exposes metrics for Prometheus on the `/metrics` endpoint. We also bundle a dashboard within Grafana so you can get started viewing your metrics faster. You can import the bundled dashboard by going to the data source edit page and click the dashboard tab. There you can find a dashboard for Grafana and one for Prometheus. Import and start viewing all the metrics!
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+  - name: Prometheus
+    type: prometheus
+    access: proxy
+    url: http://localhost:9090
+```

+ 1 - 1
package.json

@@ -136,6 +136,7 @@
     "angular-route": "^1.6.6",
     "angular-route": "^1.6.6",
     "angular-sanitize": "^1.6.6",
     "angular-sanitize": "^1.6.6",
     "babel-polyfill": "^6.26.0",
     "babel-polyfill": "^6.26.0",
+    "baron": "^3.0.3",
     "brace": "^0.10.0",
     "brace": "^0.10.0",
     "classnames": "^2.2.5",
     "classnames": "^2.2.5",
     "clipboard": "^1.7.1",
     "clipboard": "^1.7.1",
@@ -151,7 +152,6 @@
     "moment": "^2.18.1",
     "moment": "^2.18.1",
     "mousetrap": "^1.6.0",
     "mousetrap": "^1.6.0",
     "mousetrap-global-bind": "^1.1.0",
     "mousetrap-global-bind": "^1.1.0",
-    "perfect-scrollbar": "^1.2.0",
     "prop-types": "^15.6.0",
     "prop-types": "^15.6.0",
     "react": "^16.2.0",
     "react": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-dom": "^16.2.0",

+ 0 - 6
pkg/api/pluginproxy/ds_proxy.go

@@ -189,12 +189,6 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
 }
 }
 
 
 func (proxy *DataSourceProxy) validateRequest() error {
 func (proxy *DataSourceProxy) validateRequest() error {
-	if proxy.ds.Type == m.DS_INFLUXDB {
-		if proxy.ctx.Query("db") != proxy.ds.Database {
-			return errors.New("Datasource is not configured to allow this database")
-		}
-	}
-
 	if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) {
 	if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) {
 		return errors.New("Target url is not a valid target")
 		return errors.New("Target url is not a valid target")
 	}
 	}

+ 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)
 		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{
 	cmd := &m.SendWebhookSync{
 		Url:  this.Url,
 		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 {
 	if query.Limit == 0 {
-		query.Limit = 10
+		query.Limit = 100
 	}
 	}
 
 
 	sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
 	sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))

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

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-import PerfectScrollbar from 'perfect-scrollbar';
+import baron from 'baron';
 
 
 export interface Props {
 export interface Props {
   children: any;
   children: any;
@@ -8,31 +8,36 @@ export interface Props {
 
 
 export default class ScrollBar extends React.Component<Props, any> {
 export default class ScrollBar extends React.Component<Props, any> {
   private container: any;
   private container: any;
-  private ps: PerfectScrollbar;
+  private scrollbar: baron;
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
   }
   }
 
 
   componentDidMount() {
   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() {
   componentDidUpdate() {
-    this.ps.update();
+    this.scrollbar.update();
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    this.ps.destroy();
+    this.scrollbar.dispose();
   }
   }
 
 
   // methods can be invoked by outside
   // methods can be invoked by outside
   setScrollTop(top) {
   setScrollTop(top) {
     if (this.container) {
     if (this.container) {
       this.container.scrollTop = top;
       this.container.scrollTop = top;
-      this.ps.update();
+      this.scrollbar.update();
 
 
       return true;
       return true;
     }
     }
@@ -42,7 +47,7 @@ export default class ScrollBar extends React.Component<Props, any> {
   setScrollLeft(left) {
   setScrollLeft(left) {
     if (this.container) {
     if (this.container) {
       this.container.scrollLeft = left;
       this.container.scrollLeft = left;
-      this.ps.update();
+      this.scrollbar.update();
 
 
       return true;
       return true;
     }
     }
@@ -55,8 +60,14 @@ export default class ScrollBar extends React.Component<Props, any> {
 
 
   render() {
   render() {
     return (
     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>
       </div>
     );
     );
   }
   }

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

@@ -167,6 +167,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
           if (sidemenuHidden) {
           if (sidemenuHidden) {
             sidemenuHidden = false;
             sidemenuHidden = false;
             body.addClass('sidemenu-open');
             body.addClass('sidemenu-open');
+            appEvents.emit('toggle-inactive-mode');
             $timeout(function() {
             $timeout(function() {
               $rootScope.$broadcast('render');
               $rootScope.$broadcast('render');
             }, 100);
             }, 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 coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
 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() {
 export function geminiScrollbar() {
   return {
   return {
     restrict: 'A',
     restrict: 'A',
     link: function(scope, elem, attrs) {
     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;
       let lastPos = 0;
 
 
       appEvents.on(
       appEvents.on(
@@ -31,13 +60,24 @@ export function geminiScrollbar() {
         scope
         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', () => {
       scope.$on('$routeChangeSuccess', () => {
         lastPos = 0;
         lastPos = 0;
         elem[0].scrollTop = 0;
         elem[0].scrollTop = 0;
       });
       });
 
 
       scope.$on('$destroy', () => {
       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">
     <div class="search-dropdown__col_1">
     <div class="search-dropdown__col_1">
+      <div class="search-results-scroller">
         <div class="search-results-container" grafana-scrollbar>
         <div class="search-results-container" grafana-scrollbar>
           <h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
           <h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
           <dashboard-search-results
           <dashboard-search-results
@@ -27,6 +28,7 @@
             on-folder-expanding="ctrl.folderExpanding()"
             on-folder-expanding="ctrl.folderExpanding()"
             on-folder-expanded="ctrl.folderExpanded($folder)" />
             on-folder-expanded="ctrl.folderExpanded($folder)" />
         </div>
         </div>
+      </div>
     </div>
     </div>
 
 
     <div class="search-dropdown__col_2">
     <div class="search-dropdown__col_2">

+ 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 { userPicker } from './components/user_picker';
 import { teamPicker } from './components/team_picker';
 import { teamPicker } from './components/team_picker';
 import { geminiScrollbar } from './components/scroll/scroll';
 import { geminiScrollbar } from './components/scroll/scroll';
+import { pageScrollbar } from './components/scroll/page_scroll';
 import { gfPageDirective } from './components/gf_page';
 import { gfPageDirective } from './components/gf_page';
 import { orgSwitcher } from './components/org_switcher';
 import { orgSwitcher } from './components/org_switcher';
 import { profiler } from './profiler';
 import { profiler } from './profiler';
@@ -85,6 +86,7 @@ export {
   userPicker,
   userPicker,
   teamPicker,
   teamPicker,
   geminiScrollbar,
   geminiScrollbar,
+  pageScrollbar,
   gfPageDirective,
   gfPageDirective,
   orgSwitcher,
   orgSwitcher,
   manageDashboardsDirective,
   manageDashboardsDirective,

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

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

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

@@ -196,9 +196,10 @@ export class DashboardViewState {
     this.oldTimeRange = ctrl.range;
     this.oldTimeRange = ctrl.range;
     this.fullscreenPanel = panelScope;
     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.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
     this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id });
     this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id });
-    this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
   }
   }
 
 
   registerPanel(panelScope) {
   registerPanel(panelScope) {

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

@@ -1,6 +1,7 @@
 import angular from 'angular';
 import angular from 'angular';
+import $ from 'jquery';
 import Drop from 'tether-drop';
 import Drop from 'tether-drop';
-import PerfectScrollbar from 'perfect-scrollbar';
+import baron from 'baron';
 
 
 var module = angular.module('grafana.directives');
 var module = angular.module('grafana.directives');
 
 
@@ -86,6 +87,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
 
 
       function panelHeightUpdated() {
       function panelHeightUpdated() {
         panelContent.css({ height: ctrl.height + 'px' });
         panelContent.css({ height: ctrl.height + 'px' });
+      }
+
+      function resizeScrollableContent() {
         if (panelScrollbar) {
         if (panelScrollbar) {
           panelScrollbar.update();
           panelScrollbar.update();
         }
         }
@@ -100,9 +104,30 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
       // update scrollbar after mounting
       // update scrollbar after mounting
       ctrl.events.on('component-did-mount', () => {
       ctrl.events.on('component-did-mount', () => {
         if (ctrl.__proto__.constructor.scrollable) {
         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();
         ctrl.calculatePanelHeight();
         panelHeightUpdated();
         panelHeightUpdated();
         $timeout(() => {
         $timeout(() => {
+          resizeScrollableContent();
           ctrl.render();
           ctrl.render();
         });
         });
       });
       });
@@ -199,7 +225,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
         }
         }
 
 
         if (panelScrollbar) {
         if (panelScrollbar) {
-          panelScrollbar.update();
+          panelScrollbar.dispose();
         }
         }
       });
       });
     },
     },

+ 3 - 1
public/app/features/templating/query_variable.ts

@@ -198,7 +198,9 @@ export class QueryVariable implements Variable {
         }
         }
       });
       });
     } else if (sortType === 3) {
     } else if (sortType === 3) {
-      options = _.sortBy(options, opt => { return _.toLower(opt.text); });
+      options = _.sortBy(options, opt => {
+        return _.toLower(opt.text);
+      });
     }
     }
 
 
     if (reverseSort) {
     if (reverseSort) {

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

@@ -1,18 +1,18 @@
 <div dash-class ng-if="ctrl.dashboard">
 <div dash-class ng-if="ctrl.dashboard">
 	<dashnav dashboard="ctrl.dashboard"></dashnav>
 	<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>
 </div>

+ 25 - 25
public/app/plugins/datasource/influxdb/datasource.ts

@@ -82,7 +82,7 @@ export default class InfluxDatasource {
     // replace templated variables
     // replace templated variables
     allQueries = this.templateSrv.replace(allQueries, scopedVars);
     allQueries = this.templateSrv.replace(allQueries, scopedVars);
 
 
-    return this._seriesQuery(allQueries).then((data): any => {
+    return this._seriesQuery(allQueries, options).then((data): any => {
       if (!data || !data.results) {
       if (!data || !data.results) {
         return [];
         return [];
       }
       }
@@ -135,7 +135,7 @@ export default class InfluxDatasource {
     var query = options.annotation.query.replace('$timeFilter', timeFilter);
     var query = options.annotation.query.replace('$timeFilter', timeFilter);
     query = this.templateSrv.replace(query, null, 'regex');
     query = this.templateSrv.replace(query, null, 'regex');
 
 
-    return this._seriesQuery(query).then(data => {
+    return this._seriesQuery(query, options).then(data => {
       if (!data || !data.results || !data.results[0]) {
       if (!data || !data.results || !data.results[0]) {
         throw { message: 'No results in response from InfluxDB' };
         throw { message: 'No results in response from InfluxDB' };
       }
       }
@@ -164,30 +164,30 @@ export default class InfluxDatasource {
     return false;
     return false;
   }
   }
 
 
-  metricFindQuery(query) {
+  metricFindQuery(query: string, options?: any) {
     var interpolated = this.templateSrv.replace(query, null, 'regex');
     var interpolated = this.templateSrv.replace(query, null, 'regex');
 
 
-    return this._seriesQuery(interpolated).then(_.curry(this.responseParser.parse)(query));
+    return this._seriesQuery(interpolated, options).then(_.curry(this.responseParser.parse)(query));
   }
   }
 
 
   getTagKeys(options) {
   getTagKeys(options) {
     var queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
     var queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
     var query = queryBuilder.buildExploreQuery('TAG_KEYS');
     var query = queryBuilder.buildExploreQuery('TAG_KEYS');
-    return this.metricFindQuery(query);
+    return this.metricFindQuery(query, options);
   }
   }
 
 
   getTagValues(options) {
   getTagValues(options) {
     var queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
     var queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
     var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
     var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
-    return this.metricFindQuery(query);
+    return this.metricFindQuery(query, options);
   }
   }
 
 
-  _seriesQuery(query) {
+  _seriesQuery(query: string, options?: any) {
     if (!query) {
     if (!query) {
       return this.$q.when({ results: [] });
       return this.$q.when({ results: [] });
     }
     }
 
 
-    return this._influxRequest('GET', '/query', { q: query, epoch: 'ms' });
+    return this._influxRequest('GET', '/query', { q: query, epoch: 'ms' }, options);
   }
   }
 
 
   serializeParams(params) {
   serializeParams(params) {
@@ -225,21 +225,21 @@ export default class InfluxDatasource {
       });
       });
   }
   }
 
 
-  _influxRequest(method, url, data) {
-    var self = this;
+  _influxRequest(method: string, url: string, data: any, options?: any) {
+    const currentUrl = this.urls.shift();
+    this.urls.push(currentUrl);
 
 
-    var currentUrl = self.urls.shift();
-    self.urls.push(currentUrl);
+    let params: any = {};
 
 
-    var params: any = {};
-
-    if (self.username) {
-      params.u = self.username;
-      params.p = self.password;
+    if (this.username) {
+      params.u = this.username;
+      params.p = this.password;
     }
     }
 
 
-    if (self.database) {
-      params.db = self.database;
+    if (options && options.database) {
+      params.db = options.database;
+    } else if (this.database) {
+      params.db = this.database;
     }
     }
 
 
     if (method === 'GET') {
     if (method === 'GET') {
@@ -247,7 +247,7 @@ export default class InfluxDatasource {
       data = null;
       data = null;
     }
     }
 
 
-    var options: any = {
+    let req: any = {
       method: method,
       method: method,
       url: currentUrl + url,
       url: currentUrl + url,
       params: params,
       params: params,
@@ -257,15 +257,15 @@ export default class InfluxDatasource {
       paramSerializer: this.serializeParams,
       paramSerializer: this.serializeParams,
     };
     };
 
 
-    options.headers = options.headers || {};
+    req.headers = req.headers || {};
     if (this.basicAuth || this.withCredentials) {
     if (this.basicAuth || this.withCredentials) {
-      options.withCredentials = true;
+      req.withCredentials = true;
     }
     }
-    if (self.basicAuth) {
-      options.headers.Authorization = self.basicAuth;
+    if (this.basicAuth) {
+      req.headers.Authorization = this.basicAuth;
     }
     }
 
 
-    return this.backendSrv.datasourceRequest(options).then(
+    return this.backendSrv.datasourceRequest(req).then(
       result => {
       result => {
         return result.data;
         return result.data;
       },
       },

+ 14 - 0
public/app/plugins/datasource/influxdb/partials/config.html

@@ -23,6 +23,20 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
+
+<div class="gf-form-group">
+	<div class="grafana-info-box">
+		<h5>Database Access</h5>
+		<p>
+			Setting the database for this datasource does not deny access to other databases.  The InfluxDB query syntax allows
+			switching the database in the query.  For example:
+			<code>SHOW MEASUREMENTS ON _internal</code> or <code>SELECT * FROM "_internal".."database" LIMIT 10</code>
+			<br/><br/>
+			To support data isolation and security, make sure appropriate permissions are configured in InfluxDB.
+		</p>
+	</div>
+</div>
+
 <div class="gf-form-group">
 <div class="gf-form-group">
 	<div class="gf-form-inline">
 	<div class="gf-form-inline">
 		<div class="gf-form">
 		<div class="gf-form">

+ 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>
   </div>
 </div>
 </div>

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

@@ -1,7 +1,7 @@
 import angular from 'angular';
 import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
 import $ from 'jquery';
 import $ from 'jquery';
-import PerfectScrollbar from 'perfect-scrollbar';
+import baron from 'baron';
 
 
 var module = angular.module('grafana.directives');
 var module = angular.module('grafana.directives');
 
 
@@ -16,11 +16,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
       var i;
       var i;
       var legendScrollbar;
       var legendScrollbar;
       const legendRightDefaultWidth = 10;
       const legendRightDefaultWidth = 10;
+      let legendElem = elem.parent();
 
 
       scope.$on('$destroy', function() {
       scope.$on('$destroy', function() {
-        if (legendScrollbar) {
-          legendScrollbar.destroy();
-        }
+        destroyScrollbar();
       });
       });
 
 
       ctrl.events.on('render-legend', () => {
       ctrl.events.on('render-legend', () => {
@@ -112,7 +111,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
       }
       }
 
 
       function render() {
       function render() {
-        let legendWidth = elem.width();
+        let legendWidth = legendElem.width();
         if (!ctrl.panel.legend.show) {
         if (!ctrl.panel.legend.show) {
           elem.empty();
           elem.empty();
           firstRender = true;
           firstRender = true;
@@ -134,8 +133,8 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
         // Set width so it works with IE11
         // Set width so it works with IE11
         var width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
         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' : '';
         var ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : '';
-        elem.css('min-width', width);
-        elem.css('width', ieWidth);
+        legendElem.css('min-width', width);
+        legendElem.css('width', ieWidth);
 
 
         elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
         elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
 
 
@@ -241,8 +240,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
           tbodyElem.append(tableHeaderElem);
           tbodyElem.append(tableHeaderElem);
           tbodyElem.append(seriesElements);
           tbodyElem.append(seriesElements);
           elem.append(tbodyElem);
           elem.append(tbodyElem);
+          tbodyElem.wrap('<div class="graph-legend-scroll"></div>');
         } else {
         } 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)) {
         if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) {
@@ -253,23 +254,45 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
       }
       }
 
 
       function addScrollbar() {
       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) {
         if (!legendScrollbar) {
-          legendScrollbar = new PerfectScrollbar(elem[0], scrollbarOptions);
+          legendScrollbar = baron(scrollbarParams);
         } else {
         } else {
-          legendScrollbar.update();
+          destroyScrollbar();
+          legendScrollbar = baron(scrollbarParams);
         }
         }
+        legendScrollbar.scroll();
       }
       }
 
 
       function destroyScrollbar() {
       function destroyScrollbar() {
         if (legendScrollbar) {
         if (legendScrollbar) {
-          legendScrollbar.destroy();
+          legendScrollbar.dispose();
           legendScrollbar = undefined;
           legendScrollbar = undefined;
         }
         }
       }
       }

+ 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 class="graph-panel__chart" grafana-graph ng-dblclick="ctrl.zoomOut()">
   </div>
   </div>
 
 
-  <div class="graph-legend" graph-legend></div>
+  <div class="graph-legend">
+    <div class="graph-legend-content" graph-legend></div>
+  </div>
 </div>
 </div>
 `;
 `;
 
 

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

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

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

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

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

@@ -9,6 +9,11 @@
   -ms-touch-action: auto;
   -ms-touch-action: auto;
 }
 }
 
 
+// ._scrollbar {
+//   overflow-x: hidden !important;
+//   overflow-y: auto;
+// }
+
 /*
 /*
  * Scrollbar rail styles
  * Scrollbar rail styles
  */
  */
@@ -101,7 +106,7 @@
   opacity: 0.9;
   opacity: 0.9;
 }
 }
 
 
-// Srollbars
+// Scrollbars
 //
 //
 
 
 ::-webkit-scrollbar {
 ::-webkit-scrollbar {
@@ -172,3 +177,120 @@
   border-top: 1px solid $scrollbarBorder;
   border-top: 1px solid $scrollbarBorder;
   border-left: 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;
+  }
+}

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

@@ -102,14 +102,21 @@
   }
   }
 }
 }
 
 
+.search-results-scroller {
+  display: flex;
+  position: relative;
+}
+
 .search-results-container {
 .search-results-container {
-  height: 100%;
   display: block;
   display: block;
   padding: $spacer;
   padding: $spacer;
   position: relative;
   position: relative;
   flex-grow: 10;
   flex-grow: 10;
   margin-bottom: 1rem;
   margin-bottom: 1rem;
 
 
+  // Fix for search scroller in mobile view
+  height: unset;
+
   .label-tag {
   .label-tag {
     margin-left: 6px;
     margin-left: 6px;
     font-size: 11px;
     font-size: 11px;

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

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

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

@@ -16,7 +16,7 @@
   <link rel="icon" type="image/png" href="public/img/fav32.png">
   <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="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
   <link rel="apple-touch-icon" href="public/img/fav32.png">
   <link rel="apple-touch-icon" href="public/img/fav32.png">
-  
+
 </head>
 </head>
 
 
 <body ng-cloak class="theme-[[ .Theme ]]">
 <body ng-cloak class="theme-[[ .Theme ]]">
@@ -40,7 +40,7 @@
     </div>
     </div>
 
 
     <div class="main-view">
     <div class="main-view">
-      <div class="scroll-canvas" grafana-scrollbar>
+      <div class="scroll-canvas" page-scrollbar>
         <div ng-view></div>
         <div ng-view></div>
 
 
         <footer class="footer">
         <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
 exit_if_fail time go install ./pkg/cmd/grafana-server
 
 
 echo "running go test"
 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"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   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:
 base64-arraybuffer@0.1.5:
   version "0.1.5"
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
   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"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
   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:
 performance-now@^0.2.0:
   version "0.2.0"
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"