diff --git a/.circleci/config.yml b/.circleci/config.yml
index ec1fcfb411f..7b5a9e7923b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -19,7 +19,7 @@ version: 2
jobs:
mysql-integration-test:
docker:
- - image: circleci/golang:1.11.4
+ - image: circleci/golang:1.11.5
- image: circleci/mysql:5.6-ram
environment:
MYSQL_ROOT_PASSWORD: rootpass
@@ -39,7 +39,7 @@ jobs:
postgres-integration-test:
docker:
- - image: circleci/golang:1.11.4
+ - image: circleci/golang:1.11.5
- image: circleci/postgres:9.3-ram
environment:
POSTGRES_USER: grafanatest
@@ -74,27 +74,16 @@ jobs:
gometalinter:
docker:
- - image: circleci/golang:1.11.4
+ - image: circleci/golang:1.11.5
environment:
# we need CGO because of go-sqlite3
CGO_ENABLED: 1
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
- - run: 'go get -u github.com/alecthomas/gometalinter'
- - run: 'go get -u github.com/tsenart/deadcode'
- - run: 'go get -u github.com/jgautheron/goconst/cmd/goconst'
- - run: 'go get -u github.com/gordonklaus/ineffassign'
- - run: 'go get -u honnef.co/go/tools/cmd/megacheck'
- - run: 'go get -u github.com/opennota/check/cmd/structcheck'
- - run: 'go get -u github.com/mdempsky/unconvert'
- - run: 'go get -u github.com/opennota/check/cmd/varcheck'
- run:
- name: run linters
- command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=goconst --enable=gofmt --enable=ineffassign --enable=megacheck --enable=structcheck --enable=unconvert --enable=varcheck ./...'
- - run:
- name: run go vet
- command: 'go vet ./pkg/...'
+ name: Gometalinter tests
+ command: './scripts/gometalinter.sh'
test-frontend:
docker:
@@ -117,7 +106,7 @@ jobs:
test-backend:
docker:
- - image: circleci/golang:1.11.4
+ - image: circleci/golang:1.11.5
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -127,7 +116,7 @@ jobs:
build-all:
docker:
- - image: grafana/build-container:1.2.2
+ - image: grafana/build-container:1.2.3
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -175,7 +164,7 @@ jobs:
build:
docker:
- - image: grafana/build-container:1.2.2
+ - image: grafana/build-container:1.2.3
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -244,7 +233,7 @@ jobs:
build-enterprise:
docker:
- - image: grafana/build-container:1.2.2
+ - image: grafana/build-container:1.2.3
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -276,7 +265,7 @@ jobs:
build-all-enterprise:
docker:
- - image: grafana/build-container:1.2.2
+ - image: grafana/build-container:1.2.3
working_directory: /go/src/github.com/grafana/grafana
steps:
- checkout
@@ -323,7 +312,7 @@ jobs:
deploy-enterprise-master:
docker:
- - image: grafana/grafana-ci-deploy:1.1.0
+ - image: grafana/grafana-ci-deploy:1.2.0
steps:
- attach_workspace:
at: .
@@ -346,7 +335,7 @@ jobs:
deploy-enterprise-release:
docker:
- - image: grafana/grafana-ci-deploy:1.1.0
+ - image: grafana/grafana-ci-deploy:1.2.0
steps:
- attach_workspace:
at: .
@@ -370,15 +359,15 @@ jobs:
command: './scripts/build/load-signing-key.sh'
- run:
name: Update Debian repository
- command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
+ command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"'
- run:
name: Update RPM repository
- command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
+ command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"'
deploy-master:
docker:
- - image: grafana/grafana-ci-deploy:1.1.0
+ - image: grafana/grafana-ci-deploy:1.2.0
steps:
- attach_workspace:
at: .
@@ -408,7 +397,7 @@ jobs:
deploy-release:
docker:
- - image: grafana/grafana-ci-deploy:1.1.0
+ - image: grafana/grafana-ci-deploy:1.2.0
steps:
- checkout
- attach_workspace:
@@ -433,10 +422,10 @@ jobs:
command: './scripts/build/load-signing-key.sh'
- run:
name: Update Debian repository
- command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
+ command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"'
- run:
name: Update RPM repository
- command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
+ command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"'
workflows:
version: 2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9225d6545e4..67acea4e149 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,29 +1,44 @@
-# 5.5.0 (unreleased)
+# 6.0.0-beta1 (unreleased)
### New Features
* **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
* **Elasticsearch**: Support bucket script pipeline aggregations [#5968](https://github.com/grafana/grafana/issues/5968)
+* **Influxdb**: Add support for time zone (`tz`) clause [#10322](https://github.com/grafana/grafana/issues/10322), thx [@cykl](https://github.com/cykl)
* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
### Minor
+* **Alerting**: Use seperate timeouts for alert evals and notifications [#14701](https://github.com/grafana/grafana/issues/14701), thx [@sharkpc0813](https://github.com/sharkpc0813)
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
* **Elasticsearch**: Add support for moving average and derivative using doc count (metric count) [#8843](https://github.com/grafana/grafana/issues/8843) [#11175](https://github.com/grafana/grafana/issues/11175)
+* **Elasticsearch**: Add support for template variable interpolation in alias field [#4075](https://github.com/grafana/grafana/issues/4075), thx [@SamuelToh](https://github.com/SamuelToh)
+* **Influxdb**: Fix autocomplete of measurements does not escape search string properly [#11503](https://github.com/grafana/grafana/issues/11503), thx [@SamuelToh](https://github.com/SamuelToh)
+* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
+* **Cloudwatch**: Fix Assume Role Arn [#14722](https://github.com/grafana/grafana/issues/14722), thx [@jaken551](https://github.com/jaken551)
+* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
* **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
-* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
-* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK]req(https://github.com/IntegersOfK)
* **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483)
* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
-* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
+* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
+* **Dashboard**: `Min width` changed to `Max per row` for repeating panels. This lets you specify the maximum number of panels to show per row and by that repeated panels will always take up full width of row [#12991](https://github.com/grafana/grafana/pull/12991), thx [@pgiraud](https://github.com/pgiraud)
+* **Dashboard**: Retain decimal precision when exporting CSV [#13929](https://github.com/grafana/grafana/issues/13929), thx [@cinaglia](https://github.com/cinaglia)
+* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
-* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
-* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
+* **Units**: Add Floating Point Operations per Second units [#14558](https://github.com/grafana/grafana/pull/14558), thx [@hahnjo](https://github.com/hahnjo)
+* **Table**: Renders epoch string as date if date column style [#14484](https://github.com/grafana/grafana/issues/14484)
+* **Piechart/Flot**: Fixes multiple piechart instances with donut bug [#15062](https://github.com/grafana/grafana/pull/15062)
+* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
### Bug fixes
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795)
+* **Annotations**: Fix creating annotation when graph panel has no data points position the popup outside viewport [#13765](https://github.com/grafana/grafana/issues/13765), thx [@banjeremy](https://github.com/banjeremy)
+
+### Breaking changes
+* **Text Panel**: The text panel does no longer by default allow unsantizied HTML. [#4117](https://github.com/grafana/grafana/issues/4117). This means that if you have text panels with scripts tags they will no longer work as before. To enable unsafe javascript execution in text panels enable the settings `disable_sanitize_html` under the section `[panels]` in your Grafana ini file, or set env variable `GF_PANELS_DISABLE_SANITIZE_HTML=true`.
+* **Dashboard**: Panel property `minSpan` replaced by `maxPerRow`. Dashboard migration will automatically migrate all dashboard panels using the `minSpan` property to the new `maxPerRow` property [#12991](https://github.com/grafana/grafana/pull/12991)
# 5.4.3 (2019-01-14)
diff --git a/Dockerfile b/Dockerfile
index c3af89b6092..c3e59c8048e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Golang build container
-FROM golang:1.11.4
+FROM golang:1.11.5
WORKDIR $GOPATH/src/github.com/grafana/grafana
@@ -19,11 +19,13 @@ COPY package.json package.json
RUN go run build.go build
# Node build container
-FROM node:8
+FROM node:10.14.2
WORKDIR /usr/src/app/
COPY package.json yarn.lock ./
+COPY packages packages
+
RUN yarn install --pure-lockfile --no-progress
COPY Gruntfile.js tsconfig.json tslint.json ./
diff --git a/ROADMAP.md b/ROADMAP.md
index 891bc9f790b..b5e62578475 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -5,18 +5,22 @@ But it will give you an idea of our current vision and plan.
### Short term (1-2 months)
- PRs & Bugs
- - Multi-Stat panel
+ - React Panel Support
+ - React Query Editor Support
- Metrics & Log Explore UI
-
+ - Grafana UI library shared between grafana & plugins
+ - Seperate visualization from panels
+ - More reuse between Explore & dashboard
+ - Explore logging support for more data sources
+
### Mid term (2-4 months)
- - React Panels
- - Change visualization (panel type) on the fly.
- - Templating Query Editor UI Plugin hook
- - Backend plugins
+ - Drilldown links
+ - Dashboards as code workflows
+ - React migration
+ - New panels
### Long term (4 - 8 months)
- Alerting improvements (silence, per series tracking, etc)
- - Progress on React migration
### In a distant future far far away
- Meta queries
diff --git a/appveyor.yml b/appveyor.yml
index 5f97784dd38..ccf9b5a06e1 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
environment:
nodejs_version: "8"
GOPATH: C:\gopath
- GOVERSION: 1.11.4
+ GOVERSION: 1.11.5
install:
- rmdir c:\go /s /q
diff --git a/build.go b/build.go
index 4486cd3deb9..ebe240d97ef 100644
--- a/build.go
+++ b/build.go
@@ -46,6 +46,8 @@ var (
binaries []string = []string{"grafana-server", "grafana-cli"}
isDev bool = false
enterprise bool = false
+ skipRpmGen bool = false
+ skipDebGen bool = false
)
func main() {
@@ -67,6 +69,8 @@ func main() {
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
+ flag.BoolVar(&skipRpmGen, "skipRpm", skipRpmGen, "skip rpm package generation (default: false)")
+ flag.BoolVar(&skipDebGen, "skipDeb", skipDebGen, "skip deb package generation (default: false)")
flag.Parse()
buildId = shortenBuildId(buildIdRaw)
@@ -165,6 +169,7 @@ func makeLatestDistCopies() {
".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm",
".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz",
+ ".linux-armv6.tar.gz": "dist/grafana-latest.linux-armv6.tar.gz",
".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz",
}
@@ -239,6 +244,8 @@ func createDebPackages() {
previousPkgArch := pkgArch
if pkgArch == "armv7" {
pkgArch = "armhf"
+ } else if pkgArch == "armv6" {
+ pkgArch = "armel"
}
createPackage(linuxPackageOptions{
packageType: "deb",
@@ -289,8 +296,13 @@ func createRpmPackages() {
}
func createLinuxPackages() {
- createDebPackages()
- createRpmPackages()
+ if !skipDebGen {
+ createDebPackages()
+ }
+
+ if !skipRpmGen {
+ createRpmPackages()
+ }
}
func createPackage(options linuxPackageOptions) {
diff --git a/conf/defaults.ini b/conf/defaults.ini
index 7f61ac96870..6fc4cf2e4de 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -106,6 +106,22 @@ path = grafana.db
# For "sqlite3" only. cache mode setting used for connecting to the database
cache_mode = private
+#################################### Login ###############################
+
+[login]
+
+# Login cookie name
+cookie_name = grafana_session
+
+# How many days an session can be unused before we inactivate it
+login_remember_days = 7
+
+# How often should the login token be rotated. default to '10m'
+rotate_token_minutes = 10
+
+# How long should Grafana keep expired tokens before deleting them
+delete_expired_token_after_days = 30
+
#################################### Session #############################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
@@ -175,11 +191,6 @@ admin_password = admin
# used for signing
secret_key = SW2YcwTIb9zpOOhoPsMm
-# Auto-login remember days
-login_remember_days = 7
-cookie_username = grafana_user
-cookie_remember_name = grafana_remember
-
# disable gravatar profile images
disable_gravatar = false
@@ -189,6 +200,9 @@ data_source_proxy_whitelist =
# disable protection against brute force login attempts
disable_brute_force_login_protection = false
+# set cookies as https only. default is false
+https_flag_cookies = false
+
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options
@@ -570,6 +584,7 @@ callback_url =
[panels]
enable_alpha = false
+disable_sanitize_html = false
[enterprise]
license_path =
diff --git a/conf/sample.ini b/conf/sample.ini
index 014016d45bc..0f1c02dc231 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -102,6 +102,22 @@ log_queries =
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
;cache_mode = private
+#################################### Login ###############################
+
+[login]
+
+# Login cookie name
+;cookie_name = grafana_session
+
+# How many days an session can be unused before we inactivate it
+;login_remember_days = 7
+
+# How often should the login token be rotated. default to '10'
+;rotate_token_minutes = 10
+
+# How long should Grafana keep expired tokens before deleting them
+;delete_expired_token_after_days = 30
+
#################################### Session ####################################
[session]
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
@@ -162,11 +178,6 @@ log_queries =
# used for signing
;secret_key = SW2YcwTIb9zpOOhoPsMm
-# Auto-login remember days
-;login_remember_days = 7
-;cookie_username = grafana_user
-;cookie_remember_name = grafana_remember
-
# disable gravatar profile images
;disable_gravatar = false
@@ -176,6 +187,9 @@ log_queries =
# disable protection against brute force login attempts
;disable_brute_force_login_protection = false
+# set cookies as https only. default is false
+;https_flag_cookies = false
+
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options
@@ -495,3 +509,8 @@ log_queries =
# Path to a valid Grafana Enterprise license.jwt file
;license_path =
+[panels]
+;enable_alpha = false
+# If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities.
+;disable_sanitize_html = false
+
diff --git a/devenv/dev-dashboards/panel_tests_gauge.json b/devenv/dev-dashboards/panel_tests_gauge.json
new file mode 100644
index 00000000000..c6e81ececc8
--- /dev/null
+++ b/devenv/dev-dashboards/panel_tests_gauge.json
@@ -0,0 +1,1250 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "iteration": 1547810606599,
+ "links": [],
+ "panels": [
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 11,
+ "panels": [],
+ "title": "Value options tests",
+ "type": "row"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 8,
+ "w": 5,
+ "x": 0,
+ "y": 1
+ },
+ "id": 2,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "2",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "ms",
+ "valueMappings": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Average, 2 decimals, ms unit",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 5,
+ "y": 1
+ },
+ "id": 5,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "max",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "ms",
+ "valueMappings": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Max (90 ms), no decimals",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 8,
+ "w": 5,
+ "x": 11,
+ "y": 1
+ },
+ "id": 6,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "p",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "s",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Current (10 ms), no unit, prefix (p), suffix (s)",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 4,
+ "w": 3,
+ "x": 16,
+ "y": 1
+ },
+ "id": 16,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 4,
+ "w": 5,
+ "x": 19,
+ "y": 1
+ },
+ "id": 18,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10,91"
+ }
+ ],
+ "timeFrom": "1h",
+ "timeShift": null,
+ "title": "",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 4,
+ "w": 3,
+ "x": 16,
+ "y": 5
+ },
+ "id": 17,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 4,
+ "w": 5,
+ "x": 19,
+ "y": 5
+ },
+ "id": 19,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10,81"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "",
+ "type": "gauge"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 9
+ },
+ "id": 15,
+ "panels": [],
+ "title": "Value Mappings",
+ "type": "row"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 8,
+ "w": 4,
+ "x": 0,
+ "y": 10
+ },
+ "id": 12,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": [
+ {
+ "from": "",
+ "id": 1,
+ "operator": "",
+ "text": "TEN",
+ "to": "",
+ "type": 1,
+ "value": "10"
+ }
+ ]
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "value mapping 10 -> TEN",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "description": "should read N/A",
+ "gridPos": {
+ "h": 8,
+ "w": 4,
+ "x": 4,
+ "y": 10
+ },
+ "id": 13,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": [
+ {
+ "from": "",
+ "id": 1,
+ "operator": "",
+ "text": "N/A",
+ "to": "",
+ "type": 1,
+ "value": "null"
+ }
+ ]
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10,null,null,null,null"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "value mapping null -> N/A",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "description": "should read N/A",
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 8,
+ "y": 10
+ },
+ "id": 20,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": [
+ {
+ "from": "0",
+ "id": 1,
+ "operator": "",
+ "text": "OK",
+ "to": "10",
+ "type": 2,
+ "value": "null"
+ }
+ ]
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10,null,null,null,null,10"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "value mapping range, 0-10 -> OK, value 10",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "description": "should read N/A",
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 14,
+ "y": 10
+ },
+ "id": 21,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "current",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "none",
+ "valueMappings": [
+ {
+ "from": "0",
+ "id": 1,
+ "operator": "",
+ "text": "OK",
+ "to": "90",
+ "type": 2,
+ "value": "null"
+ },
+ {
+ "from": "90",
+ "id": 2,
+ "operator": "",
+ "text": "BAD",
+ "to": "100",
+ "type": 2,
+ "value": ""
+ }
+ ]
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,10,null,null,null,null,10,95"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "value mapping range, 90-100 -> BAD, value 90",
+ "type": "gauge"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 18
+ },
+ "id": 9,
+ "panels": [],
+ "title": "Templating & Repeat",
+ "type": "row"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 0,
+ "y": 19
+ },
+ "id": 7,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "2",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "$Servers",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "ms",
+ "valueMappings": []
+ },
+ "repeat": "Servers",
+ "repeatDirection": "h",
+ "scopedVars": {
+ "Servers": {
+ "selected": false,
+ "text": "server1",
+ "value": "server1"
+ }
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "repeat $Servers",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 6,
+ "y": 19
+ },
+ "id": 22,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "2",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "$Servers",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "ms",
+ "valueMappings": []
+ },
+ "repeat": null,
+ "repeatDirection": "h",
+ "repeatIteration": 1547810606599,
+ "repeatPanelId": 7,
+ "scopedVars": {
+ "Servers": {
+ "selected": false,
+ "text": "server2",
+ "value": "server2"
+ }
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "repeat $Servers",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 12,
+ "y": 19
+ },
+ "id": 23,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "2",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "$Servers",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "ms",
+ "valueMappings": []
+ },
+ "repeat": null,
+ "repeatDirection": "h",
+ "repeatIteration": 1547810606599,
+ "repeatPanelId": 7,
+ "scopedVars": {
+ "Servers": {
+ "selected": false,
+ "text": "server3",
+ "value": "server3"
+ }
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "repeat $Servers",
+ "type": "gauge"
+ },
+ {
+ "datasource": "gdev-testdata",
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 18,
+ "y": 19
+ },
+ "id": 24,
+ "links": [],
+ "nullPointMode": "null",
+ "options-gauge": {
+ "baseColor": "#299c46",
+ "decimals": "2",
+ "maxValue": 100,
+ "minValue": 0,
+ "options": {
+ "baseColor": "#299c46",
+ "decimals": 0,
+ "maxValue": 100,
+ "minValue": 0,
+ "prefix": "",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [],
+ "unit": "none",
+ "valueMappings": []
+ },
+ "prefix": "$Servers",
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true,
+ "stat": "avg",
+ "suffix": "",
+ "thresholds": [
+ {
+ "color": "#e24d42",
+ "index": 2,
+ "value": 90
+ },
+ {
+ "color": "#ef843c",
+ "index": 1,
+ "value": 75
+ },
+ {
+ "color": "#7EB26D",
+ "index": 0,
+ "value": null
+ }
+ ],
+ "unit": "ms",
+ "valueMappings": []
+ },
+ "repeat": null,
+ "repeatDirection": "h",
+ "repeatIteration": 1547810606599,
+ "repeatPanelId": 7,
+ "scopedVars": {
+ "Servers": {
+ "selected": false,
+ "text": "server4",
+ "value": "server4"
+ }
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "scenarioId": "csv_metric_values",
+ "stringInput": "1,20,90,30,5,0"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "repeat $Servers",
+ "type": "gauge"
+ }
+ ],
+ "refresh": false,
+ "schemaVersion": 17,
+ "style": "dark",
+ "tags": [
+ "gdev",
+ "panel-tests"
+ ],
+ "templating": {
+ "list": [
+ {
+ "allValue": null,
+ "current": {
+ "selected": true,
+ "tags": [],
+ "text": "All",
+ "value": [
+ "$__all"
+ ]
+ },
+ "hide": 0,
+ "includeAll": true,
+ "label": null,
+ "multi": true,
+ "name": "Servers",
+ "options": [
+ {
+ "selected": true,
+ "text": "All",
+ "value": "$__all"
+ },
+ {
+ "selected": false,
+ "text": "server1",
+ "value": "server1"
+ },
+ {
+ "selected": false,
+ "text": "server2",
+ "value": "server2"
+ },
+ {
+ "selected": false,
+ "text": "server3",
+ "value": "server3"
+ },
+ {
+ "selected": false,
+ "text": "server4",
+ "value": "server4"
+ }
+ ],
+ "query": "server1,server2,server3,server4",
+ "skipUrlSync": false,
+ "type": "custom"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-1h",
+ "to": "now"
+ },
+ "timepicker": {
+ "refresh_intervals": [
+ "5s",
+ "10s",
+ "30s",
+ "1m",
+ "5m",
+ "15m",
+ "30m",
+ "1h",
+ "2h",
+ "1d"
+ ],
+ "time_options": [
+ "5m",
+ "15m",
+ "1h",
+ "6h",
+ "12h",
+ "24h",
+ "2d",
+ "7d",
+ "30d"
+ ]
+ },
+ "timezone": "",
+ "title": "Panel Tests - Gauge",
+ "uid": "_5rDmaQiz",
+ "version": 5
+}
diff --git a/devenv/docker/ha_test/docker-compose.yaml b/devenv/docker/ha_test/docker-compose.yaml
index 1195e2a977c..504ee86404d 100644
--- a/devenv/docker/ha_test/docker-compose.yaml
+++ b/devenv/docker/ha_test/docker-compose.yaml
@@ -54,7 +54,8 @@ services:
# - GF_DATABASE_SSL_MODE=disable
# - GF_SESSION_PROVIDER=postgres
# - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
- - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug
+ - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug
+ - GF_LOGIN_ROTATE_TOKEN_MINUTES=2
ports:
- 3000
depends_on:
diff --git a/devenv/docker/loadtest/README.md b/devenv/docker/loadtest/README.md
new file mode 100644
index 00000000000..8e724637acb
--- /dev/null
+++ b/devenv/docker/loadtest/README.md
@@ -0,0 +1,69 @@
+# Grafana load test
+
+Runs load tests and checks using [k6](https://k6.io/).
+
+## Prerequisites
+
+Docker
+
+## Run
+
+Run load test for 15 minutes:
+
+```bash
+$ ./run.sh
+```
+
+Run load test for custom duration:
+
+```bash
+$ ./run.sh -d 10s
+```
+
+Example output:
+
+```bash
+
+ /\ |‾‾| /‾‾/ /‾/
+ /\ / \ | |_/ / / /
+ / \/ \ | | / ‾‾\
+ / \ | |‾\ \ | (_) |
+ / __________ \ |__| \__\ \___/ .io
+
+ execution: local
+ output: -
+ script: src/auth_token_test.js
+
+ duration: 15m0s, iterations: -
+ vus: 2, max: 2
+
+ done [==========================================================] 15m0s / 15m0s
+
+ █ user auth token test
+
+ █ user authenticates thru ui with username and password
+
+ ✓ response status is 200
+ ✓ response has cookie 'grafana_session' with 32 characters
+
+ █ batch tsdb requests
+
+ ✓ response status is 200
+
+ checks.....................: 100.00% ✓ 32844 ✗ 0
+ data_received..............: 411 MB 457 kB/s
+ data_sent..................: 12 MB 14 kB/s
+ group_duration.............: avg=95.64ms min=16.42ms med=94.35ms max=307.52ms p(90)=137.78ms p(95)=146.75ms
+ http_req_blocked...........: avg=1.27ms min=942ns med=610.08µs max=48.32ms p(90)=2.92ms p(95)=4.25ms
+ http_req_connecting........: avg=1.06ms min=0s med=456.79µs max=47.19ms p(90)=2.55ms p(95)=3.78ms
+ http_req_duration..........: avg=58.16ms min=1ms med=52.59ms max=293.35ms p(90)=109.53ms p(95)=120.19ms
+ http_req_receiving.........: avg=38.98µs min=6.43µs med=32.55µs max=16.2ms p(90)=64.63µs p(95)=78.8µs
+ http_req_sending...........: avg=328.66µs min=8.09µs med=110.77µs max=44.13ms p(90)=552.65µs p(95)=1.09ms
+ http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
+ http_req_waiting...........: avg=57.79ms min=935.02µs med=52.15ms max=293.06ms p(90)=109.04ms p(95)=119.71ms
+ http_reqs..................: 34486 38.317775/s
+ iteration_duration.........: avg=1.09s min=1.81µs med=1.09s max=1.3s p(90)=1.13s p(95)=1.14s
+ iterations.................: 1642 1.824444/s
+ vus........................: 2 min=2 max=2
+ vus_max....................: 2 min=2 max=2
+```
diff --git a/devenv/docker/loadtest/auth_token_test.js b/devenv/docker/loadtest/auth_token_test.js
new file mode 100644
index 00000000000..e1356fb6f9a
--- /dev/null
+++ b/devenv/docker/loadtest/auth_token_test.js
@@ -0,0 +1,71 @@
+import { sleep, check, group } from 'k6';
+import { createClient, createBasicAuthClient } from './modules/client.js';
+import { createTestOrgIfNotExists, createTestdataDatasourceIfNotExists } from './modules/util.js';
+
+export let options = {
+ noCookiesReset: true
+};
+
+let endpoint = __ENV.URL || 'http://localhost:3000';
+const client = createClient(endpoint);
+
+export const setup = () => {
+ const basicAuthClient = createBasicAuthClient(endpoint, 'admin', 'admin');
+ const orgId = createTestOrgIfNotExists(basicAuthClient);
+ const datasourceId = createTestdataDatasourceIfNotExists(basicAuthClient);
+ client.withOrgId(orgId);
+ return {
+ orgId: orgId,
+ datasourceId: datasourceId,
+ };
+}
+
+export default (data) => {
+ group("user auth token test", () => {
+ if (__ITER === 0) {
+ group("user authenticates thru ui with username and password", () => {
+ let res = client.ui.login('admin', 'admin');
+
+ check(res, {
+ 'response status is 200': (r) => r.status === 200,
+ 'response has cookie \'grafana_session\' with 32 characters': (r) => r.cookies.grafana_session[0].value.length === 32,
+ });
+ });
+ }
+
+ if (__ITER !== 0) {
+ group("batch tsdb requests", () => {
+ const batchCount = 20;
+ const requests = [];
+ const payload = {
+ from: '1547765247624',
+ to: '1547768847624',
+ queries: [{
+ refId: 'A',
+ scenarioId: 'random_walk',
+ intervalMs: 10000,
+ maxDataPoints: 433,
+ datasourceId: data.datasourceId,
+ }]
+ };
+
+ requests.push({ method: 'GET', url: '/api/annotations?dashboardId=2074&from=1548078832772&to=1548082432772' });
+
+ for (let n = 0; n < batchCount; n++) {
+ requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload });
+ }
+
+ let responses = client.batch(requests);
+ for (let n = 0; n < batchCount; n++) {
+ check(responses[n], {
+ 'response status is 200': (r) => r.status === 200,
+ });
+ }
+ });
+ }
+ });
+
+ sleep(1)
+}
+
+export const teardown = (data) => {}
diff --git a/devenv/docker/loadtest/modules/client.js b/devenv/docker/loadtest/modules/client.js
new file mode 100644
index 00000000000..bda0da64564
--- /dev/null
+++ b/devenv/docker/loadtest/modules/client.js
@@ -0,0 +1,187 @@
+import http from "k6/http";
+import encoding from 'k6/encoding';
+
+export const UIEndpoint = class UIEndpoint {
+ constructor(httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ login(username, pwd) {
+ const payload = { user: username, password: pwd };
+ return this.httpClient.formPost('/login', payload);
+ }
+}
+
+export const DatasourcesEndpoint = class DatasourcesEndpoint {
+ constructor(httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ getById(id) {
+ return this.httpClient.get(`/datasources/${id}`);
+ }
+
+ getByName(name) {
+ return this.httpClient.get(`/datasources/name/${name}`);
+ }
+
+ create(payload) {
+ return this.httpClient.post(`/datasources`, JSON.stringify(payload));
+ }
+
+ delete(id) {
+ return this.httpClient.delete(`/datasources/${id}`);
+ }
+}
+
+export const OrganizationsEndpoint = class OrganizationsEndpoint {
+ constructor(httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ getById(id) {
+ return this.httpClient.get(`/orgs/${id}`);
+ }
+
+ getByName(name) {
+ return this.httpClient.get(`/orgs/name/${name}`);
+ }
+
+ create(name) {
+ let payload = {
+ name: name,
+ };
+ return this.httpClient.post(`/orgs`, JSON.stringify(payload));
+ }
+
+ delete(id) {
+ return this.httpClient.delete(`/orgs/${id}`);
+ }
+}
+
+export const GrafanaClient = class GrafanaClient {
+ constructor(httpClient) {
+ httpClient.onBeforeRequest = this.onBeforeRequest;
+ this.raw = httpClient;
+ this.ui = new UIEndpoint(httpClient);
+ this.orgs = new OrganizationsEndpoint(httpClient.withUrl('/api'));
+ this.datasources = new DatasourcesEndpoint(httpClient.withUrl('/api'));
+ }
+
+ batch(requests) {
+ return this.raw.batch(requests);
+ }
+
+ withOrgId(orgId) {
+ this.orgId = orgId;
+ }
+
+ onBeforeRequest(params) {
+ if (this.orgId && this.orgId > 0) {
+ params = params.headers || {};
+ params.headers["X-Grafana-Org-Id"] = this.orgId;
+ }
+ }
+}
+
+export const BaseClient = class BaseClient {
+ constructor(url, subUrl) {
+ if (url.endsWith('/')) {
+ url = url.substring(0, url.length - 1);
+ }
+
+ if (subUrl.endsWith('/')) {
+ subUrl = subUrl.substring(0, subUrl.length - 1);
+ }
+
+ this.url = url + subUrl;
+ this.onBeforeRequest = () => {};
+ }
+
+ withUrl(subUrl) {
+ let c = new BaseClient(this.url, subUrl);
+ c.onBeforeRequest = this.onBeforeRequest;
+ return c;
+ }
+
+ beforeRequest(params) {
+
+ }
+
+ get(url, params) {
+ params = params || {};
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ return http.get(this.url + url, params);
+ }
+
+ formPost(url, body, params) {
+ params = params || {};
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ return http.post(this.url + url, body, params);
+ }
+
+ post(url, body, params) {
+ params = params || {};
+ params.headers = params.headers || {};
+ params.headers['Content-Type'] = 'application/json';
+
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ return http.post(this.url + url, body, params);
+ }
+
+ delete(url, params) {
+ params = params || {};
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ return http.del(this.url + url, null, params);
+ }
+
+ batch(requests) {
+ for (let n = 0; n < requests.length; n++) {
+ let params = requests[n].params || {};
+ params.headers = params.headers || {};
+ params.headers['Content-Type'] = 'application/json';
+ this.beforeRequest(params);
+ this.onBeforeRequest(params);
+ requests[n].params = params;
+ requests[n].url = this.url + requests[n].url;
+ if (requests[n].body) {
+ requests[n].body = JSON.stringify(requests[n].body);
+ }
+ }
+
+ return http.batch(requests);
+ }
+}
+
+export class BasicAuthClient extends BaseClient {
+ constructor(url, subUrl, username, password) {
+ super(url, subUrl);
+ this.username = username;
+ this.password = password;
+ }
+
+ withUrl(subUrl) {
+ let c = new BasicAuthClient(this.url, subUrl, this.username, this.password);
+ c.onBeforeRequest = this.onBeforeRequest;
+ return c;
+ }
+
+ beforeRequest(params) {
+ params = params || {};
+ params.headers = params.headers || {};
+ let token = `${this.username}:${this.password}`;
+ params.headers['Authorization'] = `Basic ${encoding.b64encode(token)}`;
+ }
+}
+
+export const createClient = (url) => {
+ return new GrafanaClient(new BaseClient(url, ''));
+}
+
+export const createBasicAuthClient = (url, username, password) => {
+ return new GrafanaClient(new BasicAuthClient(url, '', username, password));
+}
diff --git a/devenv/docker/loadtest/modules/util.js b/devenv/docker/loadtest/modules/util.js
new file mode 100644
index 00000000000..af6d4cdac09
--- /dev/null
+++ b/devenv/docker/loadtest/modules/util.js
@@ -0,0 +1,35 @@
+export const createTestOrgIfNotExists = (client) => {
+ let orgId = 0;
+ let res = client.orgs.getByName('k6');
+ if (res.status === 404) {
+ res = client.orgs.create('k6');
+ if (res.status !== 200) {
+ throw new Error('Expected 200 response status when creating org');
+ }
+ orgId = res.json().orgId;
+ } else {
+ orgId = res.json().id;
+ }
+
+ client.withOrgId(orgId);
+ return orgId;
+}
+
+export const createTestdataDatasourceIfNotExists = (client) => {
+ const payload = {
+ access: 'proxy',
+ isDefault: false,
+ name: 'k6-testdata',
+ type: 'testdata',
+ };
+
+ let res = client.datasources.getByName(payload.name);
+ if (res.status === 404) {
+ res = client.datasources.create(payload);
+ if (res.status !== 200) {
+ throw new Error('Expected 200 response status when creating datasource');
+ }
+ }
+
+ return res.json().id;
+}
diff --git a/devenv/docker/loadtest/run.sh b/devenv/docker/loadtest/run.sh
new file mode 100755
index 00000000000..474d75383b6
--- /dev/null
+++ b/devenv/docker/loadtest/run.sh
@@ -0,0 +1,24 @@
+#/bin/bash
+
+PWD=$(pwd)
+
+run() {
+ duration='15m'
+ url='http://localhost:3000'
+
+ while getopts ":d:u:" o; do
+ case "${o}" in
+ d)
+ duration=${OPTARG}
+ ;;
+ u)
+ url=${OPTARG}
+ ;;
+ esac
+ done
+ shift $((OPTIND-1))
+
+ docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus 2 --duration $duration src/auth_token_test.js
+}
+
+run "$@"
diff --git a/docs/sources/auth/gitlab.md b/docs/sources/auth/gitlab.md
index 541aed3fd1f..b6028b0a2a7 100644
--- a/docs/sources/auth/gitlab.md
+++ b/docs/sources/auth/gitlab.md
@@ -47,7 +47,7 @@ authentication:
```bash
[auth.gitlab]
-enabled = false
+enabled = true
allow_sign_up = false
client_id = GITLAB_APPLICATION_ID
client_secret = GITLAB_SECRET
diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md
index e2bcb50bb1d..22f9f38c854 100644
--- a/docs/sources/features/datasources/cloudwatch.md
+++ b/docs/sources/features/datasources/cloudwatch.md
@@ -38,7 +38,7 @@ Name | Description
### IAM Roles
-Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If you grafana
+Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If your Grafana
server is running on AWS you can use IAM Roles and authentication will be handled automatically.
Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md
index 9aaf29ec5f4..364b55b0cfc 100644
--- a/docs/sources/http_api/data_source.md
+++ b/docs/sources/http_api/data_source.md
@@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"defaultRegion": "us-west-1"
},
"secureJsonData": {
- "accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
- "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
+ "accessKey": "Ol4pIDpeKSA6XikgOl4p",
+ "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
}
}
```
diff --git a/docs/sources/http_api/other.md b/docs/sources/http_api/other.md
index 5bf0cde05fe..ea905bf88f0 100644
--- a/docs/sources/http_api/other.md
+++ b/docs/sources/http_api/other.md
@@ -82,4 +82,29 @@ HTTP/1.1 200
Content-Type: application/json
{"message": "Logged in"}
-```
\ No newline at end of file
+```
+
+# Health API
+
+## Returns health information about Grafana
+
+`GET /api/health`
+
+**Example Request**
+
+```http
+GET /api/health
+Accept: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200 OK
+
+{
+ "commit": "087143285",
+ "database": "ok",
+ "version": "5.1.3"
+}
+```
diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 0e5a55b3c0e..46bab83654e 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -391,6 +391,12 @@ value is `true`.
If you want to track Grafana usage via Google analytics specify *your* Universal
Analytics ID here. By default this feature is disabled.
+### check_for_updates
+
+Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used
+in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor
+send any sensitive information.
+
## [dashboards]
@@ -589,3 +595,14 @@ Default setting for how Grafana handles nodata or null values in alerting. (aler
Alert notifications can include images, but rendering many images at the same time can overload the server.
This limit will protect the server from render overloading and make sure notifications are sent out quickly. Default
value is `5`.
+
+## [panels]
+
+### enable_alpha
+Set to true if you want to test panels that are not yet ready for general usage.
+
+### disable_sanitize_html
+If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities. Default
+is false. This settings was introduced in Grafana v6.0.
+
+
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index 71ce6bdd2ae..3ef32b1b10f 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -52,6 +52,7 @@ Filter Option | Example | Raw | Interpolated | Description
`csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
+`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.
Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1).
diff --git a/package.json b/package.json
index 470101ff0c4..c794375793b 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
- "version": "5.5.0-pre1",
+ "version": "6.0.0-pre1",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -188,7 +188,8 @@
"slate-react": "^0.12.4",
"tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
- "tinycolor2": "^1.4.1"
+ "tinycolor2": "^1.4.1",
+ "xss": "^1.0.3"
},
"resolutions": {
"caniuse-db": "1.0.30000772",
diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
index 519e755b474..12b5ff8062e 100644
--- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
+++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
@@ -1,4 +1,5 @@
import React, { PureComponent } from 'react';
+import _ from 'lodash';
import Scrollbars from 'react-custom-scrollbars';
interface Props {
@@ -6,8 +7,11 @@ interface Props {
autoHide?: boolean;
autoHideTimeout?: number;
autoHideDuration?: number;
- autoMaxHeight?: string;
+ autoHeightMax?: string;
hideTracksWhenNotNeeded?: boolean;
+ scrollTop?: number;
+ setScrollTop: (event: any) => void;
+ autoHeightMin?: number | string;
}
/**
@@ -16,24 +20,67 @@ interface Props {
export class CustomScrollbar extends PureComponent {
static defaultProps: Partial = {
customClassName: 'custom-scrollbars',
- autoHide: true,
+ autoHide: false,
autoHideTimeout: 200,
autoHideDuration: 200,
- autoMaxHeight: '100%',
+ setScrollTop: () => {},
hideTracksWhenNotNeeded: false,
+ autoHeightMin: '0',
+ autoHeightMax: '100%',
};
+ private ref: React.RefObject;
+
+ constructor(props: Props) {
+ super(props);
+ this.ref = React.createRef();
+ }
+
+ updateScroll() {
+ const ref = this.ref.current;
+
+ if (ref && !_.isNil(this.props.scrollTop)) {
+ if (this.props.scrollTop > 10000) {
+ ref.scrollToBottom();
+ } else {
+ ref.scrollTop(this.props.scrollTop);
+ }
+ }
+ }
+
+ componentDidMount() {
+ this.updateScroll();
+ }
+
+ componentDidUpdate() {
+ this.updateScroll();
+ }
+
render() {
- const { customClassName, children, autoMaxHeight } = this.props;
+ const {
+ customClassName,
+ children,
+ autoHeightMax,
+ autoHeightMin,
+ setScrollTop,
+ autoHide,
+ autoHideTimeout,
+ hideTracksWhenNotNeeded,
+ } = this.props;
return (
}
renderTrackVertical={props => }
renderThumbHorizontal={props => }
diff --git a/packages/grafana-ui/src/components/FormField/FormField.test.tsx b/packages/grafana-ui/src/components/FormField/FormField.test.tsx
new file mode 100644
index 00000000000..3c89a347e86
--- /dev/null
+++ b/packages/grafana-ui/src/components/FormField/FormField.test.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { FormField, Props } from './FormField';
+
+const setup = (propOverrides?: object) => {
+ const props: Props = {
+ label: 'Test',
+ labelWidth: 11,
+ value: 10,
+ onChange: jest.fn(),
+ };
+
+ Object.assign(props, propOverrides);
+
+ return shallow();
+};
+
+describe('Render', () => {
+ it('should render component', () => {
+ const wrapper = setup();
+
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/packages/grafana-ui/src/components/FormField/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx
new file mode 100644
index 00000000000..593678c7383
--- /dev/null
+++ b/packages/grafana-ui/src/components/FormField/FormField.tsx
@@ -0,0 +1,25 @@
+import React, { InputHTMLAttributes, FunctionComponent } from 'react';
+import { FormLabel } from '..';
+
+export interface Props extends InputHTMLAttributes {
+ label: string;
+ labelWidth?: number;
+ inputWidth?: number;
+}
+
+const defaultProps = {
+ labelWidth: 6,
+ inputWidth: 12,
+};
+
+const FormField: FunctionComponent = ({ label, labelWidth, inputWidth, ...inputProps }) => {
+ return (
+
+ {label}
+
+
+ );
+};
+
+FormField.defaultProps = defaultProps;
+export { FormField };
diff --git a/packages/grafana-ui/src/components/FormField/_FormField.scss b/packages/grafana-ui/src/components/FormField/_FormField.scss
new file mode 100644
index 00000000000..36955e2fca6
--- /dev/null
+++ b/packages/grafana-ui/src/components/FormField/_FormField.scss
@@ -0,0 +1,12 @@
+.form-field {
+ margin-bottom: $gf-form-margin;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ text-align: left;
+ position: relative;
+
+ &--grow {
+ flex-grow: 1;
+ }
+}
diff --git a/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
new file mode 100644
index 00000000000..99eb0803149
--- /dev/null
+++ b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render should render component 1`] = `
+
+
+ Test
+
+
+
+`;
diff --git a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx
new file mode 100644
index 00000000000..2bd4fbc153b
--- /dev/null
+++ b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx
@@ -0,0 +1,42 @@
+import React, { FunctionComponent, ReactNode } from 'react';
+import classNames from 'classnames';
+import { Tooltip } from '..';
+
+interface Props {
+ children: ReactNode;
+ className?: string;
+ htmlFor?: string;
+ isFocused?: boolean;
+ isInvalid?: boolean;
+ tooltip?: string;
+ width?: number;
+}
+
+export const FormLabel: FunctionComponent = ({
+ children,
+ isFocused,
+ isInvalid,
+ className,
+ htmlFor,
+ tooltip,
+ width,
+ ...rest
+}) => {
+ const classes = classNames(`gf-form-label width-${width ? width : '10'}`, className, {
+ 'gf-form-label--is-focused': isFocused,
+ 'gf-form-label--is-invalid': isInvalid,
+ });
+
+ return (
+
+ );
+};
diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
new file mode 100644
index 00000000000..396b7a03162
--- /dev/null
+++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
@@ -0,0 +1,147 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { Gauge, Props } from './Gauge';
+import { TimeSeriesVMs } from '../../types/series';
+import { ValueMapping, MappingType } from '../../types';
+
+jest.mock('jquery', () => ({
+ plot: jest.fn(),
+}));
+
+const setup = (propOverrides?: object) => {
+ const props: Props = {
+ maxValue: 100,
+ valueMappings: [],
+ minValue: 0,
+ prefix: '',
+ showThresholdMarkers: true,
+ showThresholdLabels: false,
+ suffix: '',
+ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
+ unit: 'none',
+ stat: 'avg',
+ height: 300,
+ width: 300,
+ timeSeries: {} as TimeSeriesVMs,
+ decimals: 0,
+ };
+
+ Object.assign(props, propOverrides);
+
+ const wrapper = shallow();
+ const instance = wrapper.instance() as Gauge;
+
+ return {
+ instance,
+ wrapper,
+ };
+};
+
+describe('Get font color', () => {
+ it('should get first threshold color when only one threshold', () => {
+ const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
+
+ expect(instance.getFontColor(49)).toEqual('#7EB26D');
+ });
+
+ it('should get the threshold color if value is same as a threshold', () => {
+ const { instance } = setup({
+ thresholds: [
+ { index: 2, value: 75, color: '#6ED0E0' },
+ { index: 1, value: 50, color: '#EAB839' },
+ { index: 0, value: -Infinity, color: '#7EB26D' },
+ ],
+ });
+
+ expect(instance.getFontColor(50)).toEqual('#EAB839');
+ });
+
+ it('should get the nearest threshold color between thresholds', () => {
+ const { instance } = setup({
+ thresholds: [
+ { index: 2, value: 75, color: '#6ED0E0' },
+ { index: 1, value: 50, color: '#EAB839' },
+ { index: 0, value: -Infinity, color: '#7EB26D' },
+ ],
+ });
+
+ expect(instance.getFontColor(55)).toEqual('#EAB839');
+ });
+});
+
+describe('Get thresholds formatted', () => {
+ it('should return first thresholds color for min and max', () => {
+ const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
+
+ expect(instance.getFormattedThresholds()).toEqual([
+ { value: 0, color: '#7EB26D' },
+ { value: 100, color: '#7EB26D' },
+ ]);
+ });
+
+ it('should get the correct formatted values when thresholds are added', () => {
+ const { instance } = setup({
+ thresholds: [
+ { index: 2, value: 75, color: '#6ED0E0' },
+ { index: 1, value: 50, color: '#EAB839' },
+ { index: 0, value: -Infinity, color: '#7EB26D' },
+ ],
+ });
+
+ expect(instance.getFormattedThresholds()).toEqual([
+ { value: 0, color: '#7EB26D' },
+ { value: 50, color: '#7EB26D' },
+ { value: 75, color: '#EAB839' },
+ { value: 100, color: '#6ED0E0' },
+ ]);
+ });
+});
+
+describe('Format value', () => {
+ it('should return if value isNaN', () => {
+ const valueMappings: ValueMapping[] = [];
+ const value = 'N/A';
+ const { instance } = setup({ valueMappings });
+
+ const result = instance.formatValue(value);
+
+ expect(result).toEqual('N/A');
+ });
+
+ it('should return formatted value if there are no value mappings', () => {
+ const valueMappings: ValueMapping[] = [];
+ const value = '6';
+ const { instance } = setup({ valueMappings, decimals: 1 });
+
+ const result = instance.formatValue(value);
+
+ expect(result).toEqual(' 6.0 ');
+ });
+
+ it('should return formatted value if there are no matching value mappings', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+ { id: 1, operator: '', text: '1-9', type: MappingType.RangeToText, from: '1', to: '9' },
+ ];
+ const value = '10';
+ const { instance } = setup({ valueMappings, decimals: 1 });
+
+ const result = instance.formatValue(value);
+
+ expect(result).toEqual(' 10.0 ');
+ });
+
+ it('should return mapped value if there are matching value mappings', () => {
+ const valueMappings: ValueMapping[] = [
+ { id: 0, operator: '', text: '1-20', type: MappingType.RangeToText, from: '1', to: '20' },
+ { id: 1, operator: '', text: 'elva', type: MappingType.ValueToText, value: '11' },
+ ];
+ const value = '11';
+ const { instance } = setup({ valueMappings, decimals: 1 });
+
+ const result = instance.formatValue(value);
+
+ expect(result).toEqual(' 1-20 ');
+ });
+});
diff --git a/public/app/viz/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx
similarity index 59%
rename from public/app/viz/Gauge.tsx
rename to packages/grafana-ui/src/components/Gauge/Gauge.tsx
index 094e630a1c0..2dce20543fd 100644
--- a/public/app/viz/Gauge.tsx
+++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx
@@ -1,12 +1,12 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
-import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui';
-import config from '../core/config';
-import kbn from '../core/utils/kbn';
+import { ValueMapping, Threshold, ThemeName, BasicGaugeColor, ThemeNames } from '../../types/panel';
+import { TimeSeriesVMs } from '../../types/series';
+import { getValueFormat } from '../../utils/valueFormats/valueFormats';
+import { TimeSeriesValue, getMappedValue } from '../../utils/valueMappings';
export interface Props {
- baseColor: string;
decimals: number;
height: number;
valueMappings: ValueMapping[];
@@ -21,13 +21,13 @@ export interface Props {
suffix: string;
unit: string;
width: number;
+ theme?: ThemeName;
}
export class Gauge extends PureComponent {
canvasElement: any;
static defaultProps = {
- baseColor: BasicGaugeColor.Green,
maxValue: 100,
valueMappings: [],
minValue: 0,
@@ -38,6 +38,7 @@ export class Gauge extends PureComponent {
thresholds: [],
unit: 'none',
stat: 'avg',
+ theme: ThemeNames.Dark,
};
componentDidMount() {
@@ -48,88 +49,93 @@ export class Gauge extends PureComponent {
this.draw();
}
- formatWithMappings(mappings, value) {
- const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
- const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
-
- const valueMap = valueMaps.map(mapping => {
- if (mapping.value && value === mapping.value) {
- return mapping.text;
- }
- })[0];
-
- const rangeMap = rangeMaps.map(mapping => {
- if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
- return mapping.text;
- }
- })[0];
-
- return { rangeMap, valueMap };
- }
-
- formatValue(value) {
+ formatValue(value: TimeSeriesValue) {
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
- const formatFunc = kbn.valueFormats[unit];
- const formattedValue = formatFunc(value, decimals);
+ if (isNaN(value as number)) {
+ return value;
+ }
if (valueMappings.length > 0) {
- const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue);
-
- if (valueMap) {
- return `${prefix} ${valueMap} ${suffix}`;
- } else if (rangeMap) {
- return `${prefix} ${rangeMap} ${suffix}`;
+ const valueMappedValue = getMappedValue(valueMappings, value);
+ if (valueMappedValue) {
+ return `${prefix} ${valueMappedValue.text} ${suffix}`;
}
}
- if (isNaN(value)) {
- return '-';
- }
+ const formatFunc = getValueFormat(unit);
+ const formattedValue = formatFunc(value as number, decimals);
+ const handleNoValueValue = formattedValue || 'no value';
- return `${prefix} ${formattedValue} ${suffix}`;
+ return `${prefix} ${handleNoValueValue} ${suffix}`;
}
- getFontColor(value) {
- const { baseColor, maxValue, thresholds } = this.props;
+ getFontColor(value: TimeSeriesValue) {
+ const { thresholds } = this.props;
- if (thresholds.length > 0) {
- const atThreshold = thresholds.filter(threshold => value <= threshold.value);
-
- if (atThreshold.length > 0) {
- return atThreshold[0].color;
- } else if (value <= maxValue) {
- return BasicGaugeColor.Red;
- }
+ if (thresholds.length === 1) {
+ return thresholds[0].color;
}
- return baseColor;
+ const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
+ if (atThreshold) {
+ return atThreshold.color;
+ }
+
+ const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
+
+ if (belowThreshold.length > 0) {
+ const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
+ return nearestThreshold.color;
+ }
+
+ return BasicGaugeColor.Red;
+ }
+
+ getFormattedThresholds() {
+ const { maxValue, minValue, thresholds } = this.props;
+
+ const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
+ const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
+
+ const formattedThresholds = [
+ ...thresholdsSortedByIndex.map(threshold => {
+ if (threshold.index === 0) {
+ return { value: minValue, color: threshold.color };
+ }
+
+ const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
+ return { value: threshold.value, color: previousThreshold.color };
+ }),
+ { value: maxValue, color: lastThreshold.color },
+ ];
+
+ return formattedThresholds;
}
draw() {
const {
- baseColor,
maxValue,
minValue,
timeSeries,
showThresholdLabels,
showThresholdMarkers,
- thresholds,
width,
height,
stat,
+ theme,
} = this.props;
- let value: string | number = '';
+ let value: TimeSeriesValue = '';
if (timeSeries[0]) {
value = timeSeries[0].stats[stat];
} else {
- value = 'N/A';
+ value = null;
}
const dimension = Math.min(width, height * 1.3);
- const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
+ const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
const fontScale = parseInt('80', 10) / 100;
const fontSize = Math.min(dimension / 5, 100) * fontScale;
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
@@ -137,17 +143,6 @@ export class Gauge extends PureComponent {
const thresholdMarkersWidth = gaugeWidth / 5;
const thresholdLabelFontSize = fontSize / 2.5;
- const formattedThresholds = [
- { value: minValue, color: BasicGaugeColor.Green },
- ...thresholds.map((threshold, index) => {
- return {
- value: threshold.value,
- color: index === 0 ? threshold.color : thresholds[index].color,
- };
- }),
- { value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor },
- ];
-
const options = {
series: {
gauges: {
@@ -164,7 +159,7 @@ export class Gauge extends PureComponent {
layout: { margin: 0, thresholdWidth: 0 },
cell: { border: { width: 0 } },
threshold: {
- values: formattedThresholds,
+ values: this.getFormattedThresholds(),
label: {
show: showThresholdLabels,
margin: thresholdMarkersWidth + 1,
diff --git a/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx b/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx
deleted file mode 100644
index 8b80de64696..00000000000
--- a/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React, { SFC, ReactNode } from 'react';
-import classNames from 'classnames';
-
-interface Props {
- children: ReactNode;
- htmlFor?: string;
- className?: string;
- isFocused?: boolean;
- isInvalid?: boolean;
-}
-
-export const GfFormLabel: SFC = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => {
- const classes = classNames('gf-form-label', className, {
- 'gf-form-label--is-focused': isFocused,
- 'gf-form-label--is-invalid': isInvalid,
- });
-
- return (
-
- );
-};
diff --git a/packages/grafana-ui/src/components/Label/Label.tsx b/packages/grafana-ui/src/components/Label/Label.tsx
deleted file mode 100644
index 270b0161226..00000000000
--- a/packages/grafana-ui/src/components/Label/Label.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React, { SFC, ReactNode } from 'react';
-import { Tooltip } from '../Tooltip/Tooltip';
-
-interface Props {
- tooltip?: string;
- for?: string;
- children: ReactNode;
- width?: number;
- className?: string;
-}
-
-export const Label: SFC = props => {
- return (
-
- {props.children}
- {props.tooltip && (
-
-
-
-
-
- )}
-
- );
-};
diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
index 9f5d4f02695..87d5b00f3b1 100644
--- a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
+++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
@@ -6,7 +6,7 @@
}
.panel-options-group__header {
- padding: 4px 20px;
+ padding: 4px 8px;
font-size: 1.1rem;
background: $panel-options-group-header-bg;
position: relative;
diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx
index 348133c6c28..6d83968d546 100644
--- a/packages/grafana-ui/src/components/Select/Select.tsx
+++ b/packages/grafana-ui/src/components/Select/Select.tsx
@@ -16,7 +16,7 @@ import SelectOptionGroup from './SelectOptionGroup';
import IndicatorsContainer from './IndicatorsContainer';
import NoOptionsMessage from './NoOptionsMessage';
import resetSelectStyles from './resetSelectStyles';
-import { CustomScrollbar } from '@grafana/ui';
+import { CustomScrollbar } from '..';
export interface SelectOptionItem {
label?: string;
@@ -61,7 +61,7 @@ interface AsyncProps {
export const MenuList = (props: any) => {
return (
- {props.children}
+ {props.children}
);
};
@@ -202,7 +202,7 @@ export class AsyncSelect extends PureComponent {
classNamePrefix="gf-form-select-box"
className={selectClassNames}
components={{
- Option,
+ Option: SelectOption,
SingleValue,
IndicatorsContainer,
NoOptionsMessage,
diff --git a/packages/grafana-ui/src/components/Select/_Select.scss b/packages/grafana-ui/src/components/Select/_Select.scss
index bf18125d7b8..bc18ed9d369 100644
--- a/packages/grafana-ui/src/components/Select/_Select.scss
+++ b/packages/grafana-ui/src/components/Select/_Select.scss
@@ -102,6 +102,7 @@ $select-input-bg-disabled: $input-bg-disabled;
.gf-form-select-box__value-container {
display: table-cell;
padding: 6px 10px;
+ vertical-align: middle;
> div {
display: inline-block;
}
diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
index 6e8d88051f3..590aca5c7a1 100644
--- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
+++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
@@ -19,9 +19,15 @@ export class ThresholdsEditor extends PureComponent {
constructor(props: Props) {
super(props);
- const thresholds: Threshold[] =
- props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
+ const addDefaultThreshold = this.props.thresholds.length === 0;
+ const thresholds: Threshold[] = addDefaultThreshold
+ ? [{ index: 0, value: -Infinity, color: colors[0] }]
+ : props.thresholds;
this.state = { thresholds };
+
+ if (addDefaultThreshold) {
+ this.onChange();
+ }
}
onAddThreshold = (index: number) => {
@@ -62,7 +68,7 @@ export class ThresholdsEditor extends PureComponent {
},
]),
},
- () => this.updateGauge()
+ () => this.onChange()
);
};
@@ -85,7 +91,7 @@ export class ThresholdsEditor extends PureComponent {
thresholds: newThresholds.filter(t => t !== threshold),
};
},
- () => this.updateGauge()
+ () => this.onChange()
);
};
@@ -99,7 +105,7 @@ export class ThresholdsEditor extends PureComponent {
const value = isNaN(parsedValue) ? null : parsedValue;
const newThresholds = thresholds.map(t => {
- if (t === threshold) {
+ if (t === threshold && t.index !== 0) {
t = { ...t, value: value as number };
}
@@ -124,11 +130,10 @@ export class ThresholdsEditor extends PureComponent {
{
thresholds: newThresholds,
},
- () => this.updateGauge()
+ () => this.onChange()
);
};
- onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
onBlur = () => {
this.setState(prevState => {
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
@@ -139,10 +144,10 @@ export class ThresholdsEditor extends PureComponent {
return { thresholds: sortThresholds };
});
- this.updateGauge();
+ this.onChange();
};
- updateGauge = () => {
+ onChange = () => {
this.props.onChange(this.state.thresholds);
};
diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx
index 9705304d354..c5704e8bc88 100644
--- a/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx
+++ b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx
@@ -1,8 +1,7 @@
-import React, { PureComponent } from 'react';
+import React, { ChangeEvent, PureComponent } from 'react';
-import { MappingType, ValueMapping } from '../../types/panel';
-import { Label } from '../Label/Label';
-import { Select } from '../Select/Select';
+import { MappingType, ValueMapping } from '../../types';
+import { FormField, FormLabel, Select } from '..';
export interface Props {
valueMapping: ValueMapping;
@@ -32,19 +31,19 @@ export default class MappingRow extends PureComponent {
this.state = { ...props.valueMapping };
}
- onMappingValueChange = (event: React.ChangeEvent) => {
+ onMappingValueChange = (event: ChangeEvent) => {
this.setState({ value: event.target.value });
};
- onMappingFromChange = (event: React.ChangeEvent) => {
+ onMappingFromChange = (event: ChangeEvent) => {
this.setState({ from: event.target.value });
};
- onMappingToChange = (event: React.ChangeEvent) => {
+ onMappingToChange = (event: ChangeEvent) => {
this.setState({ to: event.target.value });
};
- onMappingTextChange = (event: React.ChangeEvent) => {
+ onMappingTextChange = (event: ChangeEvent) => {
this.setState({ text: event.target.value });
};
@@ -62,30 +61,28 @@ export default class MappingRow extends PureComponent {
if (type === MappingType.RangeToText) {
return (
<>
-
-
+
+
+
+ Text
-
-
-
-
-
-
-
-
@@ -95,17 +92,16 @@ export default class MappingRow extends PureComponent
{
return (
<>
-
-
-
-
+
-
+
Text
{
return (
-
+
Type