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:
|
||||
mysql-integration-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.11.4
|
||||
- image: circleci/golang:1.11.5
|
||||
- image: circleci/mysql:5.6-ram
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
postgres-integration-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.11.4
|
||||
- image: circleci/golang:1.11.5
|
||||
- image: circleci/postgres:9.3-ram
|
||||
environment:
|
||||
POSTGRES_USER: grafanatest
|
||||
@ -74,27 +74,16 @@ jobs:
|
||||
|
||||
gometalinter:
|
||||
docker:
|
||||
- image: circleci/golang:1.11.4
|
||||
- image: circleci/golang:1.11.5
|
||||
environment:
|
||||
# we need CGO because of go-sqlite3
|
||||
CGO_ENABLED: 1
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
- run: 'go get -u github.com/alecthomas/gometalinter'
|
||||
- run: 'go get -u github.com/tsenart/deadcode'
|
||||
- run: 'go get -u github.com/jgautheron/goconst/cmd/goconst'
|
||||
- run: 'go get -u github.com/gordonklaus/ineffassign'
|
||||
- run: 'go get -u honnef.co/go/tools/cmd/megacheck'
|
||||
- run: 'go get -u github.com/opennota/check/cmd/structcheck'
|
||||
- run: 'go get -u github.com/mdempsky/unconvert'
|
||||
- run: 'go get -u github.com/opennota/check/cmd/varcheck'
|
||||
- run:
|
||||
name: run linters
|
||||
command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=goconst --enable=gofmt --enable=ineffassign --enable=megacheck --enable=structcheck --enable=unconvert --enable=varcheck ./...'
|
||||
- run:
|
||||
name: run go vet
|
||||
command: 'go vet ./pkg/...'
|
||||
name: Gometalinter tests
|
||||
command: './scripts/gometalinter.sh'
|
||||
|
||||
test-frontend:
|
||||
docker:
|
||||
@ -117,7 +106,7 @@ jobs:
|
||||
|
||||
test-backend:
|
||||
docker:
|
||||
- image: circleci/golang:1.11.4
|
||||
- image: circleci/golang:1.11.5
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -127,7 +116,7 @@ jobs:
|
||||
|
||||
build-all:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.1
|
||||
- image: grafana/build-container:1.2.3
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -158,9 +147,6 @@ jobs:
|
||||
- run:
|
||||
name: sha-sum packages
|
||||
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:
|
||||
name: Test and build Grafana.com release publisher
|
||||
command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
|
||||
@ -169,13 +155,12 @@ jobs:
|
||||
paths:
|
||||
- dist/grafana*
|
||||
- scripts/*.sh
|
||||
- scripts/publish
|
||||
- scripts/build/release_publisher/release_publisher
|
||||
- scripts/build/publish.sh
|
||||
|
||||
build:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.2
|
||||
- image: grafana/build-container:1.2.3
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -200,51 +185,51 @@ jobs:
|
||||
- dist/grafana*
|
||||
|
||||
grafana-docker-master:
|
||||
docker:
|
||||
- image: docker:stable-git
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- setup_remote_docker
|
||||
- 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: 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: cd packaging/docker && ./build-enterprise.sh "master"
|
||||
|
||||
|
||||
grafana-docker-pr:
|
||||
docker:
|
||||
- image: docker:stable-git
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- setup_remote_docker
|
||||
- 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}"
|
||||
|
||||
grafana-docker-release:
|
||||
docker:
|
||||
- image: docker:stable-git
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- setup_remote_docker
|
||||
- run: docker info
|
||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||
- run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
|
||||
- run: rm 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}"
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run: docker info
|
||||
- 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 "${CIRCLE_TAG}"
|
||||
- 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: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
|
||||
|
||||
build-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.1
|
||||
- image: grafana/build-container:1.2.3
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -276,7 +261,7 @@ jobs:
|
||||
|
||||
build-all-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.1
|
||||
- image: grafana/build-container:1.2.3
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@ -323,7 +308,7 @@ jobs:
|
||||
|
||||
deploy-enterprise-master:
|
||||
docker:
|
||||
- image: grafana/grafana-ci-deploy:1.0.0
|
||||
- image: grafana/grafana-ci-deploy:1.2.0
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
@ -346,7 +331,7 @@ jobs:
|
||||
|
||||
deploy-enterprise-release:
|
||||
docker:
|
||||
- image: grafana/grafana-ci-deploy:1.0.0
|
||||
- image: grafana/grafana-ci-deploy:1.2.0
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
@ -365,10 +350,20 @@ jobs:
|
||||
- run:
|
||||
name: Deploy to Grafana.com
|
||||
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:
|
||||
docker:
|
||||
- image: grafana/grafana-ci-deploy:1.0.0
|
||||
- image: grafana/grafana-ci-deploy:1.2.0
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
@ -394,12 +389,14 @@ jobs:
|
||||
name: Publish to Grafana.com
|
||||
command: |
|
||||
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:
|
||||
docker:
|
||||
- image: grafana/grafana-ci-deploy:1.0.0
|
||||
- image: grafana/grafana-ci-deploy:1.2.0
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
@ -417,6 +414,15 @@ jobs:
|
||||
- run:
|
||||
name: Deploy to Grafana.com
|
||||
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:
|
||||
version: 2
|
||||
|
44
CHANGELOG.md
44
CHANGELOG.md
@ -1,25 +1,61 @@
|
||||
# 5.5.0 (unreleased)
|
||||
# 6.0.0-beta1 (unreleased)
|
||||
|
||||
### New Features
|
||||
* **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
|
||||
* **Elasticsearch**: Support bucket script pipeline aggregations [#5968](https://github.com/grafana/grafana/issues/5968)
|
||||
* **Influxdb**: Add support for time zone (`tz`) clause [#10322](https://github.com/grafana/grafana/issues/10322), thx [@cykl](https://github.com/cykl)
|
||||
* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Alerting**: Use seperate timeouts for alert evals and notifications [#14701](https://github.com/grafana/grafana/issues/14701), thx [@sharkpc0813](https://github.com/sharkpc0813)
|
||||
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
|
||||
* **Elasticsearch**: Add support for moving average and derivative using doc count (metric count) [#8843](https://github.com/grafana/grafana/issues/8843) [#11175](https://github.com/grafana/grafana/issues/11175)
|
||||
* **Elasticsearch**: Add support for template variable interpolation in alias field [#4075](https://github.com/grafana/grafana/issues/4075), thx [@SamuelToh](https://github.com/SamuelToh)
|
||||
* **Influxdb**: Fix autocomplete of measurements does not escape search string properly [#11503](https://github.com/grafana/grafana/issues/11503), thx [@SamuelToh](https://github.com/SamuelToh)
|
||||
* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
|
||||
* **Cloudwatch**: Fix Assume Role Arn [#14722](https://github.com/grafana/grafana/issues/14722), thx [@jaken551](https://github.com/jaken551)
|
||||
* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
|
||||
* **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
|
||||
* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
|
||||
* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
|
||||
* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
|
||||
* **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483)
|
||||
* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
|
||||
* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
|
||||
* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
|
||||
* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
|
||||
* **Dashboard**: `Min width` changed to `Max per row` for repeating panels. This lets you specify the maximum number of panels to show per row and by that repeated panels will always take up full width of row [#12991](https://github.com/grafana/grafana/pull/12991), thx [@pgiraud](https://github.com/pgiraud)
|
||||
* **Dashboard**: Retain decimal precision when exporting CSV [#13929](https://github.com/grafana/grafana/issues/13929), thx [@cinaglia](https://github.com/cinaglia)
|
||||
* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
|
||||
* **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
|
||||
* **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
|
||||
* **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)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Golang build container
|
||||
FROM golang:1.11.4
|
||||
FROM golang:1.11.5
|
||||
|
||||
WORKDIR $GOPATH/src/github.com/grafana/grafana
|
||||
|
||||
@ -19,11 +19,13 @@ COPY package.json package.json
|
||||
RUN go run build.go build
|
||||
|
||||
# Node build container
|
||||
FROM node:8
|
||||
FROM node:10.14.2
|
||||
|
||||
WORKDIR /usr/src/app/
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
COPY packages packages
|
||||
|
||||
RUN yarn install --pure-lockfile --no-progress
|
||||
|
||||
COPY Gruntfile.js tsconfig.json tslint.json ./
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Plugin Development
|
||||
|
||||
This document is not meant as 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
|
||||
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. Whenever you as a plugin author encounter an issue with your plugin after
|
||||
upgrading Grafana please check here before creating an issue.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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)
|
||||
|
||||
### Dependencies
|
||||
@ -71,7 +71,7 @@ Open grafana in your browser (default: `http://localhost:3000`) and login with a
|
||||
|
||||
### 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`
|
||||
|
||||
@ -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`
|
||||
|
||||
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
|
||||
|
||||
@ -129,9 +129,11 @@ GRAFANA_TEST_DB=postgres go test ./pkg/...
|
||||
|
||||
## Contribute
|
||||
|
||||
If you have any idea for an improvement or 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
|
||||
the kickass metrics & devops dashboard we all dream about!
|
||||
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 to help me make Grafana
|
||||
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
|
||||
|
||||
|
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)
|
||||
- PRs & Bugs
|
||||
- Multi-Stat panel
|
||||
- React Panel Support
|
||||
- React Query Editor Support
|
||||
- Metrics & Log Explore UI
|
||||
|
||||
- Grafana UI library shared between grafana & plugins
|
||||
- Seperate visualization from panels
|
||||
- More reuse between Explore & dashboard
|
||||
- Explore logging support for more data sources
|
||||
|
||||
### Mid term (2-4 months)
|
||||
- React Panels
|
||||
- Change visualization (panel type) on the fly.
|
||||
- Templating Query Editor UI Plugin hook
|
||||
- Backend plugins
|
||||
- Drilldown links
|
||||
- Dashboards as code workflows
|
||||
- React migration
|
||||
- New panels
|
||||
|
||||
### Long term (4 - 8 months)
|
||||
- Alerting improvements (silence, per series tracking, etc)
|
||||
- Progress on React migration
|
||||
|
||||
### In a distant future far far away
|
||||
- Meta queries
|
||||
|
@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
environment:
|
||||
nodejs_version: "8"
|
||||
GOPATH: C:\gopath
|
||||
GOVERSION: 1.11.4
|
||||
GOVERSION: 1.11.5
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
|
18
build.go
18
build.go
@ -46,6 +46,8 @@ var (
|
||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||
isDev bool = false
|
||||
enterprise bool = false
|
||||
skipRpmGen bool = false
|
||||
skipDebGen bool = false
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -67,6 +69,8 @@ func main() {
|
||||
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
|
||||
flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
|
||||
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
|
||||
flag.BoolVar(&skipRpmGen, "skipRpm", skipRpmGen, "skip rpm package generation (default: false)")
|
||||
flag.BoolVar(&skipDebGen, "skipDeb", skipDebGen, "skip deb package generation (default: false)")
|
||||
flag.Parse()
|
||||
|
||||
buildId = shortenBuildId(buildIdRaw)
|
||||
@ -164,6 +168,9 @@ func makeLatestDistCopies() {
|
||||
"_amd64.deb": "dist/grafana_latest_amd64.deb",
|
||||
".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm",
|
||||
".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
|
||||
".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz",
|
||||
".linux-armv6.tar.gz": "dist/grafana-latest.linux-armv6.tar.gz",
|
||||
".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz",
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
@ -237,6 +244,8 @@ func createDebPackages() {
|
||||
previousPkgArch := pkgArch
|
||||
if pkgArch == "armv7" {
|
||||
pkgArch = "armhf"
|
||||
} else if pkgArch == "armv6" {
|
||||
pkgArch = "armel"
|
||||
}
|
||||
createPackage(linuxPackageOptions{
|
||||
packageType: "deb",
|
||||
@ -287,8 +296,13 @@ func createRpmPackages() {
|
||||
}
|
||||
|
||||
func createLinuxPackages() {
|
||||
createDebPackages()
|
||||
createRpmPackages()
|
||||
if !skipDebGen {
|
||||
createDebPackages()
|
||||
}
|
||||
|
||||
if !skipRpmGen {
|
||||
createRpmPackages()
|
||||
}
|
||||
}
|
||||
|
||||
func createPackage(options linuxPackageOptions) {
|
||||
|
@ -106,6 +106,22 @@ path = grafana.db
|
||||
# For "sqlite3" only. cache mode setting used for connecting to the database
|
||||
cache_mode = private
|
||||
|
||||
#################################### Login ###############################
|
||||
|
||||
[login]
|
||||
|
||||
# Login cookie name
|
||||
cookie_name = grafana_session
|
||||
|
||||
# How many days an session can be unused before we inactivate it
|
||||
login_remember_days = 7
|
||||
|
||||
# How often should the login token be rotated. default to '10m'
|
||||
rotate_token_minutes = 10
|
||||
|
||||
# How long should Grafana keep expired tokens before deleting them
|
||||
delete_expired_token_after_days = 30
|
||||
|
||||
#################################### Session #############################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
|
||||
@ -143,6 +159,9 @@ conn_max_lifetime = 14400
|
||||
# This enables data proxy logging, default is false
|
||||
logging = false
|
||||
|
||||
# How long the data proxy should wait before timing out default is 30 (seconds)
|
||||
timeout = 30
|
||||
|
||||
#################################### Analytics ###########################
|
||||
[analytics]
|
||||
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
||||
@ -175,11 +194,6 @@ admin_password = admin
|
||||
# used for signing
|
||||
secret_key = SW2YcwTIb9zpOOhoPsMm
|
||||
|
||||
# Auto-login remember days
|
||||
login_remember_days = 7
|
||||
cookie_username = grafana_user
|
||||
cookie_remember_name = grafana_remember
|
||||
|
||||
# disable gravatar profile images
|
||||
disable_gravatar = false
|
||||
|
||||
@ -189,6 +203,9 @@ data_source_proxy_whitelist =
|
||||
# disable protection against brute force login attempts
|
||||
disable_brute_force_login_protection = false
|
||||
|
||||
# set cookies as https only. default is false
|
||||
https_flag_cookies = false
|
||||
|
||||
#################################### Snapshots ###########################
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
@ -490,7 +507,7 @@ concurrent_render_limit = 5
|
||||
#################################### Explore #############################
|
||||
[explore]
|
||||
# Enable the Explore section
|
||||
enabled = false
|
||||
enabled = true
|
||||
|
||||
#################################### Internal Grafana Metrics ############
|
||||
# Metrics available at HTTP API Url /metrics
|
||||
@ -570,6 +587,7 @@ callback_url =
|
||||
|
||||
[panels]
|
||||
enable_alpha = false
|
||||
disable_sanitize_html = false
|
||||
|
||||
[enterprise]
|
||||
license_path =
|
||||
|
@ -102,6 +102,22 @@ log_queries =
|
||||
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
|
||||
;cache_mode = private
|
||||
|
||||
#################################### Login ###############################
|
||||
|
||||
[login]
|
||||
|
||||
# Login cookie name
|
||||
;cookie_name = grafana_session
|
||||
|
||||
# How many days an session can be unused before we inactivate it
|
||||
;login_remember_days = 7
|
||||
|
||||
# How often should the login token be rotated. default to '10'
|
||||
;rotate_token_minutes = 10
|
||||
|
||||
# How long should Grafana keep expired tokens before deleting them
|
||||
;delete_expired_token_after_days = 30
|
||||
|
||||
#################################### Session ####################################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
|
||||
@ -130,6 +146,9 @@ log_queries =
|
||||
# This enables data proxy logging, default is false
|
||||
;logging = false
|
||||
|
||||
# How long the data proxy should wait before timing out default is 30 (seconds)
|
||||
;timeout = 30
|
||||
|
||||
#################################### Analytics ####################################
|
||||
[analytics]
|
||||
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
|
||||
@ -162,11 +181,6 @@ log_queries =
|
||||
# used for signing
|
||||
;secret_key = SW2YcwTIb9zpOOhoPsMm
|
||||
|
||||
# Auto-login remember days
|
||||
;login_remember_days = 7
|
||||
;cookie_username = grafana_user
|
||||
;cookie_remember_name = grafana_remember
|
||||
|
||||
# disable gravatar profile images
|
||||
;disable_gravatar = false
|
||||
|
||||
@ -176,6 +190,9 @@ log_queries =
|
||||
# disable protection against brute force login attempts
|
||||
;disable_brute_force_login_protection = false
|
||||
|
||||
# set cookies as https only. default is false
|
||||
;https_flag_cookies = false
|
||||
|
||||
#################################### Snapshots ###########################
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
@ -415,7 +432,7 @@ log_queries =
|
||||
#################################### Explore #############################
|
||||
[explore]
|
||||
# Enable the Explore section
|
||||
;enabled = false
|
||||
;enabled = true
|
||||
|
||||
#################################### Internal Grafana Metrics ##########################
|
||||
# Metrics available at HTTP API Url /metrics
|
||||
@ -495,3 +512,8 @@ log_queries =
|
||||
# Path to a valid Grafana Enterprise license.jwt file
|
||||
;license_path =
|
||||
|
||||
[panels]
|
||||
;enable_alpha = false
|
||||
# If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities.
|
||||
;disable_sanitize_html = false
|
||||
|
||||
|
@ -4,6 +4,6 @@ providers:
|
||||
- name: 'gdev dashboards'
|
||||
folder: 'gdev dashboards'
|
||||
type: file
|
||||
updateIntervalSeconds: 15
|
||||
options:
|
||||
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,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"iteration": 1542304484522,
|
||||
"iteration": 1545263815779,
|
||||
"links": [
|
||||
{
|
||||
"icon": "external link",
|
||||
@ -66,6 +66,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -168,6 +169,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -270,6 +272,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -372,6 +375,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -474,6 +478,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -576,6 +581,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2249,6 +2255,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2366,6 +2373,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2483,6 +2491,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2600,6 +2609,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2717,6 +2727,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2834,6 +2845,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -2951,6 +2963,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3068,6 +3081,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3185,6 +3199,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3302,6 +3317,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3419,6 +3435,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3536,6 +3553,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3667,6 +3685,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3780,6 +3799,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -3893,6 +3913,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4006,6 +4027,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4119,6 +4141,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4232,6 +4255,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4345,6 +4369,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4458,6 +4483,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4571,6 +4597,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4684,6 +4711,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4797,6 +4825,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -4910,6 +4939,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -5008,6 +5038,512 @@
|
||||
"x": 0,
|
||||
"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,
|
||||
"panels": [
|
||||
{
|
||||
@ -5042,6 +5578,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -5193,6 +5730,7 @@
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
@ -5328,8 +5866,8 @@
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"text": "gdev-elasticsearch-v2-metrics",
|
||||
"value": "gdev-elasticsearch-v2-metrics"
|
||||
"text": "gdev-elasticsearch-v5-metrics",
|
||||
"value": "gdev-elasticsearch-v5-metrics"
|
||||
},
|
||||
"hide": 0,
|
||||
"label": "Version One",
|
||||
@ -5343,8 +5881,8 @@
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"text": "gdev-elasticsearch-v5-metrics",
|
||||
"value": "gdev-elasticsearch-v5-metrics"
|
||||
"text": "gdev-elasticsearch-v6-metrics",
|
||||
"value": "gdev-elasticsearch-v6-metrics"
|
||||
},
|
||||
"hide": 0,
|
||||
"label": "Version Two",
|
||||
@ -5359,7 +5897,7 @@
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-3h",
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
@ -5390,5 +5928,5 @@
|
||||
"timezone": "",
|
||||
"title": "Datasource tests - Elasticsearch comparison",
|
||||
"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
|
||||
# bind-socket = "/var/run/influxdb.sock"
|
||||
flux-enabled = true
|
||||
|
||||
[subscriber]
|
||||
enabled = true
|
||||
|
@ -54,7 +54,8 @@ services:
|
||||
# - GF_DATABASE_SSL_MODE=disable
|
||||
# - GF_SESSION_PROVIDER=postgres
|
||||
# - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
|
||||
- GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug
|
||||
- GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug
|
||||
- GF_LOGIN_ROTATE_TOKEN_MINUTES=2
|
||||
ports:
|
||||
- 3000
|
||||
depends_on:
|
||||
|
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
|
||||
[auth.gitlab]
|
||||
enabled = false
|
||||
enabled = true
|
||||
allow_sign_up = false
|
||||
client_id = GITLAB_APPLICATION_ID
|
||||
client_secret = GITLAB_SECRET
|
||||
|
@ -38,7 +38,7 @@ Name | Description
|
||||
|
||||
### IAM Roles
|
||||
|
||||
Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If you grafana
|
||||
Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If your Grafana
|
||||
server is running on AWS you can use IAM Roles and authentication will be handled automatically.
|
||||
|
||||
Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
|
||||
|
@ -1,5 +1,6 @@
|
||||
+++
|
||||
title = "Explore"
|
||||
keywords = ["explore", "loki", "logs"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Explore"
|
||||
@ -8,7 +9,11 @@ parent = "features"
|
||||
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.
|
||||
|
||||
|
@ -285,7 +285,7 @@ Content-Type: application/json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{message: "User permissions updated"}
|
||||
{"message": "User permissions updated"}
|
||||
```
|
||||
|
||||
## Delete global User
|
||||
@ -308,7 +308,7 @@ Content-Type: application/json
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{message: "User deleted"}
|
||||
{"message": "User deleted"}
|
||||
```
|
||||
|
||||
## Pause all alerts
|
||||
@ -339,5 +339,5 @@ JSON Body schema:
|
||||
HTTP/1.1 200
|
||||
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"
|
||||
},
|
||||
"secureJsonData": {
|
||||
"accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
|
||||
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
|
||||
"accessKey": "Ol4pIDpeKSA6XikgOl4p",
|
||||
"secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -105,7 +105,7 @@ POST /api/folders/nErXDvCkzz/permissions
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"role": "Viewer",
|
||||
|
@ -82,4 +82,29 @@ HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"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
|
||||
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 />
|
||||
|
||||
## [dashboards]
|
||||
@ -589,3 +595,14 @@ Default setting for how Grafana handles nodata or null values in alerting. (aler
|
||||
Alert notifications can include images, but rendering many images at the same time can overload the server.
|
||||
This limit will protect the server from render overloading and make sure notifications are sent out quickly. Default
|
||||
value is `5`.
|
||||
|
||||
## [panels]
|
||||
|
||||
### enable_alpha
|
||||
Set to true if you want to test panels that are not yet ready for general usage.
|
||||
|
||||
### disable_sanitize_html
|
||||
If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities. Default
|
||||
is false. This settings was introduced in Grafana v6.0.
|
||||
|
||||
|
||||
|
@ -34,32 +34,29 @@ sudo dpkg -i grafana_<version>_amd64.deb
|
||||
Example:
|
||||
|
||||
```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 dpkg -i grafana_5.1.4_amd64.deb
|
||||
sudo dpkg -i grafana_5.4.2_amd64.deb
|
||||
```
|
||||
|
||||
## 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
|
||||
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 also a testing repository if you want beta or release
|
||||
candidates.
|
||||
There is a separate repository if you want beta releases.
|
||||
|
||||
```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
|
||||
allows you to install signed packages.
|
||||
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.
|
||||
|
||||
```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
|
||||
|
@ -32,7 +32,7 @@ $ sudo yum install <rpm package url>
|
||||
Example:
|
||||
|
||||
```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
|
||||
@ -44,7 +44,7 @@ $ wget <rpm package url>
|
||||
Example:
|
||||
|
||||
```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:
|
||||
@ -67,19 +67,27 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
|
||||
```bash
|
||||
[grafana]
|
||||
name=grafana
|
||||
baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch
|
||||
baseurl=https://packages.grafana.com/oss/rpm
|
||||
repo_gpgcheck=1
|
||||
enabled=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
|
||||
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
|
||||
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.
|
||||
@ -91,7 +99,7 @@ $ sudo yum install grafana
|
||||
### RPM GPG Key
|
||||
|
||||
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
|
||||
|
||||
|
@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
|
||||
"list": []
|
||||
},
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 16,
|
||||
"schemaVersion": 17,
|
||||
"version": 0,
|
||||
"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
|
||||
`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
|
||||
`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
|
||||
`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.
|
||||
|
||||
Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1).
|
||||
|
||||
@ -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
|
||||
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.
|
||||
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",
|
||||
"testing": "5.4.2"
|
||||
"stable": "5.4.3",
|
||||
"testing": "5.4.3"
|
||||
}
|
||||
|
10
package.json
10
package.json
@ -5,7 +5,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "5.5.0-pre1",
|
||||
"version": "6.0.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@ -24,7 +24,6 @@
|
||||
"@types/jquery": "^1.10.35",
|
||||
"@types/node": "^8.0.31",
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-dom": "^16.0.9",
|
||||
"@types/react-select": "^2.0.4",
|
||||
"angular-mocks": "1.6.6",
|
||||
@ -65,6 +64,7 @@
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^0.14.3",
|
||||
"jest": "^23.6.0",
|
||||
"jest-date-mock": "^1.0.6",
|
||||
"lint-staged": "^6.0.0",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
@ -72,8 +72,8 @@
|
||||
"ng-annotate-loader": "^0.6.1",
|
||||
"ng-annotate-webpack-plugin": "^0.3.0",
|
||||
"ngtemplate-loader": "^2.0.1",
|
||||
"npm": "^5.4.2",
|
||||
"node-sass": "^4.11.0",
|
||||
"npm": "^5.4.2",
|
||||
"optimize-css-assets-webpack-plugin": "^4.0.2",
|
||||
"phantomjs-prebuilt": "^2.1.15",
|
||||
"postcss-browser-reporter": "^0.5.0",
|
||||
@ -167,7 +167,6 @@
|
||||
"prop-types": "^15.6.2",
|
||||
"rc-cascader": "^0.14.0",
|
||||
"react": "^16.6.3",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-grid-layout": "0.16.6",
|
||||
"react-highlight-words": "0.11.0",
|
||||
@ -189,7 +188,8 @@
|
||||
"slate-react": "^0.12.4",
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||
"tinycolor2": "^1.4.1"
|
||||
"tinycolor2": "^1.4.1",
|
||||
"xss": "^1.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"caniuse-db": "1.0.30000772",
|
||||
|
@ -11,23 +11,34 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@torkelo/react-select": "2.1.1",
|
||||
"@types/react-test-renderer": "^16.0.3",
|
||||
"@types/react-transition-group": "^2.0.15",
|
||||
"classnames": "^2.2.5",
|
||||
"jquery": "^3.2.1",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.2",
|
||||
"react": "^16.6.3",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "^1.3.0",
|
||||
"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": {
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/jest": "^23.3.2",
|
||||
"@types/jquery": "^1.10.35",
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/classnames": "^2.2.6",
|
||||
"@types/jquery": "^1.10.35",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { ColorPalette } from '../components/colorpicker/ColorPalette';
|
||||
import { ColorPalette } from './ColorPalette';
|
||||
|
||||
describe('CollorPalette', () => {
|
||||
it('renders correctly', () => {
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { sortedColors } from 'app/core/utils/colors';
|
||||
import { sortedColors } from '../../utils';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
@ -9,13 +9,13 @@ export interface Props {
|
||||
export class ColorPalette extends React.Component<Props, any> {
|
||||
paletteColors: string[];
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.paletteColors = sortedColors;
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
onColorSelect(color) {
|
||||
onColorSelect(color: string) {
|
||||
return () => {
|
||||
this.props.onColorSelect(color);
|
||||
};
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Drop from 'tether-drop';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
@ -10,7 +9,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
export class ColorPicker extends React.Component<Props, any> {
|
||||
pickerElem: HTMLElement;
|
||||
pickerElem: HTMLElement | null;
|
||||
colorPickerDrop: any;
|
||||
|
||||
openColorPicker = () => {
|
||||
@ -20,7 +19,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
const drop = new Drop({
|
||||
target: this.pickerElem,
|
||||
target: this.pickerElem as Element,
|
||||
content: dropContentElem,
|
||||
position: 'top center',
|
||||
classes: 'drop-popover',
|
||||
@ -28,6 +27,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
||||
hoverCloseDelay: 200,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||
attachment: 'bottom center',
|
||||
},
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@ export class ColorPicker extends React.Component<Props, any> {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
onColorSelect = color => {
|
||||
onColorSelect = (color: string) => {
|
||||
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> {
|
||||
pickerNavElem: any;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 'palette',
|
||||
@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component<Props, any> {
|
||||
};
|
||||
}
|
||||
|
||||
setPickerNavElem(elem) {
|
||||
setPickerNavElem(elem: any) {
|
||||
this.pickerNavElem = $(elem);
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
setColor(color: string) {
|
||||
const newColor = tinycolor(color);
|
||||
if (newColor.isValid()) {
|
||||
this.setState({
|
||||
color: newColor.toString(),
|
||||
colorString: newColor.toString(),
|
||||
});
|
||||
this.setState({ color: newColor.toString(), colorString: newColor.toString() });
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
}
|
||||
|
||||
sampleColorSelected(color) {
|
||||
sampleColorSelected(color: string) {
|
||||
this.setColor(color);
|
||||
}
|
||||
|
||||
spectrumColorSelected(color) {
|
||||
spectrumColorSelected(color: any) {
|
||||
const rgbColor = color.toRgbString();
|
||||
this.setColor(rgbColor);
|
||||
}
|
||||
|
||||
onColorStringChange(e) {
|
||||
onColorStringChange(e: any) {
|
||||
const colorString = e.target.value;
|
||||
this.setState({
|
||||
colorString: colorString,
|
||||
});
|
||||
this.setState({ colorString: colorString });
|
||||
|
||||
const newColor = tinycolor(colorString);
|
||||
if (newColor.isValid()) {
|
||||
// Update only color state
|
||||
const newColorString = newColor.toString();
|
||||
this.setState({
|
||||
color: newColorString,
|
||||
});
|
||||
this.setState({ color: newColorString });
|
||||
this.props.onColorSelect(newColorString);
|
||||
}
|
||||
}
|
||||
|
||||
onColorStringBlur(e) {
|
||||
onColorStringBlur(e: any) {
|
||||
const colorString = e.target.value;
|
||||
this.setColor(colorString);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.pickerNavElem.find('li:first').addClass('active');
|
||||
this.pickerNavElem.on('show', e => {
|
||||
this.pickerNavElem.on('show', (e: any) => {
|
||||
// use href attr (#name => name)
|
||||
const tab = e.target.hash.slice(1);
|
||||
this.setState({
|
||||
tab: tab,
|
||||
});
|
||||
this.setState({ tab: tab });
|
||||
});
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
||||
onToggleAxis: () => {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: SeriesColorPickerProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
||||
remove: true,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||
attachment: 'bottom center',
|
||||
},
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
|
||||
export interface SeriesColorPickerPopoverProps {
|
||||
color: string;
|
||||
@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPic
|
||||
|
||||
interface AxisSelectorProps {
|
||||
yaxis: number;
|
||||
onToggleAxis: () => void;
|
||||
onToggleAxis?: () => void;
|
||||
}
|
||||
|
||||
interface AxisSelectorState {
|
||||
@ -30,7 +29,7 @@ interface AxisSelectorState {
|
||||
}
|
||||
|
||||
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
|
||||
constructor(props) {
|
||||
constructor(props: AxisSelectorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
yaxis: this.props.yaxis,
|
||||
@ -42,7 +41,10 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
||||
this.setState({
|
||||
yaxis: this.state.yaxis === 2 ? 1 : 2,
|
||||
});
|
||||
this.props.onToggleAxis();
|
||||
|
||||
if (this.props.onToggleAxis) {
|
||||
this.props.onToggleAxis();
|
||||
}
|
||||
}
|
||||
|
||||
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 _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import 'vendor/spectrum';
|
||||
import '../../vendor/spectrum';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
@ -13,17 +13,17 @@ export class SpectrumPicker extends React.Component<Props, any> {
|
||||
elem: any;
|
||||
isMoving: boolean;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
||||
this.setComponentElem = this.setComponentElem.bind(this);
|
||||
}
|
||||
|
||||
setComponentElem(elem) {
|
||||
setComponentElem(elem: any) {
|
||||
this.elem = $(elem);
|
||||
}
|
||||
|
||||
onSpectrumMove(color) {
|
||||
onSpectrumMove(color: any) {
|
||||
this.isMoving = true;
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component<Props, any> {
|
||||
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
|
||||
// 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
|
@ -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={
|
||||
Object {
|
||||
"height": "auto",
|
||||
"maxHeight": "inherit",
|
||||
"minHeight": "inherit",
|
||||
"maxHeight": "100%",
|
||||
"minHeight": "0",
|
||||
"overflow": "hidden",
|
||||
"position": "relative",
|
||||
"width": "100%",
|
||||
@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
"left": undefined,
|
||||
"marginBottom": 0,
|
||||
"marginRight": 0,
|
||||
"maxHeight": "calc(inherit + 0px)",
|
||||
"minHeight": "calc(inherit + 0px)",
|
||||
"maxHeight": "calc(100% + 0px)",
|
||||
"minHeight": "calc(0 + 0px)",
|
||||
"overflow": "scroll",
|
||||
"position": "relative",
|
||||
"right": undefined,
|
||||
@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
Object {
|
||||
"display": "none",
|
||||
"height": 6,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"transition": "opacity 200ms",
|
||||
}
|
||||
}
|
||||
>
|
||||
@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"transition": "opacity 200ms",
|
||||
"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 $ 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 {
|
||||
baseColor: string;
|
||||
import { ValueMapping, Threshold, ThemeName, BasicGaugeColor, ThemeNames } from '../../types/panel';
|
||||
import { TimeSeriesVMs } from '../../types/series';
|
||||
import { getValueFormat } from '../../utils/valueFormats/valueFormats';
|
||||
import { TimeSeriesValue, getMappedValue } from '../../utils/valueMappings';
|
||||
|
||||
export interface Props {
|
||||
decimals: number;
|
||||
height: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
@ -21,15 +21,15 @@ interface Props {
|
||||
suffix: string;
|
||||
unit: string;
|
||||
width: number;
|
||||
theme?: ThemeName;
|
||||
}
|
||||
|
||||
export class Gauge extends PureComponent<Props> {
|
||||
canvasElement: any;
|
||||
|
||||
static defaultProps = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
maxValue: 100,
|
||||
mappings: [],
|
||||
valueMappings: [],
|
||||
minValue: 0,
|
||||
prefix: '',
|
||||
showThresholdMarkers: true,
|
||||
@ -38,6 +38,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
theme: ThemeNames.Dark,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -48,89 +49,93 @@ export class Gauge extends PureComponent<Props> {
|
||||
this.draw();
|
||||
}
|
||||
|
||||
formatWithMappings(mappings, value) {
|
||||
const valueMaps = mappings.filter(m => m.type === MappingType.ValueToText);
|
||||
const rangeMaps = mappings.filter(m => m.type === MappingType.RangeToText);
|
||||
formatValue(value: TimeSeriesValue) {
|
||||
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
|
||||
|
||||
const valueMap = valueMaps.map(mapping => {
|
||||
if (mapping.value && value === mapping.value) {
|
||||
return mapping.text;
|
||||
if (isNaN(value as number)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (valueMappings.length > 0) {
|
||||
const valueMappedValue = getMappedValue(valueMappings, value);
|
||||
if (valueMappedValue) {
|
||||
return `${prefix} ${valueMappedValue.text} ${suffix}`;
|
||||
}
|
||||
})[0];
|
||||
}
|
||||
|
||||
const rangeMap = rangeMaps.map(mapping => {
|
||||
if (mapping.from && mapping.to && value > mapping.from && value < mapping.to) {
|
||||
return mapping.text;
|
||||
}
|
||||
})[0];
|
||||
const formatFunc = getValueFormat(unit);
|
||||
const formattedValue = formatFunc(value as number, decimals);
|
||||
const handleNoValueValue = formattedValue || 'no value';
|
||||
|
||||
return {
|
||||
rangeMap,
|
||||
valueMap,
|
||||
};
|
||||
return `${prefix} ${handleNoValueValue} ${suffix}`;
|
||||
}
|
||||
|
||||
formatValue(value) {
|
||||
const { decimals, mappings, prefix, suffix, unit } = this.props;
|
||||
getFontColor(value: TimeSeriesValue) {
|
||||
const { thresholds } = this.props;
|
||||
|
||||
const formatFunc = kbn.valueFormats[unit];
|
||||
const formattedValue = formatFunc(value, decimals);
|
||||
|
||||
if (mappings.length > 0) {
|
||||
const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue);
|
||||
|
||||
if (valueMap) {
|
||||
return valueMap;
|
||||
} else if (rangeMap) {
|
||||
return rangeMap;
|
||||
}
|
||||
if (thresholds.length === 1) {
|
||||
return thresholds[0].color;
|
||||
}
|
||||
|
||||
if (isNaN(value)) {
|
||||
return '-';
|
||||
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||
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) {
|
||||
const { baseColor, maxValue, thresholds } = this.props;
|
||||
getFormattedThresholds() {
|
||||
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) {
|
||||
return atThreshold[0].color;
|
||||
} else if (value <= maxValue) {
|
||||
return BasicGaugeColor.Red;
|
||||
}
|
||||
const formattedThresholds = [
|
||||
...thresholdsSortedByIndex.map(threshold => {
|
||||
if (threshold.index === 0) {
|
||||
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() {
|
||||
const {
|
||||
baseColor,
|
||||
maxValue,
|
||||
minValue,
|
||||
timeSeries,
|
||||
showThresholdLabels,
|
||||
showThresholdMarkers,
|
||||
thresholds,
|
||||
width,
|
||||
height,
|
||||
stat,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let value: string | number = '';
|
||||
let value: TimeSeriesValue = '';
|
||||
|
||||
if (timeSeries[0]) {
|
||||
value = timeSeries[0].stats[stat];
|
||||
} else {
|
||||
value = 'N/A';
|
||||
value = null;
|
||||
}
|
||||
|
||||
const dimension = Math.min(width, height * 1.3);
|
||||
const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
||||
const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
||||
const fontScale = parseInt('80', 10) / 100;
|
||||
const fontSize = Math.min(dimension / 5, 100) * fontScale;
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
@ -138,20 +143,6 @@ export class Gauge extends PureComponent<Props> {
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
|
||||
const formattedThresholds = [
|
||||
{ value: minValue, color: BasicGaugeColor.Green },
|
||||
...thresholds.map((threshold, index) => {
|
||||
return {
|
||||
value: threshold.value,
|
||||
color: index === 0 ? threshold.color : thresholds[index].color,
|
||||
};
|
||||
}),
|
||||
{
|
||||
value: maxValue,
|
||||
color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor,
|
||||
},
|
||||
];
|
||||
|
||||
const options = {
|
||||
series: {
|
||||
gauges: {
|
||||
@ -168,7 +159,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
layout: { margin: 0, thresholdWidth: 0 },
|
||||
cell: { border: { width: 0 } },
|
||||
threshold: {
|
||||
values: formattedThresholds,
|
||||
values: this.getFormattedThresholds(),
|
||||
label: {
|
||||
show: showThresholdLabels,
|
||||
margin: thresholdMarkersWidth + 1,
|
||||
@ -182,19 +173,14 @@ export class Gauge extends PureComponent<Props> {
|
||||
formatter: () => {
|
||||
return this.formatValue(value);
|
||||
},
|
||||
font: {
|
||||
size: fontSize,
|
||||
family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||
},
|
||||
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const plotSeries = {
|
||||
data: [[0, value]],
|
||||
};
|
||||
const plotSeries = { data: [[0, value]] };
|
||||
|
||||
try {
|
||||
$.plot(this.canvasElement, [plotSeries], options);
|
@ -98,6 +98,7 @@ export class Graph extends PureComponent<GraphProps> {
|
||||
$.plot(this.element, timeSeries, flotOptions);
|
||||
} catch (err) {
|
||||
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[];
|
||||
}
|
||||
|
||||
export const PanelOptionSection: SFC<Props> = props => {
|
||||
export const PanelOptionsGroup: SFC<Props> = props => {
|
||||
return (
|
||||
<div className="panel-option-section">
|
||||
<div className="panel-options-group">
|
||||
{props.title && (
|
||||
<div className="panel-option-section__header">
|
||||
<div className="panel-options-group__header">
|
||||
{props.title}
|
||||
{props.onClose && (
|
||||
<button className="btn btn-link" onClick={props.onClose}>
|
||||
@ -20,7 +20,7 @@ export const PanelOptionSection: SFC<Props> = props => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="panel-option-section__body">{props.children}</div>
|
||||
<div className="panel-options-group__body">{props.children}</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;
|
||||
}
|
||||
|
||||
export default class BodyPortal extends PureComponent<Props> {
|
||||
export class Portal extends PureComponent<Props> {
|
||||
node: HTMLElement = document.createElement('div');
|
||||
portalRoot: HTMLElement;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const {
|
||||
className,
|
||||
root = document.body
|
||||
} = this.props;
|
||||
const { className, root = document.body } = this.props;
|
||||
|
||||
if (className) {
|
||||
this.node.classList.add(className);
|
@ -1,7 +1,10 @@
|
||||
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';
|
||||
|
||||
export const IndicatorsContainer = props => {
|
||||
export const IndicatorsContainer = (props: any) => {
|
||||
const isOpen = props.selectProps.menuIsOpen;
|
||||
return (
|
||||
<components.IndicatorsContainer {...props}>
|
@ -1,5 +1,9 @@
|
||||
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';
|
||||
// @ts-ignore
|
||||
import { OptionProps } from '@torkelo/react-select/lib/components/Option';
|
||||
|
||||
export interface Props {
|
@ -1,17 +1,22 @@
|
||||
// Libraries
|
||||
import classNames from 'classnames';
|
||||
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';
|
||||
// @ts-ignore
|
||||
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
|
||||
// Components
|
||||
import { Option, SingleValue } from './PickerOption';
|
||||
import OptionGroup from './OptionGroup';
|
||||
import { SelectOption, SingleValue } from './SelectOption';
|
||||
import SelectOptionGroup from './SelectOptionGroup';
|
||||
import IndicatorsContainer from './IndicatorsContainer';
|
||||
import NoOptionsMessage from './NoOptionsMessage';
|
||||
import ResetStyles from './ResetStyles';
|
||||
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
|
||||
import resetSelectStyles from './resetSelectStyles';
|
||||
import { CustomScrollbar } from '..';
|
||||
|
||||
export interface SelectOptionItem {
|
||||
label?: string;
|
||||
@ -53,10 +58,10 @@ interface AsyncProps {
|
||||
loadingMessage?: () => string;
|
||||
}
|
||||
|
||||
export const MenuList = props => {
|
||||
export const MenuList = (props: any) => {
|
||||
return (
|
||||
<components.MenuList {...props}>
|
||||
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
|
||||
<CustomScrollbar autoHide={false} autoHeightMax="inherit">{props.children}</CustomScrollbar>
|
||||
</components.MenuList>
|
||||
);
|
||||
};
|
||||
@ -112,11 +117,11 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
components={{
|
||||
Option,
|
||||
Option: SelectOption,
|
||||
SingleValue,
|
||||
IndicatorsContainer,
|
||||
MenuList,
|
||||
Group: OptionGroup,
|
||||
Group: SelectOptionGroup,
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
@ -127,7 +132,7 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
placeholder={placeholder || 'Choose'}
|
||||
styles={ResetStyles}
|
||||
styles={resetSelectStyles()}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
isClearable={isClearable}
|
||||
@ -197,7 +202,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
components={{
|
||||
Option,
|
||||
Option: SelectOption,
|
||||
SingleValue,
|
||||
IndicatorsContainer,
|
||||
NoOptionsMessage,
|
||||
@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
||||
isLoading={isLoading}
|
||||
defaultOptions={defaultOptions}
|
||||
placeholder={placeholder || 'Choose'}
|
||||
styles={ResetStyles}
|
||||
styles={resetSelectStyles()}
|
||||
loadingMessage={loadingMessage}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
isDisabled={isDisabled}
|
@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
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(),
|
||||
clearValue: jest.fn(),
|
||||
onSelect: jest.fn(),
|
||||
getStyles: jest.fn(),
|
||||
getValue: jest.fn(),
|
||||
hasValue: true,
|
||||
@ -18,21 +19,31 @@ const model = {
|
||||
isFocused: false,
|
||||
isSelected: false,
|
||||
innerRef: null,
|
||||
innerProps: null,
|
||||
label: 'Option label',
|
||||
type: null,
|
||||
children: 'Model title',
|
||||
data: {
|
||||
title: 'Model title',
|
||||
imgUrl: 'url/to/avatar',
|
||||
label: 'User picker label',
|
||||
innerProps: {
|
||||
id: '',
|
||||
key: '',
|
||||
onClick: jest.fn(),
|
||||
onMouseOver: jest.fn(),
|
||||
tabIndex: 1,
|
||||
},
|
||||
label: 'Option label',
|
||||
type: 'option',
|
||||
children: 'Model title',
|
||||
className: 'class-for-user-picker',
|
||||
};
|
||||
|
||||
describe('PickerOption', () => {
|
||||
describe('SelectOption', () => {
|
||||
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();
|
||||
});
|
||||
});
|
@ -1,4 +1,7 @@
|
||||
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 { 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;
|
||||
|
||||
return (
|
||||
@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => {
|
||||
};
|
||||
|
||||
// was not able to type this without typescript error
|
||||
export const SingleValue = props => {
|
||||
export const SingleValue = (props: any) => {
|
||||
const { children, data } = props;
|
||||
|
||||
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';
|
||||
|
||||
interface ExtendedGroupProps extends GroupProps<any> {
|
||||
data: any;
|
||||
data: {
|
||||
label: string;
|
||||
expanded: boolean;
|
||||
options: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
export default class OptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
||||
export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.selectProps) {
|
||||
const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
|
||||
if (this.props.data.expanded) {
|
||||
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)) {
|
||||
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 !== '') {
|
||||
this.setState({ expanded: true });
|
||||
}
|
@ -63,6 +63,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
||||
.gf-form-select-box__menu-list {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.tag-filter .gf-form-select-box__menu {
|
||||
@ -101,6 +102,7 @@ $select-input-bg-disabled: $input-bg-disabled;
|
||||
.gf-form-select-box__value-container {
|
||||
display: table-cell;
|
||||
padding: 6px 10px;
|
||||
vertical-align: middle;
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PickerOption renders correctly 1`] = `
|
||||
<div>
|
||||
exports[`SelectOption renders correctly 1`] = `
|
||||
<div
|
||||
id=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseOver={[MockFunction]}
|
||||
tabIndex={1}
|
||||
>
|
||||
<div
|
||||
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 Portal from 'app/core/components/Portal/Portal';
|
||||
import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
|
||||
import * as PopperJS from 'popper.js';
|
||||
import { Manager, Popper as ReactPopper } from 'react-popper';
|
||||
import { Portal } from '@grafana/ui';
|
||||
import Transition from 'react-transition-group/Transition';
|
||||
|
||||
export enum Themes {
|
||||
Default = 'popper__background--default',
|
||||
Error = 'popper__background--error',
|
||||
Brand = 'popper__background--brand',
|
||||
}
|
||||
|
||||
const defaultTransitionStyles = {
|
||||
transition: 'opacity 200ms linear',
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
const transitionStyles = {
|
||||
const transitionStyles: {[key: string]: object} = {
|
||||
exited: { opacity: 0 },
|
||||
entering: { opacity: 0 },
|
||||
entered: { opacity: 1 },
|
||||
exiting: { opacity: 0 },
|
||||
};
|
||||
|
||||
interface Props {
|
||||
interface Props extends React.DOMAttributes<HTMLDivElement> {
|
||||
renderContent: (content: any) => any;
|
||||
show: boolean;
|
||||
placement?: any;
|
||||
placement?: PopperJS.Placement;
|
||||
content: string | ((props: any) => JSX.Element);
|
||||
refClassName?: string;
|
||||
referenceElement: PopperJS.ReferenceObject;
|
||||
theme?: Themes;
|
||||
}
|
||||
|
||||
class Popper extends PureComponent<Props> {
|
||||
render() {
|
||||
const { children, renderContent, show, placement, refClassName } = this.props;
|
||||
const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props;
|
||||
const { content } = this.props;
|
||||
|
||||
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div className={`popper_ref ${refClassName || ''}`} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
||||
{transitionState => (
|
||||
<Portal>
|
||||
<ReactPopper placement={placement}>
|
||||
<ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
|
||||
{({ ref, style, placement, arrowProps }) => {
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
@ -53,7 +58,7 @@ class Popper extends PureComponent<Props> {
|
||||
data-placement={placement}
|
||||
className="popper"
|
||||
>
|
||||
<div className="popper__background">
|
||||
<div className={popperBackgroundClassName}>
|
||||
{renderContent(content)}
|
||||
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
|
||||
</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 renderer from 'react-test-renderer';
|
||||
import Tooltip from './Tooltip';
|
||||
import { Tooltip } from './Tooltip';
|
||||
|
||||
describe('Tooltip', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<Tooltip className="test-class" placement="auto" content="Tooltip text">
|
||||
<a href="http://www.grafana.com">Link with tooltip</a>
|
||||
<Tooltip placement="auto" content="Tooltip text">
|
||||
<a className="test-class" href="http://www.grafana.com">
|
||||
Link with tooltip
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
.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;
|
||||
|
||||
|
||||
@mixin popper-theme($backgroundColor, $arrowColor) {
|
||||
background: $backgroundColor;
|
||||
.popper__arrow {
|
||||
border-color: $arrowColor;
|
||||
}
|
||||
}
|
||||
|
||||
.popper {
|
||||
position: absolute;
|
||||
z-index: $zindex-tooltip;
|
||||
@ -8,7 +16,24 @@ $popper-margin-from-ref: 5px;
|
||||
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;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
@ -16,17 +41,10 @@ $popper-margin-from-ref: 5px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.popper .popper__arrow {
|
||||
.popper__arrow {
|
||||
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
|
||||
.popper[data-placement^='top'] {
|
||||
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 { Label } from 'app/core/components/Label/Label';
|
||||
import { Select } from 'app/core/components/Select/Select';
|
||||
import { MappingType, RangeMap, ValueMap } from 'app/types';
|
||||
import React, { ChangeEvent, PureComponent } from 'react';
|
||||
|
||||
interface Props {
|
||||
mapping: ValueMap | RangeMap;
|
||||
updateMapping: (mapping) => void;
|
||||
removeMapping: () => void;
|
||||
import { MappingType, ValueMapping } from '../../types';
|
||||
import { FormField, FormLabel, Select } from '..';
|
||||
|
||||
export interface Props {
|
||||
valueMapping: ValueMapping;
|
||||
updateValueMapping: (valueMapping: ValueMapping) => void;
|
||||
removeValueMapping: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
from: string;
|
||||
from?: string;
|
||||
id: number;
|
||||
operator: string;
|
||||
text: string;
|
||||
to: string;
|
||||
to?: string;
|
||||
type: MappingType;
|
||||
value: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
const mappingOptions = [
|
||||
@ -25,36 +25,34 @@ const mappingOptions = [
|
||||
];
|
||||
|
||||
export default class MappingRow extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
...props.mapping,
|
||||
};
|
||||
this.state = { ...props.valueMapping };
|
||||
}
|
||||
|
||||
onMappingValueChange = event => {
|
||||
onMappingValueChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ value: event.target.value });
|
||||
};
|
||||
|
||||
onMappingFromChange = event => {
|
||||
onMappingFromChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ from: event.target.value });
|
||||
};
|
||||
|
||||
onMappingToChange = event => {
|
||||
onMappingToChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ to: event.target.value });
|
||||
};
|
||||
|
||||
onMappingTextChange = event => {
|
||||
onMappingTextChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ text: event.target.value });
|
||||
};
|
||||
|
||||
onMappingTypeChange = mappingType => {
|
||||
onMappingTypeChange = (mappingType: MappingType) => {
|
||||
this.setState({ type: mappingType });
|
||||
};
|
||||
|
||||
updateMapping = () => {
|
||||
this.props.updateMapping({ ...this.state });
|
||||
this.props.updateValueMapping({ ...this.state } as ValueMapping);
|
||||
};
|
||||
|
||||
renderRow() {
|
||||
@ -63,30 +61,28 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
if (type === MappingType.RangeToText) {
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form">
|
||||
<Label width={4}>From</Label>
|
||||
<FormField
|
||||
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
|
||||
className="gf-form-input width-8"
|
||||
value={from}
|
||||
className="gf-form-input"
|
||||
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}
|
||||
onBlur={this.updateMapping}
|
||||
onChange={this.onMappingTextChange}
|
||||
/>
|
||||
</div>
|
||||
@ -96,17 +92,16 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form">
|
||||
<Label width={4}>Value</Label>
|
||||
<input
|
||||
className="gf-form-input width-8"
|
||||
onBlur={this.updateMapping}
|
||||
onChange={this.onMappingValueChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
label="Value"
|
||||
labelWidth={4}
|
||||
onBlur={this.updateMapping}
|
||||
onChange={this.onMappingValueChange}
|
||||
value={value}
|
||||
inputWidth={8}
|
||||
/>
|
||||
<div className="gf-form gf-form--grow">
|
||||
<Label width={4}>Text</Label>
|
||||
<FormLabel width={4}>Text</FormLabel>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onBlur={this.updateMapping}
|
||||
@ -124,7 +119,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
return (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<Label width={5}>Type</Label>
|
||||
<FormLabel width={5}>Type</FormLabel>
|
||||
<Select
|
||||
placeholder="Choose type"
|
||||
isSearchable={false}
|
||||
@ -136,7 +131,7 @@ export default class MappingRow extends PureComponent<Props, State> {
|
||||
</div>
|
||||
{this.renderRow()}
|
||||
<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" />
|
||||
</button>
|
||||
</div>
|
@ -1,26 +1,23 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ValueMappings from './ValueMappings';
|
||||
import { defaultProps, OptionModuleProps } from './module';
|
||||
import { MappingType } from 'app/types';
|
||||
|
||||
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
|
||||
import { MappingType } from '../../types/panel';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: OptionModuleProps = {
|
||||
const props: Props = {
|
||||
onChange: jest.fn(),
|
||||
options: {
|
||||
...defaultProps.options,
|
||||
mappings: [
|
||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||
],
|
||||
},
|
||||
valueMappings: [
|
||||
{ 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);
|
||||
|
||||
const wrapper = shallow(<ValueMappings {...props} />);
|
||||
const wrapper = shallow(<ValueMappingsEditor {...props} />);
|
||||
|
||||
const instance = wrapper.instance() as ValueMappings;
|
||||
const instance = wrapper.instance() as ValueMappingsEditor;
|
||||
|
||||
return {
|
||||
instance,
|
||||
@ -39,18 +36,20 @@ describe('Render', () => {
|
||||
describe('On remove mapping', () => {
|
||||
it('Should remove mapping with id 0', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
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' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove mapping with id 1', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
instance.onRemoveMapping(2);
|
||||
|
||||
expect(instance.state.mappings).toEqual([
|
||||
expect(instance.state.valueMappings).toEqual([
|
||||
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
|
||||
]);
|
||||
});
|
||||
@ -66,7 +65,7 @@ describe('Next id to add', () => {
|
||||
});
|
||||
|
||||
it('should default to 1', () => {
|
||||
const { instance } = setup({ options: { ...defaultProps.options } });
|
||||
const { instance } = setup({ valueMappings: [] });
|
||||
|
||||
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
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="section gf-form-group"
|
||||
<Component
|
||||
title="Value Mappings"
|
||||
>
|
||||
<h5
|
||||
className="section-heading"
|
||||
>
|
||||
Value mappings
|
||||
</h5>
|
||||
<div>
|
||||
<MappingRow
|
||||
key="Ok-0"
|
||||
mapping={
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"id": 1,
|
||||
"operator": "",
|
||||
@ -21,12 +18,12 @@ exports[`Render should render component 1`] = `
|
||||
"value": "20",
|
||||
}
|
||||
}
|
||||
removeMapping={[Function]}
|
||||
updateMapping={[Function]}
|
||||
/>
|
||||
<MappingRow
|
||||
key="Meh-1"
|
||||
mapping={
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"from": "21",
|
||||
"id": 2,
|
||||
@ -36,8 +33,6 @@ exports[`Render should render component 1`] = `
|
||||
"type": 2,
|
||||
}
|
||||
}
|
||||
removeMapping={[Function]}
|
||||
updateMapping={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@ -57,5 +52,5 @@ exports[`Render should render component 1`] = `
|
||||
Add mapping
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
`;
|
@ -1 +1,10 @@
|
||||
@import 'CustomScrollbar/CustomScrollbar';
|
||||
@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 { 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';
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
export * from './components';
|
||||
export * from './visualizations';
|
||||
export * from './types';
|
||||
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 './time';
|
||||
export * from './panel';
|
||||
export * from './plugin';
|
||||
export * from './datasource';
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { TimeSeries, LoadingState } from './series';
|
||||
import { TimeRange } from './time';
|
||||
|
||||
export type InterpolateFunction = (value: string, format?: string | Function) => string;
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
timeSeries: TimeSeries[];
|
||||
timeRange: TimeRange;
|
||||
@ -9,6 +11,7 @@ export interface PanelProps<T = any> {
|
||||
renderCounter: number;
|
||||
width: number;
|
||||
height: number;
|
||||
onInterpolate: InterpolateFunction;
|
||||
}
|
||||
|
||||
export interface PanelOptionsProps<T = any> {
|
||||
@ -29,3 +32,44 @@ export interface PanelMenuItem {
|
||||
shortcut?: string;
|
||||
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;
|
||||
data: TimeSeriesValue[][];
|
||||
stats: TimeSeriesStats;
|
||||
allIsNull: boolean;
|
||||
allIsZero: boolean;
|
||||
}
|
||||
|
||||
export interface TimeSeriesStats {
|
||||
[key: string]: number | null;
|
||||
total: number | null;
|
||||
max: number | null;
|
||||
min: number | null;
|
||||
@ -36,8 +39,6 @@ export interface TimeSeriesStats {
|
||||
range: number | null;
|
||||
timeStep: number;
|
||||
count: number;
|
||||
allIsNull: boolean;
|
||||
allIsZero: boolean;
|
||||
}
|
||||
|
||||
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