diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 57fca7f44cb..082482fcb74 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -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? diff --git a/.gitignore b/.gitignore index ddabf2b680c..e98ded36c82 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ public/css/*.min.css conf/custom.ini fig.yml docker-compose.yml +docker-compose.yaml profile.cov /grafana .notouch diff --git a/CHANGELOG.md b/CHANGELOG.md index c0833f60e37..73da7a39ef9 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/ROADMAP.md b/ROADMAP.md index 3ce0c33f088..4273d8df6a9 100644 --- a/ROADMAP.md +++ b/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) diff --git a/appveyor.yml b/appveyor.yml index 19de1d3a793..5d67edca9d9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana environment: nodejs_version: "6" GOPATH: c:\gopath - GOVERSION: 1.9.1 + GOVERSION: 1.9.2 install: - rmdir c:\go /s /q diff --git a/circle.yml b/circle.yml index 8380bc2a2ff..4eb600bfde3 100644 --- a/circle.yml +++ b/circle.yml @@ -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 diff --git a/codecov.yml b/codecov.yml index 3d764c1a5b1..82a86e0232b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -7,5 +7,7 @@ coverage: project: yes patch: yes changes: no - -comment: false + +comment: + layout: "diff" + behavior: "once" diff --git a/docker/blocks/collectd/docker-compose.yaml b/docker/blocks/collectd/docker-compose.yaml new file mode 100644 index 00000000000..c95827f7928 --- /dev/null +++ b/docker/blocks/collectd/docker-compose.yaml @@ -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 diff --git a/docker/blocks/collectd/fig b/docker/blocks/collectd/fig deleted file mode 100644 index 99f45a66d12..00000000000 --- a/docker/blocks/collectd/fig +++ /dev/null @@ -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 diff --git a/docker/blocks/elastic/docker-compose.yaml b/docker/blocks/elastic/docker-compose.yaml new file mode 100644 index 00000000000..193b8f252f6 --- /dev/null +++ b/docker/blocks/elastic/docker-compose.yaml @@ -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 diff --git a/docker/blocks/elastic/fig b/docker/blocks/elastic/fig deleted file mode 100644 index fa79a9af59c..00000000000 --- a/docker/blocks/elastic/fig +++ /dev/null @@ -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 diff --git a/docker/blocks/elastic1/docker-compose.yaml b/docker/blocks/elastic1/docker-compose.yaml new file mode 100644 index 00000000000..518ae76e6ee --- /dev/null +++ b/docker/blocks/elastic1/docker-compose.yaml @@ -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 diff --git a/docker/blocks/elastic1/fig b/docker/blocks/elastic1/fig deleted file mode 100644 index c33e51f16a2..00000000000 --- a/docker/blocks/elastic1/fig +++ /dev/null @@ -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 diff --git a/docker/blocks/elastic5/docker-compose.yaml b/docker/blocks/elastic5/docker-compose.yaml new file mode 100644 index 00000000000..5b12be9ada4 --- /dev/null +++ b/docker/blocks/elastic5/docker-compose.yaml @@ -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" diff --git a/docker/blocks/elastic5/fig b/docker/blocks/elastic5/fig deleted file mode 100644 index 6e5cd89ab3d..00000000000 --- a/docker/blocks/elastic5/fig +++ /dev/null @@ -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" diff --git a/docker/blocks/graphite/docker-compose.yaml b/docker/blocks/graphite/docker-compose.yaml new file mode 100644 index 00000000000..2bd0dc322cc --- /dev/null +++ b/docker/blocks/graphite/docker-compose.yaml @@ -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 + diff --git a/docker/blocks/graphite/fig b/docker/blocks/graphite/fig deleted file mode 100644 index b7e030e388e..00000000000 --- a/docker/blocks/graphite/fig +++ /dev/null @@ -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 - diff --git a/docker/blocks/graphite1/Dockerfile b/docker/blocks/graphite1/Dockerfile index a3ab7c4f4af..be4f0573cad 100644 --- a/docker/blocks/graphite1/Dockerfile +++ b/docker/blocks/graphite1/Dockerfile @@ -1,5 +1,5 @@ FROM phusion/baseimage:0.9.22 -MAINTAINER Denys Zhdanov +LABEL maintainer="Denys Zhdanov " RUN apt-get -y update \ && apt-get -y upgrade \ diff --git a/docker/blocks/graphite1/docker-compose.yaml b/docker/blocks/graphite1/docker-compose.yaml new file mode 100644 index 00000000000..577a393b123 --- /dev/null +++ b/docker/blocks/graphite1/docker-compose.yaml @@ -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 + diff --git a/docker/blocks/graphite1/fig b/docker/blocks/graphite1/fig deleted file mode 100644 index 5337376ff5c..00000000000 --- a/docker/blocks/graphite1/fig +++ /dev/null @@ -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 - diff --git a/docker/blocks/influxdb/docker-compose.yaml b/docker/blocks/influxdb/docker-compose.yaml new file mode 100644 index 00000000000..3434f5d09b9 --- /dev/null +++ b/docker/blocks/influxdb/docker-compose.yaml @@ -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 + diff --git a/docker/blocks/influxdb/fig b/docker/blocks/influxdb/fig deleted file mode 100644 index 8821c010a98..00000000000 --- a/docker/blocks/influxdb/fig +++ /dev/null @@ -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 - diff --git a/docker/blocks/jaeger/docker-compose.yaml b/docker/blocks/jaeger/docker-compose.yaml new file mode 100644 index 00000000000..2b57c863425 --- /dev/null +++ b/docker/blocks/jaeger/docker-compose.yaml @@ -0,0 +1,6 @@ + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "127.0.0.1:6831:6831/udp" + - "16686:16686" + diff --git a/docker/blocks/jaeger/fig b/docker/blocks/jaeger/fig deleted file mode 100644 index ab9e2ec599b..00000000000 --- a/docker/blocks/jaeger/fig +++ /dev/null @@ -1,6 +0,0 @@ -jaeger: - image: jaegertracing/all-in-one:latest - ports: - - "localhost:6831:6831/udp" - - "16686:16686" - diff --git a/docker/blocks/memcached/docker-compose.yaml b/docker/blocks/memcached/docker-compose.yaml new file mode 100644 index 00000000000..b3201da0f95 --- /dev/null +++ b/docker/blocks/memcached/docker-compose.yaml @@ -0,0 +1,5 @@ + memcached: + image: memcached:latest + ports: + - "11211:11211" + diff --git a/docker/blocks/memcached/fig b/docker/blocks/memcached/fig deleted file mode 100644 index a0da9df2bc2..00000000000 --- a/docker/blocks/memcached/fig +++ /dev/null @@ -1,5 +0,0 @@ -memcached: - image: memcached:latest - ports: - - "11211:11211" - diff --git a/docker/blocks/mysql/docker-compose.yaml b/docker/blocks/mysql/docker-compose.yaml new file mode 100644 index 00000000000..6eee158ac43 --- /dev/null +++ b/docker/blocks/mysql/docker-compose.yaml @@ -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] + diff --git a/docker/blocks/mysql/fig b/docker/blocks/mysql/fig deleted file mode 100644 index 24cb47b61a7..00000000000 --- a/docker/blocks/mysql/fig +++ /dev/null @@ -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] - diff --git a/docker/blocks/mysql_opendata/docker-compose.yaml b/docker/blocks/mysql_opendata/docker-compose.yaml new file mode 100644 index 00000000000..594eeed284a --- /dev/null +++ b/docker/blocks/mysql_opendata/docker-compose.yaml @@ -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" diff --git a/docker/blocks/mysql_opendata/fig b/docker/blocks/mysql_opendata/fig deleted file mode 100644 index a374fbd0931..00000000000 --- a/docker/blocks/mysql_opendata/fig +++ /dev/null @@ -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" diff --git a/docker/blocks/mysql_tests/docker-compose.yaml b/docker/blocks/mysql_tests/docker-compose.yaml new file mode 100644 index 00000000000..646cc7ee369 --- /dev/null +++ b/docker/blocks/mysql_tests/docker-compose.yaml @@ -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" diff --git a/docker/blocks/mysql_tests/fig b/docker/blocks/mysql_tests/fig deleted file mode 100644 index 880c955d218..00000000000 --- a/docker/blocks/mysql_tests/fig +++ /dev/null @@ -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" diff --git a/docker/blocks/openldap/Dockerfile b/docker/blocks/openldap/Dockerfile index d16987cb3ab..d073e274356 100644 --- a/docker/blocks/openldap/Dockerfile +++ b/docker/blocks/openldap/Dockerfile @@ -1,6 +1,6 @@ FROM debian:jessie -MAINTAINER Christian Luginbühl +LABEL maintainer="Christian Luginbühl " ENV OPENLDAP_VERSION 2.4.40 diff --git a/docker/blocks/openldap/docker-compose.yaml b/docker/blocks/openldap/docker-compose.yaml new file mode 100644 index 00000000000..be06524a57d --- /dev/null +++ b/docker/blocks/openldap/docker-compose.yaml @@ -0,0 +1,10 @@ + openldap: + build: blocks/openldap + environment: + SLAPD_PASSWORD: grafana + SLAPD_DOMAIN: grafana.org + SLAPD_ADDITIONAL_MODULES: memberof + ports: + - "389:389" + + diff --git a/docker/blocks/openldap/fig b/docker/blocks/openldap/fig deleted file mode 100644 index b9528f2d4d7..00000000000 --- a/docker/blocks/openldap/fig +++ /dev/null @@ -1,10 +0,0 @@ -openldap: - build: blocks/openldap - environment: - SLAPD_PASSWORD: grafana - SLAPD_DOMAIN: grafana.org - SLAPD_ADDITIONAL_MODULES: memberof - ports: - - "389:389" - - diff --git a/docker/blocks/opentsdb/docker-compose.yaml b/docker/blocks/opentsdb/docker-compose.yaml new file mode 100644 index 00000000000..ee064bb107d --- /dev/null +++ b/docker/blocks/opentsdb/docker-compose.yaml @@ -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 + diff --git a/docker/blocks/opentsdb/fig b/docker/blocks/opentsdb/fig deleted file mode 100644 index c346475e9a3..00000000000 --- a/docker/blocks/opentsdb/fig +++ /dev/null @@ -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 - diff --git a/docker/blocks/postgres/docker-compose.yaml b/docker/blocks/postgres/docker-compose.yaml new file mode 100644 index 00000000000..eced00aafeb --- /dev/null +++ b/docker/blocks/postgres/docker-compose.yaml @@ -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 diff --git a/docker/blocks/postgres/fig b/docker/blocks/postgres/fig deleted file mode 100644 index 9f39a0ffb1d..00000000000 --- a/docker/blocks/postgres/fig +++ /dev/null @@ -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 diff --git a/docker/blocks/postgres_tests/docker-compose.yaml b/docker/blocks/postgres_tests/docker-compose.yaml new file mode 100644 index 00000000000..3d9a82c034c --- /dev/null +++ b/docker/blocks/postgres_tests/docker-compose.yaml @@ -0,0 +1,7 @@ + postgrestest: + image: postgres:latest + environment: + POSTGRES_USER: grafanatest + POSTGRES_PASSWORD: grafanatest + ports: + - "5432:5432" diff --git a/docker/blocks/postgres_tests/fig b/docker/blocks/postgres_tests/fig deleted file mode 100644 index 049afe185c8..00000000000 --- a/docker/blocks/postgres_tests/fig +++ /dev/null @@ -1,7 +0,0 @@ -postgrestest: - image: postgres:latest - environment: - POSTGRES_USER: grafanatest - POSTGRES_PASSWORD: grafanatest - ports: - - "5432:5432" diff --git a/docker/blocks/prometheus/docker-compose.yaml b/docker/blocks/prometheus/docker-compose.yaml new file mode 100644 index 00000000000..ccb1238a179 --- /dev/null +++ b/docker/blocks/prometheus/docker-compose.yaml @@ -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" diff --git a/docker/blocks/prometheus/fig b/docker/blocks/prometheus/fig deleted file mode 100644 index 7d9bea68046..00000000000 --- a/docker/blocks/prometheus/fig +++ /dev/null @@ -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" diff --git a/docker/blocks/smtp/Dockerfile b/docker/blocks/smtp/Dockerfile index c1a3adba7c8..9326e077ed9 100644 --- a/docker/blocks/smtp/Dockerfile +++ b/docker/blocks/smtp/Dockerfile @@ -1,5 +1,5 @@ FROM centos:centos7 -MAINTAINER Przemyslaw Ozgo +LABEL maintainer="Przemyslaw Ozgo " RUN \ yum update -y && \ diff --git a/docker/blocks/smtp/docker-compose.yaml b/docker/blocks/smtp/docker-compose.yaml new file mode 100644 index 00000000000..85d598b6167 --- /dev/null +++ b/docker/blocks/smtp/docker-compose.yaml @@ -0,0 +1,4 @@ + snmpd: + image: namshi/smtp + ports: + - "25:25" diff --git a/docker/blocks/smtp/fig b/docker/blocks/smtp/fig deleted file mode 100644 index 3aa25e01311..00000000000 --- a/docker/blocks/smtp/fig +++ /dev/null @@ -1,4 +0,0 @@ -snmpd: - image: namshi/smtp - ports: - - "25:25" diff --git a/docker/compose_header.yml b/docker/compose_header.yml new file mode 100644 index 00000000000..e7bf4f38b02 --- /dev/null +++ b/docker/compose_header.yml @@ -0,0 +1,2 @@ +version: "2" +services: diff --git a/docker/create_docker_compose.sh b/docker/create_docker_compose.sh index 8588c1c474a..9d28ede8e7e 100755 --- a/docker/create_docker_compose.sh +++ b/docker/create_docker_compose.sh @@ -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 diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index dceb2254e41..3748a15195f 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -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! diff --git a/docs/sources/features/datasources/testdata.md b/docs/sources/features/datasources/testdata.md index 491e5b60fbb..d99f9cabe08 100644 --- a/docs/sources/features/datasources/testdata.md +++ b/docs/sources/features/datasources/testdata.md @@ -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. diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 300e5613db4..0538754bd96 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -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 } ] -``` \ No newline at end of file +``` diff --git a/docs/sources/index.md b/docs/sources/index.md index 9226c842abc..7a431e29692 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -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" >}}) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 627a76a963e..5da485144c8 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.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 diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 7ecb6d14b0c..8f29a9bc7d1 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -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) @@ -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 ``` @@ -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 diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 181d26d694c..1b6f296d9f4 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -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. diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index e4ccedb7299..b91aacb2c2b 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -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) diff --git a/packaging/publish/publish_both.sh b/packaging/publish/publish_both.sh index 0a76851f6fa..af349567bbc 100755 --- a/packaging/publish/publish_both.sh +++ b/packaging/publish/publish_both.sh @@ -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 diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 4b155ae3208..4d7de98f2ea 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -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, }) diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 266b71ec14e..db018978a17 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -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 } diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 36c38804a01..108b81fc5f3 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -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) diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index c92020d0aae..988612fb287 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -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) diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 21400b03dfd..95932ab1c83 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -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) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index ba991e6f2d5..ff268805259 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -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() { diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 6fc9c89e7be..f6fe7797bcf 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -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) } diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index 0a7f1467933..e907fa8aae0 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -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", diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 2615a635c7e..cc3b1e46ad4 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -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"; diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index 6b6d387a2b2..3b24b9a4661 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -43,7 +43,7 @@ export class SeriesColorPicker extends React.Component { render() { return (
- {this.props.series && this.renderAxisSelection()} + {this.props.series.yaxis && this.renderAxisSelection()}
); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index c8e6ff97905..66d8b8b656d 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -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"; diff --git a/public/app/core/directives/confirm_click.js b/public/app/core/directives/confirm_click.js deleted file mode 100644 index 95b60347e10..00000000000 --- a/public/app/core/directives/confirm_click.js +++ /dev/null @@ -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)); - } - } - }); - }, - }; - }); -}); diff --git a/public/app/core/directives/ng_model_on_blur.js b/public/app/core/directives/ng_model_on_blur.ts similarity index 71% rename from public/app/core/directives/ng_model_on_blur.js rename to public/app/core/directives/ng_model_on_blur.ts index b3c73e54dba..383d04e3961 100644 --- a/public/app/core/directives/ng_model_on_blur.js +++ b/public/app/core/directives/ng_model_on_blur.ts @@ -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,22 +20,27 @@ function (coreModule, rangeUtil) { }); } }; - }); + } +} - coreModule.default.directive('emptyToNull', function () { + +export class EmptyToNull { + constructor() { return { restrict: 'A', require: 'ngModel', link: function (scope, elm, attrs, ctrl) { ctrl.$parsers.push(function (viewValue) { - if(viewValue === "") { return null; } + if (viewValue === "") { return null; } return viewValue; }); } }; - }); + } +} - 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); diff --git a/public/app/core/services/all.js b/public/app/core/services/all.js index 1fea3e5a248..a308febb219 100644 --- a/public/app/core/services/all.js +++ b/public/app/core/services/all.js @@ -3,7 +3,6 @@ define([ './util_srv', './context_srv', './timer', - './keyboard_manager', './analytics', './popover_srv', './segment_srv', diff --git a/public/app/core/services/keyboard_manager.js b/public/app/core/services/keyboard_manager.js deleted file mode 100644 index b5eefda81a6..00000000000 --- a/public/app/core/services/keyboard_manager.js +++ /dev/null @@ -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; - }]); - -}); diff --git a/public/app/core/services/timer.ts b/public/app/core/services/timer.ts index 6356e1f2910..6355105ee0e 100644 --- a/public/app/core/services/timer.ts +++ b/public/app/core/services/timer.ts @@ -21,7 +21,7 @@ export class Timer { } cancelAll() { - _.each(this.timers, function (t) { + _.each(this.timers, t => { this.$timeout.cancel(t); }); this.timers = []; diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index f1c44cfae0e..baadb604685 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -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' } + ] + } ]; }; diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index c950d3edd55..4828eb671a6 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -39,7 +39,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, text = text + '
' + event.text; } } else if (title) { - text = title + '
' + text; + text = title + '
' + (_.isString(text) ? text : ''); title = ''; } diff --git a/public/app/features/dashboard/save_as_modal.ts b/public/app/features/dashboard/save_as_modal.ts index 689718cb7e8..bbb2899f665 100644 --- a/public/app/features/dashboard/save_as_modal.ts +++ b/public/app/features/dashboard/save_as_modal.ts @@ -56,7 +56,9 @@ export class SaveDashboardAsModalCtrl { // do not want to create alert dupes if (dashboard.id > 0) { this.clone.panels.forEach(panel => { - delete panel.thresholds; + if (panel.type === "graph" && panel.alert) { + delete panel.thresholds; + } delete panel.alert; }); } diff --git a/public/app/features/dashboard/specs/save_as_modal.jest.ts b/public/app/features/dashboard/specs/save_as_modal.jest.ts new file mode 100644 index 00000000000..913209eb01f --- /dev/null +++ b/public/app/features/dashboard/specs/save_as_modal.jest.ts @@ -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); + }); +}); diff --git a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts index 7a04f5f7579..c39342a6f40 100644 --- a/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/share_modal_ctrl_specs.ts @@ -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(); diff --git a/public/app/features/dashboard/timepicker/input_date.ts b/public/app/features/dashboard/timepicker/input_date.ts index b6988a3ff16..ca3f5fa7ae9 100644 --- a/public/app/features/dashboard/timepicker/input_date.ts +++ b/public/app/features/dashboard/timepicker/input_date.ts @@ -1,5 +1,3 @@ -/// - import moment from 'moment'; import * as dateMath from 'app/core/utils/datemath'; @@ -7,16 +5,16 @@ export function inputDateDirective() { return { restrict: 'A', require: 'ngModel', - link: function ($scope, $elem, attrs, ngModel) { + link: function($scope, $elem, attrs, ngModel) { var format = 'YYYY-MM-DD HH:mm:ss'; - var fromUser = function (text) { + 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,15 +26,15 @@ 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; }; - var toUser = function (currentValue) { + var toUser = function(currentValue) { if (moment.isMoment(currentValue)) { return currentValue.format(format); } else { @@ -46,7 +44,6 @@ export function inputDateDirective() { ngModel.$parsers.push(fromUser); ngModel.$formatters.push(toUser); - } + }, }; } - diff --git a/public/app/features/dashlinks/editor.html b/public/app/features/dashlinks/editor.html index d1d73520283..11dfdf74c1f 100644 --- a/public/app/features/dashlinks/editor.html +++ b/public/app/features/dashlinks/editor.html @@ -13,9 +13,9 @@
With tags - +
- +
Title diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index bef7bca817b..f4d329908b1 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -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 diff --git a/public/app/features/panellinks/linkSrv.js b/public/app/features/panellinks/linkSrv.js deleted file mode 100644 index 89d89487c51..00000000000 --- a/public/app/features/panellinks/linkSrv.js +++ /dev/null @@ -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; - }; - - }); -}); diff --git a/public/app/features/panellinks/link_srv.ts b/public/app/features/panellinks/link_srv.ts new file mode 100644 index 00000000000..71192a86487 --- /dev/null +++ b/public/app/features/panellinks/link_srv.ts @@ -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); diff --git a/public/app/features/panellinks/module.js b/public/app/features/panellinks/module.js index 351b38f27c4..a36317dc2b3 100644 --- a/public/app/features/panellinks/module.js +++ b/public/app/features/panellinks/module.js @@ -1,7 +1,7 @@ define([ 'angular', 'lodash', - './linkSrv', + './link_srv', ], function (angular, _) { 'use strict'; diff --git a/public/app/features/panellinks/specs/link_srv.jest.ts b/public/app/features/panellinks/specs/link_srv.jest.ts new file mode 100644 index 00000000000..2ec38961e29 --- /dev/null +++ b/public/app/features/panellinks/specs/link_srv.jest.ts @@ -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'); + }); + }); + }); +}); diff --git a/public/app/features/panellinks/specs/link_srv_specs.ts b/public/app/features/panellinks/specs/link_srv_specs.ts deleted file mode 100644 index 77bb0a36c1f..00000000000 --- a/public/app/features/panellinks/specs/link_srv_specs.ts +++ /dev/null @@ -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'); - }); - }); - - }); -}); diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index c450f2e474a..c1dd3246fa1 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -40,7 +40,10 @@ System.config({ css: 'vendor/plugin-css/css.js' }, meta: { - '*': {esModule: true} + '*': { + esModule: true, + authorization: true, + } } }); diff --git a/public/app/plugins/app/testdata/datasource/datasource.ts b/public/app/plugins/app/testdata/datasource/datasource.ts index 90ae9e3aa47..2df33ea4ac6 100644 --- a/public/app/plugins/app/testdata/datasource/datasource.ts +++ b/public/app/plugins/app/testdata/datasource/datasource.ts @@ -1,7 +1,4 @@ -/// - 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, }; }); diff --git a/public/app/plugins/app/testdata/datasource/module.ts b/public/app/plugins/app/testdata/datasource/module.ts index 309b7443836..9d7eaf3cc83 100644 --- a/public/app/plugins/app/testdata/datasource/module.ts +++ b/public/app/plugins/app/testdata/datasource/module.ts @@ -1,5 +1,3 @@ -/// - import {TestDataDatasource} from './datasource'; import {TestDataQueryCtrl} from './query_ctrl'; diff --git a/public/app/plugins/app/testdata/datasource/query_ctrl.ts b/public/app/plugins/app/testdata/datasource/query_ctrl.ts index e783584eb5d..dd5f59c0a5a 100644 --- a/public/app/plugins/app/testdata/datasource/query_ctrl.ts +++ b/public/app/plugins/app/testdata/datasource/query_ctrl.ts @@ -1,14 +1,16 @@ -/// - import _ from 'lodash'; -import {QueryCtrl} from 'app/plugins/sdk'; +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,19 +18,53 @@ 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() { return this.backendSrv.get('/api/tsdb/testdata/scenarios').then(res => { this.scenarioList = res; - this.scenario = _.find(this.scenarioList, {id: this.target.scenarioId}); + this.scenario = _.find(this.scenarioList, { id: this.target.scenarioId }); }); } scenarioChanged() { - this.scenario = _.find(this.scenarioList, {id: this.target.scenarioId}); + 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(); } } - diff --git a/public/app/plugins/app/testdata/partials/query.editor.html b/public/app/plugins/app/testdata/partials/query.editor.html index a39582d5397..247918bce1f 100644 --- a/public/app/plugins/app/testdata/partials/query.editor.html +++ b/public/app/plugins/app/testdata/partials/query.editor.html @@ -1,8 +1,8 @@
- -
+ +
@@ -18,5 +18,23 @@
+
+
+ + + + + + + + +
+
+ +
+
+
+
+
diff --git a/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html b/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html index 67351358696..fd14fb7d077 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html +++ b/public/app/plugins/datasource/cloudwatch/partials/query.parameter.html @@ -49,6 +49,7 @@
  • {{stat}}
  • {{namespace}}
  • {{region}}
  • +
  • {{period}}
  • {{YOUR_DIMENSION_NAME}}
  • diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index e35896c0d04..ac5ccfeb5ca 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -15,9 +15,13 @@ export class MysqlDatasource { this.responseParser = new ResponseParser(this.$q); } - interpolateVariable(value) { + interpolateVariable(value, variable) { if (typeof value === 'string') { - return value; + if (variable.multi || variable.includeAll) { + return '\'' + value + '\''; + } else { + return value; + } } if (typeof value === 'number') { diff --git a/public/app/plugins/datasource/mysql/partials/annotations.editor.html b/public/app/plugins/datasource/mysql/partials/annotations.editor.html index 09581e2a552..b34eff5b011 100644 --- a/public/app/plugins/datasource/mysql/partials/annotations.editor.html +++ b/public/app/plugins/datasource/mysql/partials/annotations.editor.html @@ -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: time_sec for the annotation event. Format is UTC in seconds, use UNIX_TIMESTAMP(column) -- column with alias title for the annotation title - column with alias: text for the annotation text - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' diff --git a/public/app/plugins/datasource/mysql/partials/query.editor.html b/public/app/plugins/datasource/mysql/partials/query.editor.html index a7e993afd7f..22d64c9190f 100644 --- a/public/app/plugins/datasource/mysql/partials/query.editor.html +++ b/public/app/plugins/datasource/mysql/partials/query.editor.html @@ -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) diff --git a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts index f579ad15410..eb63f9c5b37 100644 --- a/public/app/plugins/datasource/mysql/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/mysql/specs/datasource_specs.ts @@ -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\''); + }); + }); + }); }); diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index 4d51b117ed4..7315485c6db 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -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 === "*"; }); } }); diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index 68471f035ff..af3d83f50d8 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -15,9 +15,13 @@ export class PostgresDatasource { this.responseParser = new ResponseParser(this.$q); } - interpolateVariable(value) { + interpolateVariable(value, variable) { if (typeof value === 'string') { - return value; + if (variable.multi || variable.includeAll) { + return '\'' + value + '\''; + } else { + return value; + } } if (typeof value === 'number') { diff --git a/public/app/plugins/datasource/postgres/partials/annotations.editor.html b/public/app/plugins/datasource/postgres/partials/annotations.editor.html index 07b838e739a..b56f7523087 100644 --- a/public/app/plugins/datasource/postgres/partials/annotations.editor.html +++ b/public/app/plugins/datasource/postgres/partials/annotations.editor.html @@ -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: time for the annotation event. Format is UTC in seconds, use extract(epoch from column) as "time" -- column with alias title for the annotation title - column with alias: text for the annotation text - column with alias: tags for annotation tags. This is a comma separated string of tags e.g. 'tag1,tag2' diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html index 1939fc47ecb..f1c7b376353 100644 --- a/public/app/plugins/datasource/postgres/partials/query.editor.html +++ b/public/app/plugins/datasource/postgres/partials/query.editor.html @@ -50,17 +50,15 @@ Macros: - $__timeEpoch -> extract(epoch from column) as "time" - $__timeFilter(column) -> column ≥ to_timestamp(1492750877) AND column ≤ to_timestamp(1492750877) - $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877 - -To group by time use $__timeGroup: --> (extract(epoch from column)/extract(epoch from column::interval))::int +- $__timeGroup(column,'5m') -> (extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int Example of group by and order by with $__timeGroup: SELECT - min(date_time_col) AS time_sec, - sum(value_double) as value + $__timeGroup(date_time_col, '1h') AS time, + sum(value) as value FROM yourtable -group by $__timeGroup(date_time_col, '1h') -order by $__timeGroup(date_time_col, '1h') ASC +GROUP BY time +ORDER BY time Or build your own conditionals using these macros which just return the values: - $__timeFrom() -> to_timestamp(1492750877) diff --git a/public/app/plugins/datasource/postgres/specs/datasource_specs.ts b/public/app/plugins/datasource/postgres/specs/datasource_specs.ts index 3f83bb76e7b..0c0b3ca303a 100644 --- a/public/app/plugins/datasource/postgres/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/postgres/specs/datasource_specs.ts @@ -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 {PostgresDatasource} from '../datasource'; +import {CustomVariable} from 'app/features/templating/custom_variable'; describe('PostgreSQLDatasource', function() { var ctx = new helpers.ServiceTestContext(); @@ -195,22 +196,41 @@ describe('PostgreSQLDatasource', 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 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 is a string', () => { + it('should return a quoted value', () => { + ctx.variable.includeAll = true; + expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('\'abc\''); + }); + }); + }); }); diff --git a/public/app/plugins/panel/alertlist/module.html b/public/app/plugins/panel/alertlist/module.html index a88c4ebadc7..0a3ff4fabb4 100644 --- a/public/app/plugins/panel/alertlist/module.html +++ b/public/app/plugins/panel/alertlist/module.html @@ -1,6 +1,17 @@
      +
    1. +
      +
      +
      +

      + No alerts in selected interval +

      +
      +
      +
      +
    2. diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.js b/public/app/plugins/panel/graph/series_overrides_ctrl.js index 2df993ff70e..5ee5b5e8e47 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.js +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.js @@ -29,7 +29,7 @@ define([ $scope.setOverride = function(item, subItem) { // handle color overrides if (item.propertyName === 'color') { - $scope.openColorSelector(); + $scope.openColorSelector($scope.override['color']); return; } @@ -52,15 +52,17 @@ define([ $scope.ctrl.render(); }; - $scope.openColorSelector = function() { + $scope.openColorSelector = function(color) { + var fakeSeries = {color: color}; popoverSrv.show({ element: $element.find(".dropdown")[0], position: 'top center', openOn: 'click', - template: '', + template: '', model: { autoClose: true, colorSelected: $scope.colorSelected, + series: fakeSeries }, onClose: function() { $scope.ctrl.render(); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 05b8f9323cf..cd1e35039dc 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import $ from 'jquery'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.gauge'; -import 'app/features/panellinks/linkSrv'; +import 'app/features/panellinks/link_srv'; import kbn from 'app/core/utils/kbn'; import config from 'app/core/config'; diff --git a/public/app/plugins/panel/text/editor.html b/public/app/plugins/panel/text/editor.html index 8e1283a39ba..eab53dc7615 100644 --- a/public/app/plugins/panel/text/editor.html +++ b/public/app/plugins/panel/text/editor.html @@ -15,5 +15,9 @@ (This area uses Markdown. HTML is not supported) - +
      +
      + + +
      +
      diff --git a/public/app/plugins/panel/text/module.ts b/public/app/plugins/panel/text/module.ts index 5f453aea15b..a3a58e968fc 100644 --- a/public/app/plugins/panel/text/module.ts +++ b/public/app/plugins/panel/text/module.ts @@ -23,6 +23,11 @@ export class TextPanelCtrl extends PanelCtrl { this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.events.on('refresh', this.onRefresh.bind(this)); this.events.on('render', this.onRender.bind(this)); + $scope.$watch('ctrl.panel.content', + _.throttle(() => { + this.render(); + }, 1000) + ); } onInitEditMode() { @@ -66,7 +71,9 @@ export class TextPanelCtrl extends PanelCtrl { }); } - this.updateContent(this.remarkable.render(content)); + this.$scope.$applyAsync(() => { + this.updateContent(this.remarkable.render(content)); + }); } updateContent(html) { diff --git a/public/test/mocks/angular.ts b/public/test/mocks/angular.ts new file mode 100644 index 00000000000..185d64214fd --- /dev/null +++ b/public/test/mocks/angular.ts @@ -0,0 +1,17 @@ +export default class AngularJSMock { + service: any; + controller: any; + directive: any; + + constructor() { + this.service = jest.fn(); + this.controller = jest.fn(); + this.directive = jest.fn(); + } + + module() { + return this; + } +} + +module.exports = AngularJSMock; diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index 89b0a1a46dd..6e48b42c8e8 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -21,7 +21,7 @@ RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A170311380 RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \ yum install -y nodejs --nogpgcheck -ENV GOLANG_VERSION 1.9.1 +ENV GOLANG_VERSION 1.9.2 RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \ yum install -y yarn --nogpgcheck && \