mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into develop
This commit is contained in:
commit
10fcf2f5be
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -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
1
.gitignore
vendored
@ -38,6 +38,7 @@ public/css/*.min.css
|
||||
conf/custom.ini
|
||||
fig.yml
|
||||
docker-compose.yml
|
||||
docker-compose.yaml
|
||||
profile.cov
|
||||
/grafana
|
||||
.notouch
|
||||
|
12
CHANGELOG.md
12
CHANGELOG.md
@ -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
|
||||
|
25
ROADMAP.md
25
ROADMAP.md
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -8,4 +8,6 @@ coverage:
|
||||
patch: yes
|
||||
changes: no
|
||||
|
||||
comment: false
|
||||
comment:
|
||||
layout: "diff"
|
||||
behavior: "once"
|
||||
|
11
docker/blocks/collectd/docker-compose.yaml
Normal file
11
docker/blocks/collectd/docker-compose.yaml
Normal 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
|
@ -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
|
8
docker/blocks/elastic/docker-compose.yaml
Normal file
8
docker/blocks/elastic/docker-compose.yaml
Normal 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
|
@ -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
|
8
docker/blocks/elastic1/docker-compose.yaml
Normal file
8
docker/blocks/elastic1/docker-compose.yaml
Normal 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
|
@ -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
|
8
docker/blocks/elastic5/docker-compose.yaml
Normal file
8
docker/blocks/elastic5/docker-compose.yaml
Normal 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"
|
@ -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"
|
16
docker/blocks/graphite/docker-compose.yaml
Normal file
16
docker/blocks/graphite/docker-compose.yaml
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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 \
|
||||
|
16
docker/blocks/graphite1/docker-compose.yaml
Normal file
16
docker/blocks/graphite1/docker-compose.yaml
Normal 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
|
||||
|
@ -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
|
||||
|
17
docker/blocks/influxdb/docker-compose.yaml
Normal file
17
docker/blocks/influxdb/docker-compose.yaml
Normal 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
|
||||
|
@ -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
|
||||
|
6
docker/blocks/jaeger/docker-compose.yaml
Normal file
6
docker/blocks/jaeger/docker-compose.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:latest
|
||||
ports:
|
||||
- "127.0.0.1:6831:6831/udp"
|
||||
- "16686:16686"
|
||||
|
@ -1,6 +0,0 @@
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:latest
|
||||
ports:
|
||||
- "localhost:6831:6831/udp"
|
||||
- "16686:16686"
|
||||
|
5
docker/blocks/memcached/docker-compose.yaml
Normal file
5
docker/blocks/memcached/docker-compose.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
memcached:
|
||||
image: memcached:latest
|
||||
ports:
|
||||
- "11211:11211"
|
||||
|
@ -1,5 +0,0 @@
|
||||
memcached:
|
||||
image: memcached:latest
|
||||
ports:
|
||||
- "11211:11211"
|
||||
|
14
docker/blocks/mysql/docker-compose.yaml
Normal file
14
docker/blocks/mysql/docker-compose.yaml
Normal 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]
|
||||
|
@ -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]
|
||||
|
9
docker/blocks/mysql_opendata/docker-compose.yaml
Normal file
9
docker/blocks/mysql_opendata/docker-compose.yaml
Normal 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"
|
@ -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"
|
9
docker/blocks/mysql_tests/docker-compose.yaml
Normal file
9
docker/blocks/mysql_tests/docker-compose.yaml
Normal 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"
|
@ -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"
|
@ -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
|
||||
|
||||
|
10
docker/blocks/openldap/docker-compose.yaml
Normal file
10
docker/blocks/openldap/docker-compose.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
openldap:
|
||||
build: blocks/openldap
|
||||
environment:
|
||||
SLAPD_PASSWORD: grafana
|
||||
SLAPD_DOMAIN: grafana.org
|
||||
SLAPD_ADDITIONAL_MODULES: memberof
|
||||
ports:
|
||||
- "389:389"
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
openldap:
|
||||
build: blocks/openldap
|
||||
environment:
|
||||
SLAPD_PASSWORD: grafana
|
||||
SLAPD_DOMAIN: grafana.org
|
||||
SLAPD_ADDITIONAL_MODULES: memberof
|
||||
ports:
|
||||
- "389:389"
|
||||
|
||||
|
11
docker/blocks/opentsdb/docker-compose.yaml
Normal file
11
docker/blocks/opentsdb/docker-compose.yaml
Normal 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
|
||||
|
@ -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
|
||||
|
9
docker/blocks/postgres/docker-compose.yaml
Normal file
9
docker/blocks/postgres/docker-compose.yaml
Normal 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
|
@ -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
|
7
docker/blocks/postgres_tests/docker-compose.yaml
Normal file
7
docker/blocks/postgres_tests/docker-compose.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
postgrestest:
|
||||
image: postgres:latest
|
||||
environment:
|
||||
POSTGRES_USER: grafanatest
|
||||
POSTGRES_PASSWORD: grafanatest
|
||||
ports:
|
||||
- "5432:5432"
|
@ -1,7 +0,0 @@
|
||||
postgrestest:
|
||||
image: postgres:latest
|
||||
environment:
|
||||
POSTGRES_USER: grafanatest
|
||||
POSTGRES_PASSWORD: grafanatest
|
||||
ports:
|
||||
- "5432:5432"
|
25
docker/blocks/prometheus/docker-compose.yaml
Normal file
25
docker/blocks/prometheus/docker-compose.yaml
Normal 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"
|
@ -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"
|
@ -1,5 +1,5 @@
|
||||
FROM centos:centos7
|
||||
MAINTAINER Przemyslaw Ozgo <linux@ozgo.info>
|
||||
LABEL maintainer="Przemyslaw Ozgo <linux@ozgo.info>"
|
||||
|
||||
RUN \
|
||||
yum update -y && \
|
||||
|
4
docker/blocks/smtp/docker-compose.yaml
Normal file
4
docker/blocks/smtp/docker-compose.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
snmpd:
|
||||
image: namshi/smtp
|
||||
ports:
|
||||
- "25:25"
|
@ -1,4 +0,0 @@
|
||||
snmpd:
|
||||
image: namshi/smtp
|
||||
ports:
|
||||
- "25:25"
|
2
docker/compose_header.yml
Normal file
2
docker/compose_header.yml
Normal file
@ -0,0 +1,2 @@
|
||||
version: "2"
|
||||
services:
|
@ -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
|
||||
|
@ -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!
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
```
|
@ -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" >}})
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
<!--
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
40
pkg/tsdb/testdata/scenarios.go
vendored
40
pkg/tsdb/testdata/scenarios.go
vendored
@ -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",
|
||||
|
@ -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";
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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";
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
@ -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);
|
@ -3,7 +3,6 @@ define([
|
||||
'./util_srv',
|
||||
'./context_srv',
|
||||
'./timer',
|
||||
'./keyboard_manager',
|
||||
'./analytics',
|
||||
'./popover_srv',
|
||||
'./segment_srv',
|
||||
|
@ -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;
|
||||
}]);
|
||||
|
||||
});
|
@ -21,7 +21,7 @@ export class Timer {
|
||||
}
|
||||
|
||||
cancelAll() {
|
||||
_.each(this.timers, function (t) {
|
||||
_.each(this.timers, t => {
|
||||
this.$timeout.cancel(t);
|
||||
});
|
||||
this.timers = [];
|
||||
|
@ -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' }
|
||||
]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
|
@ -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 = '';
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
67
public/app/features/dashboard/specs/save_as_modal.jest.ts
Normal file
67
public/app/features/dashboard/specs/save_as_modal.jest.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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()">
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
});
|
||||
});
|
113
public/app/features/panellinks/link_srv.ts
Normal file
113
public/app/features/panellinks/link_srv.ts
Normal 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);
|
@ -1,7 +1,7 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'./linkSrv',
|
||||
'./link_srv',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
47
public/app/features/panellinks/specs/link_srv.jest.ts
Normal file
47
public/app/features/panellinks/specs/link_srv.jest.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -40,7 +40,10 @@ System.config({
|
||||
css: 'vendor/plugin-css/css.js'
|
||||
},
|
||||
meta: {
|
||||
'*': {esModule: true}
|
||||
'*': {
|
||||
esModule: true,
|
||||
authorization: true,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
@ -1,5 +1,3 @@
|
||||
///<reference path="../../../../headers/common.d.ts" />
|
||||
|
||||
import {TestDataDatasource} from './datasource';
|
||||
import {TestDataQueryCtrl} from './query_ctrl';
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -49,6 +49,7 @@
|
||||
<li>{{stat}}</li>
|
||||
<li>{{namespace}}</li>
|
||||
<li>{{region}}</li>
|
||||
<li>{{period}}</li>
|
||||
<li>{{YOUR_DIMENSION_NAME}}</li>
|
||||
</ul>
|
||||
</info-popover>
|
||||
|
@ -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;
|
||||
|
@ -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'
|
||||
|
||||
|
@ -49,7 +49,15 @@ Macros:
|
||||
- $__time(column) -> UNIX_TIMESTAMP(column) as time_sec
|
||||
- $__timeFilter(column) -> UNIX_TIMESTAMP(time_date_time) ≥ 1492750877 AND UNIX_TIMESTAMP(time_date_time) ≤ 1492750877
|
||||
- $__unixEpochFilter(column) -> time_unix_epoch > 1492750877 AND time_unix_epoch < 1492750877
|
||||
- $__timeGroup(column,'5m') -> (extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int
|
||||
- $__timeGroup(column,'5m') -> 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() -> FROM_UNIXTIME(1492750877)
|
||||
|
@ -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\'');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -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 === "*";
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user