Merge branch 'master' into provisioning_datasources_examples

This commit is contained in:
Marcus Efraimsson
2018-04-13 15:25:28 +02:00
86 changed files with 1505 additions and 949 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -180,14 +180,14 @@ Content-Type: application/json
```http ```http
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/json
``` ```
Deletes the annotation that matches the specified id. Deletes the annotation that matches the specified id.
**Example Request**: **Example Request**:
```http ```http
DELETE /api/annotation/1 HTTP/1.1 DELETE /api/annotations/1 HTTP/1.1
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
@@ -204,14 +204,14 @@ Content-Type: application/json
## Delete Annotation By RegionId ## 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. 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**: **Example Request**:
```http ```http
DELETE /api/annotation/region/1 HTTP/1.1 DELETE /api/annotations/region/1 HTTP/1.1
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk

View File

@@ -15,7 +15,7 @@ weight = 1
Description | Download Description | Download
------------ | ------------- ------------ | -------------
Stable for Debian-based Linux | [grafana_5.0.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.3_amd64.deb) Stable for Debian-based Linux | [grafana_5.0.4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb)
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
installation. installation.
@@ -24,9 +24,9 @@ installation.
```bash ```bash
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.3_amd64.deb wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb
sudo apt-get install -y adduser libfontconfig sudo apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_5.0.3_amd64.deb sudo dpkg -i grafana_5.0.4_amd64.deb
``` ```
## APT Repository ## APT Repository
@@ -34,7 +34,7 @@ sudo dpkg -i grafana_5.0.3_amd64.deb
Add the following line to your `/etc/apt/sources.list` file. Add the following line to your `/etc/apt/sources.list` file.
```bash ```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. 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. candidates.
```bash ```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 Then add the [Package Cloud](https://packagecloud.io/grafana) key. This

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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');
}
});
}
};
});
});

View 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);

View File

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

View 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);

View File

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

View File

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

View File

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

View File

@@ -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' });
};
});
});

View 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<div dash-class ng-if="ctrl.dashboard"> <div dash-class ng-if="ctrl.dashboard">
<dashnav dashboard="ctrl.dashboard"></dashnav> <dashnav dashboard="ctrl.dashboard"></dashnav>
<div class="scroll-canvas scroll-canvas--dashboard" grafana-scrollbar> <div class="scroll-canvas scroll-canvas--dashboard" page-scrollbar>
<dashboard-settings dashboard="ctrl.dashboard" <dashboard-settings dashboard="ctrl.dashboard"
ng-if="ctrl.dashboardViewState.state.editview" ng-if="ctrl.dashboardViewState.state.editview"
class="dashboard-settings"> class="dashboard-settings">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()"> data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
</input> </input>
<info-popover mode="right-absolute"> <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. the label hostname.
</info-popover> </info-popover>
</div> </div>

View File

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

View File

@@ -1,4 +1,5 @@
<div class="dashlist" ng-repeat="group in ctrl.groups"> <div>
<div class="dashlist" ng-repeat="group in ctrl.groups">
<div class="dashlist-section" ng-if="group.show"> <div class="dashlist-section" ng-if="group.show">
<h6 class="dashlist-section-header" ng-show="ctrl.panel.headings"> <h6 class="dashlist-section-header" ng-show="ctrl.panel.headings">
{{group.header}} {{group.header}}
@@ -14,4 +15,5 @@
</a> </a>
</div> </div>
</div> </div>
</div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@
</div> </div>
<div class="main-view"> <div class="main-view">
<div class="scroll-canvas" grafana-scrollbar> <div class="scroll-canvas" page-scrollbar>
<div ng-view></div> <div ng-view></div>
<footer class="footer"> <footer class="footer">

View File

@@ -20,17 +20,4 @@ echo "building backend with install to cache pkgs"
exit_if_fail time go install ./pkg/cmd/grafana-server exit_if_fail time go install ./pkg/cmd/grafana-server
echo "running go test" echo "running go test"
go test ./pkg/...
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

View File

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