mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into readonly_dashboards
This commit is contained in:
commit
4a26797cdd
CHANGELOG.mdappveyor.ymlpackage.json
docs/sources
administration
features/datasources
http_api
installation
pkg/services
public
app
core
features
dashboard
panel
playlist
plugins/partials
templating
partials
plugins
sass
base
components
layout
pages
views
scripts
yarn.lock@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -138,6 +138,7 @@ datasources:
|
||||
```
|
||||
|
||||
#### Custom Settings per Datasource
|
||||
Please refer to each datasource documentation for specific provisioning examples.
|
||||
|
||||
| Datasource | Misc |
|
||||
| ---- | ---- |
|
||||
|
@ -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: "<your access key>"
|
||||
secretKey: "<your secret key>"
|
||||
```
|
||||
|
@ -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"
|
||||
```
|
@ -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"
|
||||
```
|
||||
|
@ -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
|
||||
```
|
@ -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
|
||||
```
|
@ -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
|
||||
```
|
@ -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
|
||||
|
||||
```
|
@ -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
|
||||
```
|
@ -180,14 +180,14 @@ Content-Type: application/json
|
||||
|
||||
## Delete Annotation By Id
|
||||
|
||||
`DELETE /api/annotation/:id`
|
||||
`DELETE /api/annotations/:id`
|
||||
|
||||
Deletes the annotation that matches the specified id.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/annotation/1 HTTP/1.1
|
||||
DELETE /api/annotations/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
@ -204,14 +204,14 @@ Content-Type: application/json
|
||||
|
||||
## Delete Annotation By RegionId
|
||||
|
||||
`DELETE /api/annotation/region/:id`
|
||||
`DELETE /api/annotations/region/:id`
|
||||
|
||||
Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/annotation/region/1 HTTP/1.1
|
||||
DELETE /api/annotations/region/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
@ -34,7 +34,7 @@ sudo dpkg -i grafana_5.0.4_amd64.deb
|
||||
Add the following line to your `/etc/apt/sources.list` file.
|
||||
|
||||
```bash
|
||||
deb https://packagecloud.io/grafana/stable/debian/ jessie main
|
||||
deb https://packagecloud.io/grafana/stable/debian/ stretch main
|
||||
```
|
||||
|
||||
Use the above line even if you are on Ubuntu or another Debian version.
|
||||
@ -42,7 +42,7 @@ There is also a testing repository if you want beta or release
|
||||
candidates.
|
||||
|
||||
```bash
|
||||
deb https://packagecloud.io/grafana/testing/debian/ jessie main
|
||||
deb https://packagecloud.io/grafana/testing/debian/ stretch main
|
||||
```
|
||||
|
||||
Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
import baron from 'baron';
|
||||
|
||||
export interface Props {
|
||||
children: any;
|
||||
@ -8,31 +8,36 @@ export interface Props {
|
||||
|
||||
export default class ScrollBar extends React.Component<Props, any> {
|
||||
private container: any;
|
||||
private ps: PerfectScrollbar;
|
||||
private scrollbar: baron;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.ps = new PerfectScrollbar(this.container, {
|
||||
wheelPropagation: true,
|
||||
this.scrollbar = baron({
|
||||
root: this.container.parentElement,
|
||||
scroller: this.container,
|
||||
bar: '.baron__bar',
|
||||
barOnCls: '_scrollbar',
|
||||
scrollingCls: '_scrolling',
|
||||
track: '.baron__track',
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.ps.update();
|
||||
this.scrollbar.update();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.ps.destroy();
|
||||
this.scrollbar.dispose();
|
||||
}
|
||||
|
||||
// methods can be invoked by outside
|
||||
setScrollTop(top) {
|
||||
if (this.container) {
|
||||
this.container.scrollTop = top;
|
||||
this.ps.update();
|
||||
this.scrollbar.update();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -42,7 +47,7 @@ export default class ScrollBar extends React.Component<Props, any> {
|
||||
setScrollLeft(left) {
|
||||
if (this.container) {
|
||||
this.container.scrollLeft = left;
|
||||
this.ps.update();
|
||||
this.scrollbar.update();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -55,8 +60,14 @@ export default class ScrollBar extends React.Component<Props, any> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.props.className} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
<div className="baron baron__root baron__clipper">
|
||||
<div className={this.props.className + ' baron__scroller'} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
<div className="baron__track">
|
||||
<div className="baron__bar" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -167,6 +167,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
if (sidemenuHidden) {
|
||||
sidemenuHidden = false;
|
||||
body.addClass('sidemenu-open');
|
||||
appEvents.emit('toggle-inactive-mode');
|
||||
$timeout(function() {
|
||||
$rootScope.$broadcast('render');
|
||||
}, 100);
|
||||
|
41
public/app/core/components/scroll/page_scroll.ts
Normal file
41
public/app/core/components/scroll/page_scroll.ts
Normal file
@ -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);
|
@ -1,15 +1,44 @@
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
import $ from 'jquery';
|
||||
import baron from 'baron';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
const scrollBarHTML = `
|
||||
<div class="baron__track">
|
||||
<div class="baron__bar"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const scrollRootClass = 'baron baron__root';
|
||||
const scrollerClass = 'baron__scroller';
|
||||
|
||||
export function geminiScrollbar() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem, attrs) {
|
||||
let scrollbar = new PerfectScrollbar(elem[0], {
|
||||
wheelPropagation: true,
|
||||
wheelSpeed: 3,
|
||||
});
|
||||
let scrollRoot = elem.parent();
|
||||
let scroller = elem;
|
||||
|
||||
if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
|
||||
scrollRoot = scroller;
|
||||
}
|
||||
|
||||
scrollRoot.addClass(scrollRootClass);
|
||||
$(scrollBarHTML).appendTo(scrollRoot);
|
||||
elem.addClass(scrollerClass);
|
||||
|
||||
let scrollParams = {
|
||||
root: scrollRoot[0],
|
||||
scroller: scroller[0],
|
||||
bar: '.baron__bar',
|
||||
barOnCls: '_scrollbar',
|
||||
scrollingCls: '_scrolling',
|
||||
track: '.baron__track',
|
||||
direction: 'v',
|
||||
};
|
||||
|
||||
let scrollbar = baron(scrollParams);
|
||||
|
||||
let lastPos = 0;
|
||||
|
||||
appEvents.on(
|
||||
@ -31,13 +60,24 @@ export function geminiScrollbar() {
|
||||
scope
|
||||
);
|
||||
|
||||
// force updating dashboard width
|
||||
appEvents.on('toggle-sidemenu', forceUpdate, scope);
|
||||
appEvents.on('toggle-sidemenu-hidden', forceUpdate, scope);
|
||||
appEvents.on('toggle-view-mode', forceUpdate, scope);
|
||||
appEvents.on('toggle-kiosk-mode', forceUpdate, scope);
|
||||
appEvents.on('toggle-inactive-mode', forceUpdate, scope);
|
||||
|
||||
function forceUpdate() {
|
||||
scrollbar.scroll();
|
||||
}
|
||||
|
||||
scope.$on('$routeChangeSuccess', () => {
|
||||
lastPos = 0;
|
||||
elem[0].scrollTop = 0;
|
||||
});
|
||||
|
||||
scope.$on('$destroy', () => {
|
||||
scrollbar.destroy();
|
||||
scrollbar.dispose();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
<div class="search-dropdown">
|
||||
<div class="search-dropdown__col_1">
|
||||
<div class="search-results-scroller">
|
||||
<div class="search-results-container" grafana-scrollbar>
|
||||
<h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
|
||||
<dashboard-search-results
|
||||
@ -27,6 +28,7 @@
|
||||
on-folder-expanding="ctrl.folderExpanding()"
|
||||
on-folder-expanded="ctrl.folderExpanded($folder)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-dropdown__col_2">
|
||||
|
@ -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,
|
||||
|
@ -1,36 +0,0 @@
|
||||
define([
|
||||
'lodash',
|
||||
'jquery',
|
||||
'../core_module',
|
||||
],
|
||||
function (_, $, coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.directive('dashClass', function() {
|
||||
return {
|
||||
link: function($scope, elem) {
|
||||
|
||||
$scope.onAppEvent('panel-fullscreen-enter', function() {
|
||||
elem.toggleClass('panel-in-fullscreen', true);
|
||||
});
|
||||
|
||||
$scope.onAppEvent('panel-fullscreen-exit', function() {
|
||||
elem.toggleClass('panel-in-fullscreen', false);
|
||||
});
|
||||
|
||||
$scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
|
||||
if (newValue) {
|
||||
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
|
||||
setTimeout(function() {
|
||||
elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
|
||||
}, 10);
|
||||
} else {
|
||||
elem.removeClass('dashboard-page--settings-opening');
|
||||
elem.removeClass('dashboard-page--settings-open');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
31
public/app/core/directives/dash_class.ts
Normal file
31
public/app/core/directives/dash_class.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import _ from 'lodash';
|
||||
import coreModule from '../core_module';
|
||||
|
||||
/** @ngInject */
|
||||
export function dashClass() {
|
||||
return {
|
||||
link: function($scope, elem) {
|
||||
$scope.onAppEvent('panel-fullscreen-enter', function() {
|
||||
elem.toggleClass('panel-in-fullscreen', true);
|
||||
});
|
||||
|
||||
$scope.onAppEvent('panel-fullscreen-exit', function() {
|
||||
elem.toggleClass('panel-in-fullscreen', false);
|
||||
});
|
||||
|
||||
$scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
|
||||
if (newValue) {
|
||||
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
|
||||
setTimeout(function() {
|
||||
elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
|
||||
}, 10);
|
||||
} else {
|
||||
elem.removeClass('dashboard-page--settings-opening');
|
||||
elem.removeClass('dashboard-page--settings-open');
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashClass', dashClass);
|
@ -1,246 +0,0 @@
|
||||
define([
|
||||
'lodash',
|
||||
'jquery',
|
||||
'../core_module',
|
||||
],
|
||||
function (_, $, coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.directive('metricSegment', function($compile, $sce) {
|
||||
var inputTemplate = '<input type="text" data-provide="typeahead" ' +
|
||||
' class="gf-form-input input-medium"' +
|
||||
' spellcheck="false" style="display:none"></input>';
|
||||
|
||||
var linkTemplate = '<a class="gf-form-label" ng-class="segment.cssClass" ' +
|
||||
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
|
||||
|
||||
var selectTemplate = '<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
|
||||
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
|
||||
|
||||
return {
|
||||
scope: {
|
||||
segment: "=",
|
||||
getOptions: "&",
|
||||
onChange: "&",
|
||||
debounce: "@",
|
||||
},
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
var segment = $scope.segment;
|
||||
var $button = $(segment.selectMode ? selectTemplate : linkTemplate);
|
||||
var options = null;
|
||||
var cancelBlur = null;
|
||||
var linkMode = true;
|
||||
var debounceLookup = $scope.debounce;
|
||||
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
||||
$scope.updateVariableValue = function(value) {
|
||||
if (value === '' || segment.value === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = _.unescape(value);
|
||||
|
||||
$scope.$apply(function() {
|
||||
var selected = _.find($scope.altSegments, {value: value});
|
||||
if (selected) {
|
||||
segment.value = selected.value;
|
||||
segment.html = selected.html || selected.value;
|
||||
segment.fake = false;
|
||||
segment.expandable = selected.expandable;
|
||||
|
||||
if (selected.type) {
|
||||
segment.type = selected.type;
|
||||
}
|
||||
}
|
||||
else if (segment.custom !== 'false') {
|
||||
segment.value = value;
|
||||
segment.html = $sce.trustAsHtml(value);
|
||||
segment.expandable = true;
|
||||
segment.fake = false;
|
||||
}
|
||||
|
||||
$scope.onChange();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.switchToLink = function(fromClick) {
|
||||
if (linkMode && !fromClick) { return; }
|
||||
|
||||
clearTimeout(cancelBlur);
|
||||
cancelBlur = null;
|
||||
linkMode = true;
|
||||
$input.hide();
|
||||
$button.show();
|
||||
$scope.updateVariableValue($input.val());
|
||||
};
|
||||
|
||||
$scope.inputBlur = function() {
|
||||
// happens long before the click event on the typeahead options
|
||||
// need to have long delay because the blur
|
||||
cancelBlur = setTimeout($scope.switchToLink, 200);
|
||||
};
|
||||
|
||||
$scope.source = function(query, callback) {
|
||||
$scope.$apply(function() {
|
||||
$scope.getOptions({ $query: query }).then(function(altSegments) {
|
||||
$scope.altSegments = altSegments;
|
||||
options = _.map($scope.altSegments, function(alt) {
|
||||
return _.escape(alt.value);
|
||||
});
|
||||
|
||||
// add custom values
|
||||
if (segment.custom !== 'false') {
|
||||
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
|
||||
options.unshift(segment.value);
|
||||
}
|
||||
}
|
||||
|
||||
callback(options);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updater = function(value) {
|
||||
if (value === segment.value) {
|
||||
clearTimeout(cancelBlur);
|
||||
$input.focus();
|
||||
return value;
|
||||
}
|
||||
|
||||
$input.val(value);
|
||||
$scope.switchToLink(true);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
$scope.matcher = function(item) {
|
||||
var str = this.query;
|
||||
if (str[0] === '/') { str = str.substring(1); }
|
||||
if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
|
||||
try {
|
||||
return item.toLowerCase().match(str.toLowerCase());
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
$input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater, matcher: $scope.matcher });
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
typeahead.lookup = function () {
|
||||
this.query = this.$element.val() || '';
|
||||
var items = this.source(this.query, $.proxy(this.process, this));
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
|
||||
if (debounceLookup) {
|
||||
typeahead.lookup = _.debounce(typeahead.lookup, 500, {leading: true});
|
||||
}
|
||||
|
||||
$button.keydown(function(evt) {
|
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) {
|
||||
$button.click();
|
||||
}
|
||||
});
|
||||
|
||||
$button.click(function() {
|
||||
options = null;
|
||||
$input.css('width', (Math.max($button.width(), 80) + 16) + 'px');
|
||||
|
||||
$button.hide();
|
||||
$input.show();
|
||||
$input.focus();
|
||||
|
||||
linkMode = false;
|
||||
|
||||
var typeahead = $input.data('typeahead');
|
||||
if (typeahead) {
|
||||
$input.val('');
|
||||
typeahead.lookup();
|
||||
}
|
||||
});
|
||||
|
||||
$input.blur($scope.inputBlur);
|
||||
|
||||
$compile(elem.contents())($scope);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
coreModule.default.directive('metricSegmentModel', function(uiSegmentSrv, $q) {
|
||||
return {
|
||||
template: '<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
property: "=",
|
||||
options: "=",
|
||||
getOptions: "&",
|
||||
onChange: "&",
|
||||
},
|
||||
link: {
|
||||
pre: function postLink($scope, elem, attrs) {
|
||||
var cachedOptions;
|
||||
|
||||
$scope.valueToSegment = function(value) {
|
||||
var option = _.find($scope.options, {value: value});
|
||||
var segment = {
|
||||
cssClass: attrs.cssClass,
|
||||
custom: attrs.custom,
|
||||
value: option ? option.text : value,
|
||||
selectMode: attrs.selectMode,
|
||||
};
|
||||
|
||||
return uiSegmentSrv.newSegment(segment);
|
||||
};
|
||||
|
||||
$scope.getOptionsInternal = function() {
|
||||
if ($scope.options) {
|
||||
cachedOptions = $scope.options;
|
||||
return $q.when(_.map($scope.options, function(option) {
|
||||
return {value: option.text};
|
||||
}));
|
||||
} else {
|
||||
return $scope.getOptions().then(function(options) {
|
||||
cachedOptions = options;
|
||||
return _.map(options, function(option) {
|
||||
if (option.html) {
|
||||
return option;
|
||||
}
|
||||
return {value: option.text};
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onSegmentChange = function() {
|
||||
if (cachedOptions) {
|
||||
var option = _.find(cachedOptions, {text: $scope.segment.value});
|
||||
if (option && option.value !== $scope.property) {
|
||||
$scope.property = option.value;
|
||||
} else if (attrs.custom !== 'false') {
|
||||
$scope.property = $scope.segment.value;
|
||||
}
|
||||
} else {
|
||||
$scope.property = $scope.segment.value;
|
||||
}
|
||||
|
||||
// needs to call this after digest so
|
||||
// property is synced with outerscope
|
||||
$scope.$$postDigest(function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.onChange();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.segment = $scope.valueToSegment($scope.property);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
263
public/app/core/directives/metric_segment.ts
Normal file
263
public/app/core/directives/metric_segment.ts
Normal file
@ -0,0 +1,263 @@
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from '../core_module';
|
||||
|
||||
/** @ngInject */
|
||||
export function metricSegment($compile, $sce) {
|
||||
let inputTemplate =
|
||||
'<input type="text" data-provide="typeahead" ' +
|
||||
' class="gf-form-input input-medium"' +
|
||||
' spellcheck="false" style="display:none"></input>';
|
||||
|
||||
let linkTemplate =
|
||||
'<a class="gf-form-label" ng-class="segment.cssClass" ' +
|
||||
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
|
||||
|
||||
let selectTemplate =
|
||||
'<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
|
||||
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
|
||||
|
||||
return {
|
||||
scope: {
|
||||
segment: '=',
|
||||
getOptions: '&',
|
||||
onChange: '&',
|
||||
debounce: '@',
|
||||
},
|
||||
link: function($scope, elem) {
|
||||
let $input = $(inputTemplate);
|
||||
let segment = $scope.segment;
|
||||
let $button = $(segment.selectMode ? selectTemplate : linkTemplate);
|
||||
let options = null;
|
||||
let cancelBlur = null;
|
||||
let linkMode = true;
|
||||
let debounceLookup = $scope.debounce;
|
||||
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
||||
$scope.updateVariableValue = function(value) {
|
||||
if (value === '' || segment.value === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = _.unescape(value);
|
||||
|
||||
$scope.$apply(function() {
|
||||
let selected = _.find($scope.altSegments, { value: value });
|
||||
if (selected) {
|
||||
segment.value = selected.value;
|
||||
segment.html = selected.html || selected.value;
|
||||
segment.fake = false;
|
||||
segment.expandable = selected.expandable;
|
||||
|
||||
if (selected.type) {
|
||||
segment.type = selected.type;
|
||||
}
|
||||
} else if (segment.custom !== 'false') {
|
||||
segment.value = value;
|
||||
segment.html = $sce.trustAsHtml(value);
|
||||
segment.expandable = true;
|
||||
segment.fake = false;
|
||||
}
|
||||
|
||||
$scope.onChange();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.switchToLink = function(fromClick) {
|
||||
if (linkMode && !fromClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(cancelBlur);
|
||||
cancelBlur = null;
|
||||
linkMode = true;
|
||||
$input.hide();
|
||||
$button.show();
|
||||
$scope.updateVariableValue($input.val());
|
||||
};
|
||||
|
||||
$scope.inputBlur = function() {
|
||||
// happens long before the click event on the typeahead options
|
||||
// need to have long delay because the blur
|
||||
cancelBlur = setTimeout($scope.switchToLink, 200);
|
||||
};
|
||||
|
||||
$scope.source = function(query, callback) {
|
||||
$scope.$apply(function() {
|
||||
$scope.getOptions({ $query: query }).then(function(altSegments) {
|
||||
$scope.altSegments = altSegments;
|
||||
options = _.map($scope.altSegments, function(alt) {
|
||||
return _.escape(alt.value);
|
||||
});
|
||||
|
||||
// add custom values
|
||||
if (segment.custom !== 'false') {
|
||||
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
|
||||
options.unshift(segment.value);
|
||||
}
|
||||
}
|
||||
|
||||
callback(options);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updater = function(value) {
|
||||
if (value === segment.value) {
|
||||
clearTimeout(cancelBlur);
|
||||
$input.focus();
|
||||
return value;
|
||||
}
|
||||
|
||||
$input.val(value);
|
||||
$scope.switchToLink(true);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
$scope.matcher = function(item) {
|
||||
let str = this.query;
|
||||
if (str[0] === '/') {
|
||||
str = str.substring(1);
|
||||
}
|
||||
if (str[str.length - 1] === '/') {
|
||||
str = str.substring(0, str.length - 1);
|
||||
}
|
||||
try {
|
||||
return item.toLowerCase().match(str.toLowerCase());
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
$input.typeahead({
|
||||
source: $scope.source,
|
||||
minLength: 0,
|
||||
items: 10000,
|
||||
updater: $scope.updater,
|
||||
matcher: $scope.matcher,
|
||||
});
|
||||
|
||||
let typeahead = $input.data('typeahead');
|
||||
typeahead.lookup = function() {
|
||||
this.query = this.$element.val() || '';
|
||||
let items = this.source(this.query, $.proxy(this.process, this));
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
|
||||
if (debounceLookup) {
|
||||
typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
|
||||
}
|
||||
|
||||
$button.keydown(function(evt) {
|
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) {
|
||||
$button.click();
|
||||
}
|
||||
});
|
||||
|
||||
$button.click(function() {
|
||||
options = null;
|
||||
$input.css('width', Math.max($button.width(), 80) + 16 + 'px');
|
||||
|
||||
$button.hide();
|
||||
$input.show();
|
||||
$input.focus();
|
||||
|
||||
linkMode = false;
|
||||
|
||||
let typeahead = $input.data('typeahead');
|
||||
if (typeahead) {
|
||||
$input.val('');
|
||||
typeahead.lookup();
|
||||
}
|
||||
});
|
||||
|
||||
$input.blur($scope.inputBlur);
|
||||
|
||||
$compile(elem.contents())($scope);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
export function metricSegmentModel(uiSegmentSrv, $q) {
|
||||
return {
|
||||
template:
|
||||
'<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
property: '=',
|
||||
options: '=',
|
||||
getOptions: '&',
|
||||
onChange: '&',
|
||||
},
|
||||
link: {
|
||||
pre: function postLink($scope, elem, attrs) {
|
||||
let cachedOptions;
|
||||
|
||||
$scope.valueToSegment = function(value) {
|
||||
let option = _.find($scope.options, { value: value });
|
||||
let segment = {
|
||||
cssClass: attrs.cssClass,
|
||||
custom: attrs.custom,
|
||||
value: option ? option.text : value,
|
||||
selectMode: attrs.selectMode,
|
||||
};
|
||||
|
||||
return uiSegmentSrv.newSegment(segment);
|
||||
};
|
||||
|
||||
$scope.getOptionsInternal = function() {
|
||||
if ($scope.options) {
|
||||
cachedOptions = $scope.options;
|
||||
return $q.when(
|
||||
_.map($scope.options, function(option) {
|
||||
return { value: option.text };
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return $scope.getOptions().then(function(options) {
|
||||
cachedOptions = options;
|
||||
return _.map(options, function(option) {
|
||||
if (option.html) {
|
||||
return option;
|
||||
}
|
||||
return { value: option.text };
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onSegmentChange = function() {
|
||||
if (cachedOptions) {
|
||||
let option = _.find(cachedOptions, { text: $scope.segment.value });
|
||||
if (option && option.value !== $scope.property) {
|
||||
$scope.property = option.value;
|
||||
} else if (attrs.custom !== 'false') {
|
||||
$scope.property = $scope.segment.value;
|
||||
}
|
||||
} else {
|
||||
$scope.property = $scope.segment.value;
|
||||
}
|
||||
|
||||
// needs to call this after digest so
|
||||
// property is synced with outerscope
|
||||
$scope.$$postDigest(function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.onChange();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.segment = $scope.valueToSegment($scope.property);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('metricSegment', metricSegment);
|
||||
coreModule.directive('metricSegmentModel', metricSegmentModel);
|
@ -1,111 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'../core_module',
|
||||
],
|
||||
function (angular, _, coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.service('uiSegmentSrv', function($sce, templateSrv) {
|
||||
var self = this;
|
||||
|
||||
function MetricSegment(options) {
|
||||
if (options === '*' || options.value === '*') {
|
||||
this.value = '*';
|
||||
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
|
||||
this.type = options.type;
|
||||
this.expandable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isString(options)) {
|
||||
this.value = options;
|
||||
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
return;
|
||||
}
|
||||
|
||||
// temp hack to work around legacy inconsistency in segment model
|
||||
this.text = options.value;
|
||||
|
||||
this.cssClass = options.cssClass;
|
||||
this.custom = options.custom;
|
||||
this.type = options.type;
|
||||
this.fake = options.fake;
|
||||
this.value = options.value;
|
||||
this.selectMode = options.selectMode;
|
||||
this.type = options.type;
|
||||
this.expandable = options.expandable;
|
||||
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
}
|
||||
|
||||
this.getSegmentForValue = function(value, fallbackText) {
|
||||
if (value) {
|
||||
return this.newSegment(value);
|
||||
} else {
|
||||
return this.newSegment({value: fallbackText, fake: true});
|
||||
}
|
||||
};
|
||||
|
||||
this.newSelectMeasurement = function() {
|
||||
return new MetricSegment({value: 'select measurement', fake: true});
|
||||
};
|
||||
|
||||
this.newFake = function(text, type, cssClass) {
|
||||
return new MetricSegment({value: text, fake: true, type: type, cssClass: cssClass});
|
||||
};
|
||||
|
||||
this.newSegment = function(options) {
|
||||
return new MetricSegment(options);
|
||||
};
|
||||
|
||||
this.newKey = function(key) {
|
||||
return new MetricSegment({value: key, type: 'key', cssClass: 'query-segment-key' });
|
||||
};
|
||||
|
||||
this.newKeyValue = function(value) {
|
||||
return new MetricSegment({value: value, type: 'value', cssClass: 'query-segment-value' });
|
||||
};
|
||||
|
||||
this.newCondition = function(condition) {
|
||||
return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
|
||||
};
|
||||
|
||||
this.newOperator = function(op) {
|
||||
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||
};
|
||||
|
||||
this.newOperators = function(ops) {
|
||||
return _.map(ops, function(op) {
|
||||
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||
});
|
||||
};
|
||||
|
||||
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
|
||||
return function(results) {
|
||||
var segments = _.map(results, function(segment) {
|
||||
return self.newSegment({value: segment.text, expandable: segment.expandable});
|
||||
});
|
||||
|
||||
if (addTemplateVars) {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
|
||||
segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
};
|
||||
|
||||
this.newSelectMetric = function() {
|
||||
return new MetricSegment({value: 'select metric', fake: true});
|
||||
};
|
||||
|
||||
this.newPlusButton = function() {
|
||||
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
111
public/app/core/services/segment_srv.ts
Normal file
111
public/app/core/services/segment_srv.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import _ from 'lodash';
|
||||
import coreModule from '../core_module';
|
||||
|
||||
/** @ngInject */
|
||||
export function uiSegmentSrv($sce, templateSrv) {
|
||||
let self = this;
|
||||
|
||||
function MetricSegment(options) {
|
||||
if (options === '*' || options.value === '*') {
|
||||
this.value = '*';
|
||||
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
|
||||
this.type = options.type;
|
||||
this.expandable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isString(options)) {
|
||||
this.value = options;
|
||||
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
return;
|
||||
}
|
||||
|
||||
// temp hack to work around legacy inconsistency in segment model
|
||||
this.text = options.value;
|
||||
|
||||
this.cssClass = options.cssClass;
|
||||
this.custom = options.custom;
|
||||
this.type = options.type;
|
||||
this.fake = options.fake;
|
||||
this.value = options.value;
|
||||
this.selectMode = options.selectMode;
|
||||
this.type = options.type;
|
||||
this.expandable = options.expandable;
|
||||
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||
}
|
||||
|
||||
this.getSegmentForValue = function(value, fallbackText) {
|
||||
if (value) {
|
||||
return this.newSegment(value);
|
||||
} else {
|
||||
return this.newSegment({ value: fallbackText, fake: true });
|
||||
}
|
||||
};
|
||||
|
||||
this.newSelectMeasurement = function() {
|
||||
return new MetricSegment({ value: 'select measurement', fake: true });
|
||||
};
|
||||
|
||||
this.newFake = function(text, type, cssClass) {
|
||||
return new MetricSegment({ value: text, fake: true, type: type, cssClass: cssClass });
|
||||
};
|
||||
|
||||
this.newSegment = function(options) {
|
||||
return new MetricSegment(options);
|
||||
};
|
||||
|
||||
this.newKey = function(key) {
|
||||
return new MetricSegment({ value: key, type: 'key', cssClass: 'query-segment-key' });
|
||||
};
|
||||
|
||||
this.newKeyValue = function(value) {
|
||||
return new MetricSegment({ value: value, type: 'value', cssClass: 'query-segment-value' });
|
||||
};
|
||||
|
||||
this.newCondition = function(condition) {
|
||||
return new MetricSegment({ value: condition, type: 'condition', cssClass: 'query-keyword' });
|
||||
};
|
||||
|
||||
this.newOperator = function(op) {
|
||||
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||
};
|
||||
|
||||
this.newOperators = function(ops) {
|
||||
return _.map(ops, function(op) {
|
||||
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||
});
|
||||
};
|
||||
|
||||
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
|
||||
return function(results) {
|
||||
let segments = _.map(results, function(segment) {
|
||||
return self.newSegment({ value: segment.text, expandable: segment.expandable });
|
||||
});
|
||||
|
||||
if (addTemplateVars) {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
|
||||
segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
};
|
||||
|
||||
this.newSelectMetric = function() {
|
||||
return new MetricSegment({ value: 'select metric', fake: true });
|
||||
};
|
||||
|
||||
this.newPlusButton = function() {
|
||||
return new MetricSegment({
|
||||
fake: true,
|
||||
html: '<i class="fa fa-plus "></i>',
|
||||
type: 'plus-button',
|
||||
cssClass: 'query-part',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.service('uiSegmentSrv', uiSegmentSrv);
|
@ -103,7 +103,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="panel-container">
|
||||
<div className="panel-container add-panel-container">
|
||||
<div className="add-panel">
|
||||
<div className="add-panel__header">
|
||||
<i className="gicon gicon-add-panel" />
|
||||
|
@ -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) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import angular from 'angular';
|
||||
import $ from 'jquery';
|
||||
import Drop from 'tether-drop';
|
||||
import PerfectScrollbar from 'perfect-scrollbar';
|
||||
import baron from 'baron';
|
||||
|
||||
var module = angular.module('grafana.directives');
|
||||
|
||||
@ -86,6 +87,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
||||
|
||||
function panelHeightUpdated() {
|
||||
panelContent.css({ height: ctrl.height + 'px' });
|
||||
}
|
||||
|
||||
function resizeScrollableContent() {
|
||||
if (panelScrollbar) {
|
||||
panelScrollbar.update();
|
||||
}
|
||||
@ -100,9 +104,30 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
||||
// update scrollbar after mounting
|
||||
ctrl.events.on('component-did-mount', () => {
|
||||
if (ctrl.__proto__.constructor.scrollable) {
|
||||
panelScrollbar = new PerfectScrollbar(panelContent[0], {
|
||||
wheelPropagation: true,
|
||||
const scrollRootClass = 'baron baron__root baron__clipper panel-content--scrollable';
|
||||
const scrollerClass = 'baron__scroller';
|
||||
const scrollBarHTML = `
|
||||
<div class="baron__track">
|
||||
<div class="baron__bar"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let scrollRoot = panelContent;
|
||||
let scroller = panelContent.find(':first').find(':first');
|
||||
|
||||
scrollRoot.addClass(scrollRootClass);
|
||||
$(scrollBarHTML).appendTo(scrollRoot);
|
||||
scroller.addClass(scrollerClass);
|
||||
|
||||
panelScrollbar = baron({
|
||||
root: scrollRoot[0],
|
||||
scroller: scroller[0],
|
||||
bar: '.baron__bar',
|
||||
barOnCls: '_scrollbar',
|
||||
scrollingCls: '_scrolling',
|
||||
});
|
||||
|
||||
panelScrollbar.scroll();
|
||||
}
|
||||
});
|
||||
|
||||
@ -110,6 +135,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
||||
ctrl.calculatePanelHeight();
|
||||
panelHeightUpdated();
|
||||
$timeout(() => {
|
||||
resizeScrollableContent();
|
||||
ctrl.render();
|
||||
});
|
||||
});
|
||||
@ -199,7 +225,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
||||
}
|
||||
|
||||
if (panelScrollbar) {
|
||||
panelScrollbar.update();
|
||||
panelScrollbar.dispose();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
import angular from 'angular';
|
||||
|
||||
/** @ngInject */
|
||||
function grafanaRoutes($routeProvider) {
|
||||
$routeProvider
|
||||
.when('/playlists', {
|
||||
|
@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">HTTP</h3>
|
||||
<div class="gf-form-group">
|
||||
@ -13,12 +11,12 @@
|
||||
<info-popover mode="right-absolute">
|
||||
<p>Specify a complete HTTP URL (for example http://your_server:8080)</p>
|
||||
<span ng-show="current.access === 'direct'">
|
||||
Your access method is <em>Direct</em>, this means the URL
|
||||
Your access method is <em>Browser</em>, this means the URL
|
||||
needs to be accessible from the browser.
|
||||
</span>
|
||||
<span ng-show="current.access === 'proxy'">
|
||||
Your access method is currently <em>Proxy</em>, this means the URL
|
||||
needs to be accessible from the grafana backend.
|
||||
Your access method is <em>Server</em>, this means the URL
|
||||
needs to be accessible from the grafana backend/server.
|
||||
</span>
|
||||
</info-popover>
|
||||
</div>
|
||||
@ -27,14 +25,38 @@
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-7">Access</span>
|
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
|
||||
<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
Direct = URL is used directly from browser<br>
|
||||
Proxy = Grafana backend will proxy the request
|
||||
</info-popover>
|
||||
<div class="gf-form-select-wrapper max-width-24">
|
||||
<select class="gf-form-input" ng-model="current.access" ng-options="f.key as f.value for f in [{key: 'proxy', value: 'Server (Default)'}, { key: 'direct', value: 'Browser'}]"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showAccessHelp = !ctrl.showAccessHelp">
|
||||
Help
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showAccessHelp"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showAccessHelp"> </i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info" ng-show="ctrl.showAccessHelp">
|
||||
<div class="alert-body">
|
||||
<p>
|
||||
Access mode controls how requests to the data source will be handled.
|
||||
<strong><i>Server</i></strong> should be the preferred way if nothing else stated.
|
||||
</p>
|
||||
<div class="alert-title">Server access mode (Default):</div>
|
||||
<p>
|
||||
All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to the data source
|
||||
and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements.
|
||||
The URL needs to be accessible from the grafana backend/server if you select this access mode.
|
||||
</p>
|
||||
<div class="alert-title">Browser access mode:</div>
|
||||
<p>
|
||||
All requests will be made from the browser directly to the data source and may be subject to
|
||||
Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this
|
||||
access mode.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -135,4 +157,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -1,18 +1,18 @@
|
||||
<div dash-class ng-if="ctrl.dashboard">
|
||||
<dashnav dashboard="ctrl.dashboard"></dashnav>
|
||||
|
||||
<div class="scroll-canvas scroll-canvas--dashboard" grafana-scrollbar>
|
||||
<dashboard-settings dashboard="ctrl.dashboard"
|
||||
ng-if="ctrl.dashboardViewState.state.editview"
|
||||
class="dashboard-settings">
|
||||
</dashboard-settings>
|
||||
<div class="scroll-canvas scroll-canvas--dashboard" page-scrollbar>
|
||||
<dashboard-settings dashboard="ctrl.dashboard"
|
||||
ng-if="ctrl.dashboardViewState.state.editview"
|
||||
class="dashboard-settings">
|
||||
</dashboard-settings>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
|
||||
</dashboard-submenu>
|
||||
<div class="dashboard-container">
|
||||
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
|
||||
</dashboard-submenu>
|
||||
|
||||
<dashboard-grid get-panel-container="ctrl.getPanelContainer">
|
||||
</dashboard-grid>
|
||||
</div>
|
||||
</div>
|
||||
<dashboard-grid get-panel-container="ctrl.getPanelContainer">
|
||||
</dashboard-grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,7 +14,7 @@
|
||||
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for
|
||||
Controls the name of the time series, using name or pattern. For example <span ng-non-bindable>{{hostname}}</span> will be replaced with label value for
|
||||
the label hostname.
|
||||
</info-popover>
|
||||
</div>
|
||||
|
@ -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>
|
||||
|
@ -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('<div class="graph-legend-scroll"></div>');
|
||||
} else {
|
||||
elem.append(seriesElements);
|
||||
elem.append('<div class="graph-legend-scroll"></div>');
|
||||
elem.find('.graph-legend-scroll').append(seriesElements);
|
||||
}
|
||||
|
||||
if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) {
|
||||
@ -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 = `
|
||||
<div class="baron__track">
|
||||
<div class="baron__bar"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let scrollRoot = elem;
|
||||
let scroller = elem.find('.graph-legend-scroll');
|
||||
|
||||
// clear existing scroll bar track to prevent duplication
|
||||
scrollRoot.find('.baron__track').remove();
|
||||
|
||||
scrollRoot.addClass(scrollRootClass);
|
||||
$(scrollBarHTML).appendTo(scrollRoot);
|
||||
scroller.addClass(scrollerClass);
|
||||
|
||||
let scrollbarParams = {
|
||||
root: scrollRoot[0],
|
||||
scroller: scroller[0],
|
||||
bar: '.baron__bar',
|
||||
track: '.baron__track',
|
||||
barOnCls: '_scrollbar',
|
||||
scrollingCls: '_scrolling',
|
||||
};
|
||||
|
||||
if (!legendScrollbar) {
|
||||
legendScrollbar = new PerfectScrollbar(elem[0], scrollbarOptions);
|
||||
legendScrollbar = baron(scrollbarParams);
|
||||
} else {
|
||||
legendScrollbar.update();
|
||||
destroyScrollbar();
|
||||
legendScrollbar = baron(scrollbarParams);
|
||||
}
|
||||
legendScrollbar.scroll();
|
||||
}
|
||||
|
||||
function destroyScrollbar() {
|
||||
if (legendScrollbar) {
|
||||
legendScrollbar.destroy();
|
||||
legendScrollbar.dispose();
|
||||
legendScrollbar = undefined;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ var template = `
|
||||
<div class="graph-panel__chart" grafana-graph ng-dblclick="ctrl.zoomOut()">
|
||||
</div>
|
||||
|
||||
<div class="graph-legend" graph-legend></div>
|
||||
<div class="graph-legend">
|
||||
<div class="graph-legend-content" graph-legend></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
.gicon {
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
//width: 1.1057142857em;
|
||||
//height: 1.1057142857em;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
width: 1.1057142857em;
|
||||
height: 1.1057142857em;
|
||||
text-align: center;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -123,6 +123,8 @@
|
||||
position: relative;
|
||||
opacity: 0.7;
|
||||
font-size: 130%;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.fa {
|
||||
|
@ -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;
|
||||
|
@ -33,7 +33,7 @@ div.flot-text {
|
||||
border: $panel-border;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
//height: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.panel-transparent {
|
||||
background-color: transparent;
|
||||
|
@ -16,7 +16,7 @@
|
||||
<link rel="icon" type="image/png" href="public/img/fav32.png">
|
||||
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
|
||||
<link rel="apple-touch-icon" href="public/img/fav32.png">
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-cloak class="theme-[[ .Theme ]]">
|
||||
@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<div class="main-view">
|
||||
<div class="scroll-canvas" grafana-scrollbar>
|
||||
<div class="scroll-canvas" page-scrollbar>
|
||||
<div ng-view></div>
|
||||
|
||||
<footer class="footer">
|
||||
|
@ -20,17 +20,4 @@ echo "building backend with install to cache pkgs"
|
||||
exit_if_fail time go install ./pkg/cmd/grafana-server
|
||||
|
||||
echo "running go test"
|
||||
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
time for d in $(go list ./pkg/...); do
|
||||
exit_if_fail go test -coverprofile=profile.out -covermode=atomic $d
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Publishing go code coverage"
|
||||
bash <(curl -s https://codecov.io/bash) -cF go
|
||||
go test ./pkg/...
|
||||
|
@ -1162,6 +1162,10 @@ balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
|
||||
baron@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/baron/-/baron-3.0.3.tgz#0f0a08a567062882e130a0ecfd41a46d52103f4a"
|
||||
|
||||
base64-arraybuffer@0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
|
||||
@ -7503,10 +7507,6 @@ pend@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
|
||||
perfect-scrollbar@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/perfect-scrollbar/-/perfect-scrollbar-1.2.0.tgz#ad23a2529c17f4535f21d1486f8bc3046e31a9d2"
|
||||
|
||||
performance-now@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
|
||||
|
Loading…
Reference in New Issue
Block a user