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 fb43f933731..fbd92fe0b51 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ awsconfig /tmp vendor/phantomjs/phantomjs vendor/phantomjs/phantomjs.exe +profile.out +coverage.txt docs/AWS_S3_BUCKET docs/GIT_BRANCH @@ -36,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/.hooks/pre-commit b/.hooks/pre-commit deleted file mode 100755 index 0d7c6c9ee5f..00000000000 --- a/.hooks/pre-commit +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -test -z "$(gofmt -s -l . | grep -v vendor/src/ | tee /dev/stderr)" -if [ $? -gt 0 ]; then - echo "Some files aren't formatted, please run 'go fmt ./pkg/...' to format your source code before committing" - exit 1 -fi - - -grunt test diff --git a/CHANGELOG.md b/CHANGELOG.md index ef6c3ffc273..7a261aa9e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,74 @@ - UX changes to nav & side menu - New dashboard grid layout system -# 4.6.0 (unreleased) +# 4.7.0 (unreleased) ## 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) +* **Teams**: Add Microsoft Teams notifier as [#8523](https://github.com/grafana/grafana/issues/8523), thx [@anthu](https://github.com/anthu) + +## 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) +* **Mysql**: MySQL enable MaxOpenCon and MaxIdleCon regards how constring is configured. [#9784](https://github.com/grafana/grafana/issues/9784), thx [@dfredell](https://github.com/dfredell) +* **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda) + +## Tech +* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645) + +## Fixes +* **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand) +* **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.2 (unreleased) + +* **Color picker**: Bug after using textbox input field to change/paste color string [#9769](https://github.com/grafana/grafana/issues/9769) +* **Cloudwatch**: Fix for cloudwatch templating query `ec2_instance_attribute` [#9667](https://github.com/grafana/grafana/issues/9667), thanks [@mtanda](https://github.com/mtanda) +* **Heatmap**: Fixed tooltip for "time series buckets" mode [#9332](https://github.com/grafana/grafana/issues/9332) +* **InfluxDB**: Fixed query editor issue when using `>` or `<` operators in WHERE clause [#9871](https://github.com/grafana/grafana/issues/9871) + + +# 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 +* **Alerting**: Viewer can no longer pause alert rules [#9640](https://github.com/grafana/grafana/issues/9640) +* **Playlist**: Bug where playlist controls was missing [#9639](https://github.com/grafana/grafana/issues/9639) +* **Firefox**: Creating region annotations now work in firefox [#9638](https://github.com/grafana/grafana/issues/9638) + +# 4.6.0-beta3 (2017-10-23) + +## Fixes +* **Prometheus**: Fix for browser crash for short time ranges. [#9575](https://github.com/grafana/grafana/issues/9575) +* **Heatmap**: Fix for y-axis not showing. [#9576](https://github.com/grafana/grafana/issues/9576) +* **Save to file**: Fix for save to file in export modal. [#9586](https://github.com/grafana/grafana/issues/9586) +* **Postgres**: modify group by time macro so it can be used in select clause [#9527](https://github.com/grafana/grafana/pull/9527), thanks [@svenklemm](https://github.com/svenklemm) + +# 4.6.0-beta2 (2017-10-17) + +## Fixes +* **ColorPicker**: Fix for color picker not showing [#9549](https://github.com/grafana/grafana/issues/9549) +* **Alerting**: Fix for broken test rule button in alert tab [#9539](https://github.com/grafana/grafana/issues/9539) +* **Cloudwatch**: Provide error message when failing to add cloudwatch datasource [#9534](https://github.com/grafana/grafana/pull/9534), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: Fix unused period parameter [#9536](https://github.com/grafana/grafana/pull/9536), thx [@mtanda](https://github.com/mtanda) +* **CSV Export**: Fix for broken CSV export [#9525](https://github.com/grafana/grafana/issues/9525) +* **Text panel**: Fix for issue with break lines in Firefox [#9491](https://github.com/grafana/grafana/issues/9491) +* **Annotations**: Fix for issue saving annotation event in MySQL DB [#9550](https://github.com/grafana/grafana/issues/9550), thanks [@krise3k](https://github.com/krise3k) + + +# 4.6.0-beta1 (2017-10-13) + +## New Features +* **Annotations**: Add support for creating annotations from graph panel [#8197](https://github.com/grafana/grafana/pull/8197) * **GCS**: Adds support for Google Cloud Storage [#8370](https://github.com/grafana/grafana/issues/8370) thx [@chuhlomin](https://github.com/chuhlomin) * **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187) * **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk) @@ -27,19 +92,20 @@ * **OAuth**: Verify TLS during OAuth callback [#9373](https://github.com/grafana/grafana/issues/9373), thx [@mattbostock](https://github.com/mattbostock) ## Minor -* **SMTP**: Make it possible to set specific EHLO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319) -* **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250) +* **SMTP**: Make it possible to set specific HELO for smtp client. [#9319](https://github.com/grafana/grafana/issues/9319) +* **Dataproxy**: Allow grafana to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250) * **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367) * **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739) -* **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8) +* **Slack**: Allow images to be uploaded to slack when Token is present [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8) * **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn) * **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123) * **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo) * **Kafka**: Add support for sending alert notifications to kafka [#7104](https://github.com/grafana/grafana/issues/7104), thx [@utkarshcmu](https://github.com/utkarshcmu) +* **Alerting**: add count_non_null as series reducer [#9516](https://github.com/grafana/grafana/issues/9516) ## Tech * **Go**: Grafana is now built using golang 1.9 -* **Webpack**: Changed from systemjs to webpack (see readme or building from source guide for new build instructions). Systemjs is still used to load plugins but now plugins can only import a limited set of dependencies. See [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) for more details on how this can effect some plugins. +* **Webpack**: Changed from systemjs to webpack (see readme or building from source guide for new build instructions). Systemjs is still used to load plugins but now plugins can only import a limited set of dependencies. See [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) for more details on how this can effect some plugins. # 4.5.2 (2017-09-22) diff --git a/Gruntfile.js b/Gruntfile.js index 335d8691677..a0607ef49dc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -22,9 +22,10 @@ module.exports = function (grunt) { } } + config.coverage = grunt.option('coverage'); config.phjs = grunt.option('phjsToRelease'); - config.pkg.version = grunt.option('pkgVer') || config.pkg.version; + console.log('Version', config.pkg.version); // load plugins diff --git a/PLUGIN_DEV.md b/PLUGIN_DEV.md index 1f672aa5a3d..9d831a95697 100644 --- a/PLUGIN_DEV.md +++ b/PLUGIN_DEV.md @@ -23,6 +23,6 @@ If you think we missed exposing a crucial lib or Grafana component let us know b ### Deprecated components -The angular directive `` is no deprecated (will still work for a version more) but we recommend plugin authors +The angular directive `` is now deprecated (will still work for a version more) but we recommend plugin authors to upgrade to new `` diff --git a/README.md b/README.md index 28f8bfc1ba2..aefc0c0802b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[Grafana](https://grafana.com) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/grafana)](https://goreportcard.com/report/github.com/grafana/grafana) +[Grafana](https://grafana.com) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/grafana)](https://goreportcard.com/report/github.com/grafana/grafana) [![codecov](https://codecov.io/gh/grafana/grafana/branch/master/graph/badge.svg)](https://codecov.io/gh/grafana/grafana) ================ [Website](https://grafana.com) | [Twitter](https://twitter.com/grafana) | @@ -81,6 +81,20 @@ You only need to add the options you want to override. Config files are applied In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = development`. +### Running tests + +- You can run backend Golang tests using "go test ./pkg/...". +- Execute all frontend tests with "npm run test" + +Writing & watching frontend tests (we have two test runners) + +- jest for all new tests that do not require browser context (React+more) + - Start watcher: `npm run jest` + - Jest will run all test files that end with the name ".jest.ts" +- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible). + - Start watcher: `npm run karma` + - Karma+Mocha runs all files that end with the name "_specs.ts". + ## Contribute If you have any idea for an improvement or found a bug do not hesitate to open an issue. @@ -89,8 +103,8 @@ the kickass metrics & devops dashboard we all dream about! ## Plugin development -Checkout the [Plugin Development Guide](http://docs.grafana.org/plugins/developing/development/) and checkout the [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) file for changes in Grafana that relate to -plugin development. +Checkout the [Plugin Development Guide](http://docs.grafana.org/plugins/developing/development/) and checkout the [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) file for changes in Grafana that relate to +plugin development. ## License 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 ba44172ddf3..4eb600bfde3 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 6.9.2 + version: 6.11.4 python: version: 2.7.3 services: @@ -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 @@ -30,10 +30,10 @@ dependencies: - sudo apt-get update; sudo apt-get install rpm; sudo apt-get install expect - ./scripts/build/build_container.sh - test: override: - - bash scripts/circle-test.sh + - bash scripts/circle-test-frontend.sh + - bash scripts/circle-test-backend.sh deployment: gh_branch: diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..82a86e0232b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,13 @@ +coverage: + precision: 2 + round: down + range: "50...100" + + status: + project: yes + patch: yes + changes: no + +comment: + layout: "diff" + behavior: "once" diff --git a/conf/defaults.ini b/conf/defaults.ini index 14e77449241..a4c87356a55 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -82,6 +82,9 @@ max_idle_conn = 2 # Max conn setting default is 0 (mean not set) max_open_conn = +# Set to true to log the sql calls and execution times. +log_queries = + # For "postgres", use either "disable", "require" or "verify-full" # For "mysql", use either "true", "false", or "skip-verify". ssl_mode = disable @@ -381,13 +384,6 @@ facility = # Syslog tag. By default, the process' argv[0] is used. tag = - -#################################### AMQP Event Publisher ################ -[event_publisher] -enabled = false -rabbitmq_url = amqp://localhost/ -exchange = grafana_events - #################################### Dashboard JSON files ################ [dashboards.json] enabled = false diff --git a/conf/sample.ini b/conf/sample.ini index 1aedfbf6532..6c522cffacc 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -91,6 +91,8 @@ # Max conn setting default is 0 (mean not set) ;max_open_conn = +# Set to true to log the sql calls and execution times. +log_queries = #################################### Session #################################### [session] @@ -360,12 +362,6 @@ ;tag = -#################################### AMQP Event Publisher ########################## -[event_publisher] -;enabled = false -;rabbitmq_url = amqp://localhost/ -;exchange = grafana_events - ;#################################### Dashboard JSON files ########################## [dashboards.json] ;enabled = false 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/graphite1/fig b/docker/blocks/graphite/docker-compose.yaml similarity index 54% rename from docker/blocks/graphite1/fig rename to docker/blocks/graphite/docker-compose.yaml index 8748e808133..2bd0dc322cc 100644 --- a/docker/blocks/graphite1/fig +++ b/docker/blocks/graphite/docker-compose.yaml @@ -1,28 +1,16 @@ -# version: '3' -# services: -# graphite1: -# build: -# context: blocks/graphite1 -# args: -# - version=master -graphite1: - build: blocks/graphite1 + graphite: + build: blocks/graphite ports: - "8080:80" - "2003:2003" - networks: - - graphite volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro fake-graphite-data: image: grafana/fake-data-gen - networks: - - graphite + network_mode: bridge environment: FD_DATASOURCE: graphite FD_PORT: 2003 -networks: - graphite: 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 277ed149d13..59f770e3273 100644 --- a/docker/blocks/graphite1/Dockerfile +++ b/docker/blocks/graphite1/Dockerfile @@ -43,24 +43,35 @@ ARG whisper_version=${version} ARG carbon_version=${version} ARG graphite_version=${version} -ARG statsd_version=v0.7.2 +ARG whisper_repo=https://github.com/graphite-project/whisper.git +ARG carbon_repo=https://github.com/graphite-project/carbon.git +ARG graphite_repo=https://github.com/graphite-project/graphite-web.git + +ARG statsd_version=v0.8.0 + +ARG statsd_repo=https://github.com/etsy/statsd.git # install whisper -RUN git clone -b ${whisper_version} --depth 1 https://github.com/graphite-project/whisper.git /usr/local/src/whisper +RUN git clone -b ${whisper_version} --depth 1 ${whisper_repo} /usr/local/src/whisper WORKDIR /usr/local/src/whisper RUN python ./setup.py install # install carbon -RUN git clone -b ${carbon_version} --depth 1 https://github.com/graphite-project/carbon.git /usr/local/src/carbon +RUN git clone -b ${carbon_version} --depth 1 ${carbon_repo} /usr/local/src/carbon WORKDIR /usr/local/src/carbon RUN pip install -r requirements.txt \ && python ./setup.py install # install graphite -RUN git clone -b ${graphite_version} --depth 1 https://github.com/graphite-project/graphite-web.git /usr/local/src/graphite-web +RUN git clone -b ${graphite_version} --depth 1 ${graphite_repo} /usr/local/src/graphite-web WORKDIR /usr/local/src/graphite-web RUN pip install -r requirements.txt \ && python ./setup.py install + +# install statsd +RUN git clone -b ${statsd_version} ${statsd_repo} /opt/statsd + +# config graphite ADD conf/opt/graphite/conf/*.conf /opt/graphite/conf/ ADD conf/opt/graphite/webapp/graphite/local_settings.py /opt/graphite/webapp/graphite/local_settings.py # ADD conf/opt/graphite/webapp/graphite/app_settings.py /opt/graphite/webapp/graphite/app_settings.py @@ -68,9 +79,8 @@ WORKDIR /opt/graphite/webapp RUN mkdir -p /var/log/graphite/ \ && PYTHONPATH=/opt/graphite/webapp django-admin.py collectstatic --noinput --settings=graphite.settings -# install statsd -RUN git clone -b ${statsd_version} https://github.com/etsy/statsd.git /opt/statsd -ADD conf/opt/statsd/config.js /opt/statsd/config.js +# config statsd +ADD conf/opt/statsd/config_*.js /opt/statsd/ # config nginx RUN rm /etc/nginx/sites-enabled/default @@ -102,8 +112,10 @@ RUN apt-get clean\ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # defaults -EXPOSE 80 2003-2004 2023-2024 8125/udp 8126 +EXPOSE 80 2003-2004 2023-2024 8125 8125/udp 8126 VOLUME ["/opt/graphite/conf", "/opt/graphite/storage", "/etc/nginx", "/opt/statsd", "/etc/logrotate.d", "/var/log"] WORKDIR / ENV HOME /root +ENV STATSD_INTERFACE udp + CMD ["/sbin/my_init"] diff --git a/docker/blocks/graphite1/docker-compose.yaml b/docker/blocks/graphite1/docker-compose.yaml new file mode 100644 index 00000000000..18ffc6de1b2 --- /dev/null +++ b/docker/blocks/graphite1/docker-compose.yaml @@ -0,0 +1,21 @@ + graphite: + build: + context: blocks/graphite1 + args: + - version=master + ports: + - "8080:80" + - "2003:2003" + - "8125:8125/udp" + - "8126:8126" + 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/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 049339a15c7..00000000000 --- a/docker/blocks/postgres/fig +++ /dev/null @@ -1,9 +0,0 @@ -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_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/alerting/rules.md b/docs/sources/alerting/rules.md index ead457066e3..bd5b95da856 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -27,8 +27,7 @@ and the conditions that need to be met for the alert to change state and trigger ## Execution The alert rules are evaluated in the Grafana backend in a scheduler and query execution engine that is part -of core Grafana. Only some data sources are supported right now. They include `Graphite`, `Prometheus`, -`InfluxDB` and `OpenTSDB`. +of core Grafana. Only some data sources are supported right now. They include `Graphite`, `Prometheus`, `InfluxDB`, `OpenTSDB`, `MySQL`, `Postgres` and `Cloudwatch`. ### Clustering diff --git a/docs/sources/archive.md b/docs/sources/archive.md index 1d656880286..aa36e1ee620 100644 --- a/docs/sources/archive.md +++ b/docs/sources/archive.md @@ -13,6 +13,7 @@ Here you can find links to older versions of the documentation that might be bet of Grafana. - [Latest](http://docs.grafana.org) +- [Version 4.5](http://docs.grafana.org/v4.5) - [Version 4.4](http://docs.grafana.org/v4.4) - [Version 4.3](http://docs.grafana.org/v4.3) - [Version 4.2](http://docs.grafana.org/v4.2) diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index 29da06bfb1e..bdf661dc4fc 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -29,7 +29,7 @@ Name | Description ------------ | ------------- *Name* | The data source name. This is how you refer to the data source in panels & queries. *Default* | Default data source means that it will be pre-selected for new panels. -*Credentials* profile name | Specify the name of the profile to use (if you use `~/aws/credentials` file), leave blank for default. +*Credentials* profile name | Specify the name of the profile to use (if you use `~/.aws/credentials` file), leave blank for default. *Default Region* | Used in query editor to set region (can be changed on per query basis) *Custom Metrics namespace* | Specify the CloudWatch namespace of Custom metrics *Assume Role Arn* | Specify the ARN of the role to assume @@ -169,5 +169,3 @@ Amazon provides 1 million CloudWatch API requests each month at no additional ch it costs $0.01 per 1,000 GetMetricStatistics or ListMetrics requests. For each query Grafana will issue a GetMetricStatistics request and every time you pick a dimension in the query editor Grafana will issue a ListMetrics request. - - diff --git a/docs/sources/features/datasources/index.md b/docs/sources/features/datasources/index.md index 16a8578ed3e..54606d20988 100644 --- a/docs/sources/features/datasources/index.md +++ b/docs/sources/features/datasources/index.md @@ -1,6 +1,7 @@ +++ title = "Data Sources" type = "docs" +aliases = ["/datasources/overview/"] [menu.docs] name = "Data Sources" identifier = "datasources" @@ -27,8 +28,9 @@ The following datasources are officially supported: * [InfluxDB]({{< relref "influxdb.md" >}}) * [OpenTSDB]({{< relref "opentsdb.md" >}}) * [Prometheus]({{< relref "prometheus.md" >}}) +* [MySQL]({{< relref "mysql.md" >}}) +* [Postgres]({{< relref "postgres.md" >}}) ## Data source plugins Since grafana 3.0 you can install data sources as plugins. Checkout [Grafana.net](https://grafana.com/plugins) for more data sources. - diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md index 97e523cd151..6d0918a0d01 100644 --- a/docs/sources/features/datasources/influxdb.md +++ b/docs/sources/features/datasources/influxdb.md @@ -173,6 +173,4 @@ SELECT title, description from events WHERE $timeFilter order asc For InfluxDB you need to enter a query like in the above example. You need to have the ```where $timeFilter``` part. If you only select one column you will not need to enter anything in the column mapping fields. The -Tags field can be a comma seperated string. - - +Tags field can be a comma separated string. diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index f277f45f3c8..69c6f667062 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -142,7 +142,11 @@ SELECT hostname FROM my_host WHERE region IN($region) ### Using Variables in Queries -Template variables are quoted automatically so if it is a string value do not wrap them in quotes in where clauses. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. +From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses. + +From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`. + +If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. There are two syntaxes: @@ -170,7 +174,28 @@ WHERE $__timeFilter(atimestamp) and hostname in([[hostname]]) ORDER BY atimestamp ASC ``` +## Annotations + +[Annotations]({{< relref "reference/annotations.md" >}}) allows you to overlay rich event information on top of graphs. You add annotation queries via the Dashboard menu / Annotations view. + +An example query: + +```sql +SELECT + UNIX_TIMESTAMP(atimestamp) as time_sec, + value as text, + CONCAT(tag1, ',', tag2) as tags +FROM my_table +WHERE $__timeFilter(atimestamp) +ORDER BY atimestamp ASC +``` + +Name | Description +------------ | ------------- +time_sec | The name of the date/time field. +text | Event description field. +tags | Optional field name to use for event tags as a comma separated string. + ## Alerting -Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule -conditions. +Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule conditions. diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 2f170ba40be..2b111d51f0e 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -45,10 +45,10 @@ Macro example | Description ------------ | ------------- *$__time(dateColumn)* | Will be replaced by an expression to rename the column to `time`. For example, *dateColumn as time* *$__timeSec(dateColumn)* | Will be replaced by an expression to rename the column to `time` and converting the value to unix timestamp. For example, *extract(epoch from dateColumn) as time* -*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > to_timestamp(1494410783) AND dateColumn < to_timestamp(1494497183)* +*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *extract(epoch from dateColumn) BETWEEN 1494410783 AND 1494497183* *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)* -*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int* +*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from "dateColumn")/300)::bigint*300* *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* @@ -94,26 +94,26 @@ Example with `metric` column ```sql SELECT - min(time_date_time) as time, + $__timeGroup(time_date_time,'5m') as time, min(value_double), 'min' as metric FROM test_data WHERE $__timeFilter(time_date_time) -GROUP BY metric1, (extract(epoch from time_date_time)/extract(epoch from $__interval::interval))::int -ORDER BY time asc +GROUP BY time +ORDER BY time ``` Example with multiple columns: ```sql SELECT - min(time_date_time) as time, + $__timeGroup(time_date_time,'5m') as time, min(value_double) as min_value, max(value_double) as max_value FROM test_data WHERE $__timeFilter(time_date_time) -GROUP BY metric1, (extract(epoch from time_date_time)/extract(epoch from $__interval::interval))::int -ORDER BY time asc +GROUP BY time +ORDER BY time ``` ## Templating @@ -154,7 +154,11 @@ SELECT hostname FROM host WHERE region IN($region) ### Using Variables in Queries -Template variables are quoted automatically so if it is a string value do not wrap them in quotes in where clauses. If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. +From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses. + +From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`. + +If the variable is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values. There are two syntaxes: @@ -180,6 +184,29 @@ WHERE $__timeFilter(atimestamp) and hostname in([[hostname]]) ORDER BY atimestamp ASC ``` +## Annotations + +[Annotations]({{< relref "reference/annotations.md" >}}) allow you to overlay rich event information on top of graphs. You add annotation queries via the Dashboard menu / Annotations view. + +An example query: + +```sql +SELECT + extract(epoch from time_date_time) AS time, + metric1 as text, + concat_ws(', ', metric1::text, metric2::text) as tags +FROM + public.test_data +WHERE + $__timeFilter(time_date_time) +``` + +Name | Description +------------ | ------------- +time | The name of the date/time field. +text | Event description field. +tags | Optional field name to use for event tags as a comma separated string. + ## Alerting Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule 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/guides/whats-new-in-v4-6.md b/docs/sources/guides/whats-new-in-v4-6.md index 3bc80799afc..fd75384761f 100644 --- a/docs/sources/guides/whats-new-in-v4-6.md +++ b/docs/sources/guides/whats-new-in-v4-6.md @@ -68,6 +68,7 @@ This makes exploring and filtering Prometheus data much easier. * **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn) * **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123) * **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo) +* **Postgres**: modify group by time macro so it can be used in select clause [#9527](https://github.com/grafana/grafana/pull/9527), thanks [@svenklemm](https://github.com/svenklemm) ### Tech * **Go**: Grafana is now built using golang 1.9 diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 3ef5fb1136a..716246102bc 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -102,11 +102,6 @@ Content-Type: application/json "templates_pattern":"emails/*.html", "welcome_email_on_sign_up":"false" }, - "event_publisher":{ - "enabled":"false", - "exchange":"grafana_events", - "rabbitmq_url":"amqp://localhost/" - }, "log":{ "buffer_len":"10000", "level":"Info", diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md index 2f148e9aded..7aab127cb0c 100644 --- a/docs/sources/http_api/annotations.md +++ b/docs/sources/http_api/annotations.md @@ -120,6 +120,37 @@ Content-Type: application/json {"message":"Annotation added"} ``` +## Create Annotation in Graphite format + +Creates an annotation by using Graphite-compatible event format. The `when` and `data` fields are optional. If `when` is not specified then the current time will be used as annotation's timestamp. The `tags` field can also be in prior to Graphite `0.10.0` +format (string with multiple tags being separated by a space). + +`POST /api/annotations/graphite` + +**Example Request**: + +```json +POST /api/annotations/graphite HTTP/1.1 +Accept: application/json +Content-Type: application/json + +{ + "what": "Event - deploy", + "tags": ["deploy", "production"], + "when": 1467844481, + "data": "deploy of master branch happened at Wed Jul 6 22:34:41 UTC 2016" +} +``` + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Graphite annotation added"} +``` + ## Update Annotation `PUT /api/annotations/:id` 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/http_api/org.md b/docs/sources/http_api/org.md index 6542f00fd81..4c1dff904c8 100644 --- a/docs/sources/http_api/org.md +++ b/docs/sources/http_api/org.md @@ -14,12 +14,12 @@ parent = "http_api" ## Get current Organisation -`GET /api/org` +`GET /api/org/` **Example Request**: ```http -GET /api/org HTTP/1.1 +GET /api/org/ HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk @@ -49,6 +49,8 @@ Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` +Note: The api will only work when you pass the admin name and password +to the request http url, like http://admin:admin@localhost:3000/api/orgs/1 **Example Response**: @@ -81,6 +83,8 @@ Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` +Note: The api will only work when you pass the admin name and password +to the request http url, like http://admin:admin@localhost:3000/api/orgs/name/Main%20Org%2E **Example Response**: @@ -118,6 +122,9 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "name":"New Org." } ``` +Note: The api will work in the following two ways +1) Need to set GF_USERS_ALLOW_ORG_CREATE=true +2) Set the config users.allow_org_create to true in ini file **Example Response**: @@ -279,6 +286,8 @@ Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` +Note: The api will only work when you pass the admin name and password +to the request http url, like http://admin:admin@localhost:3000/api/orgs **Example Response**: @@ -334,6 +343,9 @@ Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ``` +Note: The api will only work when you pass the admin name and password +to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users + **Example Response**: diff --git a/docs/sources/index.md b/docs/sources/index.md index f22ee82cc5c..7a431e29692 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -35,17 +35,19 @@ The back-end web server has a number of configuration options. Go the those options. -## Getting started +## Getting Started - [Getting Started]({{< relref "guides/getting_started.md" >}}) - [Basic Concepts]({{< relref "guides/basic_concepts.md" >}}) - [Screencasts]({{< relref "tutorials/screencasts.md" >}}) -## Data sources guides +## Data Source Guides - [Graphite]({{< relref "features/datasources/graphite.md" >}}) - [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}) - [InfluxDB]({{< relref "features/datasources/influxdb.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..24ddca9160f 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -224,6 +224,9 @@ The maximum number of connections in the idle connection pool. ### max_open_conn The maximum number of open connections to the database. +### log_queries +Set to `true` to log the sql calls and execution times. +
## [security] @@ -551,7 +554,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 +583,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 222a337855a..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.5.2_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.5.2_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.5.2_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.5.2_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.5.2-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.5.2-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.5.2-1.x86_64.rpm +$ sudo rpm -Uvh grafana-4.6.1-1.x86_64.rpm ``` #### On OpenSuse: ```bash -$ sudo rpm -i --nodeps grafana-4.5.2-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 ea9614c01be..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.5.2.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.2.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/plugins/developing/development.md b/docs/sources/plugins/developing/development.md index 7d67dd22323..f2e70a50c6a 100644 --- a/docs/sources/plugins/developing/development.md +++ b/docs/sources/plugins/developing/development.md @@ -10,26 +10,42 @@ weight = 1 # Developer Guide -From grafana 3.0 it's very easy to develop your own plugins and share them with other grafana users. - -There are two blog posts about authoring a plugin that might also be of interest to any plugin authors, [Timing is Everything. Writing the Clock Panel Plugin for Grafana 3.0- part 1](https://grafana.com/blog/2016/04/08/timing-is-everything.-writing-the-clock-panel-plugin-for-grafana-3.0/) and [Timing is Everything. Editor Mode in Grafana 3.0 for the Clock Panel Plugin](https://grafana.com/blog/2016/04/15/timing-is-everything.-editor-mode-in-grafana-3.0-for-the-clock-panel-plugin/). +You can extend Grafana by writing your own plugins and then share then with other users in [our plugin repository](https://grafana.com/plugins). ## Short version 1. [Setup grafana](http://docs.grafana.org/project/building_from_source/) 2. Clone an example plugin into ```/var/lib/grafana/plugins``` or `data/plugins` (relative to grafana git repo if you're running development version from source dir) -3. Code away! +3. Use one of our example plugins as starting point + +Example plugins + +- [Typescript data source example](https://github.com/grafana/typescript-template-datasource) +- [Simple json data source](https://github.com/grafana/simple-json-datasource) +- [Clock panel](https://github.com/grafana/clock-panel) +- [Pie chart panel](https://github.com/grafana/piechart-panel) + +There are two blog posts about authoring a plugin that might also be of interest to any plugin authors. + +- [Timing is Everything. Writing the Clock Panel Plugin for Grafana](https://grafana.com/blog/2016/04/08/timing-is-everything.-writing-the-clock-panel-plugin-for-grafana-3.0/) +- [Timing is Everything. Editor Mode in Grafana for the Clock Panel Plugin](https://grafana.com/blog/2016/04/15/timing-is-everything.-editor-mode-in-grafana-3.0-for-the-clock-panel-plugin/). ## What languages? -Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages. +Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since +we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages. ## Buildscript -You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository.By committing the dist folder the person who installs your plugin does not have to run any buildscript. - +You can use any build system you like that support systemjs. All the built content should end up in a folder named ```dist``` and committed to the repository. +By committing the dist folder the person who installs your plugin does not have to run any buildscript. All our example plugins have build scripted configured. +## Keep your plugin up to date + +New versions of Grafana can sometimes cause plugins to break. Checkout our [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) doc for changes in +Grafana that can impact your plugin. + ## Metadata See the [coding styleguide]({{< relref "code-styleguide.md" >}}) for details on the metadata. diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 1e4f0421a0c..dba04164d00 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -13,9 +13,10 @@ dev environment. Grafana ships with its own required backend server; also comple ## Dependencies -- [Go 1.9.1](https://golang.org/dl/) -- [NodeJS LTS](https://nodejs.org/download/) +- [Go 1.9.2](https://golang.org/dl/) - [Git](https://git-scm.com/downloads) +- [NodeJS LTS](https://nodejs.org/download/) +- node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details. ## Get Code Create a directory for the project and set your path accordingly (or use the [default Go workspace directory](https://golang.org/doc/code.html#GOPATH)). Then download and install Grafana into your $GOPATH directory: @@ -40,8 +41,8 @@ go run build.go build # (or 'go build ./pkg/cmd/grafana-server') ``` #### Building on Windows -The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on windows you need -to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). + +The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on windows you need to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download). [node-gyp](https://github.com/nodejs/node-gyp#installation) is the Node.js native addon build tool and it requires extra dependencies to be installed on Windows. In a command prompt which is run as administrator, run: @@ -84,6 +85,20 @@ bra run You'll also need to run `npm run watch` to watch for changes to the front-end (typescript, html, sass) +### Running tests + +- You can run backend Golang tests using "go test ./pkg/...". +- Execute all frontend tests with "npm run test" + +Writing & watching frontend tests (we have two test runners) + +- jest for all new tests that do not require browser context (React+more) + - Start watcher: `npm run jest` + - Jest will run all test files that end with the name ".jest.ts" +- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible). + - Start watcher: `npm run karma` + - Karma+Mocha runs all files that end with the name "_specs.ts". + ## Creating optimized release packages This step builds linux packages and requires that fpm is installed. Install fpm via `gem install fpm`. diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md index 1b904bc7c4a..de118f37d46 100644 --- a/docs/sources/reference/annotations.md +++ b/docs/sources/reference/annotations.md @@ -21,14 +21,14 @@ Grafana v4.6+ comes with a native annotation store and the ability to add annota ## Adding annotations -By holding down CTRL/CMD + mouse click. Add tags to the annotation will make it searchable from other dashboards. +By holding down **CTRL** or **CMD** + Click. Add tags to the annotation will make it searchable from other dashboards. {{< docs-imagebox img="/img/docs/annotations/annotation-still.png" max-width="600px" animated-gif="/img/docs/annotations/annotation.gif" >}} ### Adding regions events -You can also hold down CTRL/CMD and select region to create a region annotation. +You can also hold down **CTRL** or **CMD** and select region to create a region annotation. {{< docs-imagebox img="/img/docs/annotations/region-annotation-still.png" max-width="600px" animated-gif="/img/docs/annotations/region-annotation.gif" >}} @@ -69,5 +69,5 @@ The annotation query options are different for each data source. - [Elasticsearch annotation queries]({{< relref "features/datasources/elasticsearch.md#annotations" >}}) - [InfluxDB annotation queries]({{< relref "features/datasources/influxdb.md#annotations" >}}) - [Prometheus annotation queries]({{< relref "features/datasources/prometheus.md#annotations" >}}) - - +- [MySQL annotation queries]({{< relref "features/datasources/mysql.md#annotations" >}}) +- [Postgres annotation queries]({{< relref "features/datasources/postgres.md#annotations" >}}) diff --git a/docs/sources/tutorials/authproxy.md b/docs/sources/tutorials/authproxy.md index d4d2b9926fc..8003be20644 100644 --- a/docs/sources/tutorials/authproxy.md +++ b/docs/sources/tutorials/authproxy.md @@ -25,12 +25,16 @@ enabled = true header_name = X-WEBAUTH-USER header_property = username auto_sign_up = true +ldap_sync_ttl = 60 +whitelist = ``` * **enabled**: this is to toggle the feature on or off * **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in. * **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email) * **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid. +* **ldap_sync_ttl**: When both auth.proxy and auth.ldap are enabled, user's organisation and role are synchronised from ldap after the http proxy authentication. You can force ldap re-synchronisation after `ldap_sync_ttl` minutes. +* **whitelist**: Comma separated list of trusted authentication proxies IP. With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header. diff --git a/docs/yarn.lock b/docs/yarn.lock new file mode 100644 index 00000000000..fb57ccd13af --- /dev/null +++ b/docs/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000000..cbe77c4e30f --- /dev/null +++ b/jest.config.js @@ -0,0 +1,28 @@ + +module.exports = { + verbose: true, + "globals": { + "ts-jest": { + "tsConfigFile": "tsconfig.json" + } + }, + "transform": { + "^.+\\.tsx?$": "/node_modules/ts-jest/preprocessor.js" + }, + "moduleDirectories": ["node_modules", "public"], + "roots": [ + "/public" + ], + "testRegex": "(\\.|/)(jest)\\.(jsx?|tsx?)$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json" + ], + "setupFiles": [ + "./public/test/jest-shim.ts", + "./public/test/jest-setup.ts" + ] +}; diff --git a/package.json b/package.json index e037b0211cf..9a6edfb8be9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "4.6.0-beta1", + "version": "4.7.0-pre1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" @@ -12,6 +12,7 @@ "devDependencies": { "@types/d3": "^4.10.1", "@types/enzyme": "^2.8.9", + "@types/jest": "^21.1.4", "@types/node": "^8.0.31", "@types/react": "^16.0.5", "@types/react-dom": "^15.5.4", @@ -22,8 +23,8 @@ "babel-loader": "^7.1.2", "babel-preset-es2015": "^6.24.1", "css-loader": "^0.28.7", - "enzyme": "^3.0.0", - "enzyme-adapter-react-16": "^1.0.0", + "enzyme": "^3.1.0", + "enzyme-adapter-react-16": "^1.0.1", "es6-promise": "^3.0.2", "es6-shim": "^0.35.3", "expect.js": "~0.2.0", @@ -53,11 +54,11 @@ "html-loader": "^0.5.1", "html-webpack-plugin": "^2.30.1", "husky": "^0.14.3", + "jest": "^21.2.1", "jshint-stylish": "~2.2.1", "json-loader": "^0.5.7", "karma": "1.7.0", "karma-chrome-launcher": "~2.2.0", - "karma-coverage": "1.1.1", "karma-expect": "~1.1.3", "karma-mocha": "~1.3.0", "karma-phantomjs-launcher": "1.0.4", @@ -82,6 +83,7 @@ "sinon": "1.17.6", "systemjs": "0.20.19", "systemjs-plugin-css": "^0.1.36", + "ts-jest": "^21.1.3", "ts-loader": "^2.3.7", "tslint": "^5.7.0", "tslint-loader": "^3.5.3", @@ -93,12 +95,15 @@ "zone.js": "^0.7.2" }, "scripts": { - "dev": "./node_modules/.bin/webpack --progress --colors --config scripts/webpack/webpack.dev.js", - "watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", - "build": "./node_modules/.bin/grunt build", - "test": "./node_modules/.bin/grunt test", - "lint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --type-check", - "watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev" + "dev": "webpack --progress --colors --config scripts/webpack/webpack.dev.js", + "watch": "webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js", + "build": "grunt build", + "test": "grunt test", + "test:coverage": "grunt test --coverage=true", + "lint": "tslint -c tslint.json --project tsconfig.json --type-check", + "karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev", + "jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch", + "precommit": "node ./node_modules/grunt-cli/bin/grunt precommit" }, "license": "Apache-2.0", "dependencies": { @@ -124,6 +129,8 @@ "rxjs": "^5.4.3", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop", - "tinycolor2": "^1.4.1" + "tinycolor2": "^1.4.1", + "d3": "^4.11.0", + "d3-scale-chromatic": "^1.1.1" } } diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server index d01778560f7..85b0e412d35 100755 --- a/packaging/deb/init.d/grafana-server +++ b/packaging/deb/init.d/grafana-server @@ -91,7 +91,7 @@ case "$1" in then sleep 1 - # check if pid file has been written two + # check if pid file has been written to if ! [[ -s $PID_FILE ]]; then log_end_msg 1 exit 1 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/packaging/publish/publish_testing.sh b/packaging/publish/publish_testing.sh index 17fabc5e136..276193ad63f 100755 --- a/packaging/publish/publish_testing.sh +++ b/packaging/publish/publish_testing.sh @@ -1,10 +1,10 @@ #! /usr/bin/env bash -deb_ver=4.5.0-beta1 -rpm_ver=4.5.0-beta1 +deb_ver=4.6.0-beta1 +rpm_ver=4.6.0-beta1 -# wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb -# package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb +package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb package_cloud push grafana/testing/debian/stretch grafana_${deb_ver}_amd64.deb diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index a9e2988bdb7..dc63e1ef4c6 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -96,7 +96,7 @@ case "$1" in if [ $return -eq 0 ] then sleep 1 - # check if pid file has been written two + # check if pid file has been written to if ! [[ -s $PID_FILE ]]; then echo "FAILED" exit 1 diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index be069f2b07e..e6454e9cf86 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -1,7 +1,6 @@ package api import ( - "fmt" "strings" "time" @@ -41,9 +40,22 @@ func GetAnnotations(c *middleware.Context) Response { return Json(200, items) } +type CreateAnnotationError struct { + message string +} + +func (e *CreateAnnotationError) Error() string { + return e.message +} + func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response { repo := annotations.GetRepository() + if cmd.Text == "" { + err := &CreateAnnotationError{"text field should not be empty"} + return ApiError(500, "Failed to save annotation", err) + } + item := annotations.Item{ OrgId: c.OrgId, UserId: c.UserId, @@ -55,6 +67,10 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response Tags: cmd.Tags, } + if item.Epoch == 0 { + item.Epoch = time.Now().Unix() + } + if err := repo.Save(&item); err != nil { return ApiError(500, "Failed to save annotation", err) } @@ -82,21 +98,22 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response return ApiSuccess("Annotation added") } -type GraphiteAnnotationError struct { - message string -} - -func (e *GraphiteAnnotationError) Error() string { - return e.message -} - func formatGraphiteAnnotation(what string, data string) string { - return fmt.Sprintf("%s\n%s", what, data) + text := what + if data != "" { + text = text + "\n" + data + } + return text } func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotationsCmd) Response { repo := annotations.GetRepository() + if cmd.What == "" { + err := &CreateAnnotationError{"what field should not be empty"} + return ApiError(500, "Failed to save Graphite annotation", err) + } + if cmd.When == 0 { cmd.When = time.Now().Unix() } @@ -106,18 +123,22 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati var tagsArray []string switch tags := cmd.Tags.(type) { case string: - tagsArray = strings.Split(tags, " ") + if tags != "" { + tagsArray = strings.Split(tags, " ") + } else { + tagsArray = []string{} + } case []interface{}: for _, t := range tags { if tagStr, ok := t.(string); ok { tagsArray = append(tagsArray, tagStr) } else { - err := &GraphiteAnnotationError{"tag should be a string"} + err := &CreateAnnotationError{"tag should be a string"} return ApiError(500, "Failed to save Graphite annotation", err) } } default: - err := &GraphiteAnnotationError{"unsupported tags format"} + err := &CreateAnnotationError{"unsupported tags format"} return ApiError(500, "Failed to save Graphite annotation", err) } @@ -133,7 +154,7 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati return ApiError(500, "Failed to save Graphite annotation", err) } - return ApiSuccess("Graphite Annotation added") + return ApiSuccess("Graphite annotation added") } func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response { diff --git a/pkg/api/api.go b/pkg/api/api.go index 8c91120facc..ba5e7ae4e10 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -267,7 +267,7 @@ func (hs *HttpServer) registerRoutes() { apiRoute.Group("/alerts", func(alertsRoute RouteRegister) { alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest)) - alertsRoute.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert), reqEditorRole) + alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert)) alertsRoute.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert)) alertsRoute.Get("/", wrap(GetAlerts)) alertsRoute.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard)) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 9ffdc4a6d1b..747ef8f25e6 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -119,7 +119,13 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) { return } - c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id, "name": cmd.Result.Name}) + ds := convertModelToDtos(cmd.Result) + c.JSON(200, util.DynMap{ + "message": "Datasource added", + "id": cmd.Result.Id, + "name": cmd.Result.Name, + "datasource": ds, + }) } func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response { @@ -133,10 +139,19 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp err = bus.Dispatch(&cmd) if err != nil { - return ApiError(500, "Failed to update datasource", err) + if err == m.ErrDataSourceUpdatingOldVersion { + return ApiError(500, "Failed to update datasource. Reload new version and try again", err) + } else { + return ApiError(500, "Failed to update datasource", err) + } } - - return Json(200, util.DynMap{"message": "Datasource updated", "id": cmd.Id, "name": cmd.Name}) + ds := convertModelToDtos(cmd.Result) + return Json(200, util.DynMap{ + "message": "Datasource updated", + "id": cmd.Id, + "name": cmd.Name, + "datasource": ds, + }) } func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { @@ -158,8 +173,6 @@ func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { } } - // set version from db - cmd.Version = ds.Version return nil } @@ -228,6 +241,7 @@ func convertModelToDtos(ds *m.DataSource) dtos.DataSource { IsDefault: ds.IsDefault, JsonData: ds.JsonData, SecureJsonFields: map[string]bool{}, + Version: ds.Version, } for k, v := range ds.SecureJsonData { diff --git a/pkg/api/dtos/datasource.go b/pkg/api/dtos/datasource.go new file mode 100644 index 00000000000..7cb36e61ab4 --- /dev/null +++ b/pkg/api/dtos/datasource.go @@ -0,0 +1,59 @@ +package dtos + +import ( + "strings" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" +) + +type DataSource struct { + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Type string `json:"type"` + TypeLogoUrl string `json:"typeLogoUrl"` + Access m.DsAccess `json:"access"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData,omitempty"` + SecureJsonFields map[string]bool `json:"secureJsonFields"` + Version int `json:"version"` +} + +type DataSourceListItemDTO struct { + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Type string `json:"type"` + TypeLogoUrl string `json:"typeLogoUrl"` + Access m.DsAccess `json:"access"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData,omitempty"` +} + +type DataSourceList []DataSourceListItemDTO + +func (slice DataSourceList) Len() int { + return len(slice) +} + +func (slice DataSourceList) Less(i, j int) bool { + return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name) +} + +func (slice DataSourceList) Swap(i, j int) { + slice[i], slice[j] = slice[j], slice[i] +} diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index d1c346e9539..e2aa12249aa 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -37,56 +37,6 @@ type CurrentUser struct { HelpFlags1 m.HelpFlags1 `json:"helpFlags1"` } -type DataSource struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Type string `json:"type"` - TypeLogoUrl string `json:"typeLogoUrl"` - Access m.DsAccess `json:"access"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData *simplejson.Json `json:"jsonData,omitempty"` - SecureJsonFields map[string]bool `json:"secureJsonFields"` -} - -type DataSourceListItemDTO struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Type string `json:"type"` - TypeLogoUrl string `json:"typeLogoUrl"` - Access m.DsAccess `json:"access"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - IsDefault bool `json:"isDefault"` - JsonData *simplejson.Json `json:"jsonData,omitempty"` -} - -type DataSourceList []DataSourceListItemDTO - -func (slice DataSourceList) Len() int { - return len(slice) -} - -func (slice DataSourceList) Less(i, j int) bool { - return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name) -} - -func (slice DataSourceList) Swap(i, j int) { - slice[i], slice[j] = slice[j], slice[i] -} - type MetricRequest struct { From string `json:"from"` To string `json:"to"` diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 3107dcf7e4b..037d2ae98e5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -188,9 +188,8 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) { return } - promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ - DisableCompression: true, - }).ServeHTTP(ctx.Resp, ctx.Req.Request) + promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}). + ServeHTTP(ctx.Resp, ctx.Req.Request) } func (hs *HttpServer) healthHandler(ctx *macaron.Context) { diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index f6a94ac6ef0..4bbabbe3273 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -19,7 +19,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/cleanup" - "github.com/grafana/grafana/pkg/services/eventpublisher" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/setting" @@ -59,7 +58,6 @@ func (g *GrafanaServerImpl) Start() { search.Init() login.Init() social.NewOAuthService() - eventpublisher.Init() plugins.Init() closer, err := tracing.Init(setting.Cfg) diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index db36054e76d..d5980231f0e 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -121,7 +121,9 @@ func RenderToPng(params *RenderOpts) (string, error) { done := make(chan error) go func() { - cmd.Wait() + if err := cmd.Wait(); err != nil { + rendererLog.Error("failed to render an image", "error", err) + } close(done) }() diff --git a/pkg/log/log.go b/pkg/log/log.go index 56b2eb50c9f..9b3f8b27071 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -14,7 +14,7 @@ import ( "github.com/go-stack/stack" "github.com/inconshreveable/log15" - "github.com/inconshreveable/log15/term" + isatty "github.com/mattn/go-isatty" "github.com/grafana/grafana/pkg/util" ) @@ -157,7 +157,7 @@ func getFilters(filterStrArray []string) map[string]log15.Lvl { func getLogFormat(format string) log15.Format { switch format { case "console": - if term.IsTty(os.Stdout.Fd()) { + if isatty.IsTerminal(os.Stdout.Fd()) { return log15.TerminalFormat() } return log15.LogfmtFormat() 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/middleware/util.go b/pkg/middleware/util.go index c392f215450..22d1b56869e 100644 --- a/pkg/middleware/util.go +++ b/pkg/middleware/util.go @@ -21,6 +21,10 @@ func Gziper() macaron.Handler { return } + if strings.HasPrefix(requestPath, "/metrics") { + return + } + ctx.Invoke(macaronGziper) } } diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 069900cc091..7fc8f935124 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -18,14 +18,15 @@ const ( DS_KAIROSDB = "kairosdb" DS_PROMETHEUS = "prometheus" DS_POSTGRES = "postgres" + DS_MYSQL = "mysql" DS_ACCESS_DIRECT = "direct" DS_ACCESS_PROXY = "proxy" ) -// Typed errors var ( - ErrDataSourceNotFound = errors.New("Data source not found") - ErrDataSourceNameExists = errors.New("Data source with same name already exists") + ErrDataSourceNotFound = errors.New("Data source not found") + ErrDataSourceNameExists = errors.New("Data source with same name already exists") + ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource") ) type DsAccess string @@ -64,6 +65,7 @@ var knownDatasourcePlugins map[string]bool = map[string]bool{ DS_PROMETHEUS: true, DS_OPENTSDB: true, DS_POSTGRES: true, + DS_MYSQL: true, "opennms": true, "druid": true, "dalmatinerdb": true, @@ -129,10 +131,12 @@ type UpdateDataSourceCommand struct { IsDefault bool `json:"isDefault"` JsonData *simplejson.Json `json:"jsonData"` SecureJsonData map[string]string `json:"secureJsonData"` + Version int `json:"version"` - OrgId int64 `json:"-"` - Id int64 `json:"-"` - Version int `json:"-"` + OrgId int64 `json:"-"` + Id int64 `json:"-"` + + Result *DataSource } type DeleteDataSourceByIdCommand struct { diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index 8db480f947d..90c18d90044 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -40,7 +40,7 @@ func getPluginLogoUrl(pluginType, path, baseUrl string) string { } func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { - appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1) + appSubPath := strings.Replace(strings.Replace(fp.PluginDir, app.PluginDir, "", 1), "\\", "/", 1) fp.IncludedInAppId = app.Id fp.BaseUrl = app.BaseUrl diff --git a/pkg/plugins/frontend_plugin_test.go b/pkg/plugins/frontend_plugin_test.go new file mode 100644 index 00000000000..304cd38deae --- /dev/null +++ b/pkg/plugins/frontend_plugin_test.go @@ -0,0 +1,34 @@ +package plugins + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestFrontendPlugin(t *testing.T) { + + Convey("When setting paths based on App on Windows", t, func() { + setting.StaticRootPath = "c:\\grafana\\public" + + fp := &FrontendPluginBase{ + PluginBase: PluginBase{ + PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasource", + BaseUrl: "fpbase", + }, + } + app := &AppPlugin{ + FrontendPluginBase: FrontendPluginBase{ + PluginBase: PluginBase{ + PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata", + Id: "testdata", + BaseUrl: "public/app/plugins/app/testdata", + }, + }, + } + fp.setPathsBasedOnApp(app) + + So(fp.Module, ShouldEqual, "app/plugins/app/testdata/datasource/module") + }) +} diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 6eaa21b958e..0a61c13fa12 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -141,6 +141,16 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { break } } + case "count_non_null": + for _, v := range series.Points { + if v[0].Valid { + value++ + } + } + + if value > 0 { + allNull = false + } } if allNull { diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index f0147e9021a..866b574f59f 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -67,6 +67,35 @@ func TestSimpleReducer(t *testing.T) { So(reducer.Reduce(series).Valid, ShouldEqual, false) }) + Convey("count_non_null", func() { + Convey("with null values and real values", func() { + reducer := NewSimpleReducer("count_non_null") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 3)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4)) + + So(reducer.Reduce(series).Valid, ShouldEqual, true) + So(reducer.Reduce(series).Float64, ShouldEqual, 2) + }) + + Convey("with null values", func() { + reducer := NewSimpleReducer("count_non_null") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + + So(reducer.Reduce(series).Valid, ShouldEqual, false) + }) + }) + Convey("avg of number values and null values should ignore nulls", func() { reducer := NewSimpleReducer("avg") series := &tsdb.TimeSeries{ diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 5c2861b9154..79b2f231b41 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -67,7 +67,7 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { metrics.M_Alerting_Execution_Time.Observe(float64(elapsedTime)) } -// This should be move into evalContext once its been refactored. +// This should be move into evalContext once its been refactored. (Carl Bergquist) func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType { if evalContext.Error != nil { handler.log.Error("Alert Rule Result Error", diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index a4c41e14bae..9f77801d458 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -112,7 +112,7 @@ func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { } if evalContext.Rule.Message != "" { - bodyJSON.Set("message", evalContext.Rule.Message) + bodyJSON.Set("output", evalContext.Rule.Message) } body, _ := bodyJSON.MarshalJSON() diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go new file mode 100644 index 00000000000..200a8594428 --- /dev/null +++ b/pkg/services/alerting/notifiers/teams.go @@ -0,0 +1,122 @@ +package notifiers + +import ( + "encoding/json" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "teams", + Name: "Microsoft Teams", + Description: "Sends notifications using Incomming Webhook connector to Microsoft Teams", + Factory: NewTeamsNotifier, + OptionsTemplate: ` +

Teams settings

+
+ Url + +
+ `, + }) + +} + +func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + url := model.Settings.Get("url").MustString() + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} + } + + return &TeamsNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + Url: url, + log: log.New("alerting.notifier.teams"), + }, nil +} + +type TeamsNotifier struct { + NotifierBase + Url string + Recipient string + Mention string + log log.Logger +} + +func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + + fields := make([]map[string]interface{}, 0) + fieldLimitCount := 4 + for index, evt := range evalContext.EvalMatches { + fields = append(fields, map[string]interface{}{ + "name": evt.Metric, + "value": evt.Value, + }) + if index > fieldLimitCount { + break + } + } + + if evalContext.Error != nil { + fields = append(fields, map[string]interface{}{ + "name": "Error message", + "value": evalContext.Error.Error(), + }) + } + + message := this.Mention + if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. + message += " " + evalContext.Rule.Message + } + + body := map[string]interface{}{ + "@type": "MessageCard", + "@context": "http://schema.org/extensions", + "summary": message, + "title": evalContext.GetNotificationTitle(), + "themeColor": evalContext.GetStateModel().Color, + "sections": []map[string]interface{}{ + { + "title": "Details", + "facts": fields, + "images": []map[string]interface{}{ + { + "image": evalContext.ImagePublicUrl, + }, + }, + "text": message, + "potentialAction": []map[string]interface{}{ + { + "@context": "http://schema.org", + "@type": "ViewAction", + "name": "View Rule", + "target": []string{ + ruleUrl, + }, + }, + }, + }, + }, + } + + data, _ := json.Marshal(&body) + cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)} + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send teams notification", "error", err, "webhook", this.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/teams_test.go b/pkg/services/alerting/notifiers/teams_test.go new file mode 100644 index 00000000000..a9647350736 --- /dev/null +++ b/pkg/services/alerting/notifiers/teams_test.go @@ -0,0 +1,75 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestTeamsNotifier(t *testing.T) { + Convey("Teams notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "ops", + Type: "teams", + Settings: settingsJSON, + } + + _, err := NewTeamsNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("from settings", func() { + json := ` + { + "url": "http://google.com" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "ops", + Type: "teams", + Settings: settingsJSON, + } + + not, err := NewTeamsNotifier(model) + teamsNotifier := not.(*TeamsNotifier) + + So(err, ShouldBeNil) + So(teamsNotifier.Name, ShouldEqual, "ops") + So(teamsNotifier.Type, ShouldEqual, "teams") + So(teamsNotifier.Url, ShouldEqual, "http://google.com") + }) + + Convey("from settings with Recipient and Mention", func() { + json := ` + { + "url": "http://google.com" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "ops", + Type: "teams", + Settings: settingsJSON, + } + + not, err := NewTeamsNotifier(model) + teamsNotifier := not.(*TeamsNotifier) + + So(err, ShouldBeNil) + So(teamsNotifier.Name, ShouldEqual, "ops") + So(teamsNotifier.Type, ShouldEqual, "teams") + So(teamsNotifier.Url, ShouldEqual, "http://google.com") + }) + + }) + }) +} diff --git a/pkg/services/eventpublisher/eventpublisher.go b/pkg/services/eventpublisher/eventpublisher.go deleted file mode 100644 index 2854b63a9a5..00000000000 --- a/pkg/services/eventpublisher/eventpublisher.go +++ /dev/null @@ -1,150 +0,0 @@ -package eventpublisher - -import ( - "encoding/json" - "fmt" - "log" - "time" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/events" - "github.com/grafana/grafana/pkg/setting" - "github.com/streadway/amqp" -) - -var ( - url string - exchange string - conn *amqp.Connection - channel *amqp.Channel -) - -func getConnection() (*amqp.Connection, error) { - c, err := amqp.Dial(url) - if err != nil { - return nil, err - } - return c, err -} - -func getChannel() (*amqp.Channel, error) { - ch, err := conn.Channel() - if err != nil { - return nil, err - } - - err = ch.ExchangeDeclare( - exchange, // name - "topic", // type - true, // durable - false, // auto-deleted - false, // internal - false, // no-wait - nil, // arguments - ) - if err != nil { - return nil, err - } - return ch, err -} - -func Init() { - sec := setting.Cfg.Section("event_publisher") - - if !sec.Key("enabled").MustBool(false) { - return - } - - url = sec.Key("rabbitmq_url").String() - exchange = sec.Key("exchange").String() - bus.AddWildcardListener(eventListener) - - if err := Setup(); err != nil { - log.Fatal(4, "Failed to connect to notification queue: %v", err) - return - } -} - -// Every connection should declare the topology they expect -func Setup() error { - c, err := getConnection() - if err != nil { - return err - } - conn = c - ch, err := getChannel() - if err != nil { - return err - } - - channel = ch - - // listen for close events so we can reconnect. - errChan := channel.NotifyClose(make(chan *amqp.Error)) - go func() { - for e := range errChan { - fmt.Println("connection to rabbitmq lost.") - fmt.Println(e) - fmt.Println("attempting to create new rabbitmq channel.") - ch, err := getChannel() - if err == nil { - channel = ch - break - } - - //could not create channel, so lets close the connection - // and re-create. - _ = conn.Close() - - for err != nil { - time.Sleep(2 * time.Second) - fmt.Println("attempting to reconnect to rabbitmq.") - err = Setup() - } - fmt.Println("Connected to rabbitmq again.") - } - }() - - return nil -} - -func publish(routingKey string, msgString []byte) { - for { - err := channel.Publish( - exchange, //exchange - routingKey, // routing key - false, // mandatory - false, // immediate - amqp.Publishing{ - ContentType: "application/json", - Body: msgString, - }, - ) - if err == nil { - return - } - // failures are most likely because the connection was lost. - // the connection will be re-established, so just keep - // retrying every 2seconds until we successfully publish. - time.Sleep(2 * time.Second) - fmt.Println("publish failed, retrying.") - } -} - -func eventListener(event interface{}) error { - wireEvent, err := events.ToOnWriteEvent(event) - if err != nil { - return err - } - - msgString, err := json.Marshal(wireEvent) - if err != nil { - return err - } - - routingKey := fmt.Sprintf("%s.%s", wireEvent.Priority, wireEvent.EventType) - // this is run in a greenthread and we expect that publish will keep - // retrying until the message gets sent. - go publish(routingKey, msgString) - return nil -} diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 33a4cae53c2..73be7d774fd 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -94,7 +94,12 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { if i > 0 { sql.WriteString(" OR ") } - sql.WriteString("state = ? ") + if strings.HasPrefix(v, "not_") { + sql.WriteString("state <> ? ") + v = strings.TrimPrefix(v, "not_") + } else { + sql.WriteString("state = ? ") + } params = append(params, v) } sql.WriteString(")") diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index a2c5d80ac3a..d97db10f630 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -43,7 +43,7 @@ func (r *SqlAnnotationRepo) ensureTagsExist(sess *DBSession, tags []*models.Tag) var existingTag models.Tag // check if it exists - if exists, err := sess.Table("tag").Where("key=? AND value=?", tag.Key, tag.Value).Get(&existingTag); err != nil { + if exists, err := sess.Table("tag").Where("`key`=? AND `value`=?", tag.Key, tag.Value).Get(&existingTag); err != nil { return nil, err } else if exists { tag.Id = existingTag.Id diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 88dc34fd196..7069990c476 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -3,6 +3,8 @@ package sqlstore import ( "time" + "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/metrics" @@ -69,7 +71,6 @@ func DeleteDataSourceByName(cmd *m.DeleteDataSourceByNameCommand) error { } func AddDataSource(cmd *m.AddDataSourceCommand) error { - return inTransaction(func(sess *DBSession) error { existing := m.DataSource{OrgId: cmd.OrgId, Name: cmd.Name} has, _ := sess.Get(&existing) @@ -96,6 +97,7 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error { SecureJsonData: securejsondata.GetEncryptedJsonData(cmd.SecureJsonData), Created: time.Now(), Updated: time.Now(), + Version: 1, } if _, err := sess.Insert(ds); err != nil { @@ -122,7 +124,6 @@ func updateIsDefaultFlag(ds *m.DataSource, sess *DBSession) error { } func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { - return inTransaction(func(sess *DBSession) error { ds := &m.DataSource{ Id: cmd.Id, @@ -149,12 +150,29 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { sess.UseBool("basic_auth") sess.UseBool("with_credentials") - _, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds) + var updateSession *xorm.Session + if cmd.Version != 0 { + // the reason we allow cmd.version > db.version is make it possible for people to force + // updates to datasources using the datasource.yaml file without knowing exactly what version + // a datasource have in the db. + updateSession = sess.Where("id=? and org_id=? and version < ?", ds.Id, ds.OrgId, ds.Version) + + } else { + updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId) + } + + affected, err := updateSession.Update(ds) if err != nil { return err } + if affected == 0 { + return m.ErrDataSourceUpdatingOldVersion + } + err = updateIsDefaultFlag(ds, sess) + + cmd.Result = ds return err }) } diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index 51bc759fdaf..f7a0ab7c1e2 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -35,12 +35,9 @@ type Test struct { } func TestDataAccess(t *testing.T) { - Convey("Testing DB", t, func() { InitTestDB(t) - Convey("Can add datasource", func() { - err := AddDataSource(&m.AddDataSourceCommand{ OrgId: 10, Name: "laban", @@ -65,7 +62,6 @@ func TestDataAccess(t *testing.T) { }) Convey("Given a datasource", func() { - err := AddDataSource(&m.AddDataSourceCommand{ OrgId: 10, Name: "nisse", @@ -81,6 +77,89 @@ func TestDataAccess(t *testing.T) { ds := query.Result[0] + Convey(" updated ", func() { + cmd := &m.UpdateDataSourceCommand{ + Id: ds.Id, + OrgId: 10, + Name: "nisse", + Type: m.DS_GRAPHITE, + Access: m.DS_ACCESS_PROXY, + Url: "http://test", + Version: ds.Version, + } + + Convey("with same version as source", func() { + err := UpdateDataSource(cmd) + So(err, ShouldBeNil) + }) + + Convey("when someone else updated between read and update", func() { + query := m.GetDataSourcesQuery{OrgId: 10} + err = GetDataSources(&query) + So(err, ShouldBeNil) + + ds := query.Result[0] + intendedUpdate := &m.UpdateDataSourceCommand{ + Id: ds.Id, + OrgId: 10, + Name: "nisse", + Type: m.DS_GRAPHITE, + Access: m.DS_ACCESS_PROXY, + Url: "http://test", + Version: ds.Version, + } + + updateFromOtherUser := &m.UpdateDataSourceCommand{ + Id: ds.Id, + OrgId: 10, + Name: "nisse", + Type: m.DS_GRAPHITE, + Access: m.DS_ACCESS_PROXY, + Url: "http://test", + Version: ds.Version, + } + + err := UpdateDataSource(updateFromOtherUser) + So(err, ShouldBeNil) + + err = UpdateDataSource(intendedUpdate) + So(err, ShouldNotBeNil) + }) + + Convey("updating datasource without version", func() { + cmd := &m.UpdateDataSourceCommand{ + Id: ds.Id, + OrgId: 10, + Name: "nisse", + Type: m.DS_GRAPHITE, + Access: m.DS_ACCESS_PROXY, + Url: "http://test", + } + + Convey("should not raise errors", func() { + err := UpdateDataSource(cmd) + So(err, ShouldBeNil) + }) + }) + + Convey("updating datasource without higher version", func() { + cmd := &m.UpdateDataSourceCommand{ + Id: ds.Id, + OrgId: 10, + Name: "nisse", + Type: m.DS_GRAPHITE, + Access: m.DS_ACCESS_PROXY, + Url: "http://test", + Version: 90000, + } + + Convey("should not raise errors", func() { + err := UpdateDataSource(cmd) + So(err, ShouldBeNil) + }) + }) + }) + Convey("Can delete datasource by id", func() { err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId}) So(err, ShouldBeNil) @@ -104,9 +183,6 @@ func TestDataAccess(t *testing.T) { GetDataSources(&query) So(len(query.Result), ShouldEqual, 1) }) - }) - }) - } diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go index c694b5b7cfb..cc8a7f05177 100644 --- a/pkg/services/sqlstore/migrations/datasource_mig.go +++ b/pkg/services/sqlstore/migrations/datasource_mig.go @@ -120,4 +120,10 @@ func addDataSourceMigration(mg *Migrator) { {Name: "json_data", Type: DB_Text, Nullable: true}, {Name: "secure_json_data", Type: DB_Text, Nullable: true}, })) + + const setVersionToOneWhereZero = `UPDATE data_source SET version = 1 WHERE version = 0` + mg.AddMigration("Update initial version to 1", new(RawSqlMigration). + Sqlite(setVersionToOneWhereZero). + Postgres(setVersionToOneWhereZero). + Mysql(setVersionToOneWhereZero)) } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index e2fc1f29063..f37499bd60f 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -158,10 +158,14 @@ func getEngine() (*xorm.Engine, error) { } else { engine.SetMaxOpenConns(DbCfg.MaxOpenConn) engine.SetMaxIdleConns(DbCfg.MaxIdleConn) - engine.SetLogger(&xorm.DiscardLogger{}) - // engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) - // engine.ShowSQL = true - // engine.ShowInfo = true + debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false) + if !debugSql { + engine.SetLogger(&xorm.DiscardLogger{}) + } else { + engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) + engine.ShowSQL(true) + engine.ShowExecTime(true) + } } return engine, nil } @@ -190,12 +194,12 @@ func LoadConfig() { DbCfg.Host = sec.Key("host").String() DbCfg.Name = sec.Key("name").String() DbCfg.User = sec.Key("user").String() - DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0) - DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0) if len(DbCfg.Pwd) == 0 { DbCfg.Pwd = sec.Key("password").String() } } + DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0) + DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0) if DbCfg.Type == "sqlite3" { UseSQLite3 = true diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 266b71ec14e..d5bdd010269 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -17,6 +17,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/metrics" @@ -24,6 +25,7 @@ import ( type CloudWatchExecutor struct { *models.DataSource + ec2Svc ec2iface.EC2API } type DatasourceInfo struct { @@ -267,7 +269,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 +292,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 } @@ -311,7 +317,8 @@ func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatch var value float64 for _, s := range append(query.Statistics, query.ExtendedStatistics...) { series := tsdb.TimeSeries{ - Tags: map[string]string{}, + Tags: map[string]string{}, + Points: make([]tsdb.TimePoint, 0), } for _, d := range query.Dimensions { series.Tags[*d.Name] = *d.Value diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 3f4f7bea9ef..b9d4d5b6a80 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -38,7 +38,7 @@ var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCa func init() { metricsMap = map[string][]string{ "AWS/ApiGateway": {"4XXError", "5XXError", "CacheHitCount", "CacheMissCount", "Count", "IntegrationLatency", "Latency"}, - "AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "HealthyHostCount", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "IPv6ProcessedBytes", "IPv6RequestCount", "NewConnectionCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"}, + "AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "HealthyHostCount", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "IPv6ProcessedBytes", "IPv6RequestCount", "NewConnectionCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "RequestCountPerTarget", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"}, "AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, "AWS/Billing": {"EstimatedCharges"}, "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, @@ -87,6 +87,7 @@ func init() { "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"}, + "AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"}, "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"}, "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"}, @@ -132,6 +133,7 @@ func init() { "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/NATGateway": {"NatGatewayId"}, + "AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier"}, "AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DbClusterIdentifier", "DatabaseClass", "EngineName", "Role"}, @@ -183,6 +185,18 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext) break case "ec2_instance_attribute": + region := parameters.Get("region").MustString() + dsInfo := e.getDsInfo(region) + cfg, err := e.getAwsConfig(dsInfo) + if err != nil { + return nil, errors.New("Failed to call ec2:DescribeInstances") + } + sess, err := session.NewSession(cfg) + if err != nil { + return nil, errors.New("Failed to call ec2:DescribeInstances") + } + e.ec2Svc = ec2.New(sess, cfg) + data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext) break } @@ -373,14 +387,16 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context, var filters []*ec2.Filter for k, v := range filterJson { - if vv, ok := v.([]string); ok { - var vvvv []*string + if vv, ok := v.([]interface{}); ok { + var vvvvv []*string for _, vvv := range vv { - vvvv = append(vvvv, &vvv) + if vvvv, ok := vvv.(string); ok { + vvvvv = append(vvvvv, &vvvv) + } } filters = append(filters, &ec2.Filter{ Name: aws.String(k), - Values: vvvv, + Values: vvvvv, }) } } @@ -467,24 +483,13 @@ func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace stri } func (e *CloudWatchExecutor) ec2DescribeInstances(region string, filters []*ec2.Filter, instanceIds []*string) (*ec2.DescribeInstancesOutput, error) { - dsInfo := e.getDsInfo(region) - cfg, err := e.getAwsConfig(dsInfo) - if err != nil { - return nil, errors.New("Failed to call ec2:DescribeInstances") - } - sess, err := session.NewSession(cfg) - if err != nil { - return nil, errors.New("Failed to call ec2:DescribeInstances") - } - svc := ec2.New(sess, cfg) - params := &ec2.DescribeInstancesInput{ Filters: filters, InstanceIds: instanceIds, } var resp ec2.DescribeInstancesOutput - err = svc.DescribeInstancesPages(params, + err := e.ec2Svc.DescribeInstancesPages(params, func(page *ec2.DescribeInstancesOutput, lastPage bool) bool { reservations, _ := awsutil.ValuesAtPath(page, "Reservations") for _, reservation := range reservations { diff --git a/pkg/tsdb/cloudwatch/metric_find_query_test.go b/pkg/tsdb/cloudwatch/metric_find_query_test.go index 238e815fac1..255b343a33a 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query_test.go +++ b/pkg/tsdb/cloudwatch/metric_find_query_test.go @@ -1,13 +1,28 @@ package cloudwatch import ( + "context" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" ) +type mockedEc2 struct { + ec2iface.EC2API + Resp ec2.DescribeInstancesOutput +} + +func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error { + fn(&m.Resp, true) + return nil +} + func TestCloudWatchMetrics(t *testing.T) { Convey("When calling getMetricsForCustomMetrics", t, func() { @@ -66,4 +81,37 @@ func TestCloudWatchMetrics(t *testing.T) { }) }) + Convey("When calling handleGetEc2InstanceAttribute", t, func() { + executor := &CloudWatchExecutor{ + ec2Svc: mockedEc2{Resp: ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + InstanceId: aws.String("i-12345678"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Environment"), + Value: aws.String("production"), + }, + }, + }, + }, + }, + }, + }}, + } + + json := simplejson.New() + json.Set("region", "us-east-1") + json.Set("attributeName", "InstanceId") + filters := make(map[string]interface{}) + filters["tag:Environment"] = []string{"production"} + json.Set("filters", filters) + result, _ := executor.handleGetEc2InstanceAttribute(context.Background(), json, &tsdb.TsdbQuery{}) + + Convey("Should equal production InstanceId", func() { + So(result[0].Text, ShouldEqual, "i-12345678") + }) + }) } 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 3ca31591cb0..288787589ce 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" ) @@ -71,19 +72,24 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, } return fmt.Sprintf("extract(epoch from %s) as \"time\"", args[0]), nil case "__timeFilter": + // dont use to_timestamp in this macro for redshift compatibility #9566 if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= to_timestamp(%d) AND %s <= to_timestamp(%d)", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil + return fmt.Sprintf("extract(epoch from %s) BETWEEN %d AND %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil case "__timeFrom": return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil 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", args[0], 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 f181780e6ea..ff268805259 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -30,7 +30,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, "WHERE time_column >= to_timestamp(18446744066914186738) AND time_column <= to_timestamp(18446744066914187038)") + So(sql, ShouldEqual, "WHERE extract(epoch from time_column) BETWEEN 18446744066914186738 AND 18446744066914187038") }) Convey("interpolate __timeFrom function", func() { @@ -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") + 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/pkg/util/encoding.go b/pkg/util/encoding.go index e87da9d3d55..0edb721e422 100644 --- a/pkg/util/encoding.go +++ b/pkg/util/encoding.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "encoding/hex" "errors" - "fmt" "hash" "strings" ) @@ -30,7 +29,7 @@ func GetRandomString(n int, alphabets ...byte) string { func EncodePassword(password string, salt string) string { newPasswd := PBKDF2([]byte(password), []byte(salt), 10000, 50, sha256.New) - return fmt.Sprintf("%x", newPasswd) + return hex.EncodeToString(newPasswd) } // Encode string to md5 hex value. diff --git a/pkg/util/encoding_test.go b/pkg/util/encoding_test.go index abcf5425826..6b0e9331467 100644 --- a/pkg/util/encoding_test.go +++ b/pkg/util/encoding_test.go @@ -7,7 +7,6 @@ import ( ) func TestEncoding(t *testing.T) { - Convey("When generating base64 header", t, func() { result := GetBasicAuthHeader("grafana", "1234") @@ -23,4 +22,8 @@ func TestEncoding(t *testing.T) { So(password, ShouldEqual, "1234") }) + Convey("When encoding password", t, func() { + encodedPassword := EncodePassword("iamgod", "pepper") + So(encodedPassword, ShouldEqual, "e59c568621e57756495a468f47c74e07c911b037084dd464bb2ed72410970dc849cabd71b48c394faf08a5405dae53741ce9") + }) } diff --git a/public/app/app.ts b/public/app/app.ts index 369dce5d16f..8e345c6abed 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -27,7 +27,7 @@ _.move = function (array, fromIndex, toIndex) { return array; }; -import {coreModule} from './core/core'; +import {coreModule, registerAngularDirectives} from './core/core'; export class GrafanaApp { registerFunctions: any; @@ -109,6 +109,9 @@ export class GrafanaApp { // makes it possible to add dynamic stuff this.useModule(coreModule); + // register react angular wrappers + registerAngularDirectives(); + var preBootRequires = [System.import('app/features/all')]; Promise.all(preBootRequires).then(() => { diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts new file mode 100644 index 00000000000..7acdc79d55a --- /dev/null +++ b/public/app/core/angular_wrappers.ts @@ -0,0 +1,8 @@ +import { react2AngularDirective } from 'app/core/utils/react2angular'; +import { PasswordStrength } from './components/PasswordStrength'; + +export function registerAngularDirectives() { + + react2AngularDirective('passwordStrength', PasswordStrength, ['password']); + +} diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx index 6765d2a51c8..1fede3e5cd5 100644 --- a/public/app/core/components/PasswordStrength.tsx +++ b/public/app/core/components/PasswordStrength.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import coreModule from '../core_module'; export interface IProps { password: string; @@ -33,7 +32,4 @@ export class PasswordStrength extends React.Component { } } -coreModule.directive('passwordStrength', function(reactDirective) { - return reactDirective(PasswordStrength, ['password']); -}); 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/ColorPalette.tsx b/public/app/core/components/colorpicker/ColorPalette.tsx index 47e0e244e0a..07b25a32046 100644 --- a/public/app/core/components/colorpicker/ColorPalette.tsx +++ b/public/app/core/components/colorpicker/ColorPalette.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import coreModule from 'app/core/core_module'; import { sortedColors } from 'app/core/utils/colors'; export interface IProps { @@ -7,7 +6,7 @@ export interface IProps { onColorSelect: (c: string) => void; } -export class GfColorPalette extends React.Component { +export class ColorPalette extends React.Component { paletteColors: string[]; constructor(props) { @@ -23,12 +22,15 @@ export class GfColorPalette extends React.Component { } render() { - const colorPaletteItems = this.paletteColors.map((paletteColor) => { + const colorPaletteItems = this.paletteColors.map(paletteColor => { const cssClass = paletteColor.toLowerCase() === this.props.color.toLowerCase() ? 'fa-circle-o' : 'fa-circle'; return ( -   + +   ); }); @@ -40,6 +42,3 @@ export class GfColorPalette extends React.Component { } } -coreModule.directive('gfColorPalette', function (reactDirective) { - return reactDirective(GfColorPalette, ['color', 'onColorSelect']); -}); diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx index baf3f87cf81..dbba75636d0 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/public/app/core/components/colorpicker/ColorPicker.tsx @@ -2,8 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import $ from 'jquery'; import Drop from 'tether-drop'; -import coreModule from 'app/core/core_module'; import { ColorPickerPopover } from './ColorPickerPopover'; +import { react2AngularDirective } from 'app/core/utils/react2angular'; export interface IProps { color: string; @@ -27,9 +27,7 @@ export class ColorPicker extends React.Component { } openColorPicker() { - const dropContent = ( - - ); + const dropContent = ; let dropContentElem = document.createElement('div'); ReactDOM.render(dropContent, dropContentElem); @@ -38,12 +36,12 @@ export class ColorPicker extends React.Component { target: this.pickerElem[0], content: dropContentElem, position: 'top center', - classes: 'drop-popover drop-popover--form', - openOn: 'hover', + classes: 'drop-popover', + openOn: 'click', hoverCloseDelay: 200, tetherOptions: { - constraints: [{ to: 'scrollParent', attachment: "none both" }] - } + constraints: [{ to: 'scrollParent', attachment: 'none both' }], + }, }); drop.on('close', this.closeColorPicker); @@ -68,17 +66,14 @@ export class ColorPicker extends React.Component { return (
-
-
+
); } } -coreModule.directive('colorPicker', function (reactDirective) { - return reactDirective(ColorPicker, [ - 'color', - ['onChange', { watchDepth: 'reference', wrapApply: true }] - ]); -}); +react2AngularDirective('colorPicker', ColorPicker, [ + 'color', + ['onChange', { watchDepth: 'reference', wrapApply: true }], +]); diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index 09b6b8ec2c2..360c3fdd5c4 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -1,9 +1,8 @@ import React from 'react'; import $ from 'jquery'; import tinycolor from 'tinycolor2'; -import coreModule from 'app/core/core_module'; -import { GfColorPalette } from './ColorPalette'; -import { GfSpectrumPicker } from './SpectrumPicker'; +import { ColorPalette } from './ColorPalette'; +import { SpectrumPicker } from './SpectrumPicker'; const DEFAULT_COLOR = '#000000'; @@ -57,10 +56,11 @@ export class ColorPickerPopover extends React.Component { let newColor = tinycolor(colorString); if (newColor.isValid()) { // Update only color state + let newColorString = newColor.toString(); this.setState({ - color: newColor.toString(), + color: newColorString, }); - this.props.onColorSelect(newColor); + this.props.onColorSelect(newColorString); } } @@ -83,12 +83,12 @@ export class ColorPickerPopover extends React.Component { render() { const paletteTab = (
- +
); const spectrumTab = (
- +
); const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab; @@ -115,7 +115,3 @@ export class ColorPickerPopover extends React.Component { ); } } - -coreModule.directive('gfColorPickerPopover', function (reactDirective) { - return reactDirective(ColorPickerPopover, ['color', 'onColorSelect']); -}); diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index 2ee2d7571b3..3b24b9a4661 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import coreModule from 'app/core/core_module'; -import {ColorPickerPopover} from './ColorPickerPopover'; +import { ColorPickerPopover } from './ColorPickerPopover'; +import { react2AngularDirective } from 'app/core/utils/react2angular'; export interface IProps { series: any; @@ -43,13 +43,11 @@ export class SeriesColorPicker extends React.Component { render() { return (
- {this.props.series && this.renderAxisSelection()} + {this.props.series.yaxis && this.renderAxisSelection()}
); } } -coreModule.directive('seriesColorPicker', function(reactDirective) { - return reactDirective(SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); -}); +react2AngularDirective('seriesColorPicker', SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx index 2ea0fc65ddf..eef04545308 100644 --- a/public/app/core/components/colorpicker/SpectrumPicker.tsx +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import coreModule from 'app/core/core_module'; import _ from 'lodash'; import $ from 'jquery'; import 'vendor/spectrum'; @@ -10,7 +9,7 @@ export interface IProps { onColorSelect: (c: string) => void; } -export class GfSpectrumPicker extends React.Component { +export class SpectrumPicker extends React.Component { elem: any; isMoving: boolean; @@ -71,6 +70,3 @@ export class GfSpectrumPicker extends React.Component { } } -coreModule.directive('gfSpectrumPicker', function (reactDirective) { - return reactDirective(GfSpectrumPicker, ['color', 'options', 'onColorSelect']); -}); diff --git a/public/app/core/components/colorpicker/spectrum_picker.ts b/public/app/core/components/colorpicker/spectrum_picker.ts index c262eaac326..183cebffe2b 100644 --- a/public/app/core/components/colorpicker/spectrum_picker.ts +++ b/public/app/core/components/colorpicker/spectrum_picker.ts @@ -5,6 +5,7 @@ */ import coreModule from '../../core_module'; +/** @ngInject */ export function spectrumPicker() { return { restrict: 'E', diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index b1cbd9b6ced..331a7381523 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -159,6 +159,8 @@ export class FormDropdownCtrl { } updateValue(text) { + text = _.unescape(text); + if (text === '' || this.text === text) { return; } diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index b2071723677..98f806cd63e 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -1,5 +1,3 @@ -/// - import store from 'app/core/store'; import coreModule from 'app/core/core_module'; diff --git a/public/app/core/config.js b/public/app/core/config.js deleted file mode 100644 index 410d47f7b9a..00000000000 --- a/public/app/core/config.js +++ /dev/null @@ -1,13 +0,0 @@ -define([ - 'app/core/settings', -], -function (Settings) { - "use strict"; - - var bootData = window.grafanaBootData || { settings: {} }; - var options = bootData.settings; - options.bootData = bootData; - - return new Settings(options); - -}); diff --git a/public/app/core/config.ts b/public/app/core/config.ts new file mode 100644 index 00000000000..f4898b4e08b --- /dev/null +++ b/public/app/core/config.ts @@ -0,0 +1,38 @@ +import _ from 'lodash'; + +class Settings { + datasources: any; + panels: any; + appSubUrl: string; + window_title_prefix: string; + buildInfo: any; + new_panel_title: string; + bootData: any; + externalUserMngLinkUrl: string; + externalUserMngLinkName: string; + externalUserMngInfo: string; + allowOrgCreate: boolean; + disableLoginForm: boolean; + defaultDatasource: string; + alertingEnabled: boolean; + + constructor(options) { + var defaults = { + datasources: {}, + window_title_prefix: 'Grafana - ', + panels: {}, + new_panel_title: 'Panel Title', + playlist_timespan: "1m", + unsaved_changes_warning: true, + appSubUrl: "" + }; + _.extend(this, defaults, options); + } +} + +var bootData = (window).grafanaBootData || { settings: {} }; +var options = bootData.settings; +options.bootData = bootData; + +const config = new Settings(options); +export default config; diff --git a/public/app/core/controllers/inspect_ctrl.js b/public/app/core/controllers/inspect_ctrl.ts similarity index 65% rename from public/app/core/controllers/inspect_ctrl.js rename to public/app/core/controllers/inspect_ctrl.ts index 39d550658f1..70516986099 100644 --- a/public/app/core/controllers/inspect_ctrl.js +++ b/public/app/core/controllers/inspect_ctrl.ts @@ -1,27 +1,14 @@ -define([ - 'angular', - 'lodash', - 'jquery', - '../core_module', -], -function (angular, _, $, coreModule) { - 'use strict'; +import angular from 'angular'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from '../core_module'; - coreModule.default.controller('InspectCtrl', function($scope, $sanitize) { +export class InspectCtrl { + + /** @ngInject */ + constructor($scope, $sanitize) { var model = $scope.inspector; - function getParametersFromQueryString(queryString) { - var result = []; - var parameters = queryString.split("&"); - for (var i = 0; i < parameters.length; i++) { - var keyValue = parameters[i].split("="); - if (keyValue[1].length > 0) { - result.push({ key: keyValue[0], value: window.unescape(keyValue[1]) }); - } - } - return result; - } - $scope.init = function () { $scope.editor = { index: 0 }; @@ -57,7 +44,7 @@ function (angular, _, $, coreModule) { $scope.editor.index = 2; if (_.isString(model.error.config.data)) { - $scope.request_parameters = getParametersFromQueryString(model.error.config.data); + $scope.request_parameters = this.getParametersFromQueryString(model.error.config.data); } else { $scope.request_parameters = _.map(model.error.config.data, function(value, key) { return {key: key, value: angular.toJson(value, true)}; @@ -65,7 +52,18 @@ function (angular, _, $, coreModule) { } } }; + } + getParametersFromQueryString(queryString) { + var result = []; + var parameters = queryString.split("&"); + for (var i = 0; i < parameters.length; i++) { + var keyValue = parameters[i].split("="); + if (keyValue[1].length > 0) { + result.push({ key: keyValue[0], value: (window).unescape(keyValue[1]) }); + } + } + return result; + } +} - }); - -}); +coreModule.controller('InspectCtrl', InspectCtrl); diff --git a/public/app/core/controllers/invited_ctrl.js b/public/app/core/controllers/invited_ctrl.js index 298fede53d8..dfcc198f3aa 100644 --- a/public/app/core/controllers/invited_ctrl.js +++ b/public/app/core/controllers/invited_ctrl.js @@ -6,6 +6,8 @@ define([ function (angular, coreModule, config) { 'use strict'; + config = config.default; + coreModule.default.controller('InvitedCtrl', function($scope, $routeParams, contextSrv, backendSrv) { contextSrv.sidemenu = false; $scope.formModel = {}; diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js index 54323711604..5f02811a143 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.js @@ -7,6 +7,8 @@ define([ function (angular, _, coreModule, config) { 'use strict'; + config = config.default; + coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) { $scope.formModel = { user: '', diff --git a/public/app/core/core.ts b/public/app/core/core.ts index e28f8d2d7eb..fb92fd81d19 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"; @@ -43,11 +42,12 @@ import {assignModelProperties} from './utils/model_utils'; import {contextSrv} from './services/context_srv'; import {KeybindingSrv} from './services/keybindingSrv'; import {helpModal} from './components/help/help'; -import {PasswordStrength} from './components/PasswordStrength'; import {JsonExplorer} from './components/json_explorer/json_explorer'; import {NavModelSrv, NavModel} from './nav_model_srv'; +import {registerAngularDirectives} from './angular_wrappers'; export { + registerAngularDirectives, arrayJoin, coreModule, grafanaAppDirective, @@ -71,5 +71,4 @@ export { JsonExplorer, NavModelSrv, NavModel, - PasswordStrength, }; 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/dash_class.js b/public/app/core/directives/dash_class.js index 7ac13a552ec..08f4d3c7326 100644 --- a/public/app/core/directives/dash_class.js +++ b/public/app/core/directives/dash_class.js @@ -31,8 +31,8 @@ function (_, $, coreModule) { } }); - $scope.$watch('playlistSrv', function(newValue) { - elem.toggleClass('playlist-active', _.isObject(newValue)); + $scope.$watch('playlistSrv.isPlaying', function(newValue) { + elem.toggleClass('playlist-active', newValue === true); }); } }; diff --git a/public/app/core/directives/diff-view.ts b/public/app/core/directives/diff-view.ts index b82b23951b7..db76681498c 100644 --- a/public/app/core/directives/diff-view.ts +++ b/public/app/core/directives/diff-view.ts @@ -7,14 +7,15 @@ export class DeltaCtrl { observer: any; /** @ngInject */ - constructor($rootScope) { - const waitForCompile = function(mutations) { + constructor(private $rootScope) { + + const waitForCompile = (mutations) => { if (mutations.length === 1) { this.$rootScope.appEvent('json-diff-ready'); } }; - this.observer = new MutationObserver(waitForCompile.bind(this)); + this.observer = new MutationObserver(waitForCompile); const observerConfig = { attributes: true, diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 60dc61ef7f2..da21a5b3c45 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -39,6 +39,8 @@ function (_, $, coreModule) { return; } + value = _.unescape(value); + $scope.$apply(function() { var selected = _.find($scope.altSegments, {value: value}); if (selected) { diff --git a/public/app/core/directives/misc.js b/public/app/core/directives/misc.js index c43d1dd614c..f8c18ca847c 100644 --- a/public/app/core/directives/misc.js +++ b/public/app/core/directives/misc.js @@ -7,6 +7,8 @@ define([ function (angular, require, coreModule, kbn) { 'use strict'; + kbn = kbn.default; + coreModule.default.directive('tip', function($compile) { return { restrict: 'E', 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 09c87f5fabe..383d04e3961 100644 --- a/public/app/core/directives/ng_model_on_blur.js +++ b/public/app/core/directives/ng_model_on_blur.ts @@ -1,12 +1,8 @@ -define([ - '../core_module', - 'app/core/utils/kbn', - 'app/core/utils/rangeutil', -], -function (coreModule, kbn, 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, @@ -24,22 +20,27 @@ function (coreModule, kbn, 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) { @@ -55,5 +56,9 @@ function (coreModule, kbn, rangeUtil) { }; } }; - }); -}); + } +} + +coreModule.directive('ngModelOnblur', NgModelOnBlur); +coreModule.directive('emptyToNull', EmptyToNull); +coreModule.directive('validTimeSpan', ValidTimeSpan); diff --git a/public/app/core/routes/dashboard_loaders.js b/public/app/core/routes/dashboard_loaders.ts similarity index 69% rename from public/app/core/routes/dashboard_loaders.js rename to public/app/core/routes/dashboard_loaders.ts index 728b7e0092f..14e093d5169 100644 --- a/public/app/core/routes/dashboard_loaders.js +++ b/public/app/core/routes/dashboard_loaders.ts @@ -1,10 +1,9 @@ -define([ - '../core_module', -], -function (coreModule) { - "use strict"; +import coreModule from '../core_module'; - coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) { +export class LoadDashboardCtrl { + + /** @ngInject */ + constructor($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) { $scope.appEvent("dashboard-fetch-start"); if (!$routeParams.slug) { @@ -23,10 +22,13 @@ function (coreModule) { dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { $scope.initDashboard(result, $scope); }); + } +} - }); +export class NewDashboardCtrl { - coreModule.default.controller('NewDashboardCtrl', function($scope) { + /** @ngInject */ + constructor($scope) { $scope.initDashboard({ meta: { canStar: false, canShare: false, isNew: true }, dashboard: { @@ -35,12 +37,14 @@ function (coreModule) { { title: 'Dashboard Row', height: '250px', - panels:[], + panels: [], isNew: true, } ] }, }, $scope); - }); + } +} -}); +coreModule.controller('LoadDashboardCtrl', LoadDashboardCtrl); +coreModule.controller('NewDashboardCtrl', NewDashboardCtrl); 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/analytics.js b/public/app/core/services/analytics.js deleted file mode 100644 index a20a11bb2b6..00000000000 --- a/public/app/core/services/analytics.js +++ /dev/null @@ -1,39 +0,0 @@ -define([ - 'angular', - 'jquery', - 'app/core/core_module', - 'app/core/config', -], -function(angular, $, coreModule, config) { - 'use strict'; - - coreModule.default.service('googleAnalyticsSrv', function($rootScope, $location) { - - function gaInit() { - $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut - var ga = window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments); }; ga.l = +new Date; - ga('create', config.googleAnalyticsId, 'auto'); - return ga; - } - - this.init = function() { - - $rootScope.$on('$viewContentLoaded', function() { - var track = { page: $location.url() }; - - var ga = window.ga || gaInit(); - - ga('set', track); - ga('send', 'pageview'); - }); - - }; - - }).run(function(googleAnalyticsSrv) { - - if (config.googleAnalyticsId) { - googleAnalyticsSrv.init(); - } - - }); -}); diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts new file mode 100644 index 00000000000..87e84efa706 --- /dev/null +++ b/public/app/core/services/analytics.ts @@ -0,0 +1,36 @@ +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; + +export class Analytics { + + /** @ngInject */ + constructor(private $rootScope, private $location) { + } + + gaInit() { + $.getScript('https://www.google-analytics.com/analytics.js'); // jQuery shortcut + var ga = (window).ga = (window).ga || function () { (ga.q = ga.q || []).push(arguments); }; ga.l = +new Date; + ga('create', (config).googleAnalyticsId, 'auto'); + return ga; + } + + init() { + this.$rootScope.$on('$viewContentLoaded', () => { + var track = { page: this.$location.url() }; + var ga = (window).ga || this.gaInit(); + ga('set', track); + ga('send', 'pageview'); + }); + } +} + +/** @ngInject */ +function startAnalytics(googleAnalyticsSrv) { + if ((config).googleAnalyticsId) { + googleAnalyticsSrv.init(); + } +} + +coreModule.service('googleAnalyticsSrv', Analytics).run(startAnalytics); + 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.js b/public/app/core/services/timer.js deleted file mode 100644 index 67ad02367c0..00000000000 --- a/public/app/core/services/timer.js +++ /dev/null @@ -1,33 +0,0 @@ -define([ - 'angular', - 'lodash', - '../core_module', -], -function (angular, _, coreModule) { - 'use strict'; - - coreModule.default.service('timer', function($timeout) { - // This service really just tracks a list of $timeout promises to give us a - // method for cancelling them all when we need to - - var timers = []; - - this.register = function(promise) { - timers.push(promise); - return promise; - }; - - this.cancel = function(promise) { - timers = _.without(timers,promise); - $timeout.cancel(promise); - }; - - this.cancelAll = function() { - _.each(timers, function(t) { - $timeout.cancel(t); - }); - timers = []; - }; - }); - -}); diff --git a/public/app/core/services/timer.ts b/public/app/core/services/timer.ts new file mode 100644 index 00000000000..6355105ee0e --- /dev/null +++ b/public/app/core/services/timer.ts @@ -0,0 +1,31 @@ +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; + +// This service really just tracks a list of $timeout promises to give us a +// method for cancelling them all when we need to +export class Timer { + timers = []; + + /** @ngInject */ + constructor(private $timeout) { + } + + register(promise) { + this.timers.push(promise); + return promise; + } + + cancel(promise) { + this.timers = _.without(this.timers, promise); + this.$timeout.cancel(promise); + } + + cancelAll() { + _.each(this.timers, t => { + this.$timeout.cancel(t); + }); + this.timers = []; + } +} + +coreModule.service('timer', Timer); diff --git a/public/app/core/settings.js b/public/app/core/settings.js deleted file mode 100644 index 4c0b84f8da5..00000000000 --- a/public/app/core/settings.js +++ /dev/null @@ -1,20 +0,0 @@ -define([ - 'lodash', -], -function (_) { - "use strict"; - - return function Settings (options) { - var defaults = { - datasources : {}, - window_title_prefix : 'Grafana - ', - panels : {}, - new_panel_title: 'Panel Title', - playlist_timespan: "1m", - unsaved_changes_warning: true, - appSubUrl: "" - }; - - return _.extend({}, defaults, options); - }; -}); diff --git a/public/app/core/specs/ColorPalette.jest.tsx b/public/app/core/specs/ColorPalette.jest.tsx new file mode 100644 index 00000000000..fb1124aa975 --- /dev/null +++ b/public/app/core/specs/ColorPalette.jest.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { ColorPalette } from '../components/colorpicker/ColorPalette'; + +describe('CollorPalette', () => { + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/specs/PasswordStrength_specs.tsx b/public/app/core/specs/PasswordStrength.jest.tsx similarity index 68% rename from public/app/core/specs/PasswordStrength_specs.tsx rename to public/app/core/specs/PasswordStrength.jest.tsx index 7a46d6b153f..a0a2df69029 100644 --- a/public/app/core/specs/PasswordStrength_specs.tsx +++ b/public/app/core/specs/PasswordStrength.jest.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import {describe, it, expect} from 'test/lib/common'; import {shallow} from 'enzyme'; import {PasswordStrength} from '../components/PasswordStrength'; @@ -8,17 +7,17 @@ describe('PasswordStrength', () => { it('should have class bad if length below 4', () => { const wrapper = shallow(); - expect(wrapper.find(".password-strength-bad")).to.have.length(1); + expect(wrapper.find(".password-strength-bad")).toHaveLength(1); }); it('should have class ok if length below 8', () => { const wrapper = shallow(); - expect(wrapper.find(".password-strength-ok")).to.have.length(1); + expect(wrapper.find(".password-strength-ok")).toHaveLength(1); }); it('should have class good if length above 8', () => { const wrapper = shallow(); - expect(wrapper.find(".password-strength-good")).to.have.length(1); + expect(wrapper.find(".password-strength-good")).toHaveLength(1); }); }); diff --git a/public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap b/public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap new file mode 100644 index 00000000000..38611cab7cc --- /dev/null +++ b/public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap @@ -0,0 +1,628 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollorPalette renders correctly 1`] = ` +
+

+ +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + + +   + +

+
+`; diff --git a/public/test/core/utils/datemath_specs.ts b/public/app/core/specs/datemath.jest.ts similarity index 68% rename from public/test/core/utils/datemath_specs.ts rename to public/app/core/specs/datemath.jest.ts index f00befa5457..e2bdebcebce 100644 --- a/public/test/core/utils/datemath_specs.ts +++ b/public/app/core/specs/datemath.jest.ts @@ -1,4 +1,4 @@ -import {describe, beforeEach, afterEach, it, sinon, expect} from 'test/lib/common'; +import sinon from 'sinon'; import * as dateMath from 'app/core/utils/datemath'; import moment from 'moment'; @@ -13,25 +13,25 @@ describe("DateMath", () => { describe('errors', () => { it('should return undefined if passed something falsy', () => { - expect(dateMath.parse(false)).to.be(undefined); + expect(dateMath.parse(false)).toBe(undefined); }); it('should return undefined if I pass an operator besides [+-/]', () => { - expect(dateMath.parse('now&1d')).to.be(undefined); + expect(dateMath.parse('now&1d')).toBe(undefined); }); it('should return undefined if I pass a unit besides' + spans.toString(), () => { - expect(dateMath.parse('now+5f')).to.be(undefined); + expect(dateMath.parse('now+5f')).toBe(undefined); }); it('should return undefined if rounding unit is not 1', () => { - expect(dateMath.parse('now/2y')).to.be(undefined); - expect(dateMath.parse('now/0.5y')).to.be(undefined); + expect(dateMath.parse('now/2y')).toBe(undefined); + expect(dateMath.parse('now/0.5y')).toBe(undefined); }); it('should not go into an infinite loop when missing a unit', () => { - expect(dateMath.parse('now-0')).to.be(undefined); - expect(dateMath.parse('now-00')).to.be(undefined); + expect(dateMath.parse('now-0')).toBe(undefined); + expect(dateMath.parse('now-00')).toBe(undefined); }); }); @@ -43,15 +43,15 @@ describe("DateMath", () => { expected.setMilliseconds(0); var startOfDay = dateMath.parse('now/d', false).valueOf(); - expect(startOfDay).to.be(expected.getTime()); + expect(startOfDay).toBe(expected.getTime()); }); it("now/d on a utc dashboard should be start of the current day in UTC time", () => { var today = new Date(); - var expected = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0)); + var expected = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate(), 0, 0, 0, 0)); var startOfDay = dateMath.parse('now/d', false, 'utc').valueOf(); - expect(startOfDay).to.be(expected.getTime()); + expect(startOfDay).toBe(expected.getTime()); }); describe('subtraction', () => { @@ -69,11 +69,11 @@ describe("DateMath", () => { var thenEx = anchor + '||-5' + span; it('should return 5' + span + ' ago', () => { - expect(dateMath.parse(nowEx).format(format)).to.eql(now.subtract(5, span).format(format)); + expect(dateMath.parse(nowEx).format(format)).toEqual(now.subtract(5, span).format(format)); }); it('should return 5' + span + ' before ' + anchor, () => { - expect(dateMath.parse(thenEx).format(format)).to.eql(anchored.subtract(5, span).format(format)); + expect(dateMath.parse(thenEx).format(format)).toEqual(anchored.subtract(5, span).format(format)); }); }); @@ -94,11 +94,11 @@ describe("DateMath", () => { _.each(spans, (span) => { it('should round now to the beginning of the ' + span, function () { - expect(dateMath.parse('now/' + span).format(format)).to.eql(now.startOf(span).format(format)); + expect(dateMath.parse('now/' + span).format(format)).toEqual(now.startOf(span).format(format)); }); it('should round now to the end of the ' + span, function () { - expect(dateMath.parse('now/' + span, true).format(format)).to.eql(now.endOf(span).format(format)); + expect(dateMath.parse('now/' + span, true).format(format)).toEqual(now.endOf(span).format(format)); }); }); @@ -109,27 +109,27 @@ describe("DateMath", () => { describe('isValid', () => { it('should return false when invalid date text', () => { - expect(dateMath.isValid('asd')).to.be(false); + expect(dateMath.isValid('asd')).toBe(false); }); it('should return true when valid date text', () => { - expect(dateMath.isValid('now-1h')).to.be(true); + expect(dateMath.isValid('now-1h')).toBe(true); }); }); describe('relative time to date parsing', function() { it('should handle negative time', function() { var date = dateMath.parseDateMath('-2d', moment([2014, 1, 5])); - expect(date.valueOf()).to.equal(moment([2014, 1, 3]).valueOf()); + expect(date.valueOf()).toEqual(moment([2014, 1, 3]).valueOf()); }); it('should handle multiple math expressions', function() { var date = dateMath.parseDateMath('-2d-6h', moment([2014, 1, 5])); - expect(date.valueOf()).to.equal(moment([2014, 1, 2, 18]).valueOf()); + expect(date.valueOf()).toEqual(moment([2014, 1, 2, 18]).valueOf()); }); it('should return false when invalid expression', function() { var date = dateMath.parseDateMath('2', moment([2014, 1, 5])); - expect(date).to.equal(undefined); + expect(date).toEqual(undefined); }); }); diff --git a/public/test/core/utils/emitter_specs.ts b/public/app/core/specs/emitter.jest.ts similarity index 79% rename from public/test/core/utils/emitter_specs.ts rename to public/app/core/specs/emitter.jest.ts index 39559fdc32d..1de59852fcc 100644 --- a/public/test/core/utils/emitter_specs.ts +++ b/public/app/core/specs/emitter.jest.ts @@ -1,6 +1,4 @@ -import {describe, it, expect} from 'test/lib/common'; - -import {Emitter} from 'app/core/core'; +import {Emitter} from '../utils/emitter'; describe("Emitter", () => { @@ -20,8 +18,8 @@ describe("Emitter", () => { events.emit('test', null); - expect(sub1Called).to.be(true); - expect(sub2Called).to.be(true); + expect(sub1Called).toBe(true); + expect(sub2Called).toBe(true); }); it('when subscribing twice', () => { @@ -37,7 +35,7 @@ describe("Emitter", () => { events.emit('test', null); - expect(sub1Called).to.be(2); + expect(sub1Called).toBe(2); }); it('should handle errors', () => { @@ -57,8 +55,8 @@ describe("Emitter", () => { try { events.emit('test', null); } catch (_) { } try { events.emit('test', null); } catch (_) {} - expect(sub1Called).to.be(2); - expect(sub2Called).to.be(0); + expect(sub1Called).toBe(2); + expect(sub2Called).toBe(0); }); }); }); diff --git a/public/test/core/utils/flatten_specs.ts b/public/app/core/specs/flatten.jest.ts similarity index 57% rename from public/test/core/utils/flatten_specs.ts rename to public/app/core/specs/flatten.jest.ts index 5dc6c1535af..7fe07128d1e 100644 --- a/public/test/core/utils/flatten_specs.ts +++ b/public/app/core/specs/flatten.jest.ts @@ -1,5 +1,3 @@ -import {describe, it, expect} from 'test/lib/common'; - import flatten from 'app/core/utils/flatten'; describe("flatten", () => { @@ -15,9 +13,9 @@ describe("flatten", () => { } }, null); - expect(flattened['level1']).to.be('level1-value'); - expect(flattened['deeper.level2']).to.be('level2-value'); - expect(flattened['deeper.deeper.level3']).to.be('level3-value'); + expect(flattened['level1']).toBe('level1-value'); + expect(flattened['deeper.level2']).toBe('level2-value'); + expect(flattened['deeper.deeper.level3']).toBe('level3-value'); }); }); diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.jest.ts new file mode 100644 index 00000000000..9f50a05d5ad --- /dev/null +++ b/public/app/core/specs/kbn.jest.ts @@ -0,0 +1,347 @@ +import kbn from '../utils/kbn'; +import * as dateMath from '../utils/datemath'; +import moment from 'moment'; + +describe('unit format menu', function() { + var menu = kbn.getUnitFormats(); + menu.map(function(submenu) { + + describe('submenu ' + submenu.text, function() { + + it('should have a title', function() { + expect(typeof submenu.text).toBe('string'); + }); + + it('should have a submenu', function() { + expect(Array.isArray(submenu.submenu)).toBe(true); + }); + + submenu.submenu.map(function(entry) { + describe('entry ' + entry.text, function() { + it('should have a title', function() { expect(typeof entry.text).toBe('string'); }); + it('should have a format', function() { expect(typeof entry.value).toBe('string'); }); + it('should have a valid format', function() { + expect(typeof kbn.valueFormats[entry.value]).toBe('function'); + }); + }); + }); + }); + }); +}); + +function describeValueFormat(desc, value, tickSize, tickDecimals, result) { + + describe('value format: ' + desc, function() { + it('should translate ' + value + ' as ' + result, function() { + var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10); + var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals); + expect(str).toBe(result); + }); + }); + +} + +describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms'); +describeValueFormat('ms', 100, 1, 0, '100 ms'); +describeValueFormat('ms', 1250, 10, 0, '1.25 s'); +describeValueFormat('ms', 1250, 300, 0, '1.3 s'); +describeValueFormat('ms', 65150, 10000, 0, '1.1 min'); +describeValueFormat('ms', 6515000, 1500000, 0, '1.8 hour'); +describeValueFormat('ms', 651500000, 150000000, 0, '8 day'); + +describeValueFormat('none', 2.75e-10, 0, 10, '3e-10'); +describeValueFormat('none', 0, 0, 2, '0'); +describeValueFormat('dB', 10, 1000, 2, '10.00 dB'); + +describeValueFormat('percent', 0, 0, 0, '0%'); +describeValueFormat('percent', 53, 0, 1, '53.0%'); +describeValueFormat('percentunit', 0.0, 0, 0, '0%'); +describeValueFormat('percentunit', 0.278, 0, 1, '27.8%'); +describeValueFormat('percentunit', 1.0, 0, 0, '100%'); + +describeValueFormat('currencyUSD', 7.42, 10000, 2, '$7.42'); +describeValueFormat('currencyUSD', 1532.82, 1000, 1, '$1.53K'); +describeValueFormat('currencyUSD', 18520408.7, 10000000, 0, '$19M'); + +describeValueFormat('bytes', -1.57e+308, -1.57e+308, 2, 'NA'); + +describeValueFormat('ns', 25, 1, 0, '25 ns'); +describeValueFormat('ns', 2558, 50, 0, '2.56 µs'); + +describeValueFormat('ops', 123, 1, 0, '123 ops'); +describeValueFormat('rps', 456000, 1000, -1, '456K rps'); +describeValueFormat('rps', 123456789, 1000000, 2, '123.457M rps'); +describeValueFormat('wps', 789000000, 1000000, -1, '789M wps'); +describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops'); + +describeValueFormat('s', 1.23456789e-7, 1e-10, 8, '123.5 ns'); +describeValueFormat('s', 1.23456789e-4, 1e-7, 5, '123.5 µs'); +describeValueFormat('s', 1.23456789e-3, 1e-6, 4, '1.235 ms'); +describeValueFormat('s', 1.23456789e-2, 1e-5, 3, '12.35 ms'); +describeValueFormat('s', 1.23456789e-1, 1e-4, 2, '123.5 ms'); +describeValueFormat('s', 24, 1, 0, '24 s'); +describeValueFormat('s', 246, 1, 0, '4.1 min'); +describeValueFormat('s', 24567, 100, 0, '6.82 hour'); +describeValueFormat('s', 24567890, 10000, 0, '40.62 week'); +describeValueFormat('s', 24567890000, 1000000, 0, '778.53 year'); + +describeValueFormat('m', 24, 1, 0, '24 min'); +describeValueFormat('m', 246, 10, 0, '4.1 hour'); +describeValueFormat('m', 6545, 10, 0, '4.55 day'); +describeValueFormat('m', 24567, 100, 0, '2.44 week'); +describeValueFormat('m', 24567892, 10000, 0, '46.7 year'); + +describeValueFormat('h', 21, 1, 0, '21 hour'); +describeValueFormat('h', 145, 1, 0, '6.04 day'); +describeValueFormat('h', 1234, 100, 0, '7.3 week'); +describeValueFormat('h', 9458, 1000, 0, '1.08 year'); + +describeValueFormat('d', 3, 1, 0, '3 day'); +describeValueFormat('d', 245, 100, 0, '35 week'); +describeValueFormat('d', 2456, 10, 0, '6.73 year'); + +describe('date time formats', function() { + it('should format as iso date', function() { + var str = kbn.valueFormats.dateTimeAsIso(1505634997920, 1); + expect(str).toBe(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss')); + }); + + it('should format as iso date and skip date when today', function() { + var now = moment(); + var str = kbn.valueFormats.dateTimeAsIso(now.valueOf(), 1); + expect(str).toBe(now.format("HH:mm:ss")); + }); + + it('should format as US date', function() { + var str = kbn.valueFormats.dateTimeAsUS(1505634997920, 1); + expect(str).toBe(moment(1505634997920).format('MM/DD/YYYY h:mm:ss a')); + }); + + it('should format as US date and skip date when today', function() { + var now = moment(); + var str = kbn.valueFormats.dateTimeAsUS(now.valueOf(), 1); + expect(str).toBe(now.format("h:mm:ss a")); + }); + + it('should format as from now with days', function() { + var daysAgo = moment().add(-7, 'd'); + var str = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), 1); + expect(str).toBe('7 days ago'); + }); + + it('should format as from now with minutes', function() { + var daysAgo = moment().add(-2, 'm'); + var str = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), 1); + expect(str).toBe('2 minutes ago'); + }); +}); + +describe('kbn.toFixed and negative decimals', function() { + it('should treat as zero decimals', function() { + var str = kbn.toFixed(186.123, -2); + expect(str).toBe('186'); + }); +}); + +describe('kbn ms format when scaled decimals is null do not use it', function() { + it('should use specified decimals', function() { + var str = kbn.valueFormats['ms'](10000086.123, 1, null); + expect(str).toBe('2.8 hour'); + }); +}); + +describe('kbn kbytes format when scaled decimals is null do not use it', function() { + it('should use specified decimals', function() { + var str = kbn.valueFormats['kbytes'](10000000, 3, null); + expect(str).toBe('9.537 GiB'); + }); +}); + +describe('kbn deckbytes format when scaled decimals is null do not use it', function() { + it('should use specified decimals', function() { + var str = kbn.valueFormats['deckbytes'](10000000, 3, null); + expect(str).toBe('10.000 GB'); + }); +}); + +describe('kbn roundValue', function() { + it('should should handle null value', function() { + var str = kbn.roundValue(null, 2); + expect(str).toBe(null); + }); + it('should round value', function() { + var str = kbn.roundValue(200.877, 2); + expect(str).toBe(200.88); + }); +}); + +describe('calculateInterval', function() { + it('1h 100 resultion', function() { + var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') }; + var res = kbn.calculateInterval(range, 100, null); + expect(res.interval).toBe('30s'); + }); + + it('10m 1600 resolution', function() { + var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; + var res = kbn.calculateInterval(range, 1600, null); + expect(res.interval).toBe('500ms'); + expect(res.intervalMs).toBe(500); + }); + + it('fixed user min interval', function() { + var range = {from: dateMath.parse('now-10m'), to: dateMath.parse('now')}; + var res = kbn.calculateInterval(range, 1600, '10s'); + expect(res.interval).toBe('10s'); + expect(res.intervalMs).toBe(10000); + }); + + it('short time range and user low limit', function() { + var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; + var res = kbn.calculateInterval(range, 1600, '>10s'); + expect(res.interval).toBe('10s'); + }); + + it('large time range and user low limit', function() { + var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')}; + var res = kbn.calculateInterval(range, 1000, '>10s'); + expect(res.interval).toBe('20m'); + }); + + it('10s 900 resolution and user low limit in ms', function() { + var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') }; + var res = kbn.calculateInterval(range, 900, '>15ms'); + expect(res.interval).toBe('15ms'); + }); + + it('1d 1 resolution', function() { + var range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') }; + var res = kbn.calculateInterval(range, 1, null); + expect(res.interval).toBe('1d'); + expect(res.intervalMs).toBe(86400000); + }); + + it('86399s 1 resolution', function() { + var range = { from: dateMath.parse('now-86390s'), to: dateMath.parse('now') }; + var res = kbn.calculateInterval(range, 1, null); + expect(res.interval).toBe('12h'); + expect(res.intervalMs).toBe(43200000); + }); +}); + +describe('hex', function() { + it('positive integer', function() { + var str = kbn.valueFormats.hex(100, 0); + expect(str).toBe('64'); + }); + it('negative integer', function() { + var str = kbn.valueFormats.hex(-100, 0); + expect(str).toBe('-64'); + }); + it('null', function() { + var str = kbn.valueFormats.hex(null, 0); + expect(str).toBe(''); + }); + it('positive float', function() { + var str = kbn.valueFormats.hex(50.52, 1); + expect(str).toBe('32.8'); + }); + it('negative float', function() { + var str = kbn.valueFormats.hex(-50.333, 2); + expect(str).toBe('-32.547AE147AE14'); + }); +}); + +describe('hex 0x', function() { + it('positive integeter', function() { + var str = kbn.valueFormats.hex0x(7999,0); + expect(str).toBe('0x1F3F'); + }); + it('negative integer', function() { + var str = kbn.valueFormats.hex0x(-584,0); + expect(str).toBe('-0x248'); + }); + it('null', function() { + var str = kbn.valueFormats.hex0x(null, 0); + expect(str).toBe(''); + }); + it('positive float', function() { + var str = kbn.valueFormats.hex0x(74.443, 3); + expect(str).toBe('0x4A.716872B020C4'); + }); + it('negative float', function() { + var str = kbn.valueFormats.hex0x(-65.458, 1); + expect(str).toBe('-0x41.8'); + }); +}); + +describe('duration', function() { + it('null', function() { + var str = kbn.toDuration(null, 0, "millisecond"); + expect(str).toBe(''); + }); + it('0 milliseconds', function() { + var str = kbn.toDuration(0, 0, "millisecond"); + expect(str).toBe('0 milliseconds'); + }); + it('1 millisecond', function() { + var str = kbn.toDuration(1, 0, "millisecond"); + expect(str).toBe('1 millisecond'); + }); + it('-1 millisecond', function() { + var str = kbn.toDuration(-1, 0, "millisecond"); + expect(str).toBe('1 millisecond ago'); + }); + it('seconds', function() { + var str = kbn.toDuration(1, 0, "second"); + expect(str).toBe('1 second'); + }); + it('minutes', function() { + var str = kbn.toDuration(1, 0, "minute"); + expect(str).toBe('1 minute'); + }); + it('hours', function() { + var str = kbn.toDuration(1, 0, "hour"); + expect(str).toBe('1 hour'); + }); + it('days', function() { + var str = kbn.toDuration(1, 0, "day"); + expect(str).toBe('1 day'); + }); + it('weeks', function() { + var str = kbn.toDuration(1, 0, "week"); + expect(str).toBe('1 week'); + }); + it('months', function() { + var str = kbn.toDuration(1, 0, "month"); + expect(str).toBe('1 month'); + }); + it('years', function() { + var str = kbn.toDuration(1, 0, "year"); + expect(str).toBe('1 year'); + }); + it('decimal days', function() { + var str = kbn.toDuration(1.5, 2, "day"); + expect(str).toBe('1 day, 12 hours, 0 minutes'); + }); + it('decimal months', function() { + var str = kbn.toDuration(1.5, 3, "month"); + expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); + }); + it('no decimals', function() { + var str = kbn.toDuration(38898367008, 0, "millisecond"); + expect(str).toBe('1 year'); + }); + it('1 decimal', function() { + var str = kbn.toDuration(38898367008, 1, "millisecond"); + expect(str).toBe('1 year, 2 months'); + }); + it('too many decimals', function() { + var str = kbn.toDuration(38898367008, 20, "millisecond"); + expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); + }); + it('floating point error', function() { + var str = kbn.toDuration(36993906007, 8, "millisecond"); + expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); + }); +}); diff --git a/public/test/core/utils/rangeutil_specs.ts b/public/app/core/specs/rangeutil.jest.ts similarity index 70% rename from public/test/core/utils/rangeutil_specs.ts rename to public/app/core/specs/rangeutil.jest.ts index f2ce34f0edd..eb9dcfc3ace 100644 --- a/public/test/core/utils/rangeutil_specs.ts +++ b/public/app/core/specs/rangeutil.jest.ts @@ -1,5 +1,3 @@ -import {describe, it, expect} from 'test/lib/common'; - import * as rangeUtil from 'app/core/utils/rangeutil'; import _ from 'lodash'; import moment from 'moment'; @@ -9,100 +7,100 @@ describe("rangeUtil", () => { describe("Can get range grouped list of ranges", () => { it('when custom settings should return default range list', () => { var groups = rangeUtil.getRelativeTimesList({time_options: []}, 'Last 5 minutes'); - expect(_.keys(groups).length).to.be(4); - expect(groups[3][0].active).to.be(true); + expect(_.keys(groups).length).toBe(4); + expect(groups[3][0].active).toBe(true); }); }); describe("Can get range text described", () => { it('should handle simple old expression with only amount and unit', () => { var info = rangeUtil.describeTextRange('5m'); - expect(info.display).to.be('Last 5 minutes'); + expect(info.display).toBe('Last 5 minutes'); }); it('should have singular when amount is 1', () => { var info = rangeUtil.describeTextRange('1h'); - expect(info.display).to.be('Last 1 hour'); + expect(info.display).toBe('Last 1 hour'); }); it('should handle non default amount', () => { var info = rangeUtil.describeTextRange('13h'); - expect(info.display).to.be('Last 13 hours'); - expect(info.from).to.be('now-13h'); + expect(info.display).toBe('Last 13 hours'); + expect(info.from).toBe('now-13h'); }); it('should handle non default future amount', () => { var info = rangeUtil.describeTextRange('+3h'); - expect(info.display).to.be('Next 3 hours'); - expect(info.from).to.be('now'); - expect(info.to).to.be('now+3h'); + expect(info.display).toBe('Next 3 hours'); + expect(info.from).toBe('now'); + expect(info.to).toBe('now+3h'); }); it('should handle now/d', () => { var info = rangeUtil.describeTextRange('now/d'); - expect(info.display).to.be('Today so far'); + expect(info.display).toBe('Today so far'); }); it('should handle now/w', () => { var info = rangeUtil.describeTextRange('now/w'); - expect(info.display).to.be('This week so far'); + expect(info.display).toBe('This week so far'); }); it('should handle now/M', () => { var info = rangeUtil.describeTextRange('now/M'); - expect(info.display).to.be('This month so far'); + expect(info.display).toBe('This month so far'); }); it('should handle now/y', () => { var info = rangeUtil.describeTextRange('now/y'); - expect(info.display).to.be('This year so far'); + expect(info.display).toBe('This year so far'); }); }); describe("Can get date range described", () => { it('Date range with simple ranges', () => { var text = rangeUtil.describeTimeRange({from: 'now-1h', to: 'now'}); - expect(text).to.be('Last 1 hour'); + expect(text).toBe('Last 1 hour'); }); it('Date range with rounding ranges', () => { var text = rangeUtil.describeTimeRange({from: 'now/d+6h', to: 'now'}); - expect(text).to.be('now/d+6h to now'); + expect(text).toBe('now/d+6h to now'); }); it('Date range with absolute to now', () => { var text = rangeUtil.describeTimeRange({from: moment([2014,10,10,2,3,4]), to: 'now'}); - expect(text).to.be('Nov 10, 2014 02:03:04 to a few seconds ago'); + expect(text).toBe('Nov 10, 2014 02:03:04 to a few seconds ago'); }); it('Date range with absolute to relative', () => { var text = rangeUtil.describeTimeRange({from: moment([2014,10,10,2,3,4]), to: 'now-1d'}); - expect(text).to.be('Nov 10, 2014 02:03:04 to a day ago'); + expect(text).toBe('Nov 10, 2014 02:03:04 to a day ago'); }); it('Date range with relative to absolute', () => { var text = rangeUtil.describeTimeRange({from: 'now-7d', to: moment([2014,10,10,2,3,4])}); - expect(text).to.be('7 days ago to Nov 10, 2014 02:03:04'); + expect(text).toBe('7 days ago to Nov 10, 2014 02:03:04'); }); it('Date range with non matching default ranges', () => { var text = rangeUtil.describeTimeRange({from: 'now-13h', to: 'now'}); - expect(text).to.be('Last 13 hours'); + expect(text).toBe('Last 13 hours'); }); it('Date range with from and to both are in now-* format', () => { var text = rangeUtil.describeTimeRange({from: 'now-6h', to: 'now-3h'}); - expect(text).to.be('now-6h to now-3h'); + expect(text).toBe('now-6h to now-3h'); }); it('Date range with from and to both are either in now-* or now/* format', () => { var text = rangeUtil.describeTimeRange({from: 'now/d+6h', to: 'now-3h'}); - expect(text).to.be('now/d+6h to now-3h'); + expect(text).toBe('now/d+6h to now-3h'); }); it('Date range with from and to both are either in now-* or now+* format', () => { var text = rangeUtil.describeTimeRange({from: 'now-6h', to: 'now+1h'}); - expect(text).to.be('now-6h to now+1h'); + expect(text).toBe('now-6h to now+1h'); }); }); diff --git a/public/app/core/specs/store.jest.ts b/public/app/core/specs/store.jest.ts new file mode 100644 index 00000000000..44cb2a84959 --- /dev/null +++ b/public/app/core/specs/store.jest.ts @@ -0,0 +1,44 @@ +import store from '../store'; + +Object.assign(window, { + localStorage: { + removeItem(key) { + delete window.localStorage[key]; + } + } +}); + +describe('store', () => { + + it("should store", ()=> { + store.set("key1", "123"); + expect(store.get("key1")).toBe("123"); + }); + + it("get key when undefined", ()=> { + expect(store.get("key2")).toBe(undefined); + }); + + it("check if key exixts", ()=> { + store.set("key3", "123"); + expect(store.exists("key3")).toBe(true); + }); + + it("get boolean when no key", ()=> { + expect(store.getBool("key4", false)).toBe(false); + }); + + it("get boolean", ()=> { + store.set("key5", "true"); + expect(store.getBool("key5", false)).toBe(true); + }); + + it("key should be deleted", ()=> { + store.set("key6", "123"); + store.delete("key6"); + expect(store.exists("key6")).toBe(false); + }); + +}); + + diff --git a/public/test/core/table_model_specs.ts b/public/app/core/specs/table_model.jest.ts similarity index 65% rename from public/test/core/table_model_specs.ts rename to public/app/core/specs/table_model.jest.ts index 20a90498d50..05c9fbda039 100644 --- a/public/test/core/table_model_specs.ts +++ b/public/app/core/specs/table_model.jest.ts @@ -1,5 +1,3 @@ -import {describe, beforeEach, it, expect} from 'test/lib/common'; - import TableModel from 'app/core/table_model'; describe('when sorting table desc', () => { @@ -16,14 +14,14 @@ describe('when sorting table desc', () => { }); it('should sort by time', () => { - expect(table.rows[0][0]).to.be(105); - expect(table.rows[1][0]).to.be(103); - expect(table.rows[2][0]).to.be(100); + expect(table.rows[0][0]).toBe(105); + expect(table.rows[1][0]).toBe(103); + expect(table.rows[2][0]).toBe(100); }); it ('should mark column being sorted', () => { - expect(table.columns[0].sort).to.be(true); - expect(table.columns[0].desc).to.be(true); + expect(table.columns[0].sort).toBe(true); + expect(table.columns[0].desc).toBe(true); }); }); @@ -42,9 +40,9 @@ describe('when sorting table asc', () => { }); it('should sort by time', () => { - expect(table.rows[0][1]).to.be(10); - expect(table.rows[1][1]).to.be(11); - expect(table.rows[2][1]).to.be(15); + expect(table.rows[0][1]).toBe(10); + expect(table.rows[1][1]).toBe(11); + expect(table.rows[2][1]).toBe(15); }); }); diff --git a/public/app/core/specs/time_series_specs.ts b/public/app/core/specs/time_series.jest.ts similarity index 79% rename from public/app/core/specs/time_series_specs.ts rename to public/app/core/specs/time_series.jest.ts index ddf08a9f704..1a199daa59f 100644 --- a/public/app/core/specs/time_series_specs.ts +++ b/public/app/core/specs/time_series.jest.ts @@ -1,4 +1,3 @@ -import {describe, beforeEach, it, expect} from 'test/lib/common'; import TimeSeries from 'app/core/time_series2'; describe("TimeSeries", function() { @@ -19,14 +18,14 @@ describe("TimeSeries", function() { it('with connected style, should ignore nulls', function() { series = new TimeSeries(testData); points = series.getFlotPairs('connected', yAxisFormats); - expect(points.length).to.be(3); + expect(points.length).toBe(3); }); it('with null as zero style, should replace nulls with zero', function() { series = new TimeSeries(testData); points = series.getFlotPairs('null as zero', yAxisFormats); - expect(points.length).to.be(4); - expect(points[1][1]).to.be(0); + expect(points.length).toBe(4); + expect(points[1][1]).toBe(0); }); it('if last is null current should pick next to last', function() { @@ -34,7 +33,7 @@ describe("TimeSeries", function() { datapoints: [[10,1], [null, 2]] }); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.current).to.be(10); + expect(series.stats.current).toBe(10); }); it('max value should work for negative values', function() { @@ -42,13 +41,13 @@ describe("TimeSeries", function() { datapoints: [[-10,1], [-4, 2]] }); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.max).to.be(-4); + expect(series.stats.max).toBe(-4); }); it('average value should ignore nulls', function() { series = new TimeSeries(testData); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.avg).to.be(6.333333333333333); + expect(series.stats.avg).toBe(6.333333333333333); }); it('the delta value should account for nulls', function() { @@ -56,7 +55,7 @@ describe("TimeSeries", function() { datapoints: [[1,2],[3,3],[null,4],[10,5],[15,6]] }); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.delta).to.be(14); + expect(series.stats.delta).toBe(14); }); it('the delta value should account for nulls on first', function() { @@ -64,7 +63,7 @@ describe("TimeSeries", function() { datapoints: [[null,2],[1,3],[10,4],[15,5]] }); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.delta).to.be(14); + expect(series.stats.delta).toBe(14); }); it('the delta value should account for nulls on last', function() { @@ -72,7 +71,7 @@ describe("TimeSeries", function() { datapoints: [[1,2],[5,3],[10,4],[null,5]] }); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.delta).to.be(9); + expect(series.stats.delta).toBe(9); }); it('the delta value should account for resets', function() { @@ -80,7 +79,7 @@ describe("TimeSeries", function() { datapoints: [[1,2],[5,3],[10,4],[0,5],[10,6]] }); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.delta).to.be(19); + expect(series.stats.delta).toBe(19); }); it('the delta value should account for resets on last', function() { @@ -88,30 +87,30 @@ describe("TimeSeries", function() { datapoints: [[1,2],[2,3],[10,4],[8,5]] }); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.delta).to.be(17); + expect(series.stats.delta).toBe(17); }); it('the range value should be max - min', function() { series = new TimeSeries(testData); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.range).to.be(9); + expect(series.stats.range).toBe(9); }); it('first value should ingone nulls', function() { series = new TimeSeries(testData); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.first).to.be(1); + expect(series.stats.first).toBe(1); series = new TimeSeries({ datapoints: [[null,2],[1,3],[10,4],[8,5]] }); series.getFlotPairs('null', yAxisFormats); - expect(series.stats.first).to.be(1); + expect(series.stats.first).toBe(1); }); it('with null as zero style, average value should treat nulls as 0', function() { series = new TimeSeries(testData); series.getFlotPairs('null as zero', yAxisFormats); - expect(series.stats.avg).to.be(4.75); + expect(series.stats.avg).toBe(4.75); }); it('average value should be null if all values is null', function() { @@ -119,7 +118,7 @@ describe("TimeSeries", function() { datapoints: [[null,2],[null,3],[null,4],[null,5]] }); series.getFlotPairs('null'); - expect(series.stats.avg).to.be(null); + expect(series.stats.avg).toBe(null); }); }); @@ -130,7 +129,7 @@ describe("TimeSeries", function() { }); it('should set hasMsResolution to false', function() { - expect(series.hasMsResolution).to.be(false); + expect(series.hasMsResolution).toBe(false); }); }); @@ -140,7 +139,7 @@ describe("TimeSeries", function() { }); it('should show millisecond resolution tooltip', function() { - expect(series.hasMsResolution).to.be(true); + expect(series.hasMsResolution).toBe(true); }); }); @@ -150,7 +149,7 @@ describe("TimeSeries", function() { }); it('should not show millisecond resolution tooltip', function() { - expect(series.hasMsResolution).to.be(false); + expect(series.hasMsResolution).toBe(false); }); }); }); @@ -165,13 +164,13 @@ describe("TimeSeries", function() { it('missing datapoint with ms precision', function() { fakedata.datapoints[0] = [1337, 1234567890000]; series = new TimeSeries(fakedata); - expect(series.isMsResolutionNeeded()).to.be(false); + expect(series.isMsResolutionNeeded()).toBe(false); }); it('contains datapoint with ms precision', function() { fakedata.datapoints[0] = [1337, 1236547890001]; series = new TimeSeries(fakedata); - expect(series.isMsResolutionNeeded()).to.be(true); + expect(series.isMsResolutionNeeded()).toBe(true); }); }); @@ -188,8 +187,8 @@ describe("TimeSeries", function() { }); it('should set fill zero, and enable points', function() { - expect(series.lines.fill).to.be(0.001); - expect(series.points.show).to.be(true); + expect(series.lines.fill).toBe(0.001); + expect(series.points.show).toBe(true); }); }); @@ -200,8 +199,8 @@ describe("TimeSeries", function() { }); it('should disable lines, and enable bars', function() { - expect(series.lines.show).to.be(false); - expect(series.bars.show).to.be(true); + expect(series.lines.show).toBe(false); + expect(series.bars.show).toBe(true); }); }); @@ -212,8 +211,8 @@ describe("TimeSeries", function() { }); it('should disable stack, and set lineWidth', function() { - expect(series.stack).to.be(false); - expect(series.lines.lineWidth).to.be(5); + expect(series.stack).toBe(false); + expect(series.lines.lineWidth).toBe(5); }); }); @@ -224,9 +223,9 @@ describe("TimeSeries", function() { }); it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', function() { - expect(series.dashes.show).to.be(true); - expect(series.dashes.lineWidth).to.be(5); - expect(series.lines.lineWidth).to.be(0); + expect(series.dashes.show).toBe(true); + expect(series.dashes.lineWidth).toBe(5); + expect(series.lines.lineWidth).toBe(0); }); }); @@ -237,7 +236,7 @@ describe("TimeSeries", function() { }); it('should disable line fill and add fillBelowTo', function() { - expect(series.fillBelowTo).to.be('min'); + expect(series.fillBelowTo).toBe('min'); }); }); @@ -248,8 +247,8 @@ describe("TimeSeries", function() { }); it('should set pointradius, and set steppedLine', function() { - expect(series.points.radius).to.be(5); - expect(series.lines.steps).to.be(true); + expect(series.points.radius).toBe(5); + expect(series.lines.steps).toBe(true); }); }); @@ -260,7 +259,7 @@ describe("TimeSeries", function() { }); it('should match second series', function() { - expect(series.lines.show).to.be(false); + expect(series.lines.show).toBe(false); }); }); @@ -271,11 +270,11 @@ describe("TimeSeries", function() { }); it('should set yaxis', function() { - expect(series.yaxis).to.be(2); + expect(series.yaxis).toBe(2); }); it('should set zindex', function() { - expect(series.zindex).to.be(2); + expect(series.zindex).toBe(2); }); }); @@ -288,11 +287,11 @@ describe("TimeSeries", function() { }); it('should format non-numeric values as empty string', function() { - expect(series.formatValue(null)).to.be(""); - expect(series.formatValue(undefined)).to.be(""); - expect(series.formatValue(NaN)).to.be(""); - expect(series.formatValue(Infinity)).to.be(""); - expect(series.formatValue(-Infinity)).to.be(""); + expect(series.formatValue(null)).toBe(""); + expect(series.formatValue(undefined)).toBe(""); + expect(series.formatValue(NaN)).toBe(""); + expect(series.formatValue(Infinity)).toBe(""); + expect(series.formatValue(-Infinity)).toBe(""); }); }); diff --git a/public/app/core/store.js b/public/app/core/store.js deleted file mode 100644 index 504b0e5aff5..00000000000 --- a/public/app/core/store.js +++ /dev/null @@ -1,26 +0,0 @@ -define([], function() { - 'use strict'; - - return { - get: function(key) { - return window.localStorage[key]; - }, - set: function(key, value) { - window.localStorage[key] = value; - }, - getBool: function(key, def) { - if (def !== void 0 && !this.exists(key)) { - return def; - } - return window.localStorage[key] === 'true'; - }, - exists: function(key) { - return window.localStorage[key] !== void 0; - }, - delete: function(key) { - window.localStorage.removeItem(key); - } - - }; - -}); diff --git a/public/app/core/store.ts b/public/app/core/store.ts new file mode 100644 index 00000000000..cbed16510ab --- /dev/null +++ b/public/app/core/store.ts @@ -0,0 +1,29 @@ +export class Store { + + get(key) { + return window.localStorage[key]; + } + + set(key, value) { + window.localStorage[key] = value; + } + + getBool(key, def) { + if (def !== void 0 && !this.exists(key)) { + return def; + } + return window.localStorage[key] === 'true'; + } + + exists(key) { + return window.localStorage[key] !== void 0; + } + + delete(key) { + window.localStorage.removeItem(key); + } + +} + +const store = new Store(); +export default store; diff --git a/public/app/core/time_series.js b/public/app/core/time_series.js deleted file mode 100644 index 75feab32971..00000000000 --- a/public/app/core/time_series.js +++ /dev/null @@ -1,7 +0,0 @@ -define([ - './time_series2' -], function(timeSeries) { - 'use strict'; - // backward compatability hack; - return timeSeries.default; -}); diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index 1c4c94da6fb..0f3dcbc1171 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -1,5 +1,3 @@ -/// - import kbn from 'app/core/utils/kbn'; import _ from 'lodash'; @@ -23,6 +21,7 @@ export default class TimeSeries { id: string; label: string; alias: string; + aliasEscaped: string; color: string; valueFormater: any; stats: any; @@ -52,6 +51,7 @@ export default class TimeSeries { this.label = opts.alias; this.id = opts.alias; this.alias = opts.alias; + this.aliasEscaped = _.escape(opts.alias); this.color = opts.color; this.valueFormater = kbn.valueFormats.none; this.stats = {}; diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index a6070cb131c..03fcd147a3e 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -1,7 +1,6 @@ import _ from 'lodash'; import moment from 'moment'; - -declare var window: any; +import {saveAs} from 'file-saver'; const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; @@ -69,5 +68,5 @@ export function exportTableDataToCsv(table, excel = false) { export function saveSaveBlob(payload, fname) { var blob = new Blob([payload], { type: "text/csv;charset=utf-8" }); - window.saveAs(blob, fname); + saveAs(blob, fname); } diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js deleted file mode 100644 index 1daaa2f8ba3..00000000000 --- a/public/app/core/utils/kbn.js +++ /dev/null @@ -1,936 +0,0 @@ -define([ - 'jquery', - 'lodash', - 'moment' -], -function($, _, moment) { - 'use strict'; - - var kbn = {}; - kbn.valueFormats = {}; - - kbn.regexEscape = function(value) { - return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&'); - }; - - ///// HELPER FUNCTIONS ///// - - kbn.round_interval = function(interval) { - switch (true) { - // 0.015s - case (interval < 15): - return 10; // 0.01s - // 0.035s - case (interval < 35): - return 20; // 0.02s - // 0.075s - case (interval < 75): - return 50; // 0.05s - // 0.15s - case (interval < 150): - return 100; // 0.1s - // 0.35s - case (interval < 350): - return 200; // 0.2s - // 0.75s - case (interval < 750): - return 500; // 0.5s - // 1.5s - case (interval < 1500): - return 1000; // 1s - // 3.5s - case (interval < 3500): - return 2000; // 2s - // 7.5s - case (interval < 7500): - return 5000; // 5s - // 12.5s - case (interval < 12500): - return 10000; // 10s - // 17.5s - case (interval < 17500): - return 15000; // 15s - // 25s - case (interval < 25000): - return 20000; // 20s - // 45s - case (interval < 45000): - return 30000; // 30s - // 1.5m - case (interval < 90000): - return 60000; // 1m - // 3.5m - case (interval < 210000): - return 120000; // 2m - // 7.5m - case (interval < 450000): - return 300000; // 5m - // 12.5m - case (interval < 750000): - return 600000; // 10m - // 12.5m - case (interval < 1050000): - return 900000; // 15m - // 25m - case (interval < 1500000): - return 1200000; // 20m - // 45m - case (interval < 2700000): - return 1800000; // 30m - // 1.5h - case (interval < 5400000): - return 3600000; // 1h - // 2.5h - case (interval < 9000000): - return 7200000; // 2h - // 4.5h - case (interval < 16200000): - return 10800000; // 3h - // 9h - case (interval < 32400000): - return 21600000; // 6h - // 1d - case (interval < 86400000): - return 43200000; // 12h - // 1w - case (interval < 604800000): - return 86400000; // 1d - // 3w - case (interval < 1814400000): - return 604800000; // 1w - // 6w - case (interval < 3628800000): - return 2592000000; // 30d - default: - return 31536000000; // 1y - } - }; - - kbn.secondsToHms = function(seconds) { - var numyears = Math.floor(seconds / 31536000); - if(numyears){ - return numyears + 'y'; - } - var numdays = Math.floor((seconds % 31536000) / 86400); - if(numdays){ - return numdays + 'd'; - } - var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600); - if(numhours){ - return numhours + 'h'; - } - var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60); - if(numminutes){ - return numminutes + 'm'; - } - var numseconds = Math.floor((((seconds % 31536000) % 86400) % 3600) % 60); - if(numseconds){ - return numseconds + 's'; - } - var nummilliseconds = Math.floor(seconds * 1000.0); - if(nummilliseconds){ - return nummilliseconds + 'ms'; - } - - return 'less than a millisecond'; //'just now' //or other string you like; - }; - - kbn.to_percent = function(number,outof) { - return Math.floor((number/outof)*10000)/100 + "%"; - }; - - kbn.addslashes = function(str) { - str = str.replace(/\\/g, '\\\\'); - str = str.replace(/\'/g, '\\\''); - str = str.replace(/\"/g, '\\"'); - str = str.replace(/\0/g, '\\0'); - return str; - }; - - kbn.interval_regex = /(\d+(?:\.\d+)?)(ms|[Mwdhmsy])/; - - // histogram & trends - kbn.intervals_in_seconds = { - y: 31536000, - M: 2592000, - w: 604800, - d: 86400, - h: 3600, - m: 60, - s: 1, - ms: 0.001 - }; - - kbn.calculateInterval = function(range, resolution, lowLimitInterval) { - var lowLimitMs = 1; // 1 millisecond default low limit - var intervalMs; - - if (lowLimitInterval) { - if (lowLimitInterval[0] === '>') { - lowLimitInterval = lowLimitInterval.slice(1); - } - lowLimitMs = kbn.interval_to_ms(lowLimitInterval); - } - - intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution); - if (lowLimitMs > intervalMs) { - intervalMs = lowLimitMs; - } - - return { - intervalMs: intervalMs, - interval: kbn.secondsToHms(intervalMs / 1000), - }; - }; - - kbn.describe_interval = function (string) { - var matches = string.match(kbn.interval_regex); - if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) { - throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"'); - } else { - return { - sec: kbn.intervals_in_seconds[matches[2]], - type: matches[2], - count: parseInt(matches[1], 10) - }; - } - }; - - kbn.interval_to_ms = function(string) { - var info = kbn.describe_interval(string); - return info.sec * 1000 * info.count; - }; - - kbn.interval_to_seconds = function (string) { - var info = kbn.describe_interval(string); - return info.sec * info.count; - }; - - kbn.query_color_dot = function (color, diameter) { - return '
'; - }; - - kbn.slugifyForUrl = function(str) { - return str - .toLowerCase() - .replace(/[^\w ]+/g,'') - .replace(/ +/g,'-'); - }; - - kbn.stringToJsRegex = function(str) { - if (str[0] !== '/') { - return new RegExp('^' + str + '$'); - } - - var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); - return new RegExp(match[1], match[2]); - }; - - kbn.toFixed = function(value, decimals) { - if (value === null) { - return ""; - } - - var factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1; - var formatted = String(Math.round(value * factor) / factor); - - // if exponent return directly - if (formatted.indexOf('e') !== -1 || value === 0) { - return formatted; - } - - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. - if (decimals != null) { - var decimalPos = formatted.indexOf("."); - var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; - if (precision < decimals) { - return (precision ? formatted : formatted + ".") + (String(factor)).substr(1, decimals - precision); - } - } - - return formatted; - }; - - kbn.toFixedScaled = function(value, decimals, scaledDecimals, additionalDecimals, ext) { - if (scaledDecimals === null) { - return kbn.toFixed(value, decimals) + ext; - } else { - return kbn.toFixed(value, scaledDecimals + additionalDecimals) + ext; - } - }; - - kbn.roundValue = function (num, decimals) { - if (num === null) { return null; } - var n = Math.pow(10, decimals); - return Math.round((n * num).toFixed(decimals)) / n; - }; - - ///// FORMAT FUNCTION CONSTRUCTORS ///// - - kbn.formatBuilders = {}; - - // Formatter which always appends a fixed unit string to the value. No - // scaling of the value is performed. - kbn.formatBuilders.fixedUnit = function(unit) { - return function(size, decimals) { - if (size === null) { return ""; } - return kbn.toFixed(size, decimals) + ' ' + unit; - }; - }; - - // Formatter which scales the unit string geometrically according to the given - // numeric factor. Repeatedly scales the value down by the factor until it is - // less than the factor in magnitude, or the end of the array is reached. - kbn.formatBuilders.scaledUnits = function(factor, extArray) { - return function(size, decimals, scaledDecimals) { - if (size === null) { - return ""; - } - - var steps = 0; - var limit = extArray.length; - - while (Math.abs(size) >= factor) { - steps++; - size /= factor; - - if (steps >= limit) { return "NA"; } - } - - if (steps > 0 && scaledDecimals !== null) { - decimals = scaledDecimals + (3 * steps); - } - - return kbn.toFixed(size, decimals) + extArray[steps]; - }; - }; - - // Extension of the scaledUnits builder which uses SI decimal prefixes. If an - // offset is given, it adjusts the starting units at the given prefix; a value - // of 0 starts at no scale; -3 drops to nano, +2 starts at mega, etc. - kbn.formatBuilders.decimalSIPrefix = function(unit, offset) { - var prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; - prefixes = prefixes.slice(3 + (offset || 0)); - var units = prefixes.map(function(p) { return ' ' + p + unit; }); - return kbn.formatBuilders.scaledUnits(1000, units); - }; - - // Extension of the scaledUnits builder which uses SI binary prefixes. If - // offset is given, it starts the units at the given prefix; otherwise, the - // offset defaults to zero and the initial unit is not prefixed. - kbn.formatBuilders.binarySIPrefix = function(unit, offset) { - var prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); - var units = prefixes.map(function(p) { return ' ' + p + unit; }); - return kbn.formatBuilders.scaledUnits(1024, units); - }; - - // Currency formatter for prefixing a symbol onto a number. Supports scaling - // up to the trillions. - kbn.formatBuilders.currency = function(symbol) { - var units = ['', 'K', 'M', 'B', 'T']; - var scaler = kbn.formatBuilders.scaledUnits(1000, units); - return function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - var scaled = scaler(size, decimals, scaledDecimals); - return symbol + scaled; - }; - }; - - kbn.formatBuilders.simpleCountUnit = function(symbol) { - var units = ['', 'K', 'M', 'B', 'T']; - var scaler = kbn.formatBuilders.scaledUnits(1000, units); - return function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - var scaled = scaler(size, decimals, scaledDecimals); - return scaled + " " + symbol; - }; - }; - - ///// VALUE FORMATS ///// - - // Dimensionless Units - kbn.valueFormats.none = kbn.toFixed; - kbn.valueFormats.short = kbn.formatBuilders.scaledUnits(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']); - kbn.valueFormats.dB = kbn.formatBuilders.fixedUnit('dB'); - kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm'); - - kbn.valueFormats.percent = function(size, decimals) { - if (size === null) { return ""; } - return kbn.toFixed(size, decimals) + '%'; - }; - - kbn.valueFormats.percentunit = function(size, decimals) { - if (size === null) { return ""; } - return kbn.toFixed(100*size, decimals) + '%'; - }; - - /* Formats the value to hex. Uses float if specified decimals are not 0. - * There are two options, one with 0x, and one without */ - - kbn.valueFormats.hex = function(value, decimals) { - if (value == null) { return ""; } - return parseFloat(kbn.toFixed(value, decimals)).toString(16).toUpperCase(); - }; - - kbn.valueFormats.hex0x = function(value, decimals) { - if (value == null) { return ""; } - var hexString = kbn.valueFormats.hex(value, decimals); - if (hexString.substring(0,1) === "-") { - return "-0x" + hexString.substring(1); - } - return "0x" + hexString; - }; - - kbn.valueFormats.sci = function(value, decimals) { - return value.toExponential(decimals); - }; - - kbn.valueFormats.locale = function(value, decimals) { - return value.toLocaleString(undefined, {maximumFractionDigits: decimals}); - }; - - // Currencies - kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$'); - kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£'); - kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€'); - kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥'); - kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽'); - kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴'); - kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$'); - kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr'); - kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr'); - kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr'); - kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr'); - - // Data (Binary) - kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); - kbn.valueFormats.bytes = kbn.formatBuilders.binarySIPrefix('B'); - kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1); - kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2); - kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3); - - // Data (Decimal) - kbn.valueFormats.decbits = kbn.formatBuilders.decimalSIPrefix('b'); - kbn.valueFormats.decbytes = kbn.formatBuilders.decimalSIPrefix('B'); - kbn.valueFormats.deckbytes = kbn.formatBuilders.decimalSIPrefix('B', 1); - kbn.valueFormats.decmbytes = kbn.formatBuilders.decimalSIPrefix('B', 2); - kbn.valueFormats.decgbytes = kbn.formatBuilders.decimalSIPrefix('B', 3); - - // Data Rate - kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps'); - kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps'); - kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps'); - kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1); - kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1); - kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2); - kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); - kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); - kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); - - // Throughput - kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); - kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps'); - kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps'); - kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops'); - kbn.valueFormats.opm = kbn.formatBuilders.simpleCountUnit('opm'); - kbn.valueFormats.rpm = kbn.formatBuilders.simpleCountUnit('rpm'); - 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.voltamp = kbn.formatBuilders.decimalSIPrefix('VA'); - kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1); - kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var'); - kbn.valueFormats.kvoltampreact = kbn.formatBuilders.decimalSIPrefix('var', 1); - kbn.valueFormats.watth = kbn.formatBuilders.decimalSIPrefix('Wh'); - kbn.valueFormats.kwatth = kbn.formatBuilders.decimalSIPrefix('Wh', 1); - kbn.valueFormats.joule = kbn.formatBuilders.decimalSIPrefix('J'); - kbn.valueFormats.ev = kbn.formatBuilders.decimalSIPrefix('eV'); - kbn.valueFormats.amp = kbn.formatBuilders.decimalSIPrefix('A'); - 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'); - - // Temperature - kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C'); - kbn.valueFormats.farenheit = kbn.formatBuilders.fixedUnit('°F'); - kbn.valueFormats.kelvin = kbn.formatBuilders.fixedUnit('K'); - kbn.valueFormats.humidity = kbn.formatBuilders.fixedUnit('%H'); - - // Pressure - kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar'); - kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1); - kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1); - kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa'); - kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg'); - kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']); - - // Force - kbn.valueFormats.forceNm = kbn.formatBuilders.decimalSIPrefix('Nm'); - kbn.valueFormats.forcekNm = kbn.formatBuilders.decimalSIPrefix('Nm', 1); - kbn.valueFormats.forceN = kbn.formatBuilders.decimalSIPrefix('N'); - kbn.valueFormats.forcekN = kbn.formatBuilders.decimalSIPrefix('N', 1); - - // Length - 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'); - - // Mass - kbn.valueFormats.massmg = kbn.formatBuilders.decimalSIPrefix('g', -1); - kbn.valueFormats.massg = kbn.formatBuilders.decimalSIPrefix('g'); - kbn.valueFormats.masskg = kbn.formatBuilders.decimalSIPrefix('g', 1); - kbn.valueFormats.masst = kbn.formatBuilders.fixedUnit('t'); - - // Velocity - kbn.valueFormats.velocityms = kbn.formatBuilders.fixedUnit('m/s'); - kbn.valueFormats.velocitykmh = kbn.formatBuilders.fixedUnit('km/h'); - kbn.valueFormats.velocitymph = kbn.formatBuilders.fixedUnit('mph'); - kbn.valueFormats.velocityknot = kbn.formatBuilders.fixedUnit('kn'); - - // Volume - kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L'); - kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1); - kbn.valueFormats.m3 = kbn.formatBuilders.decimalSIPrefix('m3'); - kbn.valueFormats.dm3 = kbn.formatBuilders.decimalSIPrefix('dm3'); - kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal'); - - // Flow - kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm'); - kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms'); - kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); - kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); - - // Time - kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz'); - - kbn.valueFormats.ms = function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + " ms"; - } - // Less than 1 min - else if (Math.abs(size) < 60000) { - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, " s"); - } - // Less than 1 hour, devide in minutes - else if (Math.abs(size) < 3600000) { - return kbn.toFixedScaled(size / 60000, decimals, scaledDecimals, 5, " min"); - } - // Less than one day, devide in hours - else if (Math.abs(size) < 86400000) { - return kbn.toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, " hour"); - } - // Less than one year, devide in days - else if (Math.abs(size) < 31536000000) { - return kbn.toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, " day"); - } - - return kbn.toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, " year"); - }; - - kbn.valueFormats.s = function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - - // Less than 1 µs, devide in ns - if (Math.abs(size) < 0.000001) { - return kbn.toFixedScaled(size * 1.e9, decimals, scaledDecimals - decimals, -9, " ns"); - } - // Less than 1 ms, devide in µs - if (Math.abs(size) < 0.001) { - return kbn.toFixedScaled(size * 1.e6, decimals, scaledDecimals - decimals, -6, " µs"); - } - // Less than 1 second, devide in ms - if (Math.abs(size) < 1) { - return kbn.toFixedScaled(size * 1.e3, decimals, scaledDecimals - decimals, -3, " ms"); - } - - if (Math.abs(size) < 60) { - return kbn.toFixed(size, decimals) + " s"; - } - // Less than 1 hour, devide in minutes - else if (Math.abs(size) < 3600) { - return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 1, " min"); - } - // Less than one day, devide in hours - else if (Math.abs(size) < 86400) { - return kbn.toFixedScaled(size / 3600, decimals, scaledDecimals, 4, " hour"); - } - // Less than one week, devide in days - else if (Math.abs(size) < 604800) { - return kbn.toFixedScaled(size / 86400, decimals, scaledDecimals, 5, " day"); - } - // Less than one year, devide in week - else if (Math.abs(size) < 31536000) { - return kbn.toFixedScaled(size / 604800, decimals, scaledDecimals, 6, " week"); - } - - return kbn.toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, " year"); - }; - - kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + " µs"; - } - else if (Math.abs(size) < 1000000) { - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, " ms"); - } - else { - return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, " s"); - } - }; - - kbn.valueFormats.ns = function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - - if (Math.abs(size) < 1000) { - return kbn.toFixed(size, decimals) + " ns"; - } - else if (Math.abs(size) < 1000000) { - return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, " µs"); - } - else if (Math.abs(size) < 1000000000) { - return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, " ms"); - } - else if (Math.abs(size) < 60000000000){ - return kbn.toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, " s"); - } - else { - return kbn.toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, " min"); - } - }; - - kbn.valueFormats.m = function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - - if (Math.abs(size) < 60) { - return kbn.toFixed(size, decimals) + " min"; - } - else if (Math.abs(size) < 1440) { - return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 2, " hour"); - } - else if (Math.abs(size) < 10080) { - return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, " day"); - } - else if (Math.abs(size) < 604800) { - return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, " week"); - } - else { - return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, " year"); - } - }; - - kbn.valueFormats.h = function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - - if (Math.abs(size) < 24) { - return kbn.toFixed(size, decimals) + " hour"; - } - else if (Math.abs(size) < 168) { - return kbn.toFixedScaled(size / 24, decimals, scaledDecimals, 2, " day"); - } - else if (Math.abs(size) < 8760) { - return kbn.toFixedScaled(size / 168, decimals, scaledDecimals, 3, " week"); - } - else { - return kbn.toFixedScaled(size / 8760, decimals, scaledDecimals, 4, " year"); - } - }; - - kbn.valueFormats.d = function(size, decimals, scaledDecimals) { - if (size === null) { return ""; } - - if (Math.abs(size) < 7) { - return kbn.toFixed(size, decimals) + " day"; - } - else if (Math.abs(size) < 365) { - return kbn.toFixedScaled(size / 7, decimals, scaledDecimals, 2, " week"); - } - else { - return kbn.toFixedScaled(size / 365, decimals, scaledDecimals, 3, " year"); - } - }; - - kbn.toDuration = function(size, decimals, timeScale) { - if (size === null) { return ""; } - if (size === 0) { return "0 " + timeScale + "s"; } - if (size < 0) { return kbn.toDuration(-size, decimals, timeScale) + " ago"; } - - var units = [ - {short: "y", long: "year"}, - {short: "M", long: "month"}, - {short: "w", long: "week"}, - {short: "d", long: "day"}, - {short: "h", long: "hour"}, - {short: "m", long: "minute"}, - {short: "s", long: "second"}, - {short: "ms", long: "millisecond"} - ]; - // convert $size to milliseconds - // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors - size *= kbn.intervals_in_seconds[units.find(function(e) { return e.long === timeScale; }).short] * 1000; - - var string = []; - // after first value >= 1 print only $decimals more - var decrementDecimals = false; - for (var i = 0; i < units.length && decimals >= 0; i++) { - var interval = kbn.intervals_in_seconds[units[i].short] * 1000; - var value = size / interval; - if (value >= 1 || decrementDecimals) { - decrementDecimals = true; - var floor = Math.floor(value); - var unit = units[i].long + (floor !== 1 ? "s" : ""); - string.push(floor + " " + unit); - size = size % interval; - decimals--; - } - } - - return string.join(", "); - }; - - kbn.valueFormats.dtdurationms = function(size, decimals) { - return kbn.toDuration(size, decimals, 'millisecond'); - }; - - kbn.valueFormats.dtdurations = function(size, decimals) { - return kbn.toDuration(size, decimals, 'second'); - }; - - kbn.valueFormats.dateTimeAsIso = function(epoch) { - var time = moment(epoch); - - if (moment().isSame(epoch, 'day')) { - return time.format('HH:mm:ss'); - } - return time.format('YYYY-MM-DD HH:mm:ss'); - }; - - kbn.valueFormats.dateTimeAsUS = function(epoch) { - var time = moment(epoch); - - if (moment().isSame(epoch, 'day')) { - return time.format('h:mm:ss a'); - } - return time.format('MM/DD/YYYY h:mm:ss a'); - }; - - kbn.valueFormats.dateTimeFromNow = function(epoch) { - return moment(epoch).fromNow(); - }; - - ///// FORMAT MENU ///// - - kbn.getUnitFormats = function() { - return [ - { - text: 'none', - submenu: [ - {text: 'none' , value: 'none' }, - {text: 'short', value: 'short' }, - {text: 'percent (0-100)', value: 'percent' }, - {text: 'percent (0.0-1.0)', value: 'percentunit'}, - {text: 'Humidity (%H)', value: 'humidity' }, - {text: 'ppm', value: 'ppm' }, - {text: 'decibel', value: 'dB' }, - {text: 'hexadecimal (0x)', value: 'hex0x' }, - {text: 'hexadecimal', value: 'hex' }, - {text: 'scientific notation', value: 'sci' }, - {text: 'locale format', value: 'locale' }, - ] - }, - { - text: 'currency', - submenu: [ - {text: 'Dollars ($)', value: 'currencyUSD'}, - {text: 'Pounds (£)', value: 'currencyGBP'}, - {text: 'Euro (€)', value: 'currencyEUR'}, - {text: 'Yen (¥)', value: 'currencyJPY'}, - {text: 'Rubles (₽)', value: 'currencyRUB'}, - {text: 'Hryvnias (₴)', value: 'currencyUAH'}, - {text: 'Real (R$)', value: 'currencyBRL'}, - {text: 'Danish Krone (kr)', value: 'currencyDKK'}, - {text: 'Icelandic Krone (kr)', value: 'currencyISK'}, - {text: 'Norwegian Krone (kr)', value: 'currencyNOK'}, - {text: 'Swedish Krone (kr)', value: 'currencySEK'}, - ] - }, - { - text: 'time', - submenu: [ - {text: 'Hertz (1/s)', value: 'hertz'}, - {text: 'nanoseconds (ns)' , value: 'ns' }, - {text: 'microseconds (µs)', value: 'µs' }, - {text: 'milliseconds (ms)', value: 'ms' }, - {text: 'seconds (s)', value: 's' }, - {text: 'minutes (m)', value: 'm' }, - {text: 'hours (h)', value: 'h' }, - {text: 'days (d)', value: 'd' }, - {text: 'duration (ms)', value: 'dtdurationms' }, - {text: 'duration (s)', value: 'dtdurations' }, - ] - }, - { - text: 'date & time', - submenu: [ - {text: 'YYYY-MM-DD HH:mm:ss', value: 'dateTimeAsIso' }, - {text: 'DD/MM/YYYY h:mm:ss a', value: 'dateTimeAsUS' }, - {text: 'From Now', value: 'dateTimeFromNow' }, - ] - }, - { - text: 'data (IEC)', - submenu: [ - {text: 'bits', value: 'bits' }, - {text: 'bytes', value: 'bytes' }, - {text: 'kibibytes', value: 'kbytes'}, - {text: 'mebibytes', value: 'mbytes'}, - {text: 'gibibytes', value: 'gbytes'}, - ] - }, - { - text: 'data (Metric)', - submenu: [ - {text: 'bits', value: 'decbits' }, - {text: 'bytes', value: 'decbytes' }, - {text: 'kilobytes', value: 'deckbytes'}, - {text: 'megabytes', value: 'decmbytes'}, - {text: 'gigabytes', value: 'decgbytes'}, - ] - }, - { - text: 'data rate', - submenu: [ - {text: 'packets/sec', value: 'pps'}, - {text: 'bits/sec', value: 'bps'}, - {text: 'bytes/sec', value: 'Bps'}, - {text: 'kilobits/sec', value: 'Kbits'}, - {text: 'kilobytes/sec', value: 'KBs'}, - {text: 'megabits/sec', value: 'Mbits'}, - {text: 'megabytes/sec', value: 'MBs'}, - {text: 'gigabytes/sec', value: 'GBs'}, - {text: 'gigabits/sec', value: 'Gbits'}, - ] - }, - { - text: 'throughput', - submenu: [ - {text: 'ops/sec (ops)', value: 'ops' }, - {text: 'reads/sec (rps)', value: 'rps' }, - {text: 'writes/sec (wps)', value: 'wps' }, - {text: 'I/O ops/sec (iops)', value: 'iops'}, - {text: 'ops/min (opm)', value: 'opm' }, - {text: 'reads/min (rpm)', value: 'rpm' }, - {text: 'writes/min (wpm)', value: 'wpm' }, - ] - }, - { - text: 'length', - submenu: [ - {text: 'millimetre (mm)', value: 'lengthmm'}, - {text: 'meter (m)', value: 'lengthm' }, - {text: 'kilometer (km)', value: 'lengthkm'}, - {text: 'mile (mi)', value: 'lengthmi'}, - ] - }, - { - text: 'mass', - submenu: [ - {text: 'milligram (mg)', value: 'massmg'}, - {text: 'gram (g)', value: 'massg' }, - {text: 'kilogram (kg)', value: 'masskg'}, - {text: 'metric ton (t)', value: 'masst' }, - ] - }, - { - text: 'velocity', - submenu: [ - {text: 'm/s', value: 'velocityms' }, - {text: 'km/h', value: 'velocitykmh' }, - {text: 'mph', value: 'velocitymph' }, - {text: 'knot (kn)', value: 'velocityknot'}, - ] - }, - { - text: 'volume', - submenu: [ - {text: 'millilitre', value: 'mlitre' }, - {text: 'litre', value: 'litre' }, - {text: 'cubic metre', value: 'm3' }, - {text: 'cubic decimetre', value: 'dm3' }, - {text: 'gallons', value: 'gallons'}, - ] - }, - { - text: 'energy', - submenu: [ - {text: 'watt (W)', value: 'watt' }, - {text: 'kilowatt (kW)', value: 'kwatt' }, - {text: 'volt-ampere (VA)', value: 'voltamp' }, - {text: 'kilovolt-ampere (kVA)', value: 'kvoltamp' }, - {text: 'volt-ampere reactive (var)', value: 'voltampreact'}, - {text: 'kilovolt-ampere reactive (kvar)', value: 'kvoltampreact'}, - {text: 'watt-hour (Wh)', value: 'watth' }, - {text: 'kilowatt-hour (kWh)', value: 'kwatth' }, - {text: 'joule (J)', value: 'joule' }, - {text: 'electron volt (eV)', value: 'ev' }, - {text: 'Ampere (A)', value: 'amp' }, - {text: 'Kiloampere (kA)', value: 'kamp' }, - {text: 'Volt (V)', value: 'volt' }, - {text: 'Kilovolt (kV)', value: 'kvolt' }, - {text: 'Decibel-milliwatt (dBm)', value: 'dBm' }, - ] - }, - { - text: 'temperature', - submenu: [ - {text: 'Celsius (°C)', value: 'celsius' }, - {text: 'Farenheit (°F)', value: 'farenheit' }, - {text: 'Kelvin (K)', value: 'kelvin' }, - ] - }, - { - text: 'pressure', - submenu: [ - {text: 'Millibars', value: 'pressurembar'}, - {text: 'Bars', value: 'pressurebar' }, - {text: 'Kilobars', value: 'pressurekbar'}, - {text: 'Hectopascals', value: 'pressurehpa' }, - {text: 'Inches of mercury', value: 'pressurehg' }, - {text: 'PSI', value: 'pressurepsi' }, - ] - }, - { - text: 'force', - submenu: [ - {text: 'Newton-meters (Nm)', value: 'forceNm' }, - {text: 'Kilonewton-meters (kNm)', value: 'forcekNm' }, - {text: 'Newtons (N)', value: 'forceN' }, - {text: 'Kilonewtons (kN)', value: 'forcekN' }, - ] - }, - { - text: 'flow', - submenu: [ - {text: 'Gallons/min (gpm)', value: 'flowgpm' }, - {text: 'Cubic meters/sec (cms)', value: 'flowcms' }, - {text: 'Cubic feet/sec (cfs)', value: 'flowcfs' }, - {text: 'Cubic feet/min (cfm)', value: 'flowcfm' }, - ] - } - ]; - }; - - return kbn; -}); diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts new file mode 100644 index 00000000000..baadb604685 --- /dev/null +++ b/public/app/core/utils/kbn.ts @@ -0,0 +1,1012 @@ +import _ from 'lodash'; +import moment from 'moment'; + +var kbn: any = {}; + +kbn.valueFormats = {}; + +kbn.regexEscape = function(value) { + return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&'); +}; + +///// HELPER FUNCTIONS ///// + +kbn.round_interval = function(interval) { + switch (true) { + // 0.015s + case interval < 15: + return 10; // 0.01s + // 0.035s + case interval < 35: + return 20; // 0.02s + // 0.075s + case interval < 75: + return 50; // 0.05s + // 0.15s + case interval < 150: + return 100; // 0.1s + // 0.35s + case interval < 350: + return 200; // 0.2s + // 0.75s + case interval < 750: + return 500; // 0.5s + // 1.5s + case interval < 1500: + return 1000; // 1s + // 3.5s + case interval < 3500: + return 2000; // 2s + // 7.5s + case interval < 7500: + return 5000; // 5s + // 12.5s + case interval < 12500: + return 10000; // 10s + // 17.5s + case interval < 17500: + return 15000; // 15s + // 25s + case interval < 25000: + return 20000; // 20s + // 45s + case interval < 45000: + return 30000; // 30s + // 1.5m + case interval < 90000: + return 60000; // 1m + // 3.5m + case interval < 210000: + return 120000; // 2m + // 7.5m + case interval < 450000: + return 300000; // 5m + // 12.5m + case interval < 750000: + return 600000; // 10m + // 12.5m + case interval < 1050000: + return 900000; // 15m + // 25m + case interval < 1500000: + return 1200000; // 20m + // 45m + case interval < 2700000: + return 1800000; // 30m + // 1.5h + case interval < 5400000: + return 3600000; // 1h + // 2.5h + case interval < 9000000: + return 7200000; // 2h + // 4.5h + case interval < 16200000: + return 10800000; // 3h + // 9h + case interval < 32400000: + return 21600000; // 6h + // 1d + case interval < 86400000: + return 43200000; // 12h + // 1w + case interval < 604800000: + return 86400000; // 1d + // 3w + case interval < 1814400000: + return 604800000; // 1w + // 6w + case interval < 3628800000: + return 2592000000; // 30d + default: + return 31536000000; // 1y + } +}; + +kbn.secondsToHms = function(seconds) { + var numyears = Math.floor(seconds / 31536000); + if (numyears) { + return numyears + 'y'; + } + var numdays = Math.floor((seconds % 31536000) / 86400); + if (numdays) { + return numdays + 'd'; + } + var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600); + if (numhours) { + return numhours + 'h'; + } + var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60); + if (numminutes) { + return numminutes + 'm'; + } + var numseconds = Math.floor((((seconds % 31536000) % 86400) % 3600) % 60); + if (numseconds) { + return numseconds + 's'; + } + var nummilliseconds = Math.floor(seconds * 1000.0); + if (nummilliseconds) { + return nummilliseconds + 'ms'; + } + + return 'less than a millisecond'; //'just now' //or other string you like; +}; + +kbn.to_percent = function(nr, outof) { + return Math.floor(nr / outof * 10000) / 100 + '%'; +}; + +kbn.addslashes = function(str) { + str = str.replace(/\\/g, '\\\\'); + str = str.replace(/\'/g, "\\'"); + str = str.replace(/\"/g, '\\"'); + str = str.replace(/\0/g, '\\0'); + return str; +}; + +kbn.interval_regex = /(\d+(?:\.\d+)?)(ms|[Mwdhmsy])/; + +// histogram & trends +kbn.intervals_in_seconds = { + y: 31536000, + M: 2592000, + w: 604800, + d: 86400, + h: 3600, + m: 60, + s: 1, + ms: 0.001, +}; + +kbn.calculateInterval = function(range, resolution, lowLimitInterval) { + var lowLimitMs = 1; // 1 millisecond default low limit + var intervalMs; + + if (lowLimitInterval) { + if (lowLimitInterval[0] === '>') { + lowLimitInterval = lowLimitInterval.slice(1); + } + lowLimitMs = kbn.interval_to_ms(lowLimitInterval); + } + + intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution); + if (lowLimitMs > intervalMs) { + intervalMs = lowLimitMs; + } + + return { + intervalMs: intervalMs, + interval: kbn.secondsToHms(intervalMs / 1000), + }; +}; + +kbn.describe_interval = function(str) { + var matches = str.match(kbn.interval_regex); + if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) { + throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"'); + } else { + return { + sec: kbn.intervals_in_seconds[matches[2]], + type: matches[2], + count: parseInt(matches[1], 10), + }; + } +}; + +kbn.interval_to_ms = function(str) { + var info = kbn.describe_interval(str); + return info.sec * 1000 * info.count; +}; + +kbn.interval_to_seconds = function(str) { + var info = kbn.describe_interval(str); + return info.sec * info.count; +}; + +kbn.query_color_dot = function(color, diameter) { + return ( + '
' + ); +}; + +kbn.slugifyForUrl = function(str) { + return str + .toLowerCase() + .replace(/[^\w ]+/g, '') + .replace(/ +/g, '-'); +}; + +kbn.stringToJsRegex = function(str) { + if (str[0] !== '/') { + return new RegExp('^' + str + '$'); + } + + var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); + return new RegExp(match[1], match[2]); +}; + +kbn.toFixed = function(value, decimals) { + if (value === null) { + return ''; + } + + var factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1; + var formatted = String(Math.round(value * factor) / factor); + + // if exponent return directly + if (formatted.indexOf('e') !== -1 || value === 0) { + return formatted; + } + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + if (decimals != null) { + var decimalPos = formatted.indexOf('.'); + var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; + if (precision < decimals) { + return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision); + } + } + + return formatted; +}; + +kbn.toFixedScaled = function(value, decimals, scaledDecimals, additionalDecimals, ext) { + if (scaledDecimals === null) { + return kbn.toFixed(value, decimals) + ext; + } else { + return kbn.toFixed(value, scaledDecimals + additionalDecimals) + ext; + } +}; + +kbn.roundValue = function(num, decimals) { + if (num === null) { + return null; + } + var n = Math.pow(10, decimals); + var formatted = (n * num).toFixed(decimals); + return Math.round(parseFloat(formatted)) / n; +}; + +///// FORMAT FUNCTION CONSTRUCTORS ///// + +kbn.formatBuilders = {}; + +// Formatter which always appends a fixed unit string to the value. No +// scaling of the value is performed. +kbn.formatBuilders.fixedUnit = function(unit) { + return function(size, decimals) { + if (size === null) { + return ''; + } + return kbn.toFixed(size, decimals) + ' ' + unit; + }; +}; + +// Formatter which scales the unit string geometrically according to the given +// numeric factor. Repeatedly scales the value down by the factor until it is +// less than the factor in magnitude, or the end of the array is reached. +kbn.formatBuilders.scaledUnits = function(factor, extArray) { + return function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + + var steps = 0; + var limit = extArray.length; + + while (Math.abs(size) >= factor) { + steps++; + size /= factor; + + if (steps >= limit) { + return 'NA'; + } + } + + if (steps > 0 && scaledDecimals !== null) { + decimals = scaledDecimals + 3 * steps; + } + + return kbn.toFixed(size, decimals) + extArray[steps]; + }; +}; + +// Extension of the scaledUnits builder which uses SI decimal prefixes. If an +// offset is given, it adjusts the starting units at the given prefix; a value +// of 0 starts at no scale; -3 drops to nano, +2 starts at mega, etc. +kbn.formatBuilders.decimalSIPrefix = function(unit, offset) { + var prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + prefixes = prefixes.slice(3 + (offset || 0)); + var units = prefixes.map(function(p) { + return ' ' + p + unit; + }); + return kbn.formatBuilders.scaledUnits(1000, units); +}; + +// Extension of the scaledUnits builder which uses SI binary prefixes. If +// offset is given, it starts the units at the given prefix; otherwise, the +// offset defaults to zero and the initial unit is not prefixed. +kbn.formatBuilders.binarySIPrefix = function(unit, offset) { + var prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); + var units = prefixes.map(function(p) { + return ' ' + p + unit; + }); + return kbn.formatBuilders.scaledUnits(1024, units); +}; + +// Currency formatter for prefixing a symbol onto a number. Supports scaling +// up to the trillions. +kbn.formatBuilders.currency = function(symbol) { + var units = ['', 'K', 'M', 'B', 'T']; + var scaler = kbn.formatBuilders.scaledUnits(1000, units); + return function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + var scaled = scaler(size, decimals, scaledDecimals); + return symbol + scaled; + }; +}; + +kbn.formatBuilders.simpleCountUnit = function(symbol) { + var units = ['', 'K', 'M', 'B', 'T']; + var scaler = kbn.formatBuilders.scaledUnits(1000, units); + return function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + var scaled = scaler(size, decimals, scaledDecimals); + return scaled + ' ' + symbol; + }; +}; + +///// VALUE FORMATS ///// + +// Dimensionless Units +kbn.valueFormats.none = kbn.toFixed; +kbn.valueFormats.short = kbn.formatBuilders.scaledUnits(1000, [ + '', + ' K', + ' Mil', + ' Bil', + ' Tri', + ' Quadr', + ' Quint', + ' Sext', + ' Sept', +]); +kbn.valueFormats.dB = kbn.formatBuilders.fixedUnit('dB'); +kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm'); + +kbn.valueFormats.percent = function(size, decimals) { + if (size === null) { + return ''; + } + return kbn.toFixed(size, decimals) + '%'; +}; + +kbn.valueFormats.percentunit = function(size, decimals) { + if (size === null) { + return ''; + } + return kbn.toFixed(100 * size, decimals) + '%'; +}; + +/* Formats the value to hex. Uses float if specified decimals are not 0. + * There are two options, one with 0x, and one without */ + +kbn.valueFormats.hex = function(value, decimals) { + if (value == null) { + return ''; + } + return parseFloat(kbn.toFixed(value, decimals)) + .toString(16) + .toUpperCase(); +}; + +kbn.valueFormats.hex0x = function(value, decimals) { + if (value == null) { + return ''; + } + var hexString = kbn.valueFormats.hex(value, decimals); + if (hexString.substring(0, 1) === '-') { + return '-0x' + hexString.substring(1); + } + return '0x' + hexString; +}; + +kbn.valueFormats.sci = function(value, decimals) { + return value.toExponential(decimals); +}; + +kbn.valueFormats.locale = function(value, decimals) { + return value.toLocaleString(undefined, { maximumFractionDigits: decimals }); +}; + +// Currencies +kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$'); +kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£'); +kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€'); +kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥'); +kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽'); +kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴'); +kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$'); +kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr'); +kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr'); +kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr'); +kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr'); + +// Data (Binary) +kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); +kbn.valueFormats.bytes = kbn.formatBuilders.binarySIPrefix('B'); +kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1); +kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2); +kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3); + +// Data (Decimal) +kbn.valueFormats.decbits = kbn.formatBuilders.decimalSIPrefix('b'); +kbn.valueFormats.decbytes = kbn.formatBuilders.decimalSIPrefix('B'); +kbn.valueFormats.deckbytes = kbn.formatBuilders.decimalSIPrefix('B', 1); +kbn.valueFormats.decmbytes = kbn.formatBuilders.decimalSIPrefix('B', 2); +kbn.valueFormats.decgbytes = kbn.formatBuilders.decimalSIPrefix('B', 3); + +// Data Rate +kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps'); +kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps'); +kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps'); +kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1); +kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1); +kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2); +kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); +kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); +kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); + +// Throughput +kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); +kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps'); +kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps'); +kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops'); +kbn.valueFormats.opm = kbn.formatBuilders.simpleCountUnit('opm'); +kbn.valueFormats.rpm = kbn.formatBuilders.simpleCountUnit('rpm'); +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'); +kbn.valueFormats.kvoltampreact = kbn.formatBuilders.decimalSIPrefix('var', 1); +kbn.valueFormats.watth = kbn.formatBuilders.decimalSIPrefix('Wh'); +kbn.valueFormats.kwatth = kbn.formatBuilders.decimalSIPrefix('Wh', 1); +kbn.valueFormats.joule = kbn.formatBuilders.decimalSIPrefix('J'); +kbn.valueFormats.ev = kbn.formatBuilders.decimalSIPrefix('eV'); +kbn.valueFormats.amp = kbn.formatBuilders.decimalSIPrefix('A'); +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'); +kbn.valueFormats.farenheit = kbn.formatBuilders.fixedUnit('°F'); +kbn.valueFormats.kelvin = kbn.formatBuilders.fixedUnit('K'); +kbn.valueFormats.humidity = kbn.formatBuilders.fixedUnit('%H'); + +// Pressure +kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar'); +kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1); +kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1); +kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa'); +kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg'); +kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']); + +// Force +kbn.valueFormats.forceNm = kbn.formatBuilders.decimalSIPrefix('Nm'); +kbn.valueFormats.forcekNm = kbn.formatBuilders.decimalSIPrefix('Nm', 1); +kbn.valueFormats.forceN = kbn.formatBuilders.decimalSIPrefix('N'); +kbn.valueFormats.forcekN = kbn.formatBuilders.decimalSIPrefix('N', 1); + +// Length +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); +kbn.valueFormats.massg = kbn.formatBuilders.decimalSIPrefix('g'); +kbn.valueFormats.masskg = kbn.formatBuilders.decimalSIPrefix('g', 1); +kbn.valueFormats.masst = kbn.formatBuilders.fixedUnit('t'); + +// Velocity +kbn.valueFormats.velocityms = kbn.formatBuilders.fixedUnit('m/s'); +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); +kbn.valueFormats.m3 = kbn.formatBuilders.decimalSIPrefix('m3'); +kbn.valueFormats.dm3 = kbn.formatBuilders.decimalSIPrefix('dm3'); +kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal'); + +// Flow +kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm'); +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'); + +kbn.valueFormats.ms = function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return kbn.toFixed(size, decimals) + ' ms'; + } else if (Math.abs(size) < 60000) { + // Less than 1 min + return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s'); + } else if (Math.abs(size) < 3600000) { + // Less than 1 hour, devide in minutes + return kbn.toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min'); + } else if (Math.abs(size) < 86400000) { + // Less than one day, devide in hours + return kbn.toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour'); + } else if (Math.abs(size) < 31536000000) { + // Less than one year, devide in days + return kbn.toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day'); + } + + return kbn.toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); +}; + +kbn.valueFormats.s = function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + + // Less than 1 µs, devide in ns + if (Math.abs(size) < 0.000001) { + return kbn.toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); + } + // Less than 1 ms, devide in µs + if (Math.abs(size) < 0.001) { + return kbn.toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); + } + // Less than 1 second, devide in ms + if (Math.abs(size) < 1) { + return kbn.toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); + } + + if (Math.abs(size) < 60) { + return kbn.toFixed(size, decimals) + ' s'; + } else if (Math.abs(size) < 3600) { + // Less than 1 hour, devide in minutes + return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min'); + } else if (Math.abs(size) < 86400) { + // Less than one day, devide in hours + return kbn.toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour'); + } else if (Math.abs(size) < 604800) { + // Less than one week, devide in days + return kbn.toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day'); + } else if (Math.abs(size) < 31536000) { + // Less than one year, devide in week + return kbn.toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week'); + } + + return kbn.toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); +}; + +kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return kbn.toFixed(size, decimals) + ' µs'; + } else if (Math.abs(size) < 1000000) { + return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms'); + } else { + return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s'); + } +}; + +kbn.valueFormats.ns = function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return kbn.toFixed(size, decimals) + ' ns'; + } else if (Math.abs(size) < 1000000) { + return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs'); + } else if (Math.abs(size) < 1000000000) { + return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms'); + } else if (Math.abs(size) < 60000000000) { + return kbn.toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s'); + } else { + return kbn.toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min'); + } +}; + +kbn.valueFormats.m = function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 60) { + return kbn.toFixed(size, decimals) + ' min'; + } else if (Math.abs(size) < 1440) { + return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour'); + } else if (Math.abs(size) < 10080) { + return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day'); + } else if (Math.abs(size) < 604800) { + return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week'); + } else { + return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year'); + } +}; + +kbn.valueFormats.h = function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 24) { + return kbn.toFixed(size, decimals) + ' hour'; + } else if (Math.abs(size) < 168) { + return kbn.toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day'); + } else if (Math.abs(size) < 8760) { + return kbn.toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week'); + } else { + return kbn.toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year'); + } +}; + +kbn.valueFormats.d = function(size, decimals, scaledDecimals) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 7) { + return kbn.toFixed(size, decimals) + ' day'; + } else if (Math.abs(size) < 365) { + return kbn.toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week'); + } else { + return kbn.toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year'); + } +}; + +kbn.toDuration = function(size, decimals, timeScale) { + if (size === null) { + return ''; + } + if (size === 0) { + return '0 ' + timeScale + 's'; + } + if (size < 0) { + return kbn.toDuration(-size, decimals, timeScale) + ' ago'; + } + + var units = [ + { short: 'y', long: 'year' }, + { short: 'M', long: 'month' }, + { short: 'w', long: 'week' }, + { short: 'd', long: 'day' }, + { short: 'h', long: 'hour' }, + { short: 'm', long: 'minute' }, + { short: 's', long: 'second' }, + { short: 'ms', long: 'millisecond' }, + ]; + // convert $size to milliseconds + // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors + size *= + kbn.intervals_in_seconds[ + units.find(function(e) { + return e.long === timeScale; + }).short + ] * 1000; + + var strings = []; + // after first value >= 1 print only $decimals more + var decrementDecimals = false; + for (var i = 0; i < units.length && decimals >= 0; i++) { + var interval = kbn.intervals_in_seconds[units[i].short] * 1000; + var value = size / interval; + if (value >= 1 || decrementDecimals) { + decrementDecimals = true; + var floor = Math.floor(value); + var unit = units[i].long + (floor !== 1 ? 's' : ''); + strings.push(floor + ' ' + unit); + size = size % interval; + decimals--; + } + } + + return strings.join(', '); +}; + +kbn.valueFormats.dtdurationms = function(size, decimals) { + return kbn.toDuration(size, decimals, 'millisecond'); +}; + +kbn.valueFormats.dtdurations = function(size, decimals) { + return kbn.toDuration(size, decimals, 'second'); +}; + +kbn.valueFormats.dateTimeAsIso = function(epoch) { + var time = moment(epoch); + + if (moment().isSame(epoch, 'day')) { + return time.format('HH:mm:ss'); + } + return time.format('YYYY-MM-DD HH:mm:ss'); +}; + +kbn.valueFormats.dateTimeAsUS = function(epoch) { + var time = moment(epoch); + + if (moment().isSame(epoch, 'day')) { + return time.format('h:mm:ss a'); + } + return time.format('MM/DD/YYYY h:mm:ss a'); +}; + +kbn.valueFormats.dateTimeFromNow = function(epoch) { + return moment(epoch).fromNow(); +}; + +///// FORMAT MENU ///// + +kbn.getUnitFormats = function() { + return [ + { + text: 'none', + submenu: [ + { text: 'none', value: 'none' }, + { text: 'short', value: 'short' }, + { text: 'percent (0-100)', value: 'percent' }, + { text: 'percent (0.0-1.0)', value: 'percentunit' }, + { text: 'Humidity (%H)', value: 'humidity' }, + { text: 'ppm', value: 'ppm' }, + { text: 'decibel', value: 'dB' }, + { text: 'hexadecimal (0x)', value: 'hex0x' }, + { text: 'hexadecimal', value: 'hex' }, + { text: 'scientific notation', value: 'sci' }, + { text: 'locale format', value: 'locale' }, + ], + }, + { + text: 'currency', + submenu: [ + { text: 'Dollars ($)', value: 'currencyUSD' }, + { text: 'Pounds (£)', value: 'currencyGBP' }, + { text: 'Euro (€)', value: 'currencyEUR' }, + { text: 'Yen (¥)', value: 'currencyJPY' }, + { text: 'Rubles (₽)', value: 'currencyRUB' }, + { text: 'Hryvnias (₴)', value: 'currencyUAH' }, + { text: 'Real (R$)', value: 'currencyBRL' }, + { text: 'Danish Krone (kr)', value: 'currencyDKK' }, + { text: 'Icelandic Krone (kr)', value: 'currencyISK' }, + { text: 'Norwegian Krone (kr)', value: 'currencyNOK' }, + { text: 'Swedish Krone (kr)', value: 'currencySEK' }, + ], + }, + { + text: 'time', + submenu: [ + { text: 'Hertz (1/s)', value: 'hertz' }, + { text: 'nanoseconds (ns)', value: 'ns' }, + { text: 'microseconds (µs)', value: 'µs' }, + { text: 'milliseconds (ms)', value: 'ms' }, + { text: 'seconds (s)', value: 's' }, + { text: 'minutes (m)', value: 'm' }, + { text: 'hours (h)', value: 'h' }, + { text: 'days (d)', value: 'd' }, + { text: 'duration (ms)', value: 'dtdurationms' }, + { text: 'duration (s)', value: 'dtdurations' }, + ], + }, + { + text: 'date & time', + submenu: [ + { text: 'YYYY-MM-DD HH:mm:ss', value: 'dateTimeAsIso' }, + { text: 'DD/MM/YYYY h:mm:ss a', value: 'dateTimeAsUS' }, + { text: 'From Now', value: 'dateTimeFromNow' }, + ], + }, + { + text: 'data (IEC)', + submenu: [ + { text: 'bits', value: 'bits' }, + { text: 'bytes', value: 'bytes' }, + { text: 'kibibytes', value: 'kbytes' }, + { text: 'mebibytes', value: 'mbytes' }, + { text: 'gibibytes', value: 'gbytes' }, + ], + }, + { + text: 'data (Metric)', + submenu: [ + { text: 'bits', value: 'decbits' }, + { text: 'bytes', value: 'decbytes' }, + { text: 'kilobytes', value: 'deckbytes' }, + { text: 'megabytes', value: 'decmbytes' }, + { text: 'gigabytes', value: 'decgbytes' }, + ], + }, + { + text: 'data rate', + submenu: [ + { text: 'packets/sec', value: 'pps' }, + { text: 'bits/sec', value: 'bps' }, + { text: 'bytes/sec', value: 'Bps' }, + { text: 'kilobits/sec', value: 'Kbits' }, + { text: 'kilobytes/sec', value: 'KBs' }, + { text: 'megabits/sec', value: 'Mbits' }, + { text: 'megabytes/sec', value: 'MBs' }, + { text: 'gigabytes/sec', value: 'GBs' }, + { text: 'gigabits/sec', value: 'Gbits' }, + ], + }, + { + text: 'throughput', + submenu: [ + { text: 'ops/sec (ops)', value: 'ops' }, + { text: 'reads/sec (rps)', value: 'rps' }, + { text: 'writes/sec (wps)', value: 'wps' }, + { text: 'I/O ops/sec (iops)', value: 'iops' }, + { text: 'ops/min (opm)', value: 'opm' }, + { text: 'reads/min (rpm)', value: 'rpm' }, + { text: 'writes/min (wpm)', value: 'wpm' }, + ], + }, + { + text: 'length', + submenu: [ + { text: 'millimetre (mm)', value: 'lengthmm' }, + { text: 'meter (m)', value: 'lengthm' }, + { text: 'kilometer (km)', value: 'lengthkm' }, + { 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: [ + { text: 'milligram (mg)', value: 'massmg' }, + { text: 'gram (g)', value: 'massg' }, + { text: 'kilogram (kg)', value: 'masskg' }, + { text: 'metric ton (t)', value: 'masst' }, + ], + }, + { + text: 'velocity', + submenu: [ + { text: 'm/s', value: 'velocityms' }, + { text: 'km/h', value: 'velocitykmh' }, + { text: 'mph', value: 'velocitymph' }, + { text: 'knot (kn)', value: 'velocityknot' }, + ], + }, + { + text: 'volume', + submenu: [ + { text: 'millilitre', value: 'mlitre' }, + { text: 'litre', value: 'litre' }, + { text: 'cubic metre', value: 'm3' }, + { text: 'cubic decimetre', value: 'dm3' }, + { text: 'gallons', value: 'gallons' }, + ], + }, + { + text: 'energy', + submenu: [ + { text: 'watt (W)', value: 'watt' }, + { text: 'kilowatt (kW)', value: 'kwatt' }, + { text: 'volt-ampere (VA)', value: 'voltamp' }, + { text: 'kilovolt-ampere (kVA)', value: 'kvoltamp' }, + { text: 'volt-ampere reactive (var)', value: 'voltampreact' }, + { 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' }, + { text: 'Kiloampere (kA)', value: 'kamp' }, + { text: 'Volt (V)', value: 'volt' }, + { text: 'Kilovolt (kV)', value: 'kvolt' }, + { text: 'Decibel-milliwatt (dBm)', value: 'dBm' }, + { text: 'Ohm (Ω)', value: 'ohm' } + ], + }, + { + text: 'temperature', + submenu: [ + { text: 'Celsius (°C)', value: 'celsius' }, + { text: 'Farenheit (°F)', value: 'farenheit' }, + { text: 'Kelvin (K)', value: 'kelvin' }, + ], + }, + { + text: 'pressure', + submenu: [ + { text: 'Millibars', value: 'pressurembar' }, + { text: 'Bars', value: 'pressurebar' }, + { text: 'Kilobars', value: 'pressurekbar' }, + { text: 'Hectopascals', value: 'pressurehpa' }, + { text: 'Inches of mercury', value: 'pressurehg' }, + { text: 'PSI', value: 'pressurepsi' }, + ], + }, + { + text: 'force', + submenu: [ + { text: 'Newton-meters (Nm)', value: 'forceNm' }, + { text: 'Kilonewton-meters (kNm)', value: 'forcekNm' }, + { text: 'Newtons (N)', value: 'forceN' }, + { text: 'Kilonewtons (kN)', value: 'forcekN' }, + ], + }, + { + text: 'flow', + submenu: [ + { text: 'Gallons/min (gpm)', value: 'flowgpm' }, + { text: 'Cubic meters/sec (cms)', value: 'flowcms' }, + { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' }, + { 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' } + ] + } + ]; +}; + +export default kbn; diff --git a/public/app/core/utils/outline.js b/public/app/core/utils/outline.js deleted file mode 100644 index f968b3e51f9..00000000000 --- a/public/app/core/utils/outline.js +++ /dev/null @@ -1,32 +0,0 @@ -// outline.js -// based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/ -(function(d) { - "use strict"; - - var style_element = d.createElement('STYLE'), - dom_events = 'addEventListener' in d, - add_event_listener = function(type, callback) { - // Basic cross-browser event handling - if(dom_events){ - d.addEventListener(type, callback); - } else { - d.attachEvent('on' + type, callback); - } - }, - set_css = function(css_text) { - // Handle setting of