mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into nanosecond-postgresql
This commit is contained in:
commit
7023c957d7
@ -19,7 +19,7 @@ version: 2
|
|||||||
jobs:
|
jobs:
|
||||||
mysql-integration-test:
|
mysql-integration-test:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.11.4
|
- image: circleci/golang:1.11.5
|
||||||
- image: circleci/mysql:5.6-ram
|
- image: circleci/mysql:5.6-ram
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: rootpass
|
MYSQL_ROOT_PASSWORD: rootpass
|
||||||
@ -39,7 +39,7 @@ jobs:
|
|||||||
|
|
||||||
postgres-integration-test:
|
postgres-integration-test:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.11.4
|
- image: circleci/golang:1.11.5
|
||||||
- image: circleci/postgres:9.3-ram
|
- image: circleci/postgres:9.3-ram
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: grafanatest
|
POSTGRES_USER: grafanatest
|
||||||
@ -74,27 +74,16 @@ jobs:
|
|||||||
|
|
||||||
gometalinter:
|
gometalinter:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.11.4
|
- image: circleci/golang:1.11.5
|
||||||
environment:
|
environment:
|
||||||
# we need CGO because of go-sqlite3
|
# we need CGO because of go-sqlite3
|
||||||
CGO_ENABLED: 1
|
CGO_ENABLED: 1
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- 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:
|
- run:
|
||||||
name: run linters
|
name: Gometalinter tests
|
||||||
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 ./...'
|
command: './scripts/gometalinter.sh'
|
||||||
- run:
|
|
||||||
name: run go vet
|
|
||||||
command: 'go vet ./pkg/...'
|
|
||||||
|
|
||||||
test-frontend:
|
test-frontend:
|
||||||
docker:
|
docker:
|
||||||
@ -117,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
test-backend:
|
test-backend:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/golang:1.11.4
|
- image: circleci/golang:1.11.5
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@ -127,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
build-all:
|
build-all:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/build-container:1.2.1
|
- image: grafana/build-container:1.2.3
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@ -158,9 +147,6 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: sha-sum packages
|
name: sha-sum packages
|
||||||
command: 'go run build.go sha-dist'
|
command: 'go run build.go sha-dist'
|
||||||
- run:
|
|
||||||
name: Build Grafana.com master publisher
|
|
||||||
command: 'go build -o scripts/publish scripts/build/publish.go'
|
|
||||||
- run:
|
- run:
|
||||||
name: Test and build Grafana.com release publisher
|
name: Test and build Grafana.com release publisher
|
||||||
command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
|
command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
|
||||||
@ -169,13 +155,12 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- dist/grafana*
|
- dist/grafana*
|
||||||
- scripts/*.sh
|
- scripts/*.sh
|
||||||
- scripts/publish
|
|
||||||
- scripts/build/release_publisher/release_publisher
|
- scripts/build/release_publisher/release_publisher
|
||||||
- scripts/build/publish.sh
|
- scripts/build/publish.sh
|
||||||
|
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/build-container:1.2.2
|
- image: grafana/build-container:1.2.3
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@ -200,51 +185,51 @@ jobs:
|
|||||||
- dist/grafana*
|
- dist/grafana*
|
||||||
|
|
||||||
grafana-docker-master:
|
grafana-docker-master:
|
||||||
docker:
|
machine:
|
||||||
- image: docker:stable-git
|
image: circleci/classic:201808-01
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker info
|
- run: docker info
|
||||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
- run: docker run --privileged linuxkit/binfmt:v0.6
|
||||||
|
- run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker
|
||||||
- run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
|
- run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
|
||||||
- run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
|
- run: rm packaging/docker/grafana-latest.linux-*.tar.gz
|
||||||
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
|
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||||
- run: cd packaging/docker && ./build-enterprise.sh "master"
|
- run: cd packaging/docker && ./build-enterprise.sh "master"
|
||||||
|
|
||||||
|
|
||||||
grafana-docker-pr:
|
grafana-docker-pr:
|
||||||
docker:
|
machine:
|
||||||
- image: docker:stable-git
|
image: circleci/classic:201808-01
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- setup_remote_docker
|
|
||||||
- run: docker info
|
- run: docker info
|
||||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
- run: docker run --privileged linuxkit/binfmt:v0.6
|
||||||
|
- run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker
|
||||||
- run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
|
- run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
|
||||||
|
|
||||||
grafana-docker-release:
|
grafana-docker-release:
|
||||||
docker:
|
machine:
|
||||||
- image: docker:stable-git
|
image: circleci/classic:201808-01
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- setup_remote_docker
|
- run: docker info
|
||||||
- run: docker info
|
- run: docker run --privileged linuxkit/binfmt:v0.6
|
||||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
- run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker
|
||||||
- run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
|
- run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
|
||||||
- run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
|
- run: rm packaging/docker/grafana-latest.linux-*.tar.gz
|
||||||
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
|
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||||
- run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
|
- run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
|
||||||
|
|
||||||
build-enterprise:
|
build-enterprise:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/build-container:1.2.1
|
- image: grafana/build-container:1.2.3
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@ -276,7 +261,7 @@ jobs:
|
|||||||
|
|
||||||
build-all-enterprise:
|
build-all-enterprise:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/build-container:1.2.1
|
- image: grafana/build-container:1.2.3
|
||||||
working_directory: /go/src/github.com/grafana/grafana
|
working_directory: /go/src/github.com/grafana/grafana
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
@ -323,7 +308,7 @@ jobs:
|
|||||||
|
|
||||||
deploy-enterprise-master:
|
deploy-enterprise-master:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/grafana-ci-deploy:1.0.0
|
- image: grafana/grafana-ci-deploy:1.2.0
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@ -346,7 +331,7 @@ jobs:
|
|||||||
|
|
||||||
deploy-enterprise-release:
|
deploy-enterprise-release:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/grafana-ci-deploy:1.0.0
|
- image: grafana/grafana-ci-deploy:1.2.0
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@ -365,10 +350,20 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Deploy to Grafana.com
|
name: Deploy to Grafana.com
|
||||||
command: './scripts/build/publish.sh --enterprise'
|
command: './scripts/build/publish.sh --enterprise'
|
||||||
|
- run:
|
||||||
|
name: Load GPG private key
|
||||||
|
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" "enterprise-dist"'
|
||||||
|
- run:
|
||||||
|
name: Update RPM repository
|
||||||
|
command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"'
|
||||||
|
|
||||||
|
|
||||||
deploy-master:
|
deploy-master:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/grafana-ci-deploy:1.0.0
|
- image: grafana/grafana-ci-deploy:1.2.0
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
@ -394,12 +389,14 @@ jobs:
|
|||||||
name: Publish to Grafana.com
|
name: Publish to Grafana.com
|
||||||
command: |
|
command: |
|
||||||
rm dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
|
rm dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
|
||||||
./scripts/publish -apiKey ${GRAFANA_COM_API_KEY}
|
rm dist/*latest*
|
||||||
|
cd dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -from-local
|
||||||
|
|
||||||
deploy-release:
|
deploy-release:
|
||||||
docker:
|
docker:
|
||||||
- image: grafana/grafana-ci-deploy:1.0.0
|
- image: grafana/grafana-ci-deploy:1.2.0
|
||||||
steps:
|
steps:
|
||||||
|
- checkout
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
- run:
|
- run:
|
||||||
@ -417,6 +414,15 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Deploy to Grafana.com
|
name: Deploy to Grafana.com
|
||||||
command: './scripts/build/publish.sh'
|
command: './scripts/build/publish.sh'
|
||||||
|
- run:
|
||||||
|
name: Load GPG private key
|
||||||
|
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" "dist"'
|
||||||
|
- run:
|
||||||
|
name: Update RPM repository
|
||||||
|
command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"'
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
|
44
CHANGELOG.md
44
CHANGELOG.md
@ -1,25 +1,61 @@
|
|||||||
# 5.5.0 (unreleased)
|
# 6.0.0-beta1 (unreleased)
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
* **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
|
* **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)
|
* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
|
||||||
|
|
||||||
### Minor
|
### 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 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 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)
|
* **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)
|
* **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](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)
|
* **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)
|
* **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)
|
* **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)
|
* **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)
|
||||||
|
* **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)
|
||||||
|
* **Dataproxy**: Add global datasource proxy timeout setting [#5699](https://github.com/grafana/grafana/issues/5699), thx [@RangerRick](https://github.com/RangerRick)
|
||||||
|
* **Database**: Support specifying database host using IPV6 for backend database and sql datasources [#13711](https://github.com/grafana/grafana/issues/13711), thx [@ellisvlad](https://github.com/ellisvlad)
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
* **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)
|
||||||
|
|
||||||
|
### Tech
|
||||||
|
|
||||||
|
* **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach)
|
||||||
|
* **Backend**: Upgrade to golang 1.11.4 [#14580](https://github.com/grafana/grafana/issues/14580)
|
||||||
|
* **MySQL** only update session in mysql database when required [#14540](https://github.com/grafana/grafana/pull/14540)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
* **Alerting** Invalid frequency causes division by zero in alert scheduler [#14810](https://github.com/grafana/grafana/issues/14810)
|
||||||
|
* **Dashboard** Dashboard links do not update when time range changes [#14493](https://github.com/grafana/grafana/issues/14493)
|
||||||
|
* **Limits** Support more than 1000 datasources per org [#13883](https://github.com/grafana/grafana/issues/13883)
|
||||||
|
* **Backend** fix signed in user for orgId=0 result should return active org id [#14574](https://github.com/grafana/grafana/pull/14574)
|
||||||
|
* **Provisioning** Adds orgId to user dto for provisioned dashboards [#14678](https://github.com/grafana/grafana/pull/14678)
|
||||||
|
|
||||||
# 5.4.2 (2018-12-13)
|
# 5.4.2 (2018-12-13)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Golang build container
|
# Golang build container
|
||||||
FROM golang:1.11.4
|
FROM golang:1.11.5
|
||||||
|
|
||||||
WORKDIR $GOPATH/src/github.com/grafana/grafana
|
WORKDIR $GOPATH/src/github.com/grafana/grafana
|
||||||
|
|
||||||
@ -19,11 +19,13 @@ COPY package.json package.json
|
|||||||
RUN go run build.go build
|
RUN go run build.go build
|
||||||
|
|
||||||
# Node build container
|
# Node build container
|
||||||
FROM node:8
|
FROM node:10.14.2
|
||||||
|
|
||||||
WORKDIR /usr/src/app/
|
WORKDIR /usr/src/app/
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
|
COPY packages packages
|
||||||
|
|
||||||
RUN yarn install --pure-lockfile --no-progress
|
RUN yarn install --pure-lockfile --no-progress
|
||||||
|
|
||||||
COPY Gruntfile.js tsconfig.json tslint.json ./
|
COPY Gruntfile.js tsconfig.json tslint.json ./
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Plugin Development
|
# Plugin Development
|
||||||
|
|
||||||
This document is not meant as complete guide for developing plugins but more as a changelog for changes in
|
This document is not meant as a complete guide for developing plugins but more as a changelog for changes in
|
||||||
Grafana that can impact plugin development. When ever you as plugin author encounter an issue with your plugin after
|
Grafana that can impact plugin development. Whenever you as a plugin author encounter an issue with your plugin after
|
||||||
upgrading Grafana please check here before creating an issue.
|
upgrading Grafana please check here before creating an issue.
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
14
README.md
14
README.md
@ -19,7 +19,7 @@ If you have any problems please read the [troubleshooting guide](http://docs.gra
|
|||||||
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
|
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
|
||||||
|
|
||||||
## Run from master
|
## Run from master
|
||||||
If you want to build a package yourself, or contribute - Here is a guide for how to do that. You can always find
|
If you want to build a package yourself, or contribute - here is a guide for how to do that. You can always find
|
||||||
the latest master builds [here](https://grafana.com/grafana/download)
|
the latest master builds [here](https://grafana.com/grafana/download)
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
@ -71,7 +71,7 @@ Open grafana in your browser (default: `http://localhost:3000`) and login with a
|
|||||||
|
|
||||||
### Building a Docker image
|
### Building a Docker image
|
||||||
|
|
||||||
There are two different ways to build a Grafana docker image. If you're machine is setup for Grafana development and you run linux/amd64 you can build just the image. Otherwise, there is the option to build Grafana completely within Docker.
|
There are two different ways to build a Grafana docker image. If your machine is setup for Grafana development and you run linux/amd64 you can build just the image. Otherwise, there is the option to build Grafana completely within Docker.
|
||||||
|
|
||||||
Run the image you have built using: `docker run --rm -p 3000:3000 grafana/grafana:dev`
|
Run the image you have built using: `docker run --rm -p 3000:3000 grafana/grafana:dev`
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ Choose this option to build on platforms other than linux/amd64 and/or not have
|
|||||||
|
|
||||||
The resulting image will be tagged as `grafana/grafana:dev`
|
The resulting image will be tagged as `grafana/grafana:dev`
|
||||||
|
|
||||||
Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Preferences -> Advanced), otherwize you may faild at `grunt build`
|
Notice: If you are using Docker for MacOS, be sure to set the memory limit to be larger than 2 GiB (at docker -> Preferences -> Advanced), otherwise `grunt build` may fail.
|
||||||
|
|
||||||
### Dev config
|
### Dev config
|
||||||
|
|
||||||
@ -129,9 +129,11 @@ GRAFANA_TEST_DB=postgres go test ./pkg/...
|
|||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
|
If you have any ideas for improvement or have found a bug, do not hesitate to open an issue.
|
||||||
And if you have time clone this repo and submit a pull request and help me make Grafana
|
And if you have time, clone this repo and submit a pull request to help me make Grafana
|
||||||
the kickass metrics & devops dashboard we all dream about!
|
the kickass metrics & devops dashboard we all dream about!
|
||||||
|
|
||||||
|
Read the [contributing](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md) guide then check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) label to find issues that are easy and that we would like help with.
|
||||||
|
|
||||||
## Plugin development
|
## Plugin development
|
||||||
|
|
||||||
|
18
ROADMAP.md
18
ROADMAP.md
@ -5,18 +5,22 @@ But it will give you an idea of our current vision and plan.
|
|||||||
|
|
||||||
### Short term (1-2 months)
|
### Short term (1-2 months)
|
||||||
- PRs & Bugs
|
- PRs & Bugs
|
||||||
- Multi-Stat panel
|
- React Panel Support
|
||||||
|
- React Query Editor Support
|
||||||
- Metrics & Log Explore UI
|
- 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)
|
### Mid term (2-4 months)
|
||||||
- React Panels
|
- Drilldown links
|
||||||
- Change visualization (panel type) on the fly.
|
- Dashboards as code workflows
|
||||||
- Templating Query Editor UI Plugin hook
|
- React migration
|
||||||
- Backend plugins
|
- New panels
|
||||||
|
|
||||||
### Long term (4 - 8 months)
|
### Long term (4 - 8 months)
|
||||||
- Alerting improvements (silence, per series tracking, etc)
|
- Alerting improvements (silence, per series tracking, etc)
|
||||||
- Progress on React migration
|
|
||||||
|
|
||||||
### In a distant future far far away
|
### In a distant future far far away
|
||||||
- Meta queries
|
- Meta queries
|
||||||
|
@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
|
|||||||
environment:
|
environment:
|
||||||
nodejs_version: "8"
|
nodejs_version: "8"
|
||||||
GOPATH: C:\gopath
|
GOPATH: C:\gopath
|
||||||
GOVERSION: 1.11.4
|
GOVERSION: 1.11.5
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- rmdir c:\go /s /q
|
- rmdir c:\go /s /q
|
||||||
|
18
build.go
18
build.go
@ -46,6 +46,8 @@ var (
|
|||||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||||
isDev bool = false
|
isDev bool = false
|
||||||
enterprise bool = false
|
enterprise bool = false
|
||||||
|
skipRpmGen bool = false
|
||||||
|
skipDebGen bool = false
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -67,6 +69,8 @@ func main() {
|
|||||||
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
|
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
|
||||||
flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
|
flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
|
||||||
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
buildId = shortenBuildId(buildIdRaw)
|
buildId = shortenBuildId(buildIdRaw)
|
||||||
@ -164,6 +168,9 @@ func makeLatestDistCopies() {
|
|||||||
"_amd64.deb": "dist/grafana_latest_amd64.deb",
|
"_amd64.deb": "dist/grafana_latest_amd64.deb",
|
||||||
".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm",
|
".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm",
|
||||||
".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
|
".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",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
@ -237,6 +244,8 @@ func createDebPackages() {
|
|||||||
previousPkgArch := pkgArch
|
previousPkgArch := pkgArch
|
||||||
if pkgArch == "armv7" {
|
if pkgArch == "armv7" {
|
||||||
pkgArch = "armhf"
|
pkgArch = "armhf"
|
||||||
|
} else if pkgArch == "armv6" {
|
||||||
|
pkgArch = "armel"
|
||||||
}
|
}
|
||||||
createPackage(linuxPackageOptions{
|
createPackage(linuxPackageOptions{
|
||||||
packageType: "deb",
|
packageType: "deb",
|
||||||
@ -287,8 +296,13 @@ func createRpmPackages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createLinuxPackages() {
|
func createLinuxPackages() {
|
||||||
createDebPackages()
|
if !skipDebGen {
|
||||||
createRpmPackages()
|
createDebPackages()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skipRpmGen {
|
||||||
|
createRpmPackages()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPackage(options linuxPackageOptions) {
|
func createPackage(options linuxPackageOptions) {
|
||||||
|
@ -106,6 +106,22 @@ path = grafana.db
|
|||||||
# For "sqlite3" only. cache mode setting used for connecting to the database
|
# For "sqlite3" only. cache mode setting used for connecting to the database
|
||||||
cache_mode = private
|
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 #############################
|
||||||
[session]
|
[session]
|
||||||
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
|
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
|
||||||
@ -143,6 +159,9 @@ conn_max_lifetime = 14400
|
|||||||
# This enables data proxy logging, default is false
|
# This enables data proxy logging, default is false
|
||||||
logging = false
|
logging = false
|
||||||
|
|
||||||
|
# How long the data proxy should wait before timing out default is 30 (seconds)
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
#################################### Analytics ###########################
|
#################################### Analytics ###########################
|
||||||
[analytics]
|
[analytics]
|
||||||
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
||||||
@ -175,11 +194,6 @@ admin_password = admin
|
|||||||
# used for signing
|
# used for signing
|
||||||
secret_key = SW2YcwTIb9zpOOhoPsMm
|
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 profile images
|
||||||
disable_gravatar = false
|
disable_gravatar = false
|
||||||
|
|
||||||
@ -189,6 +203,9 @@ data_source_proxy_whitelist =
|
|||||||
# disable protection against brute force login attempts
|
# disable protection against brute force login attempts
|
||||||
disable_brute_force_login_protection = false
|
disable_brute_force_login_protection = false
|
||||||
|
|
||||||
|
# set cookies as https only. default is false
|
||||||
|
https_flag_cookies = false
|
||||||
|
|
||||||
#################################### Snapshots ###########################
|
#################################### Snapshots ###########################
|
||||||
[snapshots]
|
[snapshots]
|
||||||
# snapshot sharing options
|
# snapshot sharing options
|
||||||
@ -490,7 +507,7 @@ concurrent_render_limit = 5
|
|||||||
#################################### Explore #############################
|
#################################### Explore #############################
|
||||||
[explore]
|
[explore]
|
||||||
# Enable the Explore section
|
# Enable the Explore section
|
||||||
enabled = false
|
enabled = true
|
||||||
|
|
||||||
#################################### Internal Grafana Metrics ############
|
#################################### Internal Grafana Metrics ############
|
||||||
# Metrics available at HTTP API Url /metrics
|
# Metrics available at HTTP API Url /metrics
|
||||||
@ -570,6 +587,7 @@ callback_url =
|
|||||||
|
|
||||||
[panels]
|
[panels]
|
||||||
enable_alpha = false
|
enable_alpha = false
|
||||||
|
disable_sanitize_html = false
|
||||||
|
|
||||||
[enterprise]
|
[enterprise]
|
||||||
license_path =
|
license_path =
|
||||||
|
@ -102,6 +102,22 @@ log_queries =
|
|||||||
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
|
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
|
||||||
;cache_mode = private
|
;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 ####################################
|
||||||
[session]
|
[session]
|
||||||
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
|
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
|
||||||
@ -130,6 +146,9 @@ log_queries =
|
|||||||
# This enables data proxy logging, default is false
|
# This enables data proxy logging, default is false
|
||||||
;logging = false
|
;logging = false
|
||||||
|
|
||||||
|
# How long the data proxy should wait before timing out default is 30 (seconds)
|
||||||
|
;timeout = 30
|
||||||
|
|
||||||
#################################### Analytics ####################################
|
#################################### Analytics ####################################
|
||||||
[analytics]
|
[analytics]
|
||||||
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
||||||
@ -162,11 +181,6 @@ log_queries =
|
|||||||
# used for signing
|
# used for signing
|
||||||
;secret_key = SW2YcwTIb9zpOOhoPsMm
|
;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 profile images
|
||||||
;disable_gravatar = false
|
;disable_gravatar = false
|
||||||
|
|
||||||
@ -176,6 +190,9 @@ log_queries =
|
|||||||
# disable protection against brute force login attempts
|
# disable protection against brute force login attempts
|
||||||
;disable_brute_force_login_protection = false
|
;disable_brute_force_login_protection = false
|
||||||
|
|
||||||
|
# set cookies as https only. default is false
|
||||||
|
;https_flag_cookies = false
|
||||||
|
|
||||||
#################################### Snapshots ###########################
|
#################################### Snapshots ###########################
|
||||||
[snapshots]
|
[snapshots]
|
||||||
# snapshot sharing options
|
# snapshot sharing options
|
||||||
@ -415,7 +432,7 @@ log_queries =
|
|||||||
#################################### Explore #############################
|
#################################### Explore #############################
|
||||||
[explore]
|
[explore]
|
||||||
# Enable the Explore section
|
# Enable the Explore section
|
||||||
;enabled = false
|
;enabled = true
|
||||||
|
|
||||||
#################################### Internal Grafana Metrics ##########################
|
#################################### Internal Grafana Metrics ##########################
|
||||||
# Metrics available at HTTP API Url /metrics
|
# Metrics available at HTTP API Url /metrics
|
||||||
@ -495,3 +512,8 @@ log_queries =
|
|||||||
# Path to a valid Grafana Enterprise license.jwt file
|
# Path to a valid Grafana Enterprise license.jwt file
|
||||||
;license_path =
|
;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
|
||||||
|
|
||||||
|
@ -4,6 +4,6 @@ providers:
|
|||||||
- name: 'gdev dashboards'
|
- name: 'gdev dashboards'
|
||||||
folder: 'gdev dashboards'
|
folder: 'gdev dashboards'
|
||||||
type: file
|
type: file
|
||||||
|
updateIntervalSeconds: 15
|
||||||
options:
|
options:
|
||||||
path: devenv/dev-dashboards
|
path: devenv/dev-dashboards
|
||||||
|
|
||||||
|
1674
devenv/dev-dashboards-without-uid/panel_tests_graph.json
Normal file
1674
devenv/dev-dashboards-without-uid/panel_tests_graph.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,510 @@
|
|||||||
|
{
|
||||||
|
"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,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "gdev-testdata",
|
||||||
|
"fill": 2,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 2,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk",
|
||||||
|
"target": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [
|
||||||
|
{
|
||||||
|
"colorMode": "gray",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(255, 255, 255, 0.03)",
|
||||||
|
"from": "08:30",
|
||||||
|
"fromDayOfWeek": 1,
|
||||||
|
"line": false,
|
||||||
|
"lineColor": "rgba(255, 255, 255, 0.2)",
|
||||||
|
"op": "time",
|
||||||
|
"to": "16:45",
|
||||||
|
"toDayOfWeek": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Business Hours",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "gdev-testdata",
|
||||||
|
"fill": 2,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 4,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 2,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk",
|
||||||
|
"target": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [
|
||||||
|
{
|
||||||
|
"colorMode": "red",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(255, 255, 255, 0.03)",
|
||||||
|
"from": "20:00",
|
||||||
|
"fromDayOfWeek": 7,
|
||||||
|
"line": false,
|
||||||
|
"lineColor": "rgba(255, 255, 255, 0.2)",
|
||||||
|
"op": "time",
|
||||||
|
"to": "23:00",
|
||||||
|
"toDayOfWeek": 7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Sunday's 20-23",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {
|
||||||
|
"A-series": "#d683ce"
|
||||||
|
},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "gdev-testdata",
|
||||||
|
"fill": 2,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 16
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 2,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 0.5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk",
|
||||||
|
"target": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [
|
||||||
|
{
|
||||||
|
"colorMode": "custom",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(255, 0, 0, 0.22)",
|
||||||
|
"from": "",
|
||||||
|
"fromDayOfWeek": 1,
|
||||||
|
"line": true,
|
||||||
|
"lineColor": "rgba(255, 0, 0, 0.32)",
|
||||||
|
"op": "time",
|
||||||
|
"to": "",
|
||||||
|
"toDayOfWeek": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"colorMode": "custom",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(255, 127, 0, 0.22)",
|
||||||
|
"fromDayOfWeek": 2,
|
||||||
|
"line": true,
|
||||||
|
"lineColor": "rgba(255, 127, 0, 0.32)",
|
||||||
|
"op": "time",
|
||||||
|
"toDayOfWeek": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"colorMode": "custom",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(255, 255, 0, 0.22)",
|
||||||
|
"fromDayOfWeek": 3,
|
||||||
|
"line": true,
|
||||||
|
"lineColor": "rgba(255, 255, 0, 0.22)",
|
||||||
|
"op": "time",
|
||||||
|
"toDayOfWeek": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"colorMode": "custom",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(0, 255, 0, 0.22)",
|
||||||
|
"fromDayOfWeek": 4,
|
||||||
|
"line": true,
|
||||||
|
"lineColor": "rgba(0, 255, 0, 0.32)",
|
||||||
|
"op": "time",
|
||||||
|
"toDayOfWeek": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"colorMode": "custom",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(0, 0, 255, 0.22)",
|
||||||
|
"fromDayOfWeek": 5,
|
||||||
|
"line": true,
|
||||||
|
"lineColor": "rgba(0, 0, 255, 0.32)",
|
||||||
|
"op": "time",
|
||||||
|
"toDayOfWeek": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"colorMode": "custom",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(75, 0, 130, 0.22)",
|
||||||
|
"fromDayOfWeek": 6,
|
||||||
|
"line": true,
|
||||||
|
"lineColor": "rgba(75, 0, 130, 0.32)",
|
||||||
|
"op": "time",
|
||||||
|
"toDayOfWeek": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"colorMode": "custom",
|
||||||
|
"fill": true,
|
||||||
|
"fillColor": "rgba(148, 0, 211, 0.22)",
|
||||||
|
"fromDayOfWeek": 7,
|
||||||
|
"line": true,
|
||||||
|
"lineColor": "rgba(148, 0, 211, 0.32)",
|
||||||
|
"op": "time",
|
||||||
|
"toDayOfWeek": 7
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Each day of week",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "gdev-testdata",
|
||||||
|
"fill": 2,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 24
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 2,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk",
|
||||||
|
"target": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [
|
||||||
|
{
|
||||||
|
"colorMode": "red",
|
||||||
|
"fill": false,
|
||||||
|
"from": "05:00",
|
||||||
|
"line": true,
|
||||||
|
"op": "time"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "05:00",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": false,
|
||||||
|
"schemaVersion": 16,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"panel-tests"
|
||||||
|
],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-30d",
|
||||||
|
"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": "browser",
|
||||||
|
"title": "Panel Tests - Graph (Time Regions)",
|
||||||
|
"version": 1
|
||||||
|
}
|
3342
devenv/dev-dashboards-without-uid/panel_tests_polystat.json
Normal file
3342
devenv/dev-dashboards-without-uid/panel_tests_polystat.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"gnetId": null,
|
"gnetId": null,
|
||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
"iteration": 1542304484522,
|
"iteration": 1545263815779,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"icon": "external link",
|
"icon": "external link",
|
||||||
@ -66,6 +66,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -168,6 +169,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -270,6 +272,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -372,6 +375,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -474,6 +478,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -576,6 +581,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -2249,6 +2255,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -2366,6 +2373,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -2483,6 +2491,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -2600,6 +2609,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -2717,6 +2727,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -2834,6 +2845,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -2951,6 +2963,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -3068,6 +3081,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -3185,6 +3199,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -3302,6 +3317,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -3419,6 +3435,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -3536,6 +3553,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -3667,6 +3685,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -3780,6 +3799,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -3893,6 +3913,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4006,6 +4027,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4119,6 +4141,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4232,6 +4255,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4345,6 +4369,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4458,6 +4483,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4571,6 +4597,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4684,6 +4711,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4797,6 +4825,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -4910,6 +4939,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -5008,6 +5038,512 @@
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 4
|
"y": 4
|
||||||
},
|
},
|
||||||
|
"id": 60,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "$version_one",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 63,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"bucketAggs": [
|
||||||
|
{
|
||||||
|
"field": "@timestamp",
|
||||||
|
"id": "2",
|
||||||
|
"settings": {
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 0,
|
||||||
|
"trimEdges": 0
|
||||||
|
},
|
||||||
|
"type": "date_histogram"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"hide": true,
|
||||||
|
"id": "1",
|
||||||
|
"type": "count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"id": "3",
|
||||||
|
"meta": {},
|
||||||
|
"pipelineVariables": [
|
||||||
|
{
|
||||||
|
"name": "var1",
|
||||||
|
"pipelineAgg": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"script": "params.var1 * 1000"
|
||||||
|
},
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refId": "A",
|
||||||
|
"timeField": "@timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "count * 1000 (version one) - interval auto",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "$version_two",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 64,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"bucketAggs": [
|
||||||
|
{
|
||||||
|
"field": "@timestamp",
|
||||||
|
"id": "2",
|
||||||
|
"settings": {
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 0,
|
||||||
|
"trimEdges": 0
|
||||||
|
},
|
||||||
|
"type": "date_histogram"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"hide": true,
|
||||||
|
"id": "1",
|
||||||
|
"type": "count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"id": "3",
|
||||||
|
"meta": {},
|
||||||
|
"pipelineVariables": [
|
||||||
|
{
|
||||||
|
"name": "var1",
|
||||||
|
"pipelineAgg": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"script": "params.var1 * 1000"
|
||||||
|
},
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refId": "A",
|
||||||
|
"timeField": "@timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "count * 1000 (version two) - interval auto",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "$version_one",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 13
|
||||||
|
},
|
||||||
|
"id": 65,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"bucketAggs": [
|
||||||
|
{
|
||||||
|
"field": "@timestamp",
|
||||||
|
"id": "2",
|
||||||
|
"settings": {
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 0,
|
||||||
|
"trimEdges": 0
|
||||||
|
},
|
||||||
|
"type": "date_histogram"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"hide": true,
|
||||||
|
"id": "1",
|
||||||
|
"type": "count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "@value",
|
||||||
|
"hide": true,
|
||||||
|
"id": "3",
|
||||||
|
"meta": {},
|
||||||
|
"settings": {},
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"id": "4",
|
||||||
|
"meta": {},
|
||||||
|
"pipelineVariables": [
|
||||||
|
{
|
||||||
|
"name": "var1",
|
||||||
|
"pipelineAgg": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "var2",
|
||||||
|
"pipelineAgg": "3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"script": "params.var1 * params.var2"
|
||||||
|
},
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refId": "A",
|
||||||
|
"timeField": "@timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "count * avg (version one) - interval auto",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "$version_two",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 13
|
||||||
|
},
|
||||||
|
"id": 66,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 2,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"bucketAggs": [
|
||||||
|
{
|
||||||
|
"field": "@timestamp",
|
||||||
|
"id": "2",
|
||||||
|
"settings": {
|
||||||
|
"interval": "auto",
|
||||||
|
"min_doc_count": 0,
|
||||||
|
"trimEdges": 0
|
||||||
|
},
|
||||||
|
"type": "date_histogram"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": [
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"hide": true,
|
||||||
|
"id": "1",
|
||||||
|
"type": "count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "@value",
|
||||||
|
"hide": true,
|
||||||
|
"id": "3",
|
||||||
|
"meta": {},
|
||||||
|
"settings": {},
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "select field",
|
||||||
|
"id": "4",
|
||||||
|
"meta": {},
|
||||||
|
"pipelineVariables": [
|
||||||
|
{
|
||||||
|
"name": "var1",
|
||||||
|
"pipelineAgg": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "var2",
|
||||||
|
"pipelineAgg": "3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"script": "params.var1 * params.var2"
|
||||||
|
},
|
||||||
|
"type": "bucket_script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refId": "A",
|
||||||
|
"timeField": "@timestamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeRegions": [],
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "count * avg (version two) - interval auto",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Basic date histogram with bucket script aggregation",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsed": true,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
"id": 54,
|
"id": 54,
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
@ -5042,6 +5578,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -5193,6 +5730,7 @@
|
|||||||
"linewidth": 1,
|
"linewidth": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"nullPointMode": "null",
|
"nullPointMode": "null",
|
||||||
|
"paceLength": 10,
|
||||||
"percentage": false,
|
"percentage": false,
|
||||||
"pointradius": 5,
|
"pointradius": 5,
|
||||||
"points": false,
|
"points": false,
|
||||||
@ -5328,8 +5866,8 @@
|
|||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
"text": "gdev-elasticsearch-v2-metrics",
|
"text": "gdev-elasticsearch-v5-metrics",
|
||||||
"value": "gdev-elasticsearch-v2-metrics"
|
"value": "gdev-elasticsearch-v5-metrics"
|
||||||
},
|
},
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
"label": "Version One",
|
"label": "Version One",
|
||||||
@ -5343,8 +5881,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
"text": "gdev-elasticsearch-v5-metrics",
|
"text": "gdev-elasticsearch-v6-metrics",
|
||||||
"value": "gdev-elasticsearch-v5-metrics"
|
"value": "gdev-elasticsearch-v6-metrics"
|
||||||
},
|
},
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
"label": "Version Two",
|
"label": "Version Two",
|
||||||
@ -5359,7 +5897,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"from": "now-3h",
|
"from": "now-1h",
|
||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {
|
"timepicker": {
|
||||||
@ -5390,5 +5928,5 @@
|
|||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Datasource tests - Elasticsearch comparison",
|
"title": "Datasource tests - Elasticsearch comparison",
|
||||||
"uid": "fuFWehBmk",
|
"uid": "fuFWehBmk",
|
||||||
"version": 10
|
"version": 4
|
||||||
}
|
}
|
1250
devenv/dev-dashboards/panel_tests_gauge.json
Normal file
1250
devenv/dev-dashboards/panel_tests_gauge.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -69,6 +69,7 @@ reporting-disabled = false
|
|||||||
|
|
||||||
unix-socket-enabled = false # enable http service over unix domain socket
|
unix-socket-enabled = false # enable http service over unix domain socket
|
||||||
# bind-socket = "/var/run/influxdb.sock"
|
# bind-socket = "/var/run/influxdb.sock"
|
||||||
|
flux-enabled = true
|
||||||
|
|
||||||
[subscriber]
|
[subscriber]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
@ -54,7 +54,8 @@ services:
|
|||||||
# - GF_DATABASE_SSL_MODE=disable
|
# - GF_DATABASE_SSL_MODE=disable
|
||||||
# - GF_SESSION_PROVIDER=postgres
|
# - GF_SESSION_PROVIDER=postgres
|
||||||
# - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
|
# - 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:
|
ports:
|
||||||
- 3000
|
- 3000
|
||||||
depends_on:
|
depends_on:
|
||||||
|
69
devenv/docker/loadtest/README.md
Normal file
69
devenv/docker/loadtest/README.md
Normal file
@ -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
|
||||||
|
```
|
71
devenv/docker/loadtest/auth_token_test.js
Normal file
71
devenv/docker/loadtest/auth_token_test.js
Normal file
@ -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) => {}
|
187
devenv/docker/loadtest/modules/client.js
Normal file
187
devenv/docker/loadtest/modules/client.js
Normal file
@ -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));
|
||||||
|
}
|
35
devenv/docker/loadtest/modules/util.js
Normal file
35
devenv/docker/loadtest/modules/util.js
Normal file
@ -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;
|
||||||
|
}
|
24
devenv/docker/loadtest/run.sh
Executable file
24
devenv/docker/loadtest/run.sh
Executable file
@ -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 "$@"
|
@ -47,7 +47,7 @@ authentication:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
[auth.gitlab]
|
[auth.gitlab]
|
||||||
enabled = false
|
enabled = true
|
||||||
allow_sign_up = false
|
allow_sign_up = false
|
||||||
client_id = GITLAB_APPLICATION_ID
|
client_id = GITLAB_APPLICATION_ID
|
||||||
client_secret = GITLAB_SECRET
|
client_secret = GITLAB_SECRET
|
||||||
|
@ -38,7 +38,7 @@ Name | Description
|
|||||||
|
|
||||||
### IAM Roles
|
### 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.
|
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)
|
Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
+++
|
+++
|
||||||
title = "Explore"
|
title = "Explore"
|
||||||
|
keywords = ["explore", "loki", "logs"]
|
||||||
type = "docs"
|
type = "docs"
|
||||||
[menu.docs]
|
[menu.docs]
|
||||||
name = "Explore"
|
name = "Explore"
|
||||||
@ -8,7 +9,11 @@ parent = "features"
|
|||||||
weight = 5
|
weight = 5
|
||||||
+++
|
+++
|
||||||
|
|
||||||
# Introduction
|
# Explore
|
||||||
|
|
||||||
|
> Explore is only available in Grafana 6.0 and above.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
One of the major new features of Grafana 6.0 is the new query-focused Explore workflow for troubleshooting and/or for data exploration.
|
One of the major new features of Grafana 6.0 is the new query-focused Explore workflow for troubleshooting and/or for data exploration.
|
||||||
|
|
||||||
|
@ -285,7 +285,7 @@ Content-Type: application/json
|
|||||||
HTTP/1.1 200
|
HTTP/1.1 200
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{message: "User permissions updated"}
|
{"message": "User permissions updated"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Delete global User
|
## Delete global User
|
||||||
@ -308,7 +308,7 @@ Content-Type: application/json
|
|||||||
HTTP/1.1 200
|
HTTP/1.1 200
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{message: "User deleted"}
|
{"message": "User deleted"}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pause all alerts
|
## Pause all alerts
|
||||||
@ -339,5 +339,5 @@ JSON Body schema:
|
|||||||
HTTP/1.1 200
|
HTTP/1.1 200
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
|
{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
|
||||||
```
|
```
|
||||||
|
@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
|||||||
"defaultRegion": "us-west-1"
|
"defaultRegion": "us-west-1"
|
||||||
},
|
},
|
||||||
"secureJsonData": {
|
"secureJsonData": {
|
||||||
"accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
|
"accessKey": "Ol4pIDpeKSA6XikgOl4p",
|
||||||
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
|
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -105,7 +105,7 @@ POST /api/folders/nErXDvCkzz/permissions
|
|||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||||
|
{
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"role": "Viewer",
|
"role": "Viewer",
|
||||||
|
@ -82,4 +82,29 @@ HTTP/1.1 200
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{"message": "Logged in"}
|
{"message": "Logged in"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -391,6 +391,12 @@ value is `true`.
|
|||||||
If you want to track Grafana usage via Google analytics specify *your* Universal
|
If you want to track Grafana usage via Google analytics specify *your* Universal
|
||||||
Analytics ID here. By default this feature is disabled.
|
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.
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
## [dashboards]
|
## [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.
|
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
|
This limit will protect the server from render overloading and make sure notifications are sent out quickly. Default
|
||||||
value is `5`.
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,32 +34,29 @@ sudo dpkg -i grafana_<version>_amd64.deb
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb
|
wget https://dl.grafana.com/oss/release/grafana_5.4.2_amd64.deb
|
||||||
sudo apt-get install -y adduser libfontconfig
|
sudo apt-get install -y adduser libfontconfig
|
||||||
sudo dpkg -i grafana_5.1.4_amd64.deb
|
sudo dpkg -i grafana_5.4.2_amd64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
## APT Repository
|
## APT Repository
|
||||||
|
|
||||||
Add the following line to your `/etc/apt/sources.list` file.
|
Create a file `/etc/apt/sources.list.d/grafana.list` and add the following to it.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
deb https://packagecloud.io/grafana/stable/debian/ stretch main
|
deb https://packages.grafana.com/oss/deb stable main
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the above line even if you are on Ubuntu or another Debian version.
|
There is a separate repository if you want beta releases.
|
||||||
There is also a testing repository if you want beta or release
|
|
||||||
candidates.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
deb https://packagecloud.io/grafana/testing/debian/ stretch main
|
deb https://packages.grafana.com/oss/deb beta main
|
||||||
```
|
```
|
||||||
|
|
||||||
Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
|
Use the above line even if you are on Ubuntu or another Debian version. Then add our gpg key. This allows you to install signed packages.
|
||||||
allows you to install signed packages.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl https://packagecloud.io/gpg.key | sudo apt-key add -
|
curl https://packages.grafana.com/gpg.key | sudo apt-key add -
|
||||||
```
|
```
|
||||||
|
|
||||||
Update your Apt repositories and install Grafana
|
Update your Apt repositories and install Grafana
|
||||||
|
@ -32,7 +32,7 @@ $ sudo yum install <rpm package url>
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
|
$ sudo yum install https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install manually using `rpm`. First execute
|
Or install manually using `rpm`. First execute
|
||||||
@ -44,7 +44,7 @@ $ wget <rpm package url>
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm
|
$ wget https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
### On CentOS / Fedora / Redhat:
|
### On CentOS / Fedora / Redhat:
|
||||||
@ -67,19 +67,27 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
|
|||||||
```bash
|
```bash
|
||||||
[grafana]
|
[grafana]
|
||||||
name=grafana
|
name=grafana
|
||||||
baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch
|
baseurl=https://packages.grafana.com/oss/rpm
|
||||||
repo_gpgcheck=1
|
repo_gpgcheck=1
|
||||||
enabled=1
|
enabled=1
|
||||||
gpgcheck=1
|
gpgcheck=1
|
||||||
gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana
|
gpgkey=https://packages.grafana.com/gpg.key
|
||||||
sslverify=1
|
sslverify=1
|
||||||
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
|
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
|
||||||
```
|
```
|
||||||
|
|
||||||
There is also a testing repository if you want beta or release candidates.
|
There is a separate repository if you want beta releases.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch
|
[grafana]
|
||||||
|
name=grafana
|
||||||
|
baseurl=https://packages.grafana.com/oss/rpm-beta
|
||||||
|
repo_gpgcheck=1
|
||||||
|
enabled=1
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://packages.grafana.com/gpg.key
|
||||||
|
sslverify=1
|
||||||
|
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
|
||||||
```
|
```
|
||||||
|
|
||||||
Then install Grafana via the `yum` command.
|
Then install Grafana via the `yum` command.
|
||||||
@ -91,7 +99,7 @@ $ sudo yum install grafana
|
|||||||
### RPM GPG Key
|
### RPM GPG Key
|
||||||
|
|
||||||
The RPMs are signed, you can verify the signature with this [public GPG
|
The RPMs are signed, you can verify the signature with this [public GPG
|
||||||
key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana).
|
key](https://packages.grafana.com/gpg.key).
|
||||||
|
|
||||||
## Package details
|
## Package details
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
|
|||||||
"list": []
|
"list": []
|
||||||
},
|
},
|
||||||
"refresh": "5s",
|
"refresh": "5s",
|
||||||
"schemaVersion": 16,
|
"schemaVersion": 17,
|
||||||
"version": 0,
|
"version": 0,
|
||||||
"links": []
|
"links": []
|
||||||
}
|
}
|
||||||
|
@ -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
|
`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.
|
`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.
|
`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).
|
Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1).
|
||||||
|
|
||||||
@ -292,9 +293,11 @@ The `direction` controls how the panels will be arranged.
|
|||||||
|
|
||||||
By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width
|
By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width
|
||||||
of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated
|
of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated
|
||||||
panel. Each panel will never be smaller that the provided `Min width` if you have many selected values.
|
panel.
|
||||||
|
|
||||||
By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
|
Set `Max per row` to tell grafana how many panels per row you want at most. It defaults to *4* if you don't set anything.
|
||||||
|
|
||||||
|
By choosing `vertical` the panels will be arranged from top to bottom in a column. The width of the repeated panels will be the same as of the first panel (the original template) being repeated.
|
||||||
|
|
||||||
Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
|
Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build.
|
||||||
You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
|
You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"stable": "5.4.2",
|
"stable": "5.4.3",
|
||||||
"testing": "5.4.2"
|
"testing": "5.4.3"
|
||||||
}
|
}
|
||||||
|
10
package.json
10
package.json
@ -5,7 +5,7 @@
|
|||||||
"company": "Grafana Labs"
|
"company": "Grafana Labs"
|
||||||
},
|
},
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "5.5.0-pre1",
|
"version": "6.0.0-pre1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
@ -24,7 +24,6 @@
|
|||||||
"@types/jquery": "^1.10.35",
|
"@types/jquery": "^1.10.35",
|
||||||
"@types/node": "^8.0.31",
|
"@types/node": "^8.0.31",
|
||||||
"@types/react": "^16.7.6",
|
"@types/react": "^16.7.6",
|
||||||
"@types/react-custom-scrollbars": "^4.0.5",
|
|
||||||
"@types/react-dom": "^16.0.9",
|
"@types/react-dom": "^16.0.9",
|
||||||
"@types/react-select": "^2.0.4",
|
"@types/react-select": "^2.0.4",
|
||||||
"angular-mocks": "1.6.6",
|
"angular-mocks": "1.6.6",
|
||||||
@ -65,6 +64,7 @@
|
|||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"husky": "^0.14.3",
|
"husky": "^0.14.3",
|
||||||
"jest": "^23.6.0",
|
"jest": "^23.6.0",
|
||||||
|
"jest-date-mock": "^1.0.6",
|
||||||
"lint-staged": "^6.0.0",
|
"lint-staged": "^6.0.0",
|
||||||
"load-grunt-tasks": "3.5.2",
|
"load-grunt-tasks": "3.5.2",
|
||||||
"mini-css-extract-plugin": "^0.4.0",
|
"mini-css-extract-plugin": "^0.4.0",
|
||||||
@ -72,8 +72,8 @@
|
|||||||
"ng-annotate-loader": "^0.6.1",
|
"ng-annotate-loader": "^0.6.1",
|
||||||
"ng-annotate-webpack-plugin": "^0.3.0",
|
"ng-annotate-webpack-plugin": "^0.3.0",
|
||||||
"ngtemplate-loader": "^2.0.1",
|
"ngtemplate-loader": "^2.0.1",
|
||||||
"npm": "^5.4.2",
|
|
||||||
"node-sass": "^4.11.0",
|
"node-sass": "^4.11.0",
|
||||||
|
"npm": "^5.4.2",
|
||||||
"optimize-css-assets-webpack-plugin": "^4.0.2",
|
"optimize-css-assets-webpack-plugin": "^4.0.2",
|
||||||
"phantomjs-prebuilt": "^2.1.15",
|
"phantomjs-prebuilt": "^2.1.15",
|
||||||
"postcss-browser-reporter": "^0.5.0",
|
"postcss-browser-reporter": "^0.5.0",
|
||||||
@ -167,7 +167,6 @@
|
|||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"rc-cascader": "^0.14.0",
|
"rc-cascader": "^0.14.0",
|
||||||
"react": "^16.6.3",
|
"react": "^16.6.3",
|
||||||
"react-custom-scrollbars": "^4.2.1",
|
|
||||||
"react-dom": "^16.6.3",
|
"react-dom": "^16.6.3",
|
||||||
"react-grid-layout": "0.16.6",
|
"react-grid-layout": "0.16.6",
|
||||||
"react-highlight-words": "0.11.0",
|
"react-highlight-words": "0.11.0",
|
||||||
@ -189,7 +188,8 @@
|
|||||||
"slate-react": "^0.12.4",
|
"slate-react": "^0.12.4",
|
||||||
"tether": "^1.4.0",
|
"tether": "^1.4.0",
|
||||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||||
"tinycolor2": "^1.4.1"
|
"tinycolor2": "^1.4.1",
|
||||||
|
"xss": "^1.0.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"caniuse-db": "1.0.30000772",
|
"caniuse-db": "1.0.30000772",
|
||||||
|
@ -11,23 +11,34 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@torkelo/react-select": "2.1.1",
|
"@torkelo/react-select": "2.1.1",
|
||||||
|
"@types/react-test-renderer": "^16.0.3",
|
||||||
|
"@types/react-transition-group": "^2.0.15",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"jquery": "^3.2.1",
|
"jquery": "^3.2.1",
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"react": "^16.6.3",
|
"react": "^16.6.3",
|
||||||
|
"react-custom-scrollbars": "^4.2.1",
|
||||||
"react-dom": "^16.6.3",
|
"react-dom": "^16.6.3",
|
||||||
"react-highlight-words": "0.11.0",
|
"react-highlight-words": "0.11.0",
|
||||||
"react-popper": "^1.3.0",
|
"react-popper": "^1.3.0",
|
||||||
"react-transition-group": "^2.2.1",
|
"react-transition-group": "^2.2.1",
|
||||||
"react-virtualized": "^9.21.0"
|
"react-virtualized": "^9.21.0",
|
||||||
|
"tether": "^1.4.0",
|
||||||
|
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||||
|
"tinycolor2": "^1.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/classnames": "^2.2.6",
|
||||||
"@types/jest": "^23.3.2",
|
"@types/jest": "^23.3.2",
|
||||||
|
"@types/jquery": "^1.10.35",
|
||||||
"@types/lodash": "^4.14.119",
|
"@types/lodash": "^4.14.119",
|
||||||
"@types/react": "^16.7.6",
|
"@types/react": "^16.7.6",
|
||||||
"@types/classnames": "^2.2.6",
|
"@types/react-custom-scrollbars": "^4.0.5",
|
||||||
"@types/jquery": "^1.10.35",
|
"@types/react-test-renderer": "^16.0.3",
|
||||||
|
"@types/tether-drop": "^1.4.8",
|
||||||
|
"@types/tinycolor2": "^1.4.1",
|
||||||
|
"react-test-renderer": "^16.7.0",
|
||||||
"typescript": "^3.2.2"
|
"typescript": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { ColorPalette } from '../components/colorpicker/ColorPalette';
|
import { ColorPalette } from './ColorPalette';
|
||||||
|
|
||||||
describe('CollorPalette', () => {
|
describe('CollorPalette', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { sortedColors } from 'app/core/utils/colors';
|
import { sortedColors } from '../../utils';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
color: string;
|
color: string;
|
||||||
@ -9,13 +9,13 @@ export interface Props {
|
|||||||
export class ColorPalette extends React.Component<Props, any> {
|
export class ColorPalette extends React.Component<Props, any> {
|
||||||
paletteColors: string[];
|
paletteColors: string[];
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.paletteColors = sortedColors;
|
this.paletteColors = sortedColors;
|
||||||
this.onColorSelect = this.onColorSelect.bind(this);
|
this.onColorSelect = this.onColorSelect.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onColorSelect(color) {
|
onColorSelect(color: string) {
|
||||||
return () => {
|
return () => {
|
||||||
this.props.onColorSelect(color);
|
this.props.onColorSelect(color);
|
||||||
};
|
};
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
color: string;
|
color: string;
|
||||||
@ -10,7 +9,7 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ColorPicker extends React.Component<Props, any> {
|
export class ColorPicker extends React.Component<Props, any> {
|
||||||
pickerElem: HTMLElement;
|
pickerElem: HTMLElement | null;
|
||||||
colorPickerDrop: any;
|
colorPickerDrop: any;
|
||||||
|
|
||||||
openColorPicker = () => {
|
openColorPicker = () => {
|
||||||
@ -20,7 +19,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
ReactDOM.render(dropContent, dropContentElem);
|
ReactDOM.render(dropContent, dropContentElem);
|
||||||
|
|
||||||
const drop = new Drop({
|
const drop = new Drop({
|
||||||
target: this.pickerElem,
|
target: this.pickerElem as Element,
|
||||||
content: dropContentElem,
|
content: dropContentElem,
|
||||||
position: 'top center',
|
position: 'top center',
|
||||||
classes: 'drop-popover',
|
classes: 'drop-popover',
|
||||||
@ -28,6 +27,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
hoverCloseDelay: 200,
|
hoverCloseDelay: 200,
|
||||||
tetherOptions: {
|
tetherOptions: {
|
||||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||||
|
attachment: 'bottom center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
onColorSelect = color => {
|
onColorSelect = (color: string) => {
|
||||||
this.props.onChange(color);
|
this.props.onChange(color);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,8 +59,3 @@ export class ColorPicker extends React.Component<Props, any> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
react2AngularDirective('colorPicker', ColorPicker, [
|
|
||||||
'color',
|
|
||||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
|
||||||
]);
|
|
@ -14,7 +14,7 @@ export interface Props {
|
|||||||
export class ColorPickerPopover extends React.Component<Props, any> {
|
export class ColorPickerPopover extends React.Component<Props, any> {
|
||||||
pickerNavElem: any;
|
pickerNavElem: any;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
tab: 'palette',
|
tab: 'palette',
|
||||||
@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component<Props, any> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setPickerNavElem(elem) {
|
setPickerNavElem(elem: any) {
|
||||||
this.pickerNavElem = $(elem);
|
this.pickerNavElem = $(elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
setColor(color) {
|
setColor(color: string) {
|
||||||
const newColor = tinycolor(color);
|
const newColor = tinycolor(color);
|
||||||
if (newColor.isValid()) {
|
if (newColor.isValid()) {
|
||||||
this.setState({
|
this.setState({ color: newColor.toString(), colorString: newColor.toString() });
|
||||||
color: newColor.toString(),
|
|
||||||
colorString: newColor.toString(),
|
|
||||||
});
|
|
||||||
this.props.onColorSelect(color);
|
this.props.onColorSelect(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sampleColorSelected(color) {
|
sampleColorSelected(color: string) {
|
||||||
this.setColor(color);
|
this.setColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
spectrumColorSelected(color) {
|
spectrumColorSelected(color: any) {
|
||||||
const rgbColor = color.toRgbString();
|
const rgbColor = color.toRgbString();
|
||||||
this.setColor(rgbColor);
|
this.setColor(rgbColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
onColorStringChange(e) {
|
onColorStringChange(e: any) {
|
||||||
const colorString = e.target.value;
|
const colorString = e.target.value;
|
||||||
this.setState({
|
this.setState({ colorString: colorString });
|
||||||
colorString: colorString,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newColor = tinycolor(colorString);
|
const newColor = tinycolor(colorString);
|
||||||
if (newColor.isValid()) {
|
if (newColor.isValid()) {
|
||||||
// Update only color state
|
// Update only color state
|
||||||
const newColorString = newColor.toString();
|
const newColorString = newColor.toString();
|
||||||
this.setState({
|
this.setState({ color: newColorString });
|
||||||
color: newColorString,
|
|
||||||
});
|
|
||||||
this.props.onColorSelect(newColorString);
|
this.props.onColorSelect(newColorString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onColorStringBlur(e) {
|
onColorStringBlur(e: any) {
|
||||||
const colorString = e.target.value;
|
const colorString = e.target.value;
|
||||||
this.setColor(colorString);
|
this.setColor(colorString);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.pickerNavElem.find('li:first').addClass('active');
|
this.pickerNavElem.find('li:first').addClass('active');
|
||||||
this.pickerNavElem.on('show', e => {
|
this.pickerNavElem.on('show', (e: any) => {
|
||||||
// use href attr (#name => name)
|
// use href attr (#name => name)
|
||||||
const tab = e.target.hash.slice(1);
|
const tab = e.target.hash.slice(1);
|
||||||
this.setState({
|
this.setState({ tab: tab });
|
||||||
tab: tab,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
|||||||
onToggleAxis: () => {},
|
onToggleAxis: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: SeriesColorPickerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
|||||||
remove: true,
|
remove: true,
|
||||||
tetherOptions: {
|
tetherOptions: {
|
||||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||||
|
attachment: 'bottom center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
|
||||||
|
|
||||||
export interface SeriesColorPickerPopoverProps {
|
export interface SeriesColorPickerPopoverProps {
|
||||||
color: string;
|
color: string;
|
||||||
@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPic
|
|||||||
|
|
||||||
interface AxisSelectorProps {
|
interface AxisSelectorProps {
|
||||||
yaxis: number;
|
yaxis: number;
|
||||||
onToggleAxis: () => void;
|
onToggleAxis?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AxisSelectorState {
|
interface AxisSelectorState {
|
||||||
@ -30,7 +29,7 @@ interface AxisSelectorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
|
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
|
||||||
constructor(props) {
|
constructor(props: AxisSelectorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
yaxis: this.props.yaxis,
|
yaxis: this.props.yaxis,
|
||||||
@ -42,7 +41,10 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
|||||||
this.setState({
|
this.setState({
|
||||||
yaxis: this.state.yaxis === 2 ? 1 : 2,
|
yaxis: this.state.yaxis === 2 ? 1 : 2,
|
||||||
});
|
});
|
||||||
this.props.onToggleAxis();
|
|
||||||
|
if (this.props.onToggleAxis) {
|
||||||
|
this.props.onToggleAxis();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -62,9 +64,3 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
|
|
||||||
'series',
|
|
||||||
'onColorChange',
|
|
||||||
'onToggleAxis',
|
|
||||||
]);
|
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import 'vendor/spectrum';
|
import '../../vendor/spectrum';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
color: string;
|
color: string;
|
||||||
@ -13,17 +13,17 @@ export class SpectrumPicker extends React.Component<Props, any> {
|
|||||||
elem: any;
|
elem: any;
|
||||||
isMoving: boolean;
|
isMoving: boolean;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
||||||
this.setComponentElem = this.setComponentElem.bind(this);
|
this.setComponentElem = this.setComponentElem.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setComponentElem(elem) {
|
setComponentElem(elem: any) {
|
||||||
this.elem = $(elem);
|
this.elem = $(elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSpectrumMove(color) {
|
onSpectrumMove(color: any) {
|
||||||
this.isMoving = true;
|
this.isMoving = true;
|
||||||
this.props.onColorSelect(color);
|
this.props.onColorSelect(color);
|
||||||
}
|
}
|
||||||
@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component<Props, any> {
|
|||||||
this.elem.spectrum('set', this.props.color);
|
this.elem.spectrum('set', this.props.color);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUpdate(nextProps) {
|
componentWillUpdate(nextProps: any) {
|
||||||
// If user move pointer over spectrum field this produce 'move' event and component
|
// If user move pointer over spectrum field this produce 'move' event and component
|
||||||
// may update props.color. We don't want to update spectrum color in this case, so we can use
|
// may update props.color. We don't want to update spectrum color in this case, so we can use
|
||||||
// isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
|
// isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
|
@ -0,0 +1,96 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import Scrollbars from 'react-custom-scrollbars';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
customClassName?: string;
|
||||||
|
autoHide?: boolean;
|
||||||
|
autoHideTimeout?: number;
|
||||||
|
autoHideDuration?: number;
|
||||||
|
autoHeightMax?: string;
|
||||||
|
hideTracksWhenNotNeeded?: boolean;
|
||||||
|
scrollTop?: number;
|
||||||
|
setScrollTop: (event: any) => void;
|
||||||
|
autoHeightMin?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
|
||||||
|
*/
|
||||||
|
export class CustomScrollbar extends PureComponent<Props> {
|
||||||
|
static defaultProps: Partial<Props> = {
|
||||||
|
customClassName: 'custom-scrollbars',
|
||||||
|
autoHide: false,
|
||||||
|
autoHideTimeout: 200,
|
||||||
|
autoHideDuration: 200,
|
||||||
|
setScrollTop: () => {},
|
||||||
|
hideTracksWhenNotNeeded: false,
|
||||||
|
autoHeightMin: '0',
|
||||||
|
autoHeightMax: '100%',
|
||||||
|
};
|
||||||
|
|
||||||
|
private ref: React.RefObject<Scrollbars>;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.ref = React.createRef<Scrollbars>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
autoHeightMax,
|
||||||
|
autoHeightMin,
|
||||||
|
setScrollTop,
|
||||||
|
autoHide,
|
||||||
|
autoHideTimeout,
|
||||||
|
hideTracksWhenNotNeeded,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Scrollbars
|
||||||
|
ref={this.ref}
|
||||||
|
className={customClassName}
|
||||||
|
onScroll={setScrollTop}
|
||||||
|
autoHeight={true}
|
||||||
|
autoHide={autoHide}
|
||||||
|
autoHideTimeout={autoHideTimeout}
|
||||||
|
hideTracksWhenNotNeeded={hideTracksWhenNotNeeded}
|
||||||
|
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
||||||
|
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
||||||
|
autoHeightMax={autoHeightMax}
|
||||||
|
autoHeightMin={autoHeightMin}
|
||||||
|
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
|
||||||
|
renderTrackVertical={props => <div {...props} className="track-vertical" />}
|
||||||
|
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
|
||||||
|
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
|
||||||
|
renderView={props => <div {...props} className="view" />}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Scrollbars>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomScrollbar;
|
@ -0,0 +1,40 @@
|
|||||||
|
.custom-scrollbars {
|
||||||
|
// Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to
|
||||||
|
// make scroll working it should fit outer container size (scroll appears only when inner container size is
|
||||||
|
// greater than outer one).
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.view {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-vertical {
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 6px !important;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-horizontal {
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 6px !important;
|
||||||
|
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-vertical {
|
||||||
|
@include gradient-vertical($scrollbarBackground, $scrollbarBackground2);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-horizontal {
|
||||||
|
@include gradient-horizontal($scrollbarBackground, $scrollbarBackground2);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
|||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"height": "auto",
|
"height": "auto",
|
||||||
"maxHeight": "inherit",
|
"maxHeight": "100%",
|
||||||
"minHeight": "inherit",
|
"minHeight": "0",
|
||||||
"overflow": "hidden",
|
"overflow": "hidden",
|
||||||
"position": "relative",
|
"position": "relative",
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
|||||||
"left": undefined,
|
"left": undefined,
|
||||||
"marginBottom": 0,
|
"marginBottom": 0,
|
||||||
"marginRight": 0,
|
"marginRight": 0,
|
||||||
"maxHeight": "calc(inherit + 0px)",
|
"maxHeight": "calc(100% + 0px)",
|
||||||
"minHeight": "calc(inherit + 0px)",
|
"minHeight": "calc(0 + 0px)",
|
||||||
"overflow": "scroll",
|
"overflow": "scroll",
|
||||||
"position": "relative",
|
"position": "relative",
|
||||||
"right": undefined,
|
"right": undefined,
|
||||||
@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"display": "none",
|
"display": "none",
|
||||||
"height": 6,
|
"height": 6,
|
||||||
"opacity": 0,
|
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
"transition": "opacity 200ms",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
|||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"display": "none",
|
"display": "none",
|
||||||
"opacity": 0,
|
|
||||||
"position": "absolute",
|
"position": "absolute",
|
||||||
"transition": "opacity 200ms",
|
|
||||||
"width": 6,
|
"width": 6,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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(<FormField {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
25
packages/grafana-ui/src/components/FormField/FormField.tsx
Normal file
25
packages/grafana-ui/src/components/FormField/FormField.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
|
||||||
|
import { FormLabel } from '..';
|
||||||
|
|
||||||
|
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
label: string;
|
||||||
|
labelWidth?: number;
|
||||||
|
inputWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
labelWidth: 6,
|
||||||
|
inputWidth: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => {
|
||||||
|
return (
|
||||||
|
<div className="form-field">
|
||||||
|
<FormLabel width={labelWidth}>{label}</FormLabel>
|
||||||
|
<input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FormField.defaultProps = defaultProps;
|
||||||
|
export { FormField };
|
12
packages/grafana-ui/src/components/FormField/_FormField.scss
Normal file
12
packages/grafana-ui/src/components/FormField/_FormField.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="form-field"
|
||||||
|
>
|
||||||
|
<Component
|
||||||
|
width={11}
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</Component>
|
||||||
|
<input
|
||||||
|
className="gf-form-input width-12"
|
||||||
|
onChange={[MockFunction]}
|
||||||
|
type="text"
|
||||||
|
value={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
42
packages/grafana-ui/src/components/FormLabel/FormLabel.tsx
Normal file
42
packages/grafana-ui/src/components/FormLabel/FormLabel.tsx
Normal file
@ -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<Props> = ({
|
||||||
|
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 (
|
||||||
|
<label className={classes} {...rest} htmlFor={htmlFor}>
|
||||||
|
{children}
|
||||||
|
{tooltip && (
|
||||||
|
<Tooltip placement="auto" content={tooltip}>
|
||||||
|
<div className="gf-form-help-icon--right-normal">
|
||||||
|
<i className="gicon gicon-question gicon--has-hover" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
147
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
Normal file
147
packages/grafana-ui/src/components/Gauge/Gauge.test.tsx
Normal file
@ -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(<Gauge {...props} />);
|
||||||
|
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 ');
|
||||||
|
});
|
||||||
|
});
|
@ -1,15 +1,15 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { BasicGaugeColor, MappingType, RangeMap, Threshold, ValueMap } from 'app/types';
|
|
||||||
import { TimeSeriesVMs } from '@grafana/ui';
|
|
||||||
import config from '../core/config';
|
|
||||||
import kbn from '../core/utils/kbn';
|
|
||||||
|
|
||||||
interface Props {
|
import { ValueMapping, Threshold, ThemeName, BasicGaugeColor, ThemeNames } from '../../types/panel';
|
||||||
baseColor: string;
|
import { TimeSeriesVMs } from '../../types/series';
|
||||||
|
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
|
||||||
|
import { TimeSeriesValue, getMappedValue } from '../../utils/valueMappings';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
decimals: number;
|
decimals: number;
|
||||||
height: number;
|
height: number;
|
||||||
mappings: Array<RangeMap | ValueMap>;
|
valueMappings: ValueMapping[];
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
minValue: number;
|
minValue: number;
|
||||||
prefix: string;
|
prefix: string;
|
||||||
@ -21,15 +21,15 @@ interface Props {
|
|||||||
suffix: string;
|
suffix: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
width: number;
|
width: number;
|
||||||
|
theme?: ThemeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Gauge extends PureComponent<Props> {
|
export class Gauge extends PureComponent<Props> {
|
||||||
canvasElement: any;
|
canvasElement: any;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
baseColor: BasicGaugeColor.Green,
|
|
||||||
maxValue: 100,
|
maxValue: 100,
|
||||||
mappings: [],
|
valueMappings: [],
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
showThresholdMarkers: true,
|
showThresholdMarkers: true,
|
||||||
@ -38,6 +38,7 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
thresholds: [],
|
thresholds: [],
|
||||||
unit: 'none',
|
unit: 'none',
|
||||||
stat: 'avg',
|
stat: 'avg',
|
||||||
|
theme: ThemeNames.Dark,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -48,89 +49,93 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
formatWithMappings(mappings, value) {
|
formatValue(value: TimeSeriesValue) {
|
||||||
const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
|
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
||||||
const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
|
|
||||||
|
|
||||||
const valueMap = valueMaps.map(mapping => {
|
if (isNaN(value as number)) {
|
||||||
if (mapping.value && value === mapping.value) {
|
return value;
|
||||||
return mapping.text;
|
}
|
||||||
|
|
||||||
|
if (valueMappings.length > 0) {
|
||||||
|
const valueMappedValue = getMappedValue(valueMappings, value);
|
||||||
|
if (valueMappedValue) {
|
||||||
|
return `${prefix} ${valueMappedValue.text} ${suffix}`;
|
||||||
}
|
}
|
||||||
})[0];
|
}
|
||||||
|
|
||||||
const rangeMap = rangeMaps.map(mapping => {
|
const formatFunc = getValueFormat(unit);
|
||||||
if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
|
const formattedValue = formatFunc(value as number, decimals);
|
||||||
return mapping.text;
|
const handleNoValueValue = formattedValue || 'no value';
|
||||||
}
|
|
||||||
})[0];
|
|
||||||
|
|
||||||
return {
|
return `${prefix} ${handleNoValueValue} ${suffix}`;
|
||||||
rangeMap,
|
|
||||||
valueMap,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formatValue(value) {
|
getFontColor(value: TimeSeriesValue) {
|
||||||
const { decimals, mappings, prefix, suffix, unit } = this.props;
|
const { thresholds } = this.props;
|
||||||
|
|
||||||
const formatFunc = kbn.valueFormats[unit];
|
if (thresholds.length === 1) {
|
||||||
const formattedValue = formatFunc(value, decimals);
|
return thresholds[0].color;
|
||||||
|
|
||||||
if (mappings.length > 0) {
|
|
||||||
const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue);
|
|
||||||
|
|
||||||
if (valueMap) {
|
|
||||||
return valueMap;
|
|
||||||
} else if (rangeMap) {
|
|
||||||
return rangeMap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(value)) {
|
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||||
return '-';
|
if (atThreshold) {
|
||||||
|
return atThreshold.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${prefix} ${formattedValue} ${suffix}`;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFontColor(value) {
|
getFormattedThresholds() {
|
||||||
const { baseColor, maxValue, thresholds } = this.props;
|
const { maxValue, minValue, thresholds } = this.props;
|
||||||
|
|
||||||
const atThreshold = thresholds.filter(threshold => value <= threshold.value);
|
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
|
||||||
|
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
|
||||||
|
|
||||||
if (atThreshold.length > 0) {
|
const formattedThresholds = [
|
||||||
return atThreshold[0].color;
|
...thresholdsSortedByIndex.map(threshold => {
|
||||||
} else if (value <= maxValue) {
|
if (threshold.index === 0) {
|
||||||
return BasicGaugeColor.Red;
|
return { value: minValue, color: threshold.color };
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseColor;
|
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
|
||||||
|
return { value: threshold.value, color: previousThreshold.color };
|
||||||
|
}),
|
||||||
|
{ value: maxValue, color: lastThreshold.color },
|
||||||
|
];
|
||||||
|
|
||||||
|
return formattedThresholds;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
const {
|
const {
|
||||||
baseColor,
|
|
||||||
maxValue,
|
maxValue,
|
||||||
minValue,
|
minValue,
|
||||||
timeSeries,
|
timeSeries,
|
||||||
showThresholdLabels,
|
showThresholdLabels,
|
||||||
showThresholdMarkers,
|
showThresholdMarkers,
|
||||||
thresholds,
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
stat,
|
stat,
|
||||||
|
theme,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let value: string | number = '';
|
let value: TimeSeriesValue = '';
|
||||||
|
|
||||||
if (timeSeries[0]) {
|
if (timeSeries[0]) {
|
||||||
value = timeSeries[0].stats[stat];
|
value = timeSeries[0].stats[stat];
|
||||||
} else {
|
} else {
|
||||||
value = 'N/A';
|
value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dimension = Math.min(width, height * 1.3);
|
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 fontScale = parseInt('80', 10) / 100;
|
||||||
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
||||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||||
@ -138,20 +143,6 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||||
const thresholdLabelFontSize = fontSize / 2.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 = {
|
const options = {
|
||||||
series: {
|
series: {
|
||||||
gauges: {
|
gauges: {
|
||||||
@ -168,7 +159,7 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
layout: { margin: 0, thresholdWidth: 0 },
|
layout: { margin: 0, thresholdWidth: 0 },
|
||||||
cell: { border: { width: 0 } },
|
cell: { border: { width: 0 } },
|
||||||
threshold: {
|
threshold: {
|
||||||
values: formattedThresholds,
|
values: this.getFormattedThresholds(),
|
||||||
label: {
|
label: {
|
||||||
show: showThresholdLabels,
|
show: showThresholdLabels,
|
||||||
margin: thresholdMarkersWidth + 1,
|
margin: thresholdMarkersWidth + 1,
|
||||||
@ -182,19 +173,14 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
formatter: () => {
|
formatter: () => {
|
||||||
return this.formatValue(value);
|
return this.formatValue(value);
|
||||||
},
|
},
|
||||||
font: {
|
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
||||||
size: fontSize,
|
|
||||||
family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const plotSeries = {
|
const plotSeries = { data: [[0, value]] };
|
||||||
data: [[0, value]],
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$.plot(this.canvasElement, [plotSeries], options);
|
$.plot(this.canvasElement, [plotSeries], options);
|
@ -98,6 +98,7 @@ export class Graph extends PureComponent<GraphProps> {
|
|||||||
$.plot(this.element, timeSeries, flotOptions);
|
$.plot(this.element, timeSeries, flotOptions);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Graph rendering error', err, flotOptions, timeSeries);
|
console.log('Graph rendering error', err, flotOptions, timeSeries);
|
||||||
|
throw new Error('Error rendering panel');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
|
||||||
|
interface LoadingPlaceholderProps {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
|
||||||
|
<div className="gf-form-group">
|
||||||
|
{text} <i className="fa fa-spinner fa-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
@ -0,0 +1,15 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
cols?: number;
|
||||||
|
children: JSX.Element[] | JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelOptionsGrid: SFC<Props> = ({ children }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel-options-grid">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,10 @@
|
|||||||
|
.panel-options-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
grid-row-gap: 10px;
|
||||||
|
grid-column-gap: 10px;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
@ -7,11 +7,11 @@ interface Props {
|
|||||||
children: JSX.Element | JSX.Element[];
|
children: JSX.Element | JSX.Element[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PanelOptionSection: SFC<Props> = props => {
|
export const PanelOptionsGroup: SFC<Props> = props => {
|
||||||
return (
|
return (
|
||||||
<div className="panel-option-section">
|
<div className="panel-options-group">
|
||||||
{props.title && (
|
{props.title && (
|
||||||
<div className="panel-option-section__header">
|
<div className="panel-options-group__header">
|
||||||
{props.title}
|
{props.title}
|
||||||
{props.onClose && (
|
{props.onClose && (
|
||||||
<button className="btn btn-link" onClick={props.onClose}>
|
<button className="btn btn-link" onClick={props.onClose}>
|
||||||
@ -20,7 +20,7 @@ export const PanelOptionSection: SFC<Props> = props => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="panel-option-section__body">{props.children}</div>
|
<div className="panel-options-group__body">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -0,0 +1,27 @@
|
|||||||
|
.panel-options-group {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: $panel-options-group-border;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
background: $page-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-options-group__header {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
background: $panel-options-group-header-bg;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-options-group__body {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
&--queries {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
@ -6,16 +6,13 @@ interface Props {
|
|||||||
root?: HTMLElement;
|
root?: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class BodyPortal extends PureComponent<Props> {
|
export class Portal extends PureComponent<Props> {
|
||||||
node: HTMLElement = document.createElement('div');
|
node: HTMLElement = document.createElement('div');
|
||||||
portalRoot: HTMLElement;
|
portalRoot: HTMLElement;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
const {
|
const { className, root = document.body } = this.props;
|
||||||
className,
|
|
||||||
root = document.body
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (className) {
|
if (className) {
|
||||||
this.node.classList.add(className);
|
this.node.classList.add(className);
|
@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||||
|
// @ts-ignore
|
||||||
import { components } from '@torkelo/react-select';
|
import { components } from '@torkelo/react-select';
|
||||||
|
|
||||||
export const IndicatorsContainer = props => {
|
export const IndicatorsContainer = (props: any) => {
|
||||||
const isOpen = props.selectProps.menuIsOpen;
|
const isOpen = props.selectProps.menuIsOpen;
|
||||||
return (
|
return (
|
||||||
<components.IndicatorsContainer {...props}>
|
<components.IndicatorsContainer {...props}>
|
@ -1,5 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||||
|
// @ts-ignore
|
||||||
import { components } from '@torkelo/react-select';
|
import { components } from '@torkelo/react-select';
|
||||||
|
// @ts-ignore
|
||||||
import { OptionProps } from '@torkelo/react-select/lib/components/Option';
|
import { OptionProps } from '@torkelo/react-select/lib/components/Option';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
@ -1,17 +1,22 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||||
|
// @ts-ignore
|
||||||
import { default as ReactSelect } from '@torkelo/react-select';
|
import { default as ReactSelect } from '@torkelo/react-select';
|
||||||
|
// @ts-ignore
|
||||||
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
||||||
|
// @ts-ignore
|
||||||
import { components } from '@torkelo/react-select';
|
import { components } from '@torkelo/react-select';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { Option, SingleValue } from './PickerOption';
|
import { SelectOption, SingleValue } from './SelectOption';
|
||||||
import OptionGroup from './OptionGroup';
|
import SelectOptionGroup from './SelectOptionGroup';
|
||||||
import IndicatorsContainer from './IndicatorsContainer';
|
import IndicatorsContainer from './IndicatorsContainer';
|
||||||
import NoOptionsMessage from './NoOptionsMessage';
|
import NoOptionsMessage from './NoOptionsMessage';
|
||||||
import ResetStyles from './ResetStyles';
|
import resetSelectStyles from './resetSelectStyles';
|
||||||
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
|
import { CustomScrollbar } from '..';
|
||||||
|
|
||||||
export interface SelectOptionItem {
|
export interface SelectOptionItem {
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -53,10 +58,10 @@ interface AsyncProps {
|
|||||||
loadingMessage?: () => string;
|
loadingMessage?: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuList = props => {
|
export const MenuList = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<components.MenuList {...props}>
|
<components.MenuList {...props}>
|
||||||
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
|
<CustomScrollbar autoHide={false} autoHeightMax="inherit">{props.children}</CustomScrollbar>
|
||||||
</components.MenuList>
|
</components.MenuList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -112,11 +117,11 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
|||||||
classNamePrefix="gf-form-select-box"
|
classNamePrefix="gf-form-select-box"
|
||||||
className={selectClassNames}
|
className={selectClassNames}
|
||||||
components={{
|
components={{
|
||||||
Option,
|
Option: SelectOption,
|
||||||
SingleValue,
|
SingleValue,
|
||||||
IndicatorsContainer,
|
IndicatorsContainer,
|
||||||
MenuList,
|
MenuList,
|
||||||
Group: OptionGroup,
|
Group: SelectOptionGroup,
|
||||||
}}
|
}}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={value}
|
value={value}
|
||||||
@ -127,7 +132,7 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={placeholder || 'Choose'}
|
placeholder={placeholder || 'Choose'}
|
||||||
styles={ResetStyles}
|
styles={resetSelectStyles()}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
@ -197,7 +202,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
|||||||
classNamePrefix="gf-form-select-box"
|
classNamePrefix="gf-form-select-box"
|
||||||
className={selectClassNames}
|
className={selectClassNames}
|
||||||
components={{
|
components={{
|
||||||
Option,
|
Option: SelectOption,
|
||||||
SingleValue,
|
SingleValue,
|
||||||
IndicatorsContainer,
|
IndicatorsContainer,
|
||||||
NoOptionsMessage,
|
NoOptionsMessage,
|
||||||
@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
defaultOptions={defaultOptions}
|
defaultOptions={defaultOptions}
|
||||||
placeholder={placeholder || 'Choose'}
|
placeholder={placeholder || 'Choose'}
|
||||||
styles={ResetStyles}
|
styles={resetSelectStyles()}
|
||||||
loadingMessage={loadingMessage}
|
loadingMessage={loadingMessage}
|
||||||
noOptionsMessage={noOptionsMessage}
|
noOptionsMessage={noOptionsMessage}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import PickerOption from './PickerOption';
|
import SelectOption from './SelectOption';
|
||||||
|
import { OptionProps } from 'react-select/lib/components/Option';
|
||||||
|
|
||||||
const model = {
|
const model: OptionProps<any> = {
|
||||||
|
data: jest.fn(),
|
||||||
cx: jest.fn(),
|
cx: jest.fn(),
|
||||||
clearValue: jest.fn(),
|
clearValue: jest.fn(),
|
||||||
onSelect: jest.fn(),
|
|
||||||
getStyles: jest.fn(),
|
getStyles: jest.fn(),
|
||||||
getValue: jest.fn(),
|
getValue: jest.fn(),
|
||||||
hasValue: true,
|
hasValue: true,
|
||||||
@ -18,21 +19,31 @@ const model = {
|
|||||||
isFocused: false,
|
isFocused: false,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
innerRef: null,
|
innerRef: null,
|
||||||
innerProps: null,
|
innerProps: {
|
||||||
label: 'Option label',
|
id: '',
|
||||||
type: null,
|
key: '',
|
||||||
children: 'Model title',
|
onClick: jest.fn(),
|
||||||
data: {
|
onMouseOver: jest.fn(),
|
||||||
title: 'Model title',
|
tabIndex: 1,
|
||||||
imgUrl: 'url/to/avatar',
|
|
||||||
label: 'User picker label',
|
|
||||||
},
|
},
|
||||||
|
label: 'Option label',
|
||||||
|
type: 'option',
|
||||||
|
children: 'Model title',
|
||||||
className: 'class-for-user-picker',
|
className: 'class-for-user-picker',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('PickerOption', () => {
|
describe('SelectOption', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const tree = renderer.create(<PickerOption {...model} />).toJSON();
|
const tree = renderer
|
||||||
|
.create(
|
||||||
|
<SelectOption
|
||||||
|
{...model}
|
||||||
|
data={{
|
||||||
|
imgUrl: 'url/to/avatar',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
.toJSON();
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -1,4 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||||
|
// @ts-ignore
|
||||||
import { components } from '@torkelo/react-select';
|
import { components } from '@torkelo/react-select';
|
||||||
import { OptionProps } from 'react-select/lib/components/Option';
|
import { OptionProps } from 'react-select/lib/components/Option';
|
||||||
|
|
||||||
@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps<any> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Option = (props: ExtendedOptionProps) => {
|
export const SelectOption = (props: ExtendedOptionProps) => {
|
||||||
const { children, isSelected, data } = props;
|
const { children, isSelected, data } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// was not able to type this without typescript error
|
// was not able to type this without typescript error
|
||||||
export const SingleValue = props => {
|
export const SingleValue = (props: any) => {
|
||||||
const { children, data } = props;
|
const { children, data } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -41,4 +44,4 @@ export const SingleValue = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Option;
|
export default SelectOption;
|
@ -2,21 +2,27 @@ import React, { PureComponent } from 'react';
|
|||||||
import { GroupProps } from 'react-select/lib/components/Group';
|
import { GroupProps } from 'react-select/lib/components/Group';
|
||||||
|
|
||||||
interface ExtendedGroupProps extends GroupProps<any> {
|
interface ExtendedGroupProps extends GroupProps<any> {
|
||||||
data: any;
|
data: {
|
||||||
|
label: string;
|
||||||
|
expanded: boolean;
|
||||||
|
options: any[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class OptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
||||||
state = {
|
state = {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.selectProps) {
|
if (this.props.data.expanded) {
|
||||||
const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
|
this.setState({ expanded: true });
|
||||||
|
} else if (this.props.selectProps && this.props.selectProps.value) {
|
||||||
|
const { value } = this.props.selectProps.value;
|
||||||
|
|
||||||
if (value && this.props.options.some(option => option.value === value)) {
|
if (value && this.props.options.some(option => option.value === value)) {
|
||||||
this.setState({ expanded: true });
|
this.setState({ expanded: true });
|
||||||
@ -24,7 +30,7 @@ export default class OptionGroup extends PureComponent<ExtendedGroupProps, State
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(nextProps) {
|
componentDidUpdate(nextProps: ExtendedGroupProps) {
|
||||||
if (nextProps.selectProps.inputValue !== '') {
|
if (nextProps.selectProps.inputValue !== '') {
|
||||||
this.setState({ expanded: true });
|
this.setState({ expanded: true });
|
||||||
}
|
}
|
@ -63,6 +63,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
.gf-form-select-box__menu-list {
|
.gf-form-select-box__menu-list {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-filter .gf-form-select-box__menu {
|
.tag-filter .gf-form-select-box__menu {
|
||||||
@ -101,6 +102,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
|||||||
.gf-form-select-box__value-container {
|
.gf-form-select-box__value-container {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
|
vertical-align: middle;
|
||||||
> div {
|
> div {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
@ -1,7 +1,12 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`PickerOption renders correctly 1`] = `
|
exports[`SelectOption renders correctly 1`] = `
|
||||||
<div>
|
<div
|
||||||
|
id=""
|
||||||
|
onClick={[MockFunction]}
|
||||||
|
onMouseOver={[MockFunction]}
|
||||||
|
tabIndex={1}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__desc-option"
|
className="gf-form-select-box__desc-option"
|
||||||
>
|
>
|
@ -0,0 +1,27 @@
|
|||||||
|
export default function resetSelectStyles() {
|
||||||
|
return {
|
||||||
|
clearIndicator: () => ({}),
|
||||||
|
container: () => ({}),
|
||||||
|
control: () => ({}),
|
||||||
|
dropdownIndicator: () => ({}),
|
||||||
|
group: () => ({}),
|
||||||
|
groupHeading: () => ({}),
|
||||||
|
indicatorsContainer: () => ({}),
|
||||||
|
indicatorSeparator: () => ({}),
|
||||||
|
input: () => ({}),
|
||||||
|
loadingIndicator: () => ({}),
|
||||||
|
loadingMessage: () => ({}),
|
||||||
|
menu: () => ({}),
|
||||||
|
menuList: ({ maxHeight }: { maxHeight: number }) => ({
|
||||||
|
maxHeight,
|
||||||
|
}),
|
||||||
|
multiValue: () => ({}),
|
||||||
|
multiValueLabel: () => ({}),
|
||||||
|
multiValueRemove: () => ({}),
|
||||||
|
noOptionsMessage: () => ({}),
|
||||||
|
option: () => ({}),
|
||||||
|
placeholder: () => ({}),
|
||||||
|
singleValue: () => ({}),
|
||||||
|
valueContainer: () => ({}),
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
onChange: jest.fn(),
|
||||||
|
thresholds: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Initialization', () => {
|
||||||
|
it('should add a base threshold if missing', () => {
|
||||||
|
const instance = setup();
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Add threshold', () => {
|
||||||
|
it('should not add threshold at index 0', () => {
|
||||||
|
const instance = setup();
|
||||||
|
|
||||||
|
instance.onAddThreshold(0);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add threshold', () => {
|
||||||
|
const instance = setup();
|
||||||
|
|
||||||
|
instance.onAddThreshold(1);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add another threshold above a first', () => {
|
||||||
|
const instance = setup({
|
||||||
|
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.onAddThreshold(2);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add another threshold between first and second index', () => {
|
||||||
|
const instance = setup({
|
||||||
|
thresholds: [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.onAddThreshold(2);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 3, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 2, value: 62.5, color: '#EF843C' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Remove threshold', () => {
|
||||||
|
it('should not remove threshold at index 0', () => {
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
const instance = setup({ thresholds });
|
||||||
|
|
||||||
|
instance.onRemoveThreshold(thresholds[0]);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual(thresholds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove threshold', () => {
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
const instance = setup({
|
||||||
|
thresholds,
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.onRemoveThreshold(thresholds[1]);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('change threshold value', () => {
|
||||||
|
it('should not change threshold at index 0', () => {
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
const instance = setup({ thresholds });
|
||||||
|
|
||||||
|
const mockEvent = { target: { value: 12 } };
|
||||||
|
|
||||||
|
instance.onChangeThresholdValue(mockEvent, thresholds[0]);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual(thresholds);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update value', () => {
|
||||||
|
const instance = setup();
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 50, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
|
||||||
|
instance.state = {
|
||||||
|
thresholds,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockEvent = { target: { value: 78 } };
|
||||||
|
|
||||||
|
instance.onChangeThresholdValue(mockEvent, thresholds[1]);
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 78, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on blur threshold value', () => {
|
||||||
|
it('should resort rows and update indexes', () => {
|
||||||
|
const instance = setup();
|
||||||
|
const thresholds = [
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
{ index: 1, value: 78, color: '#EAB839' },
|
||||||
|
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||||
|
];
|
||||||
|
|
||||||
|
instance.state = {
|
||||||
|
thresholds,
|
||||||
|
};
|
||||||
|
|
||||||
|
instance.onBlur();
|
||||||
|
|
||||||
|
expect(instance.state.thresholds).toEqual([
|
||||||
|
{ index: 2, value: 78, color: '#EAB839' },
|
||||||
|
{ index: 1, value: 75, color: '#6ED0E0' },
|
||||||
|
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,211 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
// import tinycolor, { ColorInput } from 'tinycolor2';
|
||||||
|
|
||||||
|
import { Threshold } from '../../types';
|
||||||
|
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||||
|
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||||
|
import { colors } from '../../utils';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
thresholds: Threshold[];
|
||||||
|
onChange: (thresholds: Threshold[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
thresholds: Threshold[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const { thresholds } = this.state;
|
||||||
|
const maxValue = 100;
|
||||||
|
const minValue = 0;
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newThresholds = thresholds.map(threshold => {
|
||||||
|
if (threshold.index >= index) {
|
||||||
|
const index = threshold.index + 1;
|
||||||
|
threshold = { ...threshold, index };
|
||||||
|
}
|
||||||
|
return threshold;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setting value to a value between the previous thresholds
|
||||||
|
const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0];
|
||||||
|
const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0];
|
||||||
|
const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue;
|
||||||
|
const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue;
|
||||||
|
const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2;
|
||||||
|
|
||||||
|
// Set a color
|
||||||
|
const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0];
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
thresholds: this.sortThresholds([
|
||||||
|
...newThresholds,
|
||||||
|
{
|
||||||
|
index,
|
||||||
|
value: value as number,
|
||||||
|
color,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
() => this.onChange()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemoveThreshold = (threshold: Threshold) => {
|
||||||
|
if (threshold.index === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
prevState => {
|
||||||
|
const newThresholds = prevState.thresholds.map(t => {
|
||||||
|
if (t.index > threshold.index) {
|
||||||
|
const index = t.index - 1;
|
||||||
|
t = { ...t, index };
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
thresholds: newThresholds.filter(t => t !== threshold),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
() => this.onChange()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeThresholdValue = (event: any, threshold: Threshold) => {
|
||||||
|
if (threshold.index === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { thresholds } = this.state;
|
||||||
|
const parsedValue = parseInt(event.target.value, 10);
|
||||||
|
const value = isNaN(parsedValue) ? null : parsedValue;
|
||||||
|
|
||||||
|
const newThresholds = thresholds.map(t => {
|
||||||
|
if (t === threshold && t.index !== 0) {
|
||||||
|
t = { ...t, value: value as number };
|
||||||
|
}
|
||||||
|
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({ thresholds: newThresholds });
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeThresholdColor = (threshold: Threshold, color: string) => {
|
||||||
|
const { thresholds } = this.state;
|
||||||
|
|
||||||
|
const newThresholds = thresholds.map(t => {
|
||||||
|
if (t === threshold) {
|
||||||
|
t = { ...t, color: color };
|
||||||
|
}
|
||||||
|
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
thresholds: newThresholds,
|
||||||
|
},
|
||||||
|
() => this.onChange()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.setState(prevState => {
|
||||||
|
const sortThresholds = this.sortThresholds([...prevState.thresholds]);
|
||||||
|
let index = sortThresholds.length - 1;
|
||||||
|
sortThresholds.forEach(t => {
|
||||||
|
t.index = index--;
|
||||||
|
});
|
||||||
|
return { thresholds: sortThresholds };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = () => {
|
||||||
|
this.props.onChange(this.state.thresholds);
|
||||||
|
};
|
||||||
|
|
||||||
|
sortThresholds = (thresholds: Threshold[]) => {
|
||||||
|
return thresholds.sort((t1, t2) => {
|
||||||
|
return t2.value - t1.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
renderInput = (threshold: Threshold) => {
|
||||||
|
const value = threshold.index === 0 ? 'Base' : threshold.value;
|
||||||
|
return (
|
||||||
|
<div className="thresholds-row-input-inner">
|
||||||
|
<span className="thresholds-row-input-inner-arrow" />
|
||||||
|
<div className="thresholds-row-input-inner-color">
|
||||||
|
{threshold.color && (
|
||||||
|
<div className="thresholds-row-input-inner-color-colorpicker">
|
||||||
|
<ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="thresholds-row-input-inner-value">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
||||||
|
value={value}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
readOnly={threshold.index === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{threshold.index > 0 && (
|
||||||
|
<div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
|
||||||
|
<i className="fa fa-times" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { thresholds } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelOptionsGroup title="Thresholds">
|
||||||
|
<div className="thresholds">
|
||||||
|
{thresholds.map((threshold, index) => {
|
||||||
|
return (
|
||||||
|
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||||
|
<div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
|
||||||
|
<i className="fa fa-plus" />
|
||||||
|
</div>
|
||||||
|
<div className="thresholds-row-color-indicator" style={{ backgroundColor: threshold.color }} />
|
||||||
|
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PanelOptionsGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
.thresholds {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row:first-child > .thresholds-row-color-indicator {
|
||||||
|
border-top-left-radius: $border-radius;
|
||||||
|
border-top-right-radius: $border-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row:last-child > .thresholds-row-color-indicator {
|
||||||
|
border-bottom-left-radius: $border-radius;
|
||||||
|
border-bottom-right-radius: $border-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-add-button {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
color: $green;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
background-color: $green;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-add-button > i {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-color-indicator {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input {
|
||||||
|
margin-top: 49px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner > *:last-child {
|
||||||
|
border-top-right-radius: $border-radius;
|
||||||
|
border-bottom-right-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner-arrow {
|
||||||
|
align-self: center;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 6px solid transparent;
|
||||||
|
border-bottom: 6px solid transparent;
|
||||||
|
border-right: 6px solid $input-label-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner-value > input {
|
||||||
|
height: $gf-form-input-height;
|
||||||
|
padding: $input-padding-y $input-padding-x;
|
||||||
|
width: 150px;
|
||||||
|
border-top: 1px solid $input-label-border-color;
|
||||||
|
border-bottom: 1px solid $input-label-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner-color {
|
||||||
|
width: 42px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: $input-bg;
|
||||||
|
border: 1px solid $input-label-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner-color-colorpicker {
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-row-input-inner-remove {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: $gf-form-input-height;
|
||||||
|
padding: $input-padding-y $input-padding-x;
|
||||||
|
width: 42px;
|
||||||
|
background-color: $input-label-bg;
|
||||||
|
border: 1px solid $input-label-border-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -1,49 +1,54 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import Portal from 'app/core/components/Portal/Portal';
|
import * as PopperJS from 'popper.js';
|
||||||
import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
|
import { Manager, Popper as ReactPopper } from 'react-popper';
|
||||||
|
import { Portal } from '@grafana/ui';
|
||||||
import Transition from 'react-transition-group/Transition';
|
import Transition from 'react-transition-group/Transition';
|
||||||
|
|
||||||
|
export enum Themes {
|
||||||
|
Default = 'popper__background--default',
|
||||||
|
Error = 'popper__background--error',
|
||||||
|
Brand = 'popper__background--brand',
|
||||||
|
}
|
||||||
|
|
||||||
const defaultTransitionStyles = {
|
const defaultTransitionStyles = {
|
||||||
transition: 'opacity 200ms linear',
|
transition: 'opacity 200ms linear',
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const transitionStyles = {
|
const transitionStyles: {[key: string]: object} = {
|
||||||
exited: { opacity: 0 },
|
exited: { opacity: 0 },
|
||||||
entering: { opacity: 0 },
|
entering: { opacity: 0 },
|
||||||
entered: { opacity: 1 },
|
entered: { opacity: 1 },
|
||||||
exiting: { opacity: 0 },
|
exiting: { opacity: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props extends React.DOMAttributes<HTMLDivElement> {
|
||||||
renderContent: (content: any) => any;
|
renderContent: (content: any) => any;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
placement?: any;
|
placement?: PopperJS.Placement;
|
||||||
content: string | ((props: any) => JSX.Element);
|
content: string | ((props: any) => JSX.Element);
|
||||||
refClassName?: string;
|
referenceElement: PopperJS.ReferenceObject;
|
||||||
|
theme?: Themes;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Popper extends PureComponent<Props> {
|
class Popper extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { children, renderContent, show, placement, refClassName } = this.props;
|
const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props;
|
||||||
const { content } = this.props;
|
const { content } = this.props;
|
||||||
|
|
||||||
|
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Manager>
|
<Manager>
|
||||||
<Reference>
|
|
||||||
{({ ref }) => (
|
|
||||||
<div className={`popper_ref ${refClassName || ''}`} ref={ref}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
||||||
{transitionState => (
|
{transitionState => (
|
||||||
<Portal>
|
<Portal>
|
||||||
<ReactPopper placement={placement}>
|
<ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
|
||||||
{({ ref, style, placement, arrowProps }) => {
|
{({ ref, style, placement, arrowProps }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
@ -53,7 +58,7 @@ class Popper extends PureComponent<Props> {
|
|||||||
data-placement={placement}
|
data-placement={placement}
|
||||||
className="popper"
|
className="popper"
|
||||||
>
|
>
|
||||||
<div className="popper__background">
|
<div className={popperBackgroundClassName}>
|
||||||
{renderContent(content)}
|
{renderContent(content)}
|
||||||
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
|
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
|
||||||
</div>
|
</div>
|
@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import * as PopperJS from 'popper.js';
|
||||||
|
import { Themes } from './Popper';
|
||||||
|
|
||||||
|
type PopperContent = string | (() => JSX.Element);
|
||||||
|
|
||||||
|
export interface UsingPopperProps {
|
||||||
|
show?: boolean;
|
||||||
|
placement?: PopperJS.Placement;
|
||||||
|
content: PopperContent;
|
||||||
|
children: JSX.Element;
|
||||||
|
renderContent?: (content: PopperContent) => JSX.Element;
|
||||||
|
theme?: Themes;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PopperControllerRenderProp = (
|
||||||
|
showPopper: () => void,
|
||||||
|
hidePopper: () => void,
|
||||||
|
popperProps: {
|
||||||
|
show: boolean;
|
||||||
|
placement: PopperJS.Placement;
|
||||||
|
content: string | ((props: any) => JSX.Element);
|
||||||
|
renderContent: (content: any) => any;
|
||||||
|
theme?: Themes;
|
||||||
|
}
|
||||||
|
) => JSX.Element;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
placement?: PopperJS.Placement;
|
||||||
|
content: PopperContent;
|
||||||
|
className?: string;
|
||||||
|
children: PopperControllerRenderProp;
|
||||||
|
theme?: Themes;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
placement: PopperJS.Placement;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PopperController extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
placement: this.props.placement || 'auto',
|
||||||
|
show: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
|
if (nextProps.placement && nextProps.placement !== this.state.placement) {
|
||||||
|
this.setState((prevState: State) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
placement: nextProps.placement || 'auto',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showPopper = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
show: true,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
hidePopper = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
show: false,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
renderContent(content: PopperContent) {
|
||||||
|
if (typeof content === 'function') {
|
||||||
|
// If it's a function we assume it's a React component
|
||||||
|
const ReactComponent = content;
|
||||||
|
return <ReactComponent />;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children, content, theme } = this.props;
|
||||||
|
const { show, placement } = this.state;
|
||||||
|
|
||||||
|
return children(this.showPopper, this.hidePopper, {
|
||||||
|
show,
|
||||||
|
placement,
|
||||||
|
content,
|
||||||
|
renderContent: this.renderContent,
|
||||||
|
theme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PopperController;
|
@ -1,13 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import Tooltip from './Tooltip';
|
import { Tooltip } from './Tooltip';
|
||||||
|
|
||||||
describe('Tooltip', () => {
|
describe('Tooltip', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const tree = renderer
|
const tree = renderer
|
||||||
.create(
|
.create(
|
||||||
<Tooltip className="test-class" placement="auto" content="Tooltip text">
|
<Tooltip placement="auto" content="Tooltip text">
|
||||||
<a href="http://www.grafana.com">Link with tooltip</a>
|
<a className="test-class" href="http://www.grafana.com">
|
||||||
|
Link with tooltip
|
||||||
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
.toJSON();
|
.toJSON();
|
32
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
Normal file
32
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { createRef } from 'react';
|
||||||
|
import * as PopperJS from 'popper.js';
|
||||||
|
import Popper from './Popper';
|
||||||
|
import PopperController, { UsingPopperProps } from './PopperController';
|
||||||
|
|
||||||
|
export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPopperProps) => {
|
||||||
|
const tooltipTriggerRef = createRef<PopperJS.ReferenceObject>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopperController {...controllerProps}>
|
||||||
|
{(showPopper, hidePopper, popperProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tooltipTriggerRef.current && (
|
||||||
|
<Popper
|
||||||
|
{...popperProps}
|
||||||
|
onMouseEnter={showPopper}
|
||||||
|
onMouseLeave={hidePopper}
|
||||||
|
referenceElement={tooltipTriggerRef.current}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{React.cloneElement(children, {
|
||||||
|
ref: tooltipTriggerRef,
|
||||||
|
onMouseEnter: showPopper,
|
||||||
|
onMouseLeave: hidePopper,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</PopperController>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,13 @@
|
|||||||
$popper-margin-from-ref: 5px;
|
$popper-margin-from-ref: 5px;
|
||||||
|
|
||||||
|
|
||||||
|
@mixin popper-theme($backgroundColor, $arrowColor) {
|
||||||
|
background: $backgroundColor;
|
||||||
|
.popper__arrow {
|
||||||
|
border-color: $arrowColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.popper {
|
.popper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: $zindex-tooltip;
|
z-index: $zindex-tooltip;
|
||||||
@ -8,7 +16,24 @@ $popper-margin-from-ref: 5px;
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popper .popper__arrow {
|
.popper__background {
|
||||||
|
background: $tooltipBackground;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
// Themes
|
||||||
|
&.popper__background--error {
|
||||||
|
@include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.popper__background--brand {
|
||||||
|
@include popper-theme($tooltipBackgroundBrand, $tooltipBackgroundBrand);
|
||||||
|
@include gradient-vertical($red, $orange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popper__arrow {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
@ -16,17 +41,10 @@ $popper-margin-from-ref: 5px;
|
|||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popper .popper__arrow {
|
.popper__arrow {
|
||||||
border-color: $tooltipBackground;
|
border-color: $tooltipBackground;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popper__background {
|
|
||||||
background: $tooltipBackground;
|
|
||||||
border-radius: $border-radius;
|
|
||||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Top
|
// Top
|
||||||
.popper[data-placement^='top'] {
|
.popper[data-placement^='top'] {
|
||||||
padding-bottom: $popper-margin-from-ref;
|
padding-bottom: $popper-margin-from-ref;
|
@ -0,0 +1,12 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Tooltip renders correctly 1`] = `
|
||||||
|
<a
|
||||||
|
className="test-class"
|
||||||
|
href="http://www.grafana.com"
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
>
|
||||||
|
Link with tooltip
|
||||||
|
</a>
|
||||||
|
`;
|
@ -1,22 +1,22 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { ChangeEvent, PureComponent } from 'react';
|
||||||
import { Label } from 'app/core/components/Label/Label';
|
|
||||||
import { Select } from 'app/core/components/Select/Select';
|
|
||||||
import { MappingType, RangeMap, ValueMap } from 'app/types';
|
|
||||||
|
|
||||||
interface Props {
|
import { MappingType, ValueMapping } from '../../types';
|
||||||
mapping: ValueMap | RangeMap;
|
import { FormField, FormLabel, Select } from '..';
|
||||||
updateMapping: (mapping) => void;
|
|
||||||
removeMapping: () => void;
|
export interface Props {
|
||||||
|
valueMapping: ValueMapping;
|
||||||
|
updateValueMapping: (valueMapping: ValueMapping) => void;
|
||||||
|
removeValueMapping: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
from: string;
|
from?: string;
|
||||||
id: number;
|
id: number;
|
||||||
operator: string;
|
operator: string;
|
||||||
text: string;
|
text: string;
|
||||||
to: string;
|
to?: string;
|
||||||
type: MappingType;
|
type: MappingType;
|
||||||
value: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappingOptions = [
|
const mappingOptions = [
|
||||||
@ -25,36 +25,34 @@ const mappingOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default class MappingRow extends PureComponent<Props, State> {
|
export default class MappingRow extends PureComponent<Props, State> {
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = { ...props.valueMapping };
|
||||||
...props.mapping,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMappingValueChange = event => {
|
onMappingValueChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({ value: event.target.value });
|
this.setState({ value: event.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMappingFromChange = event => {
|
onMappingFromChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({ from: event.target.value });
|
this.setState({ from: event.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMappingToChange = event => {
|
onMappingToChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({ to: event.target.value });
|
this.setState({ to: event.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMappingTextChange = event => {
|
onMappingTextChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({ text: event.target.value });
|
this.setState({ text: event.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMappingTypeChange = mappingType => {
|
onMappingTypeChange = (mappingType: MappingType) => {
|
||||||
this.setState({ type: mappingType });
|
this.setState({ type: mappingType });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateMapping = () => {
|
updateMapping = () => {
|
||||||
this.props.updateMapping({ ...this.state });
|
this.props.updateValueMapping({ ...this.state } as ValueMapping);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderRow() {
|
renderRow() {
|
||||||
@ -63,30 +61,28 @@ export default class MappingRow extends PureComponent<Props, State> {
|
|||||||
if (type === MappingType.RangeToText) {
|
if (type === MappingType.RangeToText) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form">
|
<FormField
|
||||||
<Label width={4}>From</Label>
|
label="From"
|
||||||
|
labelWidth={4}
|
||||||
|
inputWidth={8}
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingFromChange}
|
||||||
|
value={from}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="To"
|
||||||
|
labelWidth={4}
|
||||||
|
inputWidth={8}
|
||||||
|
onBlur={this.updateMapping}
|
||||||
|
onChange={this.onMappingToChange}
|
||||||
|
value={to}
|
||||||
|
/>
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<FormLabel width={4}>Text</FormLabel>
|
||||||
<input
|
<input
|
||||||
className="gf-form-input width-8"
|
className="gf-form-input"
|
||||||
value={from}
|
|
||||||
onBlur={this.updateMapping}
|
onBlur={this.updateMapping}
|
||||||
onChange={this.onMappingFromChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form">
|
|
||||||
<Label width={4}>To</Label>
|
|
||||||
<input
|
|
||||||
className="gf-form-input width-8"
|
|
||||||
value={to}
|
|
||||||
onBlur={this.updateMapping}
|
|
||||||
onChange={this.onMappingToChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="gf-form">
|
|
||||||
<Label width={4}>Text</Label>
|
|
||||||
<input
|
|
||||||
className="gf-form-input width-10"
|
|
||||||
value={text}
|
value={text}
|
||||||
onBlur={this.updateMapping}
|
|
||||||
onChange={this.onMappingTextChange}
|
onChange={this.onMappingTextChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -96,17 +92,16 @@ export default class MappingRow extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form">
|
<FormField
|
||||||
<Label width={4}>Value</Label>
|
label="Value"
|
||||||
<input
|
labelWidth={4}
|
||||||
className="gf-form-input width-8"
|
onBlur={this.updateMapping}
|
||||||
onBlur={this.updateMapping}
|
onChange={this.onMappingValueChange}
|
||||||
onChange={this.onMappingValueChange}
|
value={value}
|
||||||
value={value}
|
inputWidth={8}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<Label width={4}>Text</Label>
|
<FormLabel width={4}>Text</FormLabel>
|
||||||
<input
|
<input
|
||||||
className="gf-form-input"
|
className="gf-form-input"
|
||||||
onBlur={this.updateMapping}
|
onBlur={this.updateMapping}
|
||||||
@ -124,7 +119,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<Label width={5}>Type</Label>
|
<FormLabel width={5}>Type</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
placeholder="Choose type"
|
placeholder="Choose type"
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
@ -136,7 +131,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
{this.renderRow()}
|
{this.renderRow()}
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<button onClick={this.props.removeMapping} className="gf-form-label gf-form-label--btn">
|
<button onClick={this.props.removeValueMapping} className="gf-form-label gf-form-label--btn">
|
||||||
<i className="fa fa-times" />
|
<i className="fa fa-times" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
@ -1,26 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import ValueMappings from './ValueMappings';
|
|
||||||
import { defaultProps, OptionModuleProps } from './module';
|
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
|
||||||
import { MappingType } from 'app/types';
|
import { MappingType } from '../../types/panel';
|
||||||
|
|
||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: OptionModuleProps = {
|
const props: Props = {
|
||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
options: {
|
valueMappings: [
|
||||||
...defaultProps.options,
|
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||||
mappings: [
|
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
],
|
||||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.assign(props, propOverrides);
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
const wrapper = shallow(<ValueMappings {...props} />);
|
const wrapper = shallow(<ValueMappingsEditor {...props} />);
|
||||||
|
|
||||||
const instance = wrapper.instance() as ValueMappings;
|
const instance = wrapper.instance() as ValueMappingsEditor;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
instance,
|
instance,
|
||||||
@ -39,18 +36,20 @@ describe('Render', () => {
|
|||||||
describe('On remove mapping', () => {
|
describe('On remove mapping', () => {
|
||||||
it('Should remove mapping with id 0', () => {
|
it('Should remove mapping with id 0', () => {
|
||||||
const { instance } = setup();
|
const { instance } = setup();
|
||||||
|
|
||||||
instance.onRemoveMapping(1);
|
instance.onRemoveMapping(1);
|
||||||
|
|
||||||
expect(instance.state.mappings).toEqual([
|
expect(instance.state.valueMappings).toEqual([
|
||||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove mapping with id 1', () => {
|
it('should remove mapping with id 1', () => {
|
||||||
const { instance } = setup();
|
const { instance } = setup();
|
||||||
|
|
||||||
instance.onRemoveMapping(2);
|
instance.onRemoveMapping(2);
|
||||||
|
|
||||||
expect(instance.state.mappings).toEqual([
|
expect(instance.state.valueMappings).toEqual([
|
||||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -66,7 +65,7 @@ describe('Next id to add', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should default to 1', () => {
|
it('should default to 1', () => {
|
||||||
const { instance } = setup({ options: { ...defaultProps.options } });
|
const { instance } = setup({ valueMappings: [] });
|
||||||
|
|
||||||
expect(instance.state.nextIdToAdd).toEqual(1);
|
expect(instance.state.nextIdToAdd).toEqual(1);
|
||||||
});
|
});
|
@ -0,0 +1,105 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import MappingRow from './MappingRow';
|
||||||
|
import { MappingType, ValueMapping } from '../../types/panel';
|
||||||
|
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
valueMappings: ValueMapping[];
|
||||||
|
onChange: (valueMappings: ValueMapping[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
valueMappings: ValueMapping[];
|
||||||
|
nextIdToAdd: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValueMappingsEditor extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const mappings = props.valueMappings;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
valueMappings: mappings,
|
||||||
|
nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromValueMappings(mappings) : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxIdFromValueMappings(mappings: ValueMapping[]) {
|
||||||
|
return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMapping = () =>
|
||||||
|
this.setState(prevState => ({
|
||||||
|
valueMappings: [
|
||||||
|
...prevState.valueMappings,
|
||||||
|
{
|
||||||
|
id: prevState.nextIdToAdd,
|
||||||
|
operator: '',
|
||||||
|
value: '',
|
||||||
|
text: '',
|
||||||
|
type: MappingType.ValueToText,
|
||||||
|
from: '',
|
||||||
|
to: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nextIdToAdd: prevState.nextIdToAdd + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
onRemoveMapping = (id: number) => {
|
||||||
|
this.setState(
|
||||||
|
prevState => ({
|
||||||
|
valueMappings: prevState.valueMappings.filter(m => {
|
||||||
|
return m.id !== id;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
this.props.onChange(this.state.valueMappings);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateGauge = (mapping: ValueMapping) => {
|
||||||
|
this.setState(
|
||||||
|
prevState => ({
|
||||||
|
valueMappings: prevState.valueMappings.map(m => {
|
||||||
|
if (m.id === mapping.id) {
|
||||||
|
return { ...mapping };
|
||||||
|
}
|
||||||
|
|
||||||
|
return m;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
this.props.onChange(this.state.valueMappings);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { valueMappings } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelOptionsGroup title="Value Mappings">
|
||||||
|
<div>
|
||||||
|
{valueMappings.length > 0 &&
|
||||||
|
valueMappings.map((valueMapping, index) => (
|
||||||
|
<MappingRow
|
||||||
|
key={`${valueMapping.text}-${index}`}
|
||||||
|
valueMapping={valueMapping}
|
||||||
|
updateValueMapping={this.updateGauge}
|
||||||
|
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="add-mapping-row" onClick={this.addMapping}>
|
||||||
|
<div className="add-mapping-row-icon">
|
||||||
|
<i className="fa fa-plus" />
|
||||||
|
</div>
|
||||||
|
<div className="add-mapping-row-label">Add mapping</div>
|
||||||
|
</div>
|
||||||
|
</PanelOptionsGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,15 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
exports[`Render should render component 1`] = `
|
||||||
<div
|
<Component
|
||||||
className="section gf-form-group"
|
title="Value Mappings"
|
||||||
>
|
>
|
||||||
<h5
|
|
||||||
className="section-heading"
|
|
||||||
>
|
|
||||||
Value mappings
|
|
||||||
</h5>
|
|
||||||
<div>
|
<div>
|
||||||
<MappingRow
|
<MappingRow
|
||||||
key="Ok-0"
|
key="Ok-0"
|
||||||
mapping={
|
removeValueMapping={[Function]}
|
||||||
|
updateValueMapping={[Function]}
|
||||||
|
valueMapping={
|
||||||
Object {
|
Object {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"operator": "",
|
"operator": "",
|
||||||
@ -21,12 +18,12 @@ exports[`Render should render component 1`] = `
|
|||||||
"value": "20",
|
"value": "20",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removeMapping={[Function]}
|
|
||||||
updateMapping={[Function]}
|
|
||||||
/>
|
/>
|
||||||
<MappingRow
|
<MappingRow
|
||||||
key="Meh-1"
|
key="Meh-1"
|
||||||
mapping={
|
removeValueMapping={[Function]}
|
||||||
|
updateValueMapping={[Function]}
|
||||||
|
valueMapping={
|
||||||
Object {
|
Object {
|
||||||
"from": "21",
|
"from": "21",
|
||||||
"id": 2,
|
"id": 2,
|
||||||
@ -36,8 +33,6 @@ exports[`Render should render component 1`] = `
|
|||||||
"type": 2,
|
"type": 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removeMapping={[Function]}
|
|
||||||
updateMapping={[Function]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -57,5 +52,5 @@ exports[`Render should render component 1`] = `
|
|||||||
Add mapping
|
Add mapping
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Component>
|
||||||
`;
|
`;
|
@ -1 +1,10 @@
|
|||||||
|
@import 'CustomScrollbar/CustomScrollbar';
|
||||||
@import 'DeleteButton/DeleteButton';
|
@import 'DeleteButton/DeleteButton';
|
||||||
|
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||||
|
@import 'Tooltip/Tooltip';
|
||||||
|
@import 'Select/Select';
|
||||||
|
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||||
|
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||||
|
@import 'ColorPicker/ColorPicker';
|
||||||
|
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||||
|
@import "FormField/FormField";
|
||||||
|
@ -1 +1,25 @@
|
|||||||
export { DeleteButton } from './DeleteButton/DeleteButton';
|
export { DeleteButton } from './DeleteButton/DeleteButton';
|
||||||
|
export { Tooltip } from './Tooltip/Tooltip';
|
||||||
|
export { Portal } from './Portal/Portal';
|
||||||
|
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||||
|
|
||||||
|
// Select
|
||||||
|
export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
|
||||||
|
export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
||||||
|
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
||||||
|
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||||
|
|
||||||
|
// Forms
|
||||||
|
export { FormLabel } from './FormLabel/FormLabel';
|
||||||
|
export { FormField } from './FormField/FormField';
|
||||||
|
|
||||||
|
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||||
|
export { ColorPicker } from './ColorPicker/ColorPicker';
|
||||||
|
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
|
||||||
|
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
|
||||||
|
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
|
||||||
|
export { Graph } from './Graph/Graph';
|
||||||
|
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||||
|
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||||
|
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||||
|
export { Gauge } from './Gauge/Gauge';
|
||||||
|
@ -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<Props> = ({ 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 (
|
|
||||||
<label className={classes} {...rest} htmlFor={htmlFor}>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export { GfFormLabel } from './GfFormLabel/GfFormLabel';
|
|
@ -1 +1,3 @@
|
|||||||
|
@import 'vendor/spectrum';
|
||||||
@import 'components/index';
|
@import 'components/index';
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
export * from './components';
|
export * from './components';
|
||||||
export * from './visualizations';
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './forms';
|
|
||||||
|
89
packages/grafana-ui/src/types/datasource.ts
Normal file
89
packages/grafana-ui/src/types/datasource.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { TimeRange, RawTimeRange } from './time';
|
||||||
|
import { TimeSeries } from './series';
|
||||||
|
import { PluginMeta } from './plugin';
|
||||||
|
|
||||||
|
export interface DataQueryResponse {
|
||||||
|
data: TimeSeries[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataQuery {
|
||||||
|
/**
|
||||||
|
* A - Z
|
||||||
|
*/
|
||||||
|
refId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* true if query is disabled (ie not executed / sent to TSDB)
|
||||||
|
*/
|
||||||
|
hide?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique, guid like, string used in explore mode
|
||||||
|
*/
|
||||||
|
key?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For mixed data sources the selected datasource is on the query level.
|
||||||
|
* For non mixed scenarios this is undefined.
|
||||||
|
*/
|
||||||
|
datasource?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
|
||||||
|
timezone: string;
|
||||||
|
range: TimeRange;
|
||||||
|
rangeRaw: RawTimeRange;
|
||||||
|
targets: TQuery[];
|
||||||
|
panelId: number;
|
||||||
|
dashboardId: number;
|
||||||
|
cacheTimeout?: string;
|
||||||
|
interval: string;
|
||||||
|
intervalMs: number;
|
||||||
|
maxDataPoints: number;
|
||||||
|
scopedVars: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryFix {
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
action?: QueryFixAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryFixAction {
|
||||||
|
type: string;
|
||||||
|
query?: string;
|
||||||
|
preventSubmit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryHint {
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
fix?: QueryFix;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataSourceSettings {
|
||||||
|
id: number;
|
||||||
|
orgId: number;
|
||||||
|
name: string;
|
||||||
|
typeLogoUrl: string;
|
||||||
|
type: string;
|
||||||
|
access: string;
|
||||||
|
url: string;
|
||||||
|
password: string;
|
||||||
|
user: string;
|
||||||
|
database: string;
|
||||||
|
basicAuth: boolean;
|
||||||
|
basicAuthPassword: string;
|
||||||
|
basicAuthUser: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
jsonData: { authType: string; defaultRegion: string };
|
||||||
|
readOnly: boolean;
|
||||||
|
withCredentials: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataSourceSelectItem {
|
||||||
|
name: string;
|
||||||
|
value: string | null;
|
||||||
|
meta: PluginMeta;
|
||||||
|
sort: string;
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
export * from './series';
|
export * from './series';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
export * from './panel';
|
export * from './panel';
|
||||||
|
export * from './plugin';
|
||||||
|
export * from './datasource';
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { TimeSeries, LoadingState } from './series';
|
import { TimeSeries, LoadingState } from './series';
|
||||||
import { TimeRange } from './time';
|
import { TimeRange } from './time';
|
||||||
|
|
||||||
|
export type InterpolateFunction = (value: string, format?: string | Function) => string;
|
||||||
|
|
||||||
export interface PanelProps<T = any> {
|
export interface PanelProps<T = any> {
|
||||||
timeSeries: TimeSeries[];
|
timeSeries: TimeSeries[];
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
@ -9,6 +11,7 @@ export interface PanelProps<T = any> {
|
|||||||
renderCounter: number;
|
renderCounter: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
onInterpolate: InterpolateFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelOptionsProps<T = any> {
|
export interface PanelOptionsProps<T = any> {
|
||||||
@ -29,3 +32,44 @@ export interface PanelMenuItem {
|
|||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
subMenu?: PanelMenuItem[];
|
subMenu?: PanelMenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Threshold {
|
||||||
|
index: number;
|
||||||
|
value: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BasicGaugeColor {
|
||||||
|
Green = '#299c46',
|
||||||
|
Red = '#d44a3a',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MappingType {
|
||||||
|
ValueToText = 1,
|
||||||
|
RangeToText = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseMap {
|
||||||
|
id: number;
|
||||||
|
operator: string;
|
||||||
|
text: string;
|
||||||
|
type: MappingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ValueMapping = ValueMap | RangeMap;
|
||||||
|
|
||||||
|
export interface ValueMap extends BaseMap {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RangeMap extends BaseMap {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeName = 'dark' | 'light';
|
||||||
|
|
||||||
|
export enum ThemeNames {
|
||||||
|
Dark = 'dark',
|
||||||
|
Light = 'light',
|
||||||
|
}
|
||||||
|
118
packages/grafana-ui/src/types/plugin.ts
Normal file
118
packages/grafana-ui/src/types/plugin.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { ComponentClass } from 'react';
|
||||||
|
import { PanelProps, PanelOptionsProps } from './panel';
|
||||||
|
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
|
||||||
|
|
||||||
|
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||||
|
/**
|
||||||
|
* min interval range
|
||||||
|
*/
|
||||||
|
interval?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports queries from a different datasource
|
||||||
|
*/
|
||||||
|
importQueries?(queries: TQuery[], originMeta: PluginMeta): Promise<TQuery[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a datasource after instantiation
|
||||||
|
*/
|
||||||
|
init?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main metrics / data query action
|
||||||
|
*/
|
||||||
|
query(options: DataQueryOptions<TQuery>): Promise<DataQueryResponse>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test & verify datasource settings & connection details
|
||||||
|
*/
|
||||||
|
testDatasource(): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hints for query improvements
|
||||||
|
*/
|
||||||
|
getQueryHints?(query: TQuery, results: any[], ...rest: any): QueryHint[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set after constructor is called by Grafana
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
meta?: PluginMeta;
|
||||||
|
pluginExports?: PluginExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||||
|
datasource: DSType;
|
||||||
|
query: TQuery;
|
||||||
|
onExecuteQuery?: () => void;
|
||||||
|
onQueryChange?: (value: TQuery) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginExports {
|
||||||
|
Datasource?: DataSourceApi;
|
||||||
|
QueryCtrl?: any;
|
||||||
|
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
|
||||||
|
ConfigCtrl?: any;
|
||||||
|
AnnotationsQueryCtrl?: any;
|
||||||
|
VariableQueryEditor?: any;
|
||||||
|
ExploreQueryField?: any;
|
||||||
|
ExploreStartPage?: any;
|
||||||
|
|
||||||
|
// Panel plugin
|
||||||
|
PanelCtrl?: any;
|
||||||
|
Panel?: ComponentClass<PanelProps>;
|
||||||
|
PanelOptions?: ComponentClass<PanelOptionsProps>;
|
||||||
|
PanelDefaults?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginMeta {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
info: PluginMetaInfo;
|
||||||
|
includes: PluginInclude[];
|
||||||
|
|
||||||
|
// Datasource-specific
|
||||||
|
metrics?: boolean;
|
||||||
|
tables?: boolean;
|
||||||
|
logs?: boolean;
|
||||||
|
explore?: boolean;
|
||||||
|
annotations?: boolean;
|
||||||
|
mixed?: boolean;
|
||||||
|
hasQueryHelp?: boolean;
|
||||||
|
queryOptions?: PluginMetaQueryOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginMetaQueryOptions {
|
||||||
|
cacheTimeout?: boolean;
|
||||||
|
maxDataPoints?: boolean;
|
||||||
|
minInterval?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginInclude {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginMetaInfoLink {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginMetaInfo {
|
||||||
|
author: {
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
description: string;
|
||||||
|
links: PluginMetaInfoLink[];
|
||||||
|
logos: {
|
||||||
|
large: string;
|
||||||
|
small: string;
|
||||||
|
};
|
||||||
|
screenshots: any[];
|
||||||
|
updated: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -21,9 +21,12 @@ export interface TimeSeriesVM {
|
|||||||
color: string;
|
color: string;
|
||||||
data: TimeSeriesValue[][];
|
data: TimeSeriesValue[][];
|
||||||
stats: TimeSeriesStats;
|
stats: TimeSeriesStats;
|
||||||
|
allIsNull: boolean;
|
||||||
|
allIsZero: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeSeriesStats {
|
export interface TimeSeriesStats {
|
||||||
|
[key: string]: number | null;
|
||||||
total: number | null;
|
total: number | null;
|
||||||
max: number | null;
|
max: number | null;
|
||||||
min: number | null;
|
min: number | null;
|
||||||
@ -36,8 +39,6 @@ export interface TimeSeriesStats {
|
|||||||
range: number | null;
|
range: number | null;
|
||||||
timeStep: number;
|
timeStep: number;
|
||||||
count: number;
|
count: number;
|
||||||
allIsNull: boolean;
|
|
||||||
allIsZero: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NullValueMode {
|
export enum NullValueMode {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user