Merge branch 'master' into develop

This commit is contained in:
Torkel Ödegaard 2017-11-02 15:56:09 +01:00
commit 10fcf2f5be
109 changed files with 844 additions and 801 deletions

View File

@ -4,8 +4,6 @@ Read before posting:
- Checkout FAQ: https://community.grafana.com/c/howto/faq
- Checkout How to troubleshoot metric query issues: https://community.grafana.com/t/how-to-troubleshoot-metric-query-issues/50
Please prefix your title with [Bug] or [Feature request].
Please include this information:
- What Grafana version are you using?
- What datasource are you using?

1
.gitignore vendored
View File

@ -38,6 +38,7 @@ public/css/*.min.css
conf/custom.ini
fig.yml
docker-compose.yml
docker-compose.yaml
profile.cov
/grafana
.notouch

View File

@ -11,7 +11,11 @@
## New Features
* **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson)
* **Postgres/MySQL**: add __timeGroup macro for mysql [#9596](https://github.com/grafana/grafana/pull/9596), thanks [@svenklemm](https://github.com/svenklemm)
* **Text**: Text panel are now edited in the ace editor. [#9698](https://github.com/grafana/grafana/pull/9698), thx [@mtanda](https://github.com/mtanda)
## Minor
* **Alert panel**: Adds placeholder text when no alerts are within the time range [#9624](https://github.com/grafana/grafana/issues/9624), thx [@straend](https://github.com/straend)
## Tech
* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
@ -21,6 +25,14 @@
* **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
* **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
# 4.6.1 (2017-11-01)
* **Singlestat**: Lost thresholds when using save dashboard as [#9681](https://github.com/grafana/grafana/issues/9681)
* **Graph**: Fix for series override color picker [#9715](https://github.com/grafana/grafana/issues/9715)
* **Go**: build using golang 1.9.2 [#9713](https://github.com/grafana/grafana/issues/9713)
* **Plugins**: Fixed problem with loading plugin js files behind auth proxy [#9509](https://github.com/grafana/grafana/issues/9509)
* **Graphite**: Annotation tooltip should render empty string when undefined [#9707](https://github.com/grafana/grafana/issues/9707)
# 4.6.0 (2017-10-26)
## Fixes

View File

@ -1,29 +1,36 @@
# Roadmap (2017-08-29)
# Roadmap (2017-10-31)
This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change.
But it will give you an idea of our current vision and plan.
### Short term (1-4 months)
- Release Grafana v4.5 with fixes and minor enhancements
- Release Grafana v5
- User groups
- Dashboard folders
- Dashboard permissions (on folders as well), permissions on groups or users
- Dashboard & folder permissions (assigned to users or groups)
- New Dashboard layout engine
- New sidemenu & nav UX
- Elasticsearch alerting
- React migration foundation (core components)
- Graphite 1.1 Tags Support
### Long term
### Long term (4 - 8 months)
- Backend plugins to support more Auth options, Alerting data sources & notifications
- Universal time series transformations for any data source (meta queries)
- Reporting
- Web socket & live data streams
- Migrate to Angular2 or react
- Alerting improvements (silence, per series tracking, etc)
- Dashboard as configuration and other automation / provisioning improvements
- Progress on React migration
- Change visualization (panel type) on the fly.
- Multi stat panel (vertical version of singlestat with bars/graph mode with big number etc)
- Repeat panel by query results
### In a distant future far far away
- Meta queries
- Integrated light weight TSDB
- Web socket & live data sources
### Outside contributions
We know this is being worked on right now by contributors (and we hope to merge it when it's ready).
- Clustering for alert engine (load distribution)

View File

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

View File

@ -9,7 +9,7 @@ machine:
GOPATH: "/home/ubuntu/.go_workspace"
ORG_PATH: "github.com/grafana"
REPO_PATH: "${ORG_PATH}/grafana"
GODIST: "go1.9.1.linux-amd64.tar.gz"
GODIST: "go1.9.2.linux-amd64.tar.gz"
post:
- mkdir -p ~/download
- mkdir -p ~/docker

View File

@ -8,4 +8,6 @@ coverage:
patch: yes
changes: no
comment: false
comment:
layout: "diff"
behavior: "once"

View File

@ -0,0 +1,11 @@
collectd:
build: blocks/collectd
environment:
HOST_NAME: myserver
GRAPHITE_HOST: graphite
GRAPHITE_PORT: 2003
GRAPHITE_PREFIX: collectd.
REPORT_BY_CPU: 'false'
COLLECT_INTERVAL: 10
links:
- graphite

View File

@ -1,11 +0,0 @@
collectd:
build: blocks/collectd
environment:
HOST_NAME: myserver
GRAPHITE_HOST: graphite
GRAPHITE_PORT: 2003
GRAPHITE_PREFIX: collectd.
REPORT_BY_CPU: 'false'
COLLECT_INTERVAL: 10
links:
- graphite

View File

@ -0,0 +1,8 @@
elasticsearch:
image: elasticsearch:2.4.1
command: elasticsearch -Des.network.host=0.0.0.0
ports:
- "9200:9200"
- "9300:9300"
volumes:
- ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml

View File

@ -1,8 +0,0 @@
elasticsearch:
image: elasticsearch:2.4.1
command: elasticsearch -Des.network.host=0.0.0.0
ports:
- "9200:9200"
- "9300:9300"
volumes:
- ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml

View File

@ -0,0 +1,8 @@
elasticsearch1:
image: elasticsearch:1.7.6
command: elasticsearch -Des.network.host=0.0.0.0
ports:
- "11200:9200"
- "11300:9300"
volumes:
- ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml

View File

@ -1,8 +0,0 @@
elasticsearch1:
image: elasticsearch:1.7.6
command: elasticsearch -Des.network.host=0.0.0.0
ports:
- "11200:9200"
- "11300:9300"
volumes:
- ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml

View File

@ -0,0 +1,8 @@
# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine
elasticsearch5:
image: elasticsearch:5
command: elasticsearch
ports:
- "10200:9200"
- "10300:9300"

View File

@ -1,8 +0,0 @@
# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine
elasticsearch5:
image: elasticsearch:5
command: elasticsearch
ports:
- "10200:9200"
- "10300:9300"

View File

@ -0,0 +1,16 @@
graphite:
build: blocks/graphite
ports:
- "8080:80"
- "2003:2003"
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
fake-graphite-data:
image: grafana/fake-data-gen
network_mode: bridge
environment:
FD_DATASOURCE: graphite
FD_PORT: 2003

View File

@ -1,16 +0,0 @@
graphite:
build: blocks/graphite
ports:
- "8080:80"
- "2003:2003"
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
fake-graphite-data:
image: grafana/fake-data-gen
net: bridge
environment:
FD_DATASOURCE: graphite
FD_PORT: 2003

View File

@ -1,5 +1,5 @@
FROM phusion/baseimage:0.9.22
MAINTAINER Denys Zhdanov <denis.zhdanov@gmail.com>
LABEL maintainer="Denys Zhdanov <denis.zhdanov@gmail.com>"
RUN apt-get -y update \
&& apt-get -y upgrade \

View File

@ -0,0 +1,16 @@
graphite:
build: blocks/graphite1
ports:
- "8080:80"
- "2003:2003"
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
fake-graphite-data:
image: grafana/fake-data-gen
network_mode: bridge
environment:
FD_DATASOURCE: graphite
FD_PORT: 2003

View File

@ -1,16 +0,0 @@
graphite:
build: blocks/graphite1
ports:
- "8080:80"
- "2003:2003"
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
fake-graphite-data:
image: grafana/fake-data-gen
net: bridge
environment:
FD_DATASOURCE: graphite
FD_PORT: 2003

View File

@ -0,0 +1,17 @@
influxdb:
image: influxdb:latest
container_name: influxdb
ports:
- "2004:2004"
- "8083:8083"
- "8086:8086"
volumes:
- ./blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf
fake-influxdb-data:
image: grafana/fake-data-gen
network_mode: bridge
environment:
FD_DATASOURCE: influxdb
FD_PORT: 8086

View File

@ -1,17 +0,0 @@
influxdb:
image: influxdb:latest
container_name: influxdb
ports:
- "2004:2004"
- "8083:8083"
- "8086:8086"
volumes:
- ./blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf
fake-influxdb-data:
image: grafana/fake-data-gen
net: bridge
environment:
FD_DATASOURCE: influxdb
FD_PORT: 8086

View File

@ -0,0 +1,6 @@
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "127.0.0.1:6831:6831/udp"
- "16686:16686"

View File

@ -1,6 +0,0 @@
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "localhost:6831:6831/udp"
- "16686:16686"

View File

@ -0,0 +1,5 @@
memcached:
image: memcached:latest
ports:
- "11211:11211"

View File

@ -1,5 +0,0 @@
memcached:
image: memcached:latest
ports:
- "11211:11211"

View File

@ -0,0 +1,14 @@
mysql:
image: mysql:latest
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: grafana
MYSQL_USER: grafana
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all]

View File

@ -1,14 +0,0 @@
mysql:
image: mysql:latest
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: grafana
MYSQL_USER: grafana
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all]

View File

@ -0,0 +1,9 @@
mysql_opendata:
build: blocks/mysql_opendata
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: testdata
MYSQL_USER: grafana
MYSQL_PASSWORD: password
ports:
- "3307:3306"

View File

@ -1,9 +0,0 @@
mysql_opendata:
build: blocks/mysql_opendata
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: testdata
MYSQL_USER: grafana
MYSQL_PASSWORD: password
ports:
- "3307:3306"

View File

@ -0,0 +1,9 @@
mysqltests:
image: mysql:latest
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: grafana_tests
MYSQL_USER: grafana
MYSQL_PASSWORD: password
ports:
- "3306:3306"

View File

@ -1,9 +0,0 @@
mysqltests:
image: mysql:latest
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: grafana_tests
MYSQL_USER: grafana
MYSQL_PASSWORD: password
ports:
- "3306:3306"

View File

@ -1,6 +1,6 @@
FROM debian:jessie
MAINTAINER Christian Luginbühl <dinke@pimprecords.com>
LABEL maintainer="Christian Luginbühl <dinke@pimprecords.com>"
ENV OPENLDAP_VERSION 2.4.40

View File

@ -0,0 +1,10 @@
openldap:
build: blocks/openldap
environment:
SLAPD_PASSWORD: grafana
SLAPD_DOMAIN: grafana.org
SLAPD_ADDITIONAL_MODULES: memberof
ports:
- "389:389"

View File

@ -1,10 +0,0 @@
openldap:
build: blocks/openldap
environment:
SLAPD_PASSWORD: grafana
SLAPD_DOMAIN: grafana.org
SLAPD_ADDITIONAL_MODULES: memberof
ports:
- "389:389"

View File

@ -0,0 +1,11 @@
opentsdb:
image: opower/opentsdb:latest
ports:
- "4242:4242"
fake-opentsdb-data:
image: grafana/fake-data-gen
network_mode: bridge
environment:
FD_DATASOURCE: opentsdb

View File

@ -1,11 +0,0 @@
opentsdb:
image: opower/opentsdb:latest
ports:
- "4242:4242"
fake-opentsdb-data:
image: grafana/fake-data-gen
net: bridge
environment:
FD_DATASOURCE: opentsdb

View File

@ -0,0 +1,9 @@
postgrestest:
image: postgres:latest
environment:
POSTGRES_USER: grafana
POSTGRES_PASSWORD: password
POSTGRES_DATABASE: grafana
ports:
- "5432:5432"
command: postgres -c log_connections=on -c logging_collector=on -c log_destination=stderr -c log_directory=/var/log/postgresql

View File

@ -1,9 +0,0 @@
postgrestest:
image: postgres:9.4.14
environment:
POSTGRES_USER: grafana
POSTGRES_PASSWORD: password
POSTGRES_DATABASE: grafana
ports:
- "5432:5432"
command: postgres -c log_connections=on -c logging_collector=on -c log_destination=stderr -c log_directory=/var/log/postgresql

View File

@ -0,0 +1,7 @@
postgrestest:
image: postgres:latest
environment:
POSTGRES_USER: grafanatest
POSTGRES_PASSWORD: grafanatest
ports:
- "5432:5432"

View File

@ -1,7 +0,0 @@
postgrestest:
image: postgres:latest
environment:
POSTGRES_USER: grafanatest
POSTGRES_PASSWORD: grafanatest
ports:
- "5432:5432"

View File

@ -0,0 +1,25 @@
prometheus:
build: blocks/prometheus
network_mode: host
ports:
- "9090:9090"
node_exporter:
image: prom/node-exporter
network_mode: host
ports:
- "9100:9100"
fake-prometheus-data:
image: grafana/fake-data-gen
network_mode: host
ports:
- "9091:9091"
environment:
FD_DATASOURCE: prom
alertmanager:
image: quay.io/prometheus/alertmanager
network_mode: host
ports:
- "9093:9093"

View File

@ -1,25 +0,0 @@
prometheus:
build: blocks/prometheus
net: host
ports:
- "9090:9090"
node_exporter:
image: prom/node-exporter
net: host
ports:
- "9100:9100"
fake-prometheus-data:
image: grafana/fake-data-gen
net: host
ports:
- "9091:9091"
environment:
FD_DATASOURCE: prom
alertmanager:
image: quay.io/prometheus/alertmanager
net: host
ports:
- "9093:9093"

View File

@ -1,5 +1,5 @@
FROM centos:centos7
MAINTAINER Przemyslaw Ozgo <linux@ozgo.info>
LABEL maintainer="Przemyslaw Ozgo <linux@ozgo.info>"
RUN \
yum update -y && \

View File

@ -0,0 +1,4 @@
snmpd:
image: namshi/smtp
ports:
- "25:25"

View File

@ -1,4 +0,0 @@
snmpd:
image: namshi/smtp
ports:
- "25:25"

View File

@ -0,0 +1,2 @@
version: "2"
services:

View File

@ -7,8 +7,9 @@ template_dir=templates
grafana_config_file=conf.tmp
grafana_config=config
fig_file=docker-compose.yml
fig_config=fig
compose_header_file=compose_header.yml
fig_file=docker-compose.yaml
fig_config=docker-compose.yaml
if [ "$#" == 0 ]; then
blocks=`ls $blocks_dir`
@ -23,13 +24,16 @@ if [ "$#" == 0 ]; then
exit 0
fi
for file in $gogs_config_file $fig_file; do
for file in $grafana_config_file $fig_file; do
if [ -e $file ]; then
echo "Deleting $file"
rm $file
fi
done
echo "Adding Compose header to $fig_file"
cat $compose_header_file >> $fig_file
for dir in $@; do
current_dir=$blocks_dir/$dir
if [ ! -d "$current_dir" ]; then
@ -45,7 +49,7 @@ for dir in $@; do
if [ -e $current_dir/$fig_config ]; then
echo "Adding $current_dir/$fig_config to $fig_file"
cat $current_dir/fig >> $fig_file
cat $current_dir/$fig_config >> $fig_file
echo "" >> $fig_file
fi
done

View File

@ -95,3 +95,7 @@ Prometheus supports two ways to query annotations.
- A Prometheus query for pending and firing alerts (for details see [Inspecting alerts during runtime](https://prometheus.io/docs/alerting/rules/#inspecting-alerts-during-runtime))
The step option is useful to limit the number of events returned from your query.
## Getting Grafana metrics into Prometheus
Since 4.6.0 Grafana exposes metrics for Prometheus on the `/metrics` endpoint. We also bundle a dashboard within Grafana so you can get started viewing your metrics faster. You can import the bundled dashboard by going to the data source edit page and click the dashboard tab. There you can find a dashboard for Grafana and one for Prometheus. Import and start viewing all the metrics!

View File

@ -17,7 +17,7 @@ This make is much easier to verify functionally since the data can be shared ver
## Enable
`Grafana TestData` is not enabled by default. To enable it you have to go to `/plugins/testdata/edit` and click the enable button to enable.
`Grafana TestData` is not enabled by default. To enable it, first navigate to the Plugins section, found in your Grafana main menu. Click the Apps tabs in the Plugins section and select the Grafana TestData App. (Or navigate to http://your_grafana_instance/plugins/testdata/edit to go directly there). Finally click the enable button to enable.
## Create mock data.

View File

@ -258,7 +258,7 @@ Query parameters:
**Example Request**:
```http
GET /api/search?query=MyDashboard&starred=true&tag=prod HTTP/1.1
GET /api/search?query=Production%20Overview&starred=true&tag=prod HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
@ -276,8 +276,8 @@ Content-Type: application/json
"title":"Production Overview",
"uri":"db/production-overview",
"type":"dash-db",
"tags":[],
"isStarred":false
"tags":[prod],
"isStarred":true
}
]
```

View File

@ -46,8 +46,8 @@ those options.
- [Graphite]({{< relref "features/datasources/graphite.md" >}})
- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}})
- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}})
- [Prometheus]({{< relref "features/datasources/influxdb.md" >}})
- [OpenTSDB]({{< relref "features/datasources/prometheus.md" >}})
- [Prometheus]({{< relref "features/datasources/prometheus.md" >}})
- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}})
- [MySQL]({{< relref "features/datasources/mysql.md" >}})
- [Postgres]({{< relref "features/datasources/postgres.md" >}})
- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}})

View File

@ -551,7 +551,7 @@ session provider you have configured.
- **file:** session file path, e.g. `data/sessions`
- **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name`
- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=require
- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full
- **memcache:** ex: 127.0.0.1:11211
- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`
@ -580,7 +580,7 @@ CREATE TABLE session (
);
```
Postgres valid `sslmode` are `disable`, `require` (default), `verify-ca`, and `verify-full`.
Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default).
### cookie_name

View File

@ -15,7 +15,7 @@ weight = 1
Description | Download
------------ | -------------
Stable for Debian-based Linux | [grafana_4.6.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.0_amd64.deb)
Stable for Debian-based Linux | [grafana_4.6.1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.1_amd64.deb)
<!-- Beta for Debian-based Linux | [grafana_4.5.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.5.0-beta1_amd64.deb) -->
@ -26,9 +26,9 @@ installation.
```bash
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.0_amd64.deb
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.1_amd64.deb
sudo apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_4.6.0_amd64.deb
sudo dpkg -i grafana_4.6.1_amd64.deb
```
<!--

View File

@ -15,7 +15,7 @@ weight = 2
Description | Download
------------ | -------------
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.0 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.0-1.x86_64.rpm)
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.1-1.x86_64.rpm)
<!-- Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [4.5.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.0-beta1.x86_64.rpm) -->
@ -27,7 +27,7 @@ installation.
You can install Grafana using Yum directly.
```bash
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.0-1.x86_64.rpm
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.1-1.x86_64.rpm
```
Or install manually using `rpm`.
@ -35,15 +35,15 @@ Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
```bash
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.0-1.x86_64.rpm
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.1-1.x86_64.rpm
$ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-4.6.0-1.x86_64.rpm
$ sudo rpm -Uvh grafana-4.6.1-1.x86_64.rpm
```
#### On OpenSuse:
```bash
$ sudo rpm -i --nodeps grafana-4.6.0-1.x86_64.rpm
$ sudo rpm -i --nodeps grafana-4.6.1-1.x86_64.rpm
```
## Install via YUM Repository

View File

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

View File

@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
## Dependencies
- [Go 1.9.1](https://golang.org/dl/)
- [Go 1.9.2](https://golang.org/dl/)
- [NodeJS LTS](https://nodejs.org/download/)
- [Git](https://git-scm.com/downloads)

View File

@ -1,5 +1,5 @@
#! /usr/bin/env bash
version=4.5.2
version=4.6.1
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb

View File

@ -225,7 +225,7 @@ func init() {
M_DataSource_ProxyReq_Timer = prometheus.NewSummary(prometheus.SummaryOpts{
Name: "api_dataproxy_request_all_milliseconds",
Help: "summary for dashboard search duration",
Help: "summary for dataproxy request duration",
Namespace: exporterName,
})

View File

@ -267,7 +267,10 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
period = int(d.Seconds())
}
alias := model.Get("alias").MustString("{{metric}}_{{stat}}")
alias := model.Get("alias").MustString()
if alias == "" {
alias = "{{metric}}_{{stat}}"
}
return &CloudWatchQuery{
Region: region,
@ -287,6 +290,7 @@ func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]stri
data["namespace"] = query.Namespace
data["metric"] = query.MetricName
data["stat"] = stat
data["period"] = strconv.Itoa(query.Period)
for k, v := range dimensions {
data[k] = v
}

View File

@ -3,6 +3,8 @@ package mysql
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
@ -25,7 +27,7 @@ func (m *MySqlMacroEngine) Interpolate(timeRange *tsdb.TimeRange, sql string) (s
var macroError error
sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string {
res, err := m.evaluateMacro(groups[1], groups[2:])
res, err := m.evaluateMacro(groups[1], strings.Split(groups[2], ","))
if err != nil && macroError == nil {
macroError = err
return "macro_error()"
@ -73,6 +75,15 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
case "__timeTo":
return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
case "__timeGroup":
if len(args) != 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
}
interval, err := time.ParseDuration(strings.Trim(args[1], `'" `))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
return fmt.Sprintf("cast(cast(UNIX_TIMESTAMP(%s)/(%.0f) as signed)*%.0f as signed)", args[0], interval.Seconds(), interval.Seconds()), nil
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)

View File

@ -40,6 +40,14 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, "select FROM_UNIXTIME(18446744066914186738)")
})
Convey("interpolate __timeGroup function", func() {
sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(timeRange, "select $__timeTo(time_column)")
So(err, ShouldBeNil)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
@ -80,10 +81,14 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
case "__timeTo":
return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
case "__timeGroup":
if len(args) < 2 {
if len(args) != 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
}
return fmt.Sprintf("(extract(epoch from \"%s\")/extract(epoch from %s::interval))::int*extract(epoch from %s::interval)", args[0], args[1], args[1]), nil
interval, err := time.ParseDuration(strings.Trim(args[1], `' `))
if err != nil {
return "", fmt.Errorf("error parsing interval %v", args[1])
}
return fmt.Sprintf("(extract(epoch from \"%s\")/%v)::bigint*%v", args[0], interval.Seconds(), interval.Seconds()), nil
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)

View File

@ -45,7 +45,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/extract(epoch from '5m'::interval))::int*extract(epoch from '5m'::interval)")
So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/300)::bigint*300")
})
Convey("interpolate __timeTo function", func() {

View File

@ -51,7 +51,7 @@ func generateConnectionString(datasource *models.DataSource) string {
}
}
sslmode := datasource.JsonData.Get("sslmode").MustString("require")
sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full")
return fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", datasource.User, password, datasource.Url, datasource.Database, sslmode)
}

View File

@ -1,6 +1,7 @@
package testdata
import (
"encoding/json"
"math/rand"
"strconv"
"strings"
@ -142,6 +143,45 @@ func init() {
},
})
registerScenario(&Scenario{
Id: "manual_entry",
Name: "Manual Entry",
Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult {
queryRes := tsdb.NewQueryResult()
points := query.Model.Get("points").MustArray()
series := newSeriesForQuery(query)
startTime := context.TimeRange.GetFromAsMsEpoch()
endTime := context.TimeRange.GetToAsMsEpoch()
for _, val := range points {
pointValues := val.([]interface{})
var value null.Float
var time int64
if valueFloat, err := strconv.ParseFloat(string(pointValues[0].(json.Number)), 64); err == nil {
value = null.FloatFrom(valueFloat)
}
if timeInt, err := strconv.ParseInt(string(pointValues[1].(json.Number)), 10, 64); err != nil {
continue
} else {
time = timeInt
}
if time >= startTime && time <= endTime {
series.Points = append(series.Points, tsdb.NewTimePoint(value, float64(time)))
}
}
queryRes.Series = append(queryRes.Series, series)
return queryRes
},
})
registerScenario(&Scenario{
Id: "csv_metric_values",
Name: "CSV Metric Values",

View File

@ -36,6 +36,8 @@ import 'brace/mode/text';
import 'brace/snippets/text';
import 'brace/mode/sql';
import 'brace/snippets/sql';
import 'brace/mode/markdown';
import 'brace/snippets/markdown';
const DEFAULT_THEME_DARK = "ace/theme/grafana-dark";
const DEFAULT_THEME_LIGHT = "ace/theme/textmate";

View File

@ -43,7 +43,7 @@ export class SeriesColorPicker extends React.Component<IProps, any> {
render() {
return (
<div className="graph-legend-popover">
{this.props.series && this.renderAxisSelection()}
{this.props.series.yaxis && this.renderAxisSelection()}
<ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
</div>
);

View File

@ -1,5 +1,4 @@
import "./directives/dash_class";
import "./directives/confirm_click";
import "./directives/dash_edit_link";
import "./directives/dropdown_typeahead";
import "./directives/metric_segment";

View File

@ -1,23 +0,0 @@
define([
'../core_module',
],
function (coreModule) {
'use strict';
coreModule.default.directive('confirmClick', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
elem.bind('click', function() {
var message = attrs.confirmation || "Are you sure you want to do that?";
if (window.confirm(message)) {
var action = attrs.confirmClick;
if (action) {
scope.$apply(scope.$eval(action));
}
}
});
},
};
});
});

View File

@ -1,11 +1,8 @@
define([
'../core_module',
'app/core/utils/rangeutil',
],
function (coreModule, rangeUtil) {
'use strict';
import coreModule from '../core_module';
import * as rangeUtil from 'app/core/utils/rangeutil';
coreModule.default.directive('ngModelOnblur', function() {
export class NgModelOnBlur {
constructor() {
return {
restrict: 'A',
priority: 1,
@ -23,9 +20,12 @@ function (coreModule, rangeUtil) {
});
}
};
});
}
}
coreModule.default.directive('emptyToNull', function () {
export class EmptyToNull {
constructor() {
return {
restrict: 'A',
require: 'ngModel',
@ -36,9 +36,11 @@ function (coreModule, rangeUtil) {
});
}
};
});
}
}
coreModule.default.directive('validTimeSpan', function() {
export class ValidTimeSpan {
constructor() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
@ -54,5 +56,9 @@ function (coreModule, rangeUtil) {
};
}
};
});
});
}
}
coreModule.directive('ngModelOnblur', NgModelOnBlur);
coreModule.directive('emptyToNull', EmptyToNull);
coreModule.directive('validTimeSpan', ValidTimeSpan);

View File

@ -3,7 +3,6 @@ define([
'./util_srv',
'./context_srv',
'./timer',
'./keyboard_manager',
'./analytics',
'./popover_srv',
'./segment_srv',

View File

@ -1,291 +0,0 @@
define([
'angular',
'lodash',
'../core_module',
],
function (angular, _, coreModule) {
'use strict';
// This service was based on OpenJS library available in BSD License
// http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php
coreModule.default.factory('keyboardManager', ['$window', '$timeout', function ($window, $timeout) {
var keyboardManagerService = {};
var defaultOpt = {
'type': 'keydown',
'propagate': false,
'inputDisabled': false,
'target': $window.document,
'keyCode': false
};
// Store all keyboard combination shortcuts
keyboardManagerService.keyboardEvent = {};
// Add a new keyboard combination shortcut
keyboardManagerService.bind = function (label, callback, opt) {
var fct, elt, code, k;
// Initialize opt object
opt = angular.extend({}, defaultOpt, opt);
label = label.toLowerCase();
elt = opt.target;
if (typeof opt.target === 'string') {
elt = document.getElementById(opt.target);
}
fct = function (e) {
e = e || $window.event;
// Disable event handler when focus input and textarea
if (opt['inputDisabled']) {
var elt;
if (e.target) {
elt = e.target;
}
else if (e.srcElement) {
elt = e.srcElement;
}
if (elt.nodeType === 3) {
elt = elt.parentNode;
}
if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA') {
return;
}
}
// Find out which key is pressed
if (e.keyCode) {
code = e.keyCode;
}
else if (e.which) {
code = e.which;
}
var character = String.fromCharCode(code).toLowerCase();
if (code === 188) {
character = ","; // If the user presses , when the type is onkeydown
}
if (code === 190) {
character = "."; // If the user presses , when the type is onkeydown
}
var keys = label.split("+");
// Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
var kp = 0;
// Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
var shift_nums = {
"`": "~",
"1": "!",
"2": "@",
"3": "#",
"4": "$",
"5": "%",
"6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
";": ":",
"'": "\"",
",": "<",
".": ">",
"/": "?",
"»": "?",
"«": "?",
"¿": "?",
"\\": "|"
};
// Special Keys - and their codes
var special_keys = {
'esc': 27,
'escape': 27,
'tab': 9,
'space': 32,
'return': 13,
'enter': 13,
'backspace': 8,
'scrolllock': 145,
'scroll_lock': 145,
'scroll': 145,
'capslock': 20,
'caps_lock': 20,
'caps': 20,
'numlock': 144,
'num_lock': 144,
'num': 144,
'pause': 19,
'break': 19,
'insert': 45,
'home': 36,
'delete': 46,
'end': 35,
'pageup': 33,
'page_up': 33,
'pu': 33,
'pagedown': 34,
'page_down': 34,
'pd': 34,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
'f1': 112,
'f2': 113,
'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123
};
// Some modifiers key
var modifiers = {
shift: {
wanted: false,
pressed: e.shiftKey ? true : false
},
ctrl : {
wanted: false,
pressed: e.ctrlKey ? true : false
},
alt : {
wanted: false,
pressed: e.altKey ? true : false
},
meta : { //Meta is Mac specific
wanted: false,
pressed: e.metaKey ? true : false
}
};
// Foreach keys in label (split on +)
for (var i = 0, l = keys.length; k = keys[i], i < l; i++) {
switch (k) {
case 'ctrl':
case 'control':
kp++;
modifiers.ctrl.wanted = true;
break;
case 'shift':
case 'alt':
case 'meta':
kp++;
modifiers[k].wanted = true;
break;
}
if (k.length > 1) { // If it is a special key
if (special_keys[k] === code) {
kp++;
}
} else if (opt['keyCode']) { // If a specific key is set into the config
if (opt['keyCode'] === code) {
kp++;
}
} else { // The special keys did not match
if (character === k) {
kp++;
}
else {
if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase
character = shift_nums[character];
if (character === k) {
kp++;
}
}
}
}
}
if (kp === keys.length &&
modifiers.ctrl.pressed === modifiers.ctrl.wanted &&
modifiers.shift.pressed === modifiers.shift.wanted &&
modifiers.alt.pressed === modifiers.alt.wanted &&
modifiers.meta.pressed === modifiers.meta.wanted) {
$timeout(function() {
callback(e);
}, 1);
if (!opt['propagate']) { // Stop the event
// e.cancelBubble is supported by IE - this will kill the bubbling process.
e.cancelBubble = true;
e.returnValue = false;
// e.stopPropagation works in Firefox.
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
return false;
}
}
};
// Store shortcut
keyboardManagerService.keyboardEvent[label] = {
'callback': fct,
'target': elt,
'event': opt['type']
};
//Attach the function with the event
if (elt.addEventListener) {
elt.addEventListener(opt['type'], fct, false);
}
else if (elt.attachEvent) {
elt.attachEvent('on' + opt['type'], fct);
}
else {
elt['on' + opt['type']] = fct;
}
};
keyboardManagerService.unbindAll = function() {
_.each(keyboardManagerService.keyboardEvent, function(value, key) {
keyboardManagerService.unbind(key);
});
};
// Remove the shortcut - just specify the shortcut and I will remove the binding
keyboardManagerService.unbind = function (label) {
label = label.toLowerCase();
var binding = keyboardManagerService.keyboardEvent[label];
delete(keyboardManagerService.keyboardEvent[label]);
if (!binding) {
return;
}
var type = binding['event'],
elt = binding['target'],
callback = binding['callback'];
if (elt.detachEvent) {
elt.detachEvent('on' + type, callback);
}
else if (elt.removeEventListener) {
elt.removeEventListener(type, callback, false);
}
else {
elt['on' + type] = false;
}
};
//
return keyboardManagerService;
}]);
});

View File

@ -21,7 +21,7 @@ export class Timer {
}
cancelAll() {
_.each(this.timers, function (t) {
_.each(this.timers, t => {
this.$timeout.cancel(t);
});
this.timers = [];

View File

@ -475,6 +475,7 @@ kbn.valueFormats.wpm = kbn.formatBuilders.simpleCountUnit('wpm');
// Energy
kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W');
kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1);
kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1);
kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA');
kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1);
kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var');
@ -488,6 +489,7 @@ kbn.valueFormats.kamp = kbn.formatBuilders.decimalSIPrefix('A', 1);
kbn.valueFormats.volt = kbn.formatBuilders.decimalSIPrefix('V');
kbn.valueFormats.kvolt = kbn.formatBuilders.decimalSIPrefix('V', 1);
kbn.valueFormats.dBm = kbn.formatBuilders.decimalSIPrefix('dBm');
kbn.valueFormats.ohm = kbn.formatBuilders.decimalSIPrefix('Ω');
// Temperature
kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C');
@ -514,6 +516,12 @@ kbn.valueFormats.lengthm = kbn.formatBuilders.decimalSIPrefix('m');
kbn.valueFormats.lengthmm = kbn.formatBuilders.decimalSIPrefix('m', -1);
kbn.valueFormats.lengthkm = kbn.formatBuilders.decimalSIPrefix('m', 1);
kbn.valueFormats.lengthmi = kbn.formatBuilders.fixedUnit('mi');
kbn.valueFormats.lengthft = kbn.formatBuilders.fixedUnit('ft');
// Area
kbn.valueFormats.areaM2 = kbn.formatBuilders.fixedUnit('m²');
kbn.valueFormats.areaF2 = kbn.formatBuilders.fixedUnit('ft²');
kbn.valueFormats.areaMI2 = kbn.formatBuilders.fixedUnit('mi²');
// Mass
kbn.valueFormats.massmg = kbn.formatBuilders.decimalSIPrefix('g', -1);
@ -527,6 +535,11 @@ kbn.valueFormats.velocitykmh = kbn.formatBuilders.fixedUnit('km/h');
kbn.valueFormats.velocitymph = kbn.formatBuilders.fixedUnit('mph');
kbn.valueFormats.velocityknot = kbn.formatBuilders.fixedUnit('kn');
// Acceleration
kbn.valueFormats.accMS2 = kbn.formatBuilders.fixedUnit('m/sec²');
kbn.valueFormats.accFS2 = kbn.formatBuilders.fixedUnit('f/sec²');
kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g');
// Volume
kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L');
kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1);
@ -540,6 +553,11 @@ kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms');
kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
// Angle
kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
kbn.valueFormats.radian = kbn.formatBuilders.fixedUnit('rad');
kbn.valueFormats.grad = kbn.formatBuilders.fixedUnit('grad');
// Time
kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
@ -877,6 +895,14 @@ kbn.getUnitFormats = function() {
{ text: 'mile (mi)', value: 'lengthmi' },
],
},
{
text: 'area',
submenu: [
{text: 'Square Meters (m²)', value: 'areaM2' },
{text: 'Square Feet (ft²)', value: 'areaF2' },
{text: 'Square Miles (mi²)', value: 'areaMI2'},
]
},
{
text: 'mass',
submenu: [
@ -916,6 +942,7 @@ kbn.getUnitFormats = function() {
{ text: 'kilovolt-ampere reactive (kvar)', value: 'kvoltampreact' },
{ text: 'watt-hour (Wh)', value: 'watth' },
{ text: 'kilowatt-hour (kWh)', value: 'kwatth' },
{ text: 'kilowatt-min (kWm)', value: 'kwattm' },
{ text: 'joule (J)', value: 'joule' },
{ text: 'electron volt (eV)', value: 'ev' },
{ text: 'Ampere (A)', value: 'amp' },
@ -923,6 +950,7 @@ kbn.getUnitFormats = function() {
{ text: 'Volt (V)', value: 'volt' },
{ text: 'Kilovolt (kV)', value: 'kvolt' },
{ text: 'Decibel-milliwatt (dBm)', value: 'dBm' },
{ text: 'Ohm (Ω)', value: 'ohm' }
],
},
{
@ -962,6 +990,22 @@ kbn.getUnitFormats = function() {
{ text: 'Cubic feet/min (cfm)', value: 'flowcfm' },
],
},
{
text: 'angle',
submenu: [
{ text: 'Degrees (°)', value: 'degree' },
{ text: 'Radians', value: 'radian' },
{ text: 'Gradian', value: 'grad' }
]
},
{
text: 'acceleration',
submenu: [
{ text: 'Meters/sec²', value: 'accMS2' },
{ text: 'Feet/sec²', value: 'accFS2' },
{ text: 'G unit', value: 'accG' }
]
}
];
};

View File

@ -39,7 +39,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
text = text + '<br />' + event.text;
}
} else if (title) {
text = title + '<br />' + text;
text = title + '<br />' + (_.isString(text) ? text : '');
title = '';
}

View File

@ -56,7 +56,9 @@ export class SaveDashboardAsModalCtrl {
// do not want to create alert dupes
if (dashboard.id > 0) {
this.clone.panels.forEach(panel => {
if (panel.type === "graph" && panel.alert) {
delete panel.thresholds;
}
delete panel.alert;
});
}

View File

@ -0,0 +1,67 @@
import {SaveDashboardAsModalCtrl} from '../save_as_modal';
import {describe, it, expect} from 'test/lib/common';
describe('saving dashboard as', () => {
function scenario(name, panel, verify) {
describe(name, () => {
var json = {
title: "name",
rows: [ { panels: [
panel
]}]
};
var mockDashboardSrv = {
getCurrent: function() {
return {
id: 5,
getSaveModelClone: function() {
return json;
}
};
}
};
var ctrl = new SaveDashboardAsModalCtrl(mockDashboardSrv);
var ctx: any = {
clone: ctrl.clone,
ctrl: ctrl,
panel: {}
};
for (let row of ctrl.clone.rows) {
for (let panel of row.panels) {
ctx.panel = panel;
}
}
it("verify", () => {
verify(ctx);
});
});
}
scenario("default values", {}, (ctx) => {
var clone = ctx.clone;
expect(clone.id).toBe(null);
expect(clone.title).toBe("name Copy");
expect(clone.editable).toBe(true);
expect(clone.hideControls).toBe(false);
});
var graphPanel = { id: 1, type: "graph", alert: { rule: 1}, thresholds: { value: 3000} };
scenario("should remove alert from graph panel", graphPanel , (ctx) => {
expect(ctx.panel.alert).toBe(undefined);
});
scenario("should remove threshold from graph panel", graphPanel, (ctx) => {
expect(ctx.panel.thresholds).toBe(undefined);
});
scenario("singlestat should keep threshold", { id: 1, type: "singlestat", thresholds: { value: 3000} }, (ctx) => {
expect(ctx.panel.thresholds).not.toBe(undefined);
});
scenario("table should keep threshold", { id: 1, type: "table", thresholds: { value: 3000} }, (ctx) => {
expect(ctx.panel.thresholds).not.toBe(undefined);
});
});

View File

@ -2,7 +2,7 @@ import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/co
import helpers from 'test/specs/helpers';
import '../shareModalCtrl';
import config from 'app/core/config';
import 'app/features/panellinks/linkSrv';
import 'app/features/panellinks/link_srv';
describe('ShareModalCtrl', function() {
var ctx = new helpers.ControllerTestContext();

View File

@ -1,5 +1,3 @@
///<reference path="../../../headers/common.d.ts" />
import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
@ -13,10 +11,10 @@ export function inputDateDirective() {
var fromUser = function(text) {
if (text.indexOf('now') !== -1) {
if (!dateMath.isValid(text)) {
ngModel.$setValidity("error", false);
ngModel.$setValidity('error', false);
return undefined;
}
ngModel.$setValidity("error", true);
ngModel.$setValidity('error', true);
return text;
}
@ -28,11 +26,11 @@ export function inputDateDirective() {
}
if (!parsed.isValid()) {
ngModel.$setValidity("error", false);
ngModel.$setValidity('error', false);
return undefined;
}
ngModel.$setValidity("error", true);
ngModel.$setValidity('error', true);
return parsed;
};
@ -46,7 +44,6 @@ export function inputDateDirective() {
ngModel.$parsers.push(fromUser);
ngModel.$formatters.push(toUser);
}
},
};
}

View File

@ -13,9 +13,9 @@
</div>
<div class="gf-form" ng-show="link.type === 'dashboards'">
<span class="gf-form-label width-8">With tags</span>
<bootstrap-tagsinput ng-model="link.tags" class="width-10" tagclass="label label-tag" placeholder="add tags" style="margin-right: .25rem"></bootstrap-tagsinput>
<bootstrap-tagsinput ng-model="link.tags" tagclass="label label-tag" placeholder="add tags" style="margin-right: .25rem"></bootstrap-tagsinput>
</div>
<gf-form-switch ng-show="link.type === 'dashboards'" class="gf-form" label="As dropdown" checked="link.asDropdown" switch-class="max-width-4" label-class="width-8"></gf-form-switch>
<gf-form-switch ng-show="link.type === 'dashboards'" class="gf-form" label="As dropdown" checked="link.asDropdown" switch-class="max-width-4" label-class="width-8" on-change="updated()"></gf-form-switch>
<div class="gf-form" ng-show="link.type === 'dashboards' && link.asDropdown">
<span class="gf-form-label width-8">Title</span>
<input type="text" ng-model="link.title" class="gf-form-input max-width-10" ng-model-onblur ng-change="updated()">

View File

@ -226,7 +226,6 @@ class MetricsPanelCtrl extends PanelCtrl {
interval: this.interval,
intervalMs: this.intervalMs,
targets: this.panel.targets,
format: this.panel.renderer === 'png' ? 'png' : 'json',
maxDataPoints: this.resolution,
scopedVars: scopedVars,
cacheTimeout: this.panel.cacheTimeout

View File

@ -1,118 +0,0 @@
define([
'angular',
'lodash',
'app/core/utils/kbn',
],
function (angular, _, kbn) {
'use strict';
kbn = kbn.default;
angular
.module('grafana.services')
.service('linkSrv', function(templateSrv, timeSrv) {
this.getLinkUrl = function(link) {
var url = templateSrv.replace(link.url || '');
var params = {};
if (link.keepTime) {
var range = timeSrv.timeRangeForUrl();
params['from'] = range.from;
params['to'] = range.to;
}
if (link.includeVars) {
templateSrv.fillVariableValuesForUrl(params);
}
return this.addParamsToUrl(url, params);
};
this.addParamsToUrl = function(url, params) {
var paramsArray = [];
_.each(params, function(value, key) {
if (value === null) { return; }
if (value === true) {
paramsArray.push(key);
}
else if (_.isArray(value)) {
_.each(value, function(instance) {
paramsArray.push(key + '=' + encodeURIComponent(instance));
});
}
else {
paramsArray.push(key + '=' + encodeURIComponent(value));
}
});
if (paramsArray.length === 0) {
return url;
}
return this.appendToQueryString(url, paramsArray.join('&'));
};
this.appendToQueryString = function(url, stringToAppend) {
if (!_.isUndefined(stringToAppend) && stringToAppend !== null && stringToAppend !== '') {
var pos = url.indexOf('?');
if (pos !== -1) {
if (url.length - pos > 1) {
url += '&';
}
} else {
url += '?';
}
url += stringToAppend;
}
return url;
};
this.getAnchorInfo = function(link) {
var info = {};
info.href = this.getLinkUrl(link);
info.title = templateSrv.replace(link.title || '');
return info;
};
this.getPanelLinkAnchorInfo = function(link, scopedVars) {
var info = {};
if (link.type === 'absolute') {
info.target = link.targetBlank ? '_blank' : '_self';
info.href = templateSrv.replace(link.url || '', scopedVars);
info.title = templateSrv.replace(link.title || '', scopedVars);
}
else if (link.dashUri) {
info.href = 'dashboard/' + link.dashUri + '?';
info.title = templateSrv.replace(link.title || '', scopedVars);
info.target = link.targetBlank ? '_blank' : '';
}
else {
info.title = templateSrv.replace(link.title || '', scopedVars);
var slug = kbn.slugifyForUrl(link.dashboard || '');
info.href = 'dashboard/db/' + slug + '?';
}
var params = {};
if (link.keepTime) {
var range = timeSrv.timeRangeForUrl();
params['from'] = range.from;
params['to'] = range.to;
}
if (link.includeVars) {
templateSrv.fillVariableValuesForUrl(params, scopedVars);
}
info.href = this.addParamsToUrl(info.href, params);
if (link.params) {
info.href = this.appendToQueryString(info.href, templateSrv.replace(link.params, scopedVars));
}
return info;
};
});
});

View File

@ -0,0 +1,113 @@
import angular from 'angular';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
export class LinkSrv {
/** @ngInject */
constructor(private templateSrv, private timeSrv) {}
getLinkUrl(link) {
var url = this.templateSrv.replace(link.url || '');
var params = {};
if (link.keepTime) {
var range = this.timeSrv.timeRangeForUrl();
params['from'] = range.from;
params['to'] = range.to;
}
if (link.includeVars) {
this.templateSrv.fillVariableValuesForUrl(params);
}
return this.addParamsToUrl(url, params);
}
addParamsToUrl(url, params) {
var paramsArray = [];
_.each(params, function(value, key) {
if (value === null) {
return;
}
if (value === true) {
paramsArray.push(key);
} else if (_.isArray(value)) {
_.each(value, function(instance) {
paramsArray.push(key + '=' + encodeURIComponent(instance));
});
} else {
paramsArray.push(key + '=' + encodeURIComponent(value));
}
});
if (paramsArray.length === 0) {
return url;
}
return this.appendToQueryString(url, paramsArray.join('&'));
}
appendToQueryString(url, stringToAppend) {
if (!_.isUndefined(stringToAppend) && stringToAppend !== null && stringToAppend !== '') {
var pos = url.indexOf('?');
if (pos !== -1) {
if (url.length - pos > 1) {
url += '&';
}
} else {
url += '?';
}
url += stringToAppend;
}
return url;
}
getAnchorInfo(link) {
var info: any = {};
info.href = this.getLinkUrl(link);
info.title = this.templateSrv.replace(link.title || '');
return info;
}
getPanelLinkAnchorInfo(link, scopedVars) {
var info: any = {};
if (link.type === 'absolute') {
info.target = link.targetBlank ? '_blank' : '_self';
info.href = this.templateSrv.replace(link.url || '', scopedVars);
info.title = this.templateSrv.replace(link.title || '', scopedVars);
} else if (link.dashUri) {
info.href = 'dashboard/' + link.dashUri + '?';
info.title = this.templateSrv.replace(link.title || '', scopedVars);
info.target = link.targetBlank ? '_blank' : '';
} else {
info.title = this.templateSrv.replace(link.title || '', scopedVars);
var slug = kbn.slugifyForUrl(link.dashboard || '');
info.href = 'dashboard/db/' + slug + '?';
}
var params = {};
if (link.keepTime) {
var range = this.timeSrv.timeRangeForUrl();
params['from'] = range.from;
params['to'] = range.to;
}
if (link.includeVars) {
this.templateSrv.fillVariableValuesForUrl(params, scopedVars);
}
info.href = this.addParamsToUrl(info.href, params);
if (link.params) {
info.href = this.appendToQueryString(info.href, this.templateSrv.replace(link.params, scopedVars));
}
return info;
}
}
angular.module('grafana.services').service('linkSrv', LinkSrv);

View File

@ -1,7 +1,7 @@
define([
'angular',
'lodash',
'./linkSrv',
'./link_srv',
],
function (angular, _) {
'use strict';

View File

@ -0,0 +1,47 @@
import { LinkSrv } from '../link_srv';
import _ from 'lodash';
jest.mock('angular', () => {
let AngularJSMock = require('test/mocks/angular');
return new AngularJSMock();
});
describe('linkSrv', function() {
var linkSrv;
var templateSrvMock = {};
var timeSrvMock = {};
beforeEach(() => {
linkSrv = new LinkSrv(templateSrvMock, timeSrvMock);
});
describe('when appending query strings', function() {
it('add ? to URL if not present', function() {
var url = linkSrv.appendToQueryString('http://example.com', 'foo=bar');
expect(url).toBe('http://example.com?foo=bar');
});
it('do not add & to URL if ? is present but query string is empty', function() {
var url = linkSrv.appendToQueryString('http://example.com?', 'foo=bar');
expect(url).toBe('http://example.com?foo=bar');
});
it('add & to URL if query string is present', function() {
var url = linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world');
expect(url).toBe('http://example.com?foo=bar&hello=world');
});
it('do not change the URL if there is nothing to append', function() {
_.each(['', undefined, null], function(toAppend) {
var url1 = linkSrv.appendToQueryString('http://example.com', toAppend);
expect(url1).toBe('http://example.com');
var url2 = linkSrv.appendToQueryString('http://example.com?', toAppend);
expect(url2).toBe('http://example.com?');
var url3 = linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend);
expect(url3).toBe('http://example.com?foo=bar');
});
});
});
});

View File

@ -1,46 +0,0 @@
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
import 'app/features/panellinks/linkSrv';
import _ from 'lodash';
describe('linkSrv', function() {
var _linkSrv;
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.inject(function(linkSrv) {
_linkSrv = linkSrv;
}));
describe('when appending query strings', function() {
it('add ? to URL if not present', function() {
var url = _linkSrv.appendToQueryString('http://example.com', 'foo=bar');
expect(url).to.be('http://example.com?foo=bar');
});
it('do not add & to URL if ? is present but query string is empty', function() {
var url = _linkSrv.appendToQueryString('http://example.com?', 'foo=bar');
expect(url).to.be('http://example.com?foo=bar');
});
it('add & to URL if query string is present', function() {
var url = _linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world');
expect(url).to.be('http://example.com?foo=bar&hello=world');
});
it('do not change the URL if there is nothing to append', function() {
_.each(['', undefined, null], function(toAppend) {
var url1 = _linkSrv.appendToQueryString('http://example.com', toAppend);
expect(url1).to.be('http://example.com');
var url2 = _linkSrv.appendToQueryString('http://example.com?', toAppend);
expect(url2).to.be('http://example.com?');
var url3 = _linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend);
expect(url3).to.be('http://example.com?foo=bar');
});
});
});
});

View File

@ -40,7 +40,10 @@ System.config({
css: 'vendor/plugin-css/css.js'
},
meta: {
'*': {esModule: true}
'*': {
esModule: true,
authorization: true,
}
}
});

View File

@ -1,7 +1,4 @@
///<reference path="../../../../headers/common.d.ts" />
import _ from 'lodash';
import angular from 'angular';
class TestDataDatasource {
id: any;
@ -21,7 +18,8 @@ class TestDataDatasource {
intervalMs: options.intervalMs,
maxDataPoints: options.maxDataPoints,
stringInput: item.stringInput,
jsonInput: angular.fromJson(item.jsonInput),
points: item.points,
alias: item.alias,
datasourceId: this.id,
};
});

View File

@ -1,5 +1,3 @@
///<reference path="../../../../headers/common.d.ts" />
import {TestDataDatasource} from './datasource';
import {TestDataQueryCtrl} from './query_ctrl';

View File

@ -1,14 +1,16 @@
///<reference path="../../../../headers/common.d.ts" />
import _ from 'lodash';
import { QueryCtrl } from 'app/plugins/sdk';
import moment from 'moment';
export class TestDataQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
scenarioList: any;
scenario: any;
newPointValue: number;
newPointTime: any;
selectedPoint: any;
/** @ngInject **/
constructor($scope, $injector, private backendSrv) {
@ -16,6 +18,34 @@ export class TestDataQueryCtrl extends QueryCtrl {
this.target.scenarioId = this.target.scenarioId || 'random_walk';
this.scenarioList = [];
this.newPointTime = moment();
this.selectedPoint = { text: 'Select point', value: null };
}
getPoints() {
return _.map(this.target.points, (point, index) => {
return {
text: moment(point[1]).format('MMMM Do YYYY, H:mm:ss') + ' : ' + point[0],
value: index,
};
});
}
pointSelected(option) {
this.selectedPoint = option;
}
deletePoint() {
this.target.points.splice(this.selectedPoint.value, 1);
this.selectedPoint = { text: 'Select point', value: null };
this.refresh();
}
addPoint() {
this.target.points = this.target.points || [];
this.target.points.push([this.newPointValue, this.newPointTime.valueOf()]);
this.target.points = _.sortBy(this.target.points, p => p[1]);
this.refresh();
}
$onInit() {
@ -28,7 +58,13 @@ export class TestDataQueryCtrl extends QueryCtrl {
scenarioChanged() {
this.scenario = _.find(this.scenarioList, { id: this.target.scenarioId });
this.target.stringInput = this.scenario.stringInput;
if (this.target.scenarioId === 'manual_entry') {
this.target.points = this.target.points || [];
} else {
delete this.target.points;
}
this.refresh();
}
}

View File

@ -1,8 +1,8 @@
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword">Scenario</label>
<div class="gf-form-select-wrapper">
<label class="gf-form-label query-keyword width-7">Scenario</label>
<div class="gf-form-select-wrapper width-15">
<select class="gf-form-input" ng-model="ctrl.target.scenarioId" ng-options="v.id as v.name for v in ctrl.scenarioList" ng-change="ctrl.scenarioChanged()"></select>
</div>
</div>
@ -18,5 +18,23 @@
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.scenario.id === 'manual_entry'">
<div class="gf-form gf-form">
<label class="gf-form-label query-keyword width-7">New value</label>
<input type="number" class="gf-form-input width-15" placeholder="value" ng-model="ctrl.newPointValue">
<label class="gf-form-label query-keyword">Time</label>
<input type="string" class="gf-form-input width-12" placeholder="time" ng-model="ctrl.newPointTime" input-datetime>
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addPoint()">Add</button>
<label class="gf-form-label query-keyword">All values</label>
<gf-form-dropdown css-class="width-12" model="ctrl.selectedPoint" get-options="ctrl.getPoints()" on-change="ctrl.pointSelected($option)">
</gf-form-dropdown>
</div>
<div class="gf-form gf-form" ng-if="ctrl.selectedPoint.value !== null">
<button class="btn btn-danger gf-form-btn" ng-click="ctrl.deletePoint()">Delete</button>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</query-editor-row>

View File

@ -49,6 +49,7 @@
<li>{{stat}}</li>
<li>{{namespace}}</li>
<li>{{region}}</li>
<li>{{period}}</li>
<li>{{YOUR_DIMENSION_NAME}}</li>
</ul>
</info-popover>

View File

@ -15,10 +15,14 @@ export class MysqlDatasource {
this.responseParser = new ResponseParser(this.$q);
}
interpolateVariable(value) {
interpolateVariable(value, variable) {
if (typeof value === 'string') {
if (variable.multi || variable.includeAll) {
return '\'' + value + '\'';
} else {
return value;
}
}
if (typeof value === 'number') {
return value;

View File

@ -21,7 +21,6 @@
An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time_sec</b> for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column)
- column with alias <b>title</b> for the annotation title
- column with alias: <b>text</b> for the annotation text
- column with alias: <b>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'

View File

@ -49,7 +49,15 @@ Macros:
- $__time(column) -&gt; UNIX_TIMESTAMP(column) as time_sec
- $__timeFilter(column) -&gt; UNIX_TIMESTAMP(time_date_time) &ge; 1492750877 AND UNIX_TIMESTAMP(time_date_time) &le; 1492750877
- $__unixEpochFilter(column) -&gt; time_unix_epoch &gt; 1492750877 AND time_unix_epoch &lt; 1492750877
- $__timeGroup(column,'5m') -&gt; (extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int
- $__timeGroup(column,'5m') -&gt; cast(cast(UNIX_TIMESTAMP(column)/(300) as signed)*300 as signed)
Example of group by and order by with $__timeGroup:
SELECT
$__timeGroup(timestamp_col, '1h') AS time,
sum(value_double) as value
FROM yourtable
GROUP BY 1
ORDER BY 1
Or build your own conditionals using these macros which just return the values:
- $__timeFrom() -&gt; FROM_UNIXTIME(1492750877)

View File

@ -2,6 +2,7 @@ import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import {MysqlDatasource} from '../datasource';
import {CustomVariable} from 'app/features/templating/custom_variable';
describe('MySQLDatasource', function() {
var ctx = new helpers.ServiceTestContext();
@ -195,22 +196,41 @@ describe('MySQLDatasource', function() {
});
describe('When interpolating variables', () => {
beforeEach(function() {
ctx.variable = new CustomVariable({},{});
});
describe('and value is a string', () => {
it('should return an unquoted value', () => {
expect(ctx.ds.interpolateVariable('abc')).to.eql('abc');
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc');
});
});
describe('and value is a number', () => {
it('should return an unquoted value', () => {
expect(ctx.ds.interpolateVariable(1000)).to.eql(1000);
expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000);
});
});
describe('and value is an array of strings', () => {
it('should return comma separated quoted values', () => {
expect(ctx.ds.interpolateVariable(['a', 'b', 'c'])).to.eql('\'a\',\'b\',\'c\'');
expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql('\'a\',\'b\',\'c\'');
});
});
describe('and variable allows multi-value and value is a string', () => {
it('should return a quoted value', () => {
ctx.variable.multi = true;
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('\'abc\'');
});
});
describe('and variable allows all and value is a string', () => {
it('should return a quoted value', () => {
ctx.variable.includeAll = true;
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('\'abc\'');
});
});
});
});

View File

@ -441,7 +441,7 @@ function (angular, _, dateMath) {
}
function mapMetricsToTargets(metrics, options, tsdbVersion) {
var interpolatedTagValue;
var interpolatedTagValue, arrTagV;
return _.map(metrics, function(metricData) {
if (tsdbVersion === 3) {
return metricData.query.index;
@ -453,7 +453,8 @@ function (angular, _, dateMath) {
return target.metric === metricData.metric &&
_.every(target.tags, function(tagV, tagK) {
interpolatedTagValue = templateSrv.replace(tagV, options.scopedVars, 'pipe');
return metricData.tags[tagK] === interpolatedTagValue || interpolatedTagValue === "*";
arrTagV = interpolatedTagValue.split('|');
return _.includes(arrTagV, metricData.tags[tagK]) || interpolatedTagValue === "*";
});
}
});

View File

@ -15,10 +15,14 @@ export class PostgresDatasource {
this.responseParser = new ResponseParser(this.$q);
}
interpolateVariable(value) {
interpolateVariable(value, variable) {
if (typeof value === 'string') {
if (variable.multi || variable.includeAll) {
return '\'' + value + '\'';
} else {
return value;
}
}
if (typeof value === 'number') {
return value;

View File

@ -21,7 +21,6 @@
An annotation is an event that is overlayed on top of graphs. The query can have up to four columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time</b> for the annotation event. Format is UTC in seconds, use extract(epoch from column) as "time"
- column with alias <b>title</b> for the annotation title
- column with alias: <b>text</b> for the annotation text
- column with alias: <b>tags</b> for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2'

Some files were not shown because too many files have changed in this diff Show More