diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f763a15c82..170d366cb24 100644 --- a/CHANGELOG.md +++ b/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) * **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) -* **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) diff --git a/appveyor.yml b/appveyor.yml index 5d67edca9d9..2b0bddde162 100644 --- a/appveyor.yml +++ b/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 diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index b43ea68bcd8..7936a1708eb 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -138,6 +138,7 @@ datasources: ``` #### Custom Settings per Datasource +Please refer to each datasource documentation for specific provisioning examples. | Datasource | Misc | | ---- | ---- | diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index f7f8138b5e9..f5586bef2d8 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/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. @@ -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 issue a GetMetricStatistics request and every time you pick a dimension in the query editor 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: "" + secretKey: "" +``` diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 6ce17113a9b..db17aafd271 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/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. Text | Event description field. 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" +``` \ No newline at end of file diff --git a/docs/sources/features/datasources/graphite.md b/docs/sources/features/datasources/graphite.md index 7c4187da9ae..97233572878 100644 --- a/docs/sources/features/datasources/graphite.md +++ b/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, 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" +``` diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md index 6d0918a0d01..b49e0f9dfc6 100644 --- a/docs/sources/features/datasources/influxdb.md +++ b/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``` 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. + +## 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 +``` \ No newline at end of file diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 7fae7441b6d..8eaab86a906 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/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 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 +``` \ No newline at end of file diff --git a/docs/sources/features/datasources/opentsdb.md b/docs/sources/features/datasources/opentsdb.md index 03795473ff7..6333861dca7 100644 --- a/docs/sources/features/datasources/opentsdb.md +++ b/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 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 +``` \ No newline at end of file diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 7d52df2fd3e..405e8a7fdec 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/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 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 + +``` \ No newline at end of file diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index c9bb16441ca..17a24ed0684 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -34,7 +34,7 @@ Name | Description *Basic Auth* | Enable basic authentication to the Prometheus data source. *User* | Name of your Prometheus user *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 @@ -100,3 +100,19 @@ The step option is useful to limit the number of events returned from your query ## 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! + +## 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 +``` \ No newline at end of file diff --git a/package.json b/package.json index 030219fe587..ce861a25f7b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 3e1a93abb3b..f4eb1140aa0 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -189,12 +189,6 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { } 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) { return errors.New("Target url is not a valid target") } diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index e32b9d34f91..14eacef5831 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/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, diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 76f1819a18c..18794281e33 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/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)) diff --git a/public/app/core/components/ScrollBar/ScrollBar.tsx b/public/app/core/components/ScrollBar/ScrollBar.tsx index 7d9e015df94..a358dc1926a 100644 --- a/public/app/core/components/ScrollBar/ScrollBar.tsx +++ b/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 { 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 { 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 { render() { return ( -
- {this.props.children} +
+
+ {this.props.children} +
+ +
+
+
); } diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 798a40cb1bf..1e3f0cb9119 100644 --- a/public/app/core/components/grafana_app.ts +++ b/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); diff --git a/public/app/core/components/scroll/page_scroll.ts b/public/app/core/components/scroll/page_scroll.ts new file mode 100644 index 00000000000..e6db344a4d6 --- /dev/null +++ b/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); diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index fbf5fd6cd37..3f9865e6dce 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/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 = ` +
+
+
+`; + +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(); }); }, }; diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index acaf0730a6b..afb9e723cad 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -19,6 +19,7 @@
+
No dashboards matching your query were found.
+
diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 353d8762a9a..fb7021fe883 100644 --- a/public/app/core/core.ts +++ b/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, diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index aeb840c317a..98d1657f4bd 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -103,7 +103,7 @@ export class AddPanelPanel extends React.Component +
diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 576b8b6fce8..fa471b89989 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/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) { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index dec7868a553..e549ca262d3 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/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 = ` +
+
+
+ `; + + 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(); } }); }, diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index b87167ad646..54bd7bb660c 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -198,7 +198,9 @@ export class QueryVariable implements Variable { } }); } else if (sortType === 3) { - options = _.sortBy(options, opt => { return _.toLower(opt.text); }); + options = _.sortBy(options, opt => { + return _.toLower(opt.text); + }); } if (reverseSort) { diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 210275d2200..9506587c515 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -1,18 +1,18 @@
-
- - +
+ + -
- - +
+ + - - -
-
+ + +
+
diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 1eff9bfa527..8b213ca0f36 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -82,7 +82,7 @@ export default class InfluxDatasource { // replace templated variables 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) { return []; } @@ -135,7 +135,7 @@ export default class InfluxDatasource { var query = options.annotation.query.replace('$timeFilter', timeFilter); 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]) { throw { message: 'No results in response from InfluxDB' }; } @@ -164,30 +164,30 @@ export default class InfluxDatasource { return false; } - metricFindQuery(query) { + metricFindQuery(query: string, options?: any) { 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) { var queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database); var query = queryBuilder.buildExploreQuery('TAG_KEYS'); - return this.metricFindQuery(query); + return this.metricFindQuery(query, options); } getTagValues(options) { var queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database); 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) { 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) { @@ -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') { @@ -247,7 +247,7 @@ export default class InfluxDatasource { data = null; } - var options: any = { + let req: any = { method: method, url: currentUrl + url, params: params, @@ -257,15 +257,15 @@ export default class InfluxDatasource { paramSerializer: this.serializeParams, }; - options.headers = options.headers || {}; + req.headers = req.headers || {}; 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 => { return result.data; }, diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 9cb6f5ba749..a70a1de98a4 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -23,6 +23,20 @@
+ +
+
+
Database Access
+

+ 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: + SHOW MEASUREMENTS ON _internal or SELECT * FROM "_internal".."database" LIMIT 10 +

+ To support data isolation and security, make sure appropriate permissions are configured in InfluxDB. +

+
+
+
diff --git a/public/app/plugins/panel/dashlist/module.html b/public/app/plugins/panel/dashlist/module.html index 8fa3e7ef71f..fdba0c79f35 100644 --- a/public/app/plugins/panel/dashlist/module.html +++ b/public/app/plugins/panel/dashlist/module.html @@ -1,17 +1,19 @@ -
-
-
- {{group.header}} -
-
- - - {{dash.title}} - - - - - +
+
+
+
+ {{group.header}} +
+
diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 4dfeb75ff55..b668555b6a6 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/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; @@ -134,8 +133,8 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { // 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' : ''; - 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); @@ -241,8 +240,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { tbodyElem.append(tableHeaderElem); tbodyElem.append(seriesElements); elem.append(tbodyElem); + tbodyElem.wrap('
'); } else { - elem.append(seriesElements); + elem.append('
'); + elem.find('.graph-legend-scroll').append(seriesElements); } if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) { @@ -253,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 = ` +
+
+
+ `; + + 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; } } diff --git a/public/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts index 0b9eb8227df..c897327fe1a 100644 --- a/public/app/plugins/panel/graph/template.ts +++ b/public/app/plugins/panel/graph/template.ts @@ -3,7 +3,9 @@ var template = `
-
+
+
+
`; diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index 51754a54d92..4a17438ffeb 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/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; diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index e15cd576367..72f3ca3dbbe 100644 --- a/public/sass/components/_panel_graph.scss +++ b/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; diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index 42818e786f6..78173b73f47 100644 --- a/public/sass/components/_scrollbar.scss +++ b/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; + } +} diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 99033b90ff1..8338a5d72ae 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -102,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; diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index 03941a47408..c80d461541e 100644 --- a/public/sass/layout/_page.scss +++ b/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; diff --git a/public/views/index.template.html b/public/views/index.template.html index 2d408f70f8c..79da1d7179c 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -16,7 +16,7 @@ - + @@ -40,7 +40,7 @@
-
+