Merge remote-tracking branch 'grafana/master'

* grafana/master: (835 commits)
  changes some info logging to debug
  add missing ngInject annotation
  typing data
  changelog: adds note about closing #10780
  Do not render time region line or fill if colors not provided
  Fixed row options html template location, fixes #15157
  creating table data type
  build: enterprise release co project.
  Moved dashboard state components to state folder
  Moved time_srv to services folder, this should not belong to dashboard feature but it is too dependant on dashboard to move it out now, needs a bigger refactoring to isolate from dashboard
  Moved a few things around
  Updated what's new article
  Update CHANGELOG.md
  Update CHANGELOG.md
  Replace usages of kbn.valueFormats with ui/getValueFormat
  Fix anchor
  Added download links to docs
  Updated docs
  Updated version again
  Updated version and made some changes to changelog and what's new article
  ...
This commit is contained in:
ryan
2019-01-31 12:56:07 -08:00
846 changed files with 41085 additions and 15731 deletions
+59 -52
View File
@@ -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,8 +331,9 @@ jobs:
deploy-enterprise-release:
docker:
- image: grafana/grafana-ci-deploy:1.0.0
- image: grafana/grafana-ci-deploy:1.2.0
steps:
- checkout
- attach_workspace:
at: .
- run:
@@ -365,10 +351,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 +390,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 +415,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
+53 -4
View File
@@ -1,25 +1,74 @@
# 5.5.0 (unreleased)
# 6.0.0-beta2 (unreleased)
### Minor
* **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae)
# 6.0.0-beta1 (2019-01-30)
### 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)
* **Provisioning**: Provisioning support for alert notifiers [#10487](https://github.com/grafana/grafana/issues/10487), thx [@pbakulev](https://github.com/pbakulev)
* **Explore**: A whole new way to do ad-hoc metric queries and exploration. Split view in half and compare metrics & logs and much much more. [Read more here](http://docs.grafana.org/features/explore/)
### Minor
* **Templating**: Built in time range variables `$__from` and `$__to`, [#1909](https://github.com/grafana/grafana/issues/1909)
* **Alerting**: Use separate 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)
* **Postgres/MySQL/MSSQL**: Nanosecond timestamp support (`$__unixEpochNanoFilter`, `$__unixEpochNanoFrom`, `$__unixEpochNanoTo`) [#14711](https://github.com/grafana/grafana/pull/14711), thx [@ander26](https://github.com/ander26)
* **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)
* **Templating**: Add percentencode formatting to variable interpolation to be used mainly for url escaping [#12764](https://github.com/grafana/grafana/issues/12764), thx [@cxcv](https://github.com/cxcv)
* **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)
* **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)
* **Database**: Support defining additonal database connection string args when using `url` property in database settings [#14709](https://github.com/grafana/grafana/pull/14709), thx [@tpetr](https://github.com/tpetr)
* **Stackdriver**: crossSeriesAggregation not being sent with the query [#15129](https://github.com/grafana/grafana/issues/15129), thx [@Legogris](https://github.com/Legogris)
### 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)
* **Piechart/Flot**: Fixes multiple piechart instances with donut bug [#15062](https://github.com/grafana/grafana/pull/15062)
### 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)
+1 -1
View File
@@ -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
+4 -2
View File
@@ -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 ./
+2 -2
View File
@@ -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
+8 -6
View File
@@ -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
+11 -7
View File
@@ -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
+1 -1
View File
@@ -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
+16 -2
View File
@@ -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) {
+24 -6
View File
@@ -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 =
+25
View File
@@ -0,0 +1,25 @@
# # config file version
apiVersion: 1
# notifiers:
# - name: default-slack-temp
# type: slack
# org_name: Main Org.
# is_default: true
# uid: notifier1
# settings:
# recipient: "XXX"
# token: "xoxb"
# uploadImage: true
# url: https://slack.com
# - name: default-email
# type: email
# org_id: 1
# uid: notifier2
# is_default: false
# settings:
# addresses: example11111@example.com
# delete_notifiers:
# - name: default-slack-temp
# org_name: Main Org.
# uid: notifier1
+28 -6
View File
@@ -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
+1 -1
View File
@@ -4,6 +4,6 @@ providers:
- name: 'gdev dashboards'
folder: 'gdev dashboards'
type: file
updateIntervalSeconds: 15
options:
path: devenv/dev-dashboards
+6
View File
@@ -152,4 +152,10 @@ datasources:
authType: credentials
defaultRegion: eu-west-2
- name: gdev-loki
type: loki
access: proxy
url: http://localhost:3100
editable: false
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
}
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
}
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
@@ -0,0 +1,22 @@
version: "3"
networks:
loki:
services:
loki:
image: grafana/loki:master
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- loki
promtail:
image: grafana/promtail:master
volumes:
- /var/log:/var/log
command:
-config.file=/etc/promtail/docker-config.yaml
networks:
- loki
+2 -1
View File
@@ -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
View 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
View 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
View 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
View 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
View 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 "$@"
+184
View File
@@ -231,3 +231,187 @@ By default Grafana will delete dashboards in the database if the file is removed
> which leads to problems if you re-use settings that are supposed to be unique.
> Be careful not to re-use the same `title` multiple times within a folder
> or `uid` within the same installation as this will cause weird behaviors.
## Alert Notification Channels
Alert Notification Channels can be provisioned by adding one or more yaml config files in the [`provisioning/notifiers`](/installation/configuration/#provisioning) directory.
Each config file can contain the following top-level fields:
- `notifiers`, a list of alert notifications that will be added or updated during start up. If the notification channel already exists, Grafana will update it to match the configuration file.
- `delete_notifiers`, a list of alert notifications to be deleted before before inserting/updating those in the `notifiers` list.
Provisioning looks up alert notifications by uid, and will update any existing notification with the provided uid.
By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `uid` can be optionally specified to specify a string identifier for the alert name.
```json
{
...
"alert": {
...,
"conditions": [...],
"frequency": "24h",
"noDataState": "ok",
"notifications": [
{"uid": "notifier1"},
{"uid": "notifier2"},
]
}
...
}
```
### Example Alert Notification Channels Config File
```yaml
notifiers:
- name: notification-channel-1
type: slack
uid: notifier1
# either
org_id: 2
# or
org_name: Main Org.
is_default: true
# See `Supported Settings` section for settings supporter for each
# alert notification type.
settings:
recipient: "XXX"
token: "xoxb"
uploadImage: true
url: https://slack.com
delete_notifiers:
- name: notification-channel-1
uid: notifier1
# either
org_id: 2
# or
org_name: Main Org.
- name: notification-channel-2
# default org_id: 1
```
### Supported Settings
The following sections detail the supported settings for each alert notification type.
#### Alert notification `pushover`
| Name |
| ---- |
| apiToken |
| userKey |
| device |
| retry |
| expire |
#### Alert notification `slack`
| Name |
| ---- |
| url |
| recipient |
| username |
| iconEmoji |
| iconUrl |
| uploadImage |
| mention |
| token |
#### Alert notification `victorops`
| Name |
| ---- |
| url |
#### Alert notification `kafka`
| Name |
| ---- |
| kafkaRestProxy |
| kafkaTopic |
#### Alert notification `LINE`
| Name |
| ---- |
| token |
#### Alert notification `pagerduty`
| Name |
| ---- |
| integrationKey |
#### Alert notification `sensu`
| Name |
| ---- |
| url |
| source |
| handler |
| username |
| password |
#### Alert notification `prometheus-alertmanager`
| Name |
| ---- |
| url |
#### Alert notification `teams`
| Name |
| ---- |
| url |
#### Alert notification `dingding`
| Name |
| ---- |
| url |
#### Alert notification `email`
| Name |
| ---- |
| addresses |
#### Alert notification `hipchat`
| Name |
| ---- |
| url |
| apikey |
| roomid |
#### Alert notification `opsgenie`
| Name |
| ---- |
| apiKey |
| apiUrl |
#### Alert notification `telegram`
| Name |
| ---- |
| bottoken |
| chatid |
#### Alert notification `threema`
| Name |
| ---- |
| gateway_id |
| recipient_id |
| api_secret |
#### Alert notification `webhook`
| Name |
| ---- |
| url |
| username |
| password |
+1 -1
View File
@@ -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)
@@ -110,6 +110,9 @@ Macro example | Description
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
*$__unixEpochNanoFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as nanosecond timestamp. For example, *dateColumn > 1494410783152415214 AND dateColumn < 1494497183142514872*
*$__unixEpochNanoFrom()* | Will be replaced by the start of the currently active time selection as nanosecond timestamp. For example, *1494410783152415214*
*$__unixEpochNanoTo()* | Will be replaced by the end of the currently active time selection as nanosecond timestamp. For example, *1494497183142514872*
*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
@@ -144,6 +144,9 @@ Macro example | Description
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
*$__unixEpochNanoFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as nanosecond timestamp. For example, *dateColumn > 1494410783152415214 AND dateColumn < 1494497183142514872*
*$__unixEpochNanoFrom()* | Will be replaced by the start of the currently active time selection as nanosecond timestamp. For example, *1494410783152415214*
*$__unixEpochNanoTo()* | Will be replaced by the end of the currently active time selection as nanosecond timestamp. For example, *1494497183142514872*
*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
@@ -154,6 +154,9 @@ Macro example | Description
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamps. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
*$__unixEpochNanoFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as nanosecond timestamps. For example, *dateColumn >= 1494410783152415214 AND dateColumn <= 1494497183142514872*
*$__unixEpochNanoFrom()* | Will be replaced by the start of the currently active time selection as nanosecond timestamp. For example, *1494410783152415214*
*$__unixEpochNanoTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183142514872*
*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup, but for times stored as unix timestamp (only available in Grafana 5.3+).
*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above, but also adds a column alias (only available in Grafana 5.3+).
@@ -12,8 +12,8 @@ weight = 11
# Using Google Stackdriver in Grafana
> Only available in Grafana v5.3+.
> The datasource is currently a beta feature and is subject to change.
> Available as a beta feature in Grafana v5.3.x and v5.4.x.
> Officially released in Grafana v6.0.0
Grafana ships with built-in support for Google Stackdriver. Just add it as a datasource and you are ready to build dashboards for your Stackdriver metrics.
+14 -19
View File
@@ -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.
@@ -22,23 +27,6 @@ For infrastructure monitoring and incident response, you no longer need to switc
If you just want to explore your data and do not want to create a dashboard then Explore makes this much easier. Explore will show the results as both a graph and a table enabling you to see trends in the data and more detail at the same time (if the datasource supports both graph and table data).
## Turning the Explore Feature On
Explore will be officially released in Grafana 6.0. It is however already in the latest nightly builds of Grafana and can be turned using a feature flag in the config file. Restart Grafana after making the config file change.
```ini
[explore]
# Enable the Explore section
enabled = true
```
Or if using docker:
```bash
docker pull grafana/grafana:master
docker run --name grafana -p 3000:3000 -e "GF_EXPLORE_ENABLED=true" grafana/grafana:master
```
## How to Start Exploring
There is a new Explore icon on the menu bar to the left. This opens a new empty Explore tab.
@@ -111,7 +99,14 @@ The Logs Explorer (the `Log labels` button) next to the query field shows a list
Once the result is returned, the log panel shows a list of log rows and a bar chart where the x-axis shows the time and the y-axis shows the frequency/count.
{{< docs-imagebox img="/img/docs/v60/explore_loki.png" class="docs-image--no-shadow" caption="Explore Loki Log Streams" >}}
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br />
#### Log Stream Selector
+1 -1
View File
@@ -26,7 +26,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article
{{< docs-imagebox img="/img/docs/v53/stackdriver-with-heatmap.png" max-width= "600px" class="docs-image--no-shadow docs-image--right" >}}
Grafana v5.3 ships with built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) and enables you to visualize your Stackdriver metrics in Grafana.
Grafana v5.3 ships with built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) and enables you to visualize your Stackdriver metrics in Grafana.
Getting started with the plugin is easy. Simply create a GCE Service account that has access to the Stackdriver API scope, download the Service Account key file from Google and upload it on the Stackdriver datasource config page in Grafana and you should have a secure server-to-server authentication setup. Like other core plugins, Stackdriver has built-in support for alerting. It also comes with support for heatmaps and basic variables.
+159
View File
@@ -0,0 +1,159 @@
+++
title = "What's New in Grafana v6.0"
description = "Feature & improvement highlights for Grafana v6.0"
keywords = ["grafana", "new", "documentation", "6.0"]
type = "docs"
[menu.docs]
name = "Version 6.0"
identifier = "v6.0"
parent = "whatsnew"
weight = -11
+++
# What's New in Grafana v6.0
This update to Grafana introduces a new way of exploring your data, support for log data and tons of other features.
Grafana v6.0 is out in **Beta**, [Download Now!](https://grafana.com/grafana/download/beta)
The main highlights are:
- [Explore]({{< relref "#explore" >}}) - A new query focused workflow for ad-hoc data exploration and troubleshooting.
- [Grafana Loki]({{< relref "#explore-and-grafana-loki" >}}) - Integration with the new open source log aggregation system from Grafana Labs.
- [Gauge Panel]({{< relref "#gauge-panel" >}}) - A new standalone panel for gauges.
- [New Panel Editor UX]({{< relref "#new-panel-editor" >}}) improves panel editing
and enables easy switching between different visualizations.
- [Google Stackdriver Datasource]({{< relref "#google-stackdriver-datasource" >}}) is out of beta and is officially released.
- [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource
- [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins.
- [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker.
## Explore
{{< docs-imagebox img="/img/docs/v60/explore_prometheus.png" max-width="800px" class="docs-image--right" caption="Screenshot of the new Explore option in the panel menu" >}}
Grafana's dashboard UI is all about building dashboards for visualization. **Explore** strips away all the dashboard and panel options so that you can focus on the query & metric exploration. Iterate until you have a working query and then think about building a dashboard. You can also jump from a dashboard panel into **Explore** and from there do some ad-hoc query exporation with the panel queries as a starting point.
For infrastructure monitoring and incident response, you no longer need to switch to other tools to debug what went wrong. **Explore** allows you to dig deeper into your metrics and logs to find the cause. Grafana's new logging datasource, [Loki](https://github.com/grafana/loki) is tightly integrated into Explore and allows you to correlate metrics and logs by viewing them side-by-side.
**Explore** is a new paradigm for Grafana. It creates a new interactive debugging workflow that integrates two pillars
of observability - metrics and logs. Explore works with every datasource but for Prometheus we have customized the
query editor and the experience to provide the best possible exploration UX.
### Explore and Prometheus
Explore features a new [Prometheus query editor](/features/explore/#prometheus-specific-features). This new editor has improved autocomplete, metric tree selector,
integrations with the Explore table view for easy label filtering and useful query hints that can automatically apply
functions to your query. There is also integration between Prometheus and Grafana Loki (see more about Loki below) that
enabled jumping between metrics query and logs query with preserved label filters.
### Explore splits
Explore supports splitting the view so you can compare different queries, different datasources and metrics & logs side by side!
{{< docs-imagebox img="/img/docs/v60/explore_split.png" max-width="800px" caption="Screenshot of the new Explore option in the panel menu" >}}
<br />
### Explore and Grafana Loki
The log exploration & visualization features in Explore are available to any data source but are currently only implemented by the new open source log
aggregation system from Grafana Lab called [Grafana Loki](https://github.com/grafana/loki).
Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is designed to be very cost effective, as it does not index the contents of the logs, but rather a set of labels for each log stream. The logs from Loki are queried in a similar way to querying with label selectors in Prometheus. It uses labels to group log streams which can be made to match up with your Prometheus labels.
Read more about Grafana Loki [here](https://github.com/grafana/loki) or [Grafana Labs hosted Loki](https://grafana.com/loki).
The Explore feature allows you to query logs and features a new log panel. In the near future, we will be adding support
for other log sources to Explore and the next planned integration is Elasticsearch.
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br />
## New Panel Editor
Grafana v6.0 has a completely redesigned UX around editing panels. You can now resize the visualization area if you want
more space for queries & options and vice versa. You can now also change visualization (panel type) from within the new
panel edit mode. No need to add a new panel to try out different visualizations! Checkout the
video below to see the new Panel Editor in action.
<div class="medium-6 columns">
<video width="800" height="500" controls>
<source src="/assets/videos/panel_change_viz.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<br>
### Gauge Panel
We have created a new separate Gauge panel as we felt having this visualization be a hidden option in the Singlestat panel
was not ideal. When it supports 100% of the Singlestat Gauge features we plan to add a migration so all
singlestats that use it become Gauge panels instead. This new panel contains a new **Threshold** editor that we will
continue to refine and start using in other panels.
{{< docs-imagebox img="/img/docs/v60/gauge_panel.png" max-width="600px" caption="Gauge Panel" >}}
<br>
### React Panels & Query Editors
A major part of all the work that has gone into Grafana v6.0 has been on the migration to React. This investment
is part of the future proofing of Grafana and it's code base and ecosystem. Starting in v6.0 **Panels** and **Data
source** plugins can be written in React using our published `@grafana/ui` sdk library. More information on this
will be shared closer to or just after release.
{{< docs-imagebox img="/img/docs/v60/react_panels.png" max-width="600px" caption="React Panel" >}}
<br />
### Google Stackdriver Datasource
Built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) is officially released in Grafana 6.0. Beta support was added in Grafana 5.3 and we have added lots of improvements since then.
To get started read the guide: [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/).
### Azure Monitor Datasource
One of the goals of the Grafana v6.0 release is to add support for the three major clouds. Amazon Cloudwatch has been a core datasource for years and Google Stackdriver is also now supported. We developed an external plugin for Azure Monitor last year and for this release the [plugin](https://grafana.com/plugins/grafana-azure-monitor-datasource) is being moved into Grafana to be one of the built-in datasources. For users of the external plugin, Grafana will automatically start using the built-in version. As a core datasource, the Azure Monitor datasource will get alerting support for the official 6.0 release.
The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics.
### Provisioning support for alert notifiers
Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance.
### Auth and session token improvements
The previous session storage implementation in Grafana was causing problems in larger HA setups due to too many write requests to the database. The remember me token also have several security issues which is why we decided to rewrite auth middleware in Grafana and remove the session storage since most operations using the session storage could be rewritten to use cookies or data already made available earlier in the request.
If you are using `Auth proxy` for authentication the session storage will still be used but our goal is to remove this ASAP as well.
This release will force all users to log in again since their previous token is not valid anymore.
### Named Colors
{{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}}
We have updated the color picker to show named colors and primary colors. We hope this will improve accessibility and
helps making colors more consistent across dashboards. We hope to do more in this color picker in the future, like show
colors used in the dashboard.
Named colors also enables Grafana to adapt colors to the current theme.
<div class="clearfix"></div>
### Other features
- The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per bucket computations like the difference or ratio between two metrics.
- Support for Google Hangouts Chat alert notifications
- New built in template variables for the current time range in `$__from` and `$__to`
## Changelog
Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list of new features, changes, and bug fixes.
+3 -3
View File
@@ -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}
```
+2 -2
View File
@@ -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"
}
}
```
+1 -1
View File
@@ -105,7 +105,7 @@ POST /api/folders/nErXDvCkzz/permissions
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"items": [
{
"role": "Viewer",
+26 -1
View File
@@ -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.
+8 -11
View File
@@ -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
+15 -7
View File
@@ -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
+1 -1
View File
@@ -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": []
}
+10 -2
View File
@@ -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).
@@ -244,6 +245,11 @@ summarize($myinterval, sum, false)
Grafana has global built-in variables that can be used in expressions in the query editor.
### Time range variables
Grafana has two built in time range variables in `$__from` and `$__to`. They are currently always interpolated
as epoch milliseconds. These variables are only available in Grafana v6.0 and above.
### The $__interval Variable
This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB, MySQL, Postgres, MSSQL), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite).
@@ -292,9 +298,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.
+2 -2
View File
@@ -1,4 +1,4 @@
{
"stable": "5.4.2",
"testing": "5.4.2"
"stable": "5.4.3",
"testing": "5.4.3"
}
+11 -6
View File
@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "5.5.0-pre1",
"version": "6.0.0-prebeta2",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -24,9 +24,10 @@
"@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-grid-layout": "^0.16.6",
"@types/react-select": "^2.0.4",
"@types/react-virtualized": "^9.18.12",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
"axios": "^0.17.1",
@@ -65,15 +66,17 @@
"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",
"mocha": "^4.0.1",
"monaco-editor": "^0.15.6",
"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",
@@ -82,6 +85,7 @@
"prettier": "1.9.2",
"react-hot-loader": "^4.3.6",
"react-test-renderer": "^16.5.0",
"regexp-replace-loader": "^1.0.1",
"sass-lint": "^1.10.2",
"sass-loader": "^7.0.1",
"sinon": "1.17.6",
@@ -114,7 +118,8 @@
"typecheck": "tsc --noEmit",
"jest": "jest --notify --watch",
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
"precommit": "grunt precommit"
"precommit": "grunt precommit",
"storybook": "cd packages/grafana-ui && yarn storybook"
},
"husky": {
"hooks": {
@@ -167,7 +172,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 +193,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",
+2
View File
@@ -0,0 +1,2 @@
import '@storybook/addon-knobs/register';
import '@storybook/addon-actions/register';
+12
View File
@@ -0,0 +1,12 @@
import { configure } from '@storybook/react';
import '../../../public/sass/grafana.light.scss';
// automatically import all files ending in *.stories.tsx
const req = require.context('../src/components', true, /.story.tsx$/);
function loadStories() {
req.keys().forEach(req);
}
configure(loadStories, module);
@@ -0,0 +1,56 @@
const path = require('path');
module.exports = (baseConfig, env, config) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
{
loader: require.resolve('awesome-typescript-loader'),
},
],
});
config.module.rules.push({
test: /\.scss$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
importLoaders: 2,
url: false,
sourceMap: false,
minimize: false,
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: false,
config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' },
},
},
{ loader: 'sass-loader', options: { sourceMap: false } },
],
});
config.module.rules.push({
test: require.resolve('jquery'),
use: [
{
loader: 'expose-loader',
query: 'jQuery',
},
{
loader: 'expose-loader',
query: '$',
},
],
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
};
+33 -5
View File
@@ -5,29 +5,57 @@
"main": "src/index.ts",
"scripts": {
"tslint": "tslint -c tslint.json --project tsconfig.json",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"storybook": "start-storybook -p 9001 -c .storybook -s ../../public"
},
"author": "",
"license": "ISC",
"dependencies": {
"@torkelo/react-select": "2.1.1",
"@types/react-color": "^2.14.0",
"classnames": "^2.2.5",
"jquery": "^3.2.1",
"lodash": "^4.17.10",
"moment": "^2.22.2",
"react": "^16.6.3",
"react-color": "^2.17.0",
"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/jest": "^23.3.2",
"@types/lodash": "^4.14.119",
"@types/react": "^16.7.6",
"@storybook/addon-actions": "^4.1.7",
"@storybook/addon-info": "^4.1.6",
"@storybook/addon-knobs": "^4.1.7",
"@storybook/react": "^4.1.4",
"@types/classnames": "^2.2.6",
"@types/jest": "^23.3.2",
"@types/jquery": "^1.10.35",
"@types/lodash": "^4.14.119",
"@types/node": "^10.12.18",
"@types/react": "^16.7.6",
"@types/react-custom-scrollbars": "^4.0.5",
"@types/react-test-renderer": "^16.0.3",
"@types/react-transition-group": "^2.0.15",
"@types/storybook__addon-actions": "^3.4.1",
"@types/storybook__addon-info": "^3.4.2",
"@types/storybook__addon-knobs": "^4.0.0",
"@types/storybook__react": "^4.0.0",
"@types/tether-drop": "^1.4.8",
"@types/tinycolor2": "^1.4.1",
"awesome-typescript-loader": "^5.2.1",
"react-docgen-typescript-loader": "^3.0.0",
"react-docgen-typescript-webpack-plugin": "^1.1.0",
"react-test-renderer": "^16.7.0",
"typescript": "^3.2.2"
},
"resolutions": {
"@types/lodash": "4.14.119"
}
}
@@ -0,0 +1,94 @@
import React from 'react';
import { ColorPickerProps } from './ColorPicker';
import tinycolor from 'tinycolor2';
import { debounce } from 'lodash';
interface ColorInputState {
previousColor: string;
value: string;
}
interface ColorInputProps extends ColorPickerProps {
style?: React.CSSProperties;
}
class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
constructor(props: ColorInputProps) {
super(props);
this.state = {
previousColor: props.color,
value: props.color,
};
this.updateColor = debounce(this.updateColor, 100);
}
static getDerivedStateFromProps(props: ColorPickerProps, state: ColorInputState) {
const newColor = tinycolor(props.color);
if (newColor.isValid() && props.color !== state.previousColor) {
return {
...state,
previousColor: props.color,
value: newColor.toString(),
};
}
return state;
}
updateColor = (color: string) => {
this.props.onChange(color);
};
handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const newColor = tinycolor(event.currentTarget.value);
this.setState({
value: event.currentTarget.value,
});
if (newColor.isValid()) {
this.updateColor(newColor.toString());
}
};
handleBlur = () => {
const newColor = tinycolor(this.state.value);
if (!newColor.isValid()) {
this.setState({
value: this.props.color,
});
}
};
render() {
const { value } = this.state;
return (
<div
style={{
display: 'flex',
...this.props.style,
}}
>
<div
style={{
background: this.props.color,
width: '35px',
height: '35px',
flexGrow: 0,
borderRadius: '3px 0 0 3px',
}}
/>
<div
style={{
flexGrow: 1,
}}
>
<input className="gf-form-input" value={value} onChange={this.handleChange} onBlur={this.handleBlur} />
</div>
</div>
);
}
}
export default ColorInput;
@@ -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);
};
@@ -0,0 +1,63 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs, boolean } from '@storybook/addon-knobs';
import { SeriesColorPicker, ColorPicker } from './ColorPicker';
import { action } from '@storybook/addon-actions';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
import { getThemeKnob } from '../../utils/storybook/themeKnob';
const getColorPickerKnobs = () => {
return {
selectedTheme: getThemeKnob(),
enableNamedColors: boolean('Enable named colors', false),
};
};
const ColorPickerStories = storiesOf('UI/ColorPicker/Pickers', module);
ColorPickerStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
ColorPickerStories.add('default', () => {
const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
return (
<UseState initialState="#00ff00">
{(selectedColor, updateSelectedColor) => {
return (
<ColorPicker
enableNamedColors={enableNamedColors}
color={selectedColor}
onChange={color => {
action('Color changed')(color);
updateSelectedColor(color);
}}
theme={selectedTheme || undefined}
/>
);
}}
</UseState>
);
});
ColorPickerStories.add('Series color picker', () => {
const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
return (
<UseState initialState="#00ff00">
{(selectedColor, updateSelectedColor) => {
return (
<SeriesColorPicker
enableNamedColors={enableNamedColors}
yaxis={1}
onToggleAxis={() => {}}
color={selectedColor}
onChange={color => updateSelectedColor(color)}
theme={selectedTheme || undefined}
>
<div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div>
</SeriesColorPicker>
);
}}
</UseState>
);
});
@@ -0,0 +1,114 @@
import React, { Component, createRef } from 'react';
import PopperController from '../Tooltip/PopperController';
import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper';
import { ColorPickerPopover } from './ColorPickerPopover';
import { Themeable, GrafanaTheme } from '../../types';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
import propDeprecationWarning from '../../utils/propDeprecationWarning';
type ColorPickerChangeHandler = (color: string) => void;
export interface ColorPickerProps extends Themeable {
color: string;
onChange: ColorPickerChangeHandler;
/**
* @deprecated Use onChange instead
*/
onColorChange?: ColorPickerChangeHandler;
enableNamedColors?: boolean;
withArrow?: boolean;
children?: JSX.Element;
}
export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => {
const { onColorChange } = props;
if (onColorChange) {
propDeprecationWarning(componentName, 'onColorChange', 'onChange');
}
};
export const colorPickerFactory = <T extends ColorPickerProps>(
popover: React.ComponentType<T>,
displayName = 'ColorPicker',
renderPopoverArrowFunction?: RenderPopperArrowFn
) => {
return class ColorPicker extends Component<T, any> {
static displayName = displayName;
pickerTriggerRef = createRef<HTMLDivElement>();
handleColorChange = (color: string) => {
const { onColorChange, onChange } = this.props;
const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler;
return changeHandler(color);
};
render() {
const popoverElement = React.createElement(popover, {
...this.props,
onChange: this.handleColorChange,
});
const { theme, withArrow, children } = this.props;
const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => {
return (
<div
{...arrowProps}
data-placement={placement}
className={`ColorPicker__arrow ColorPicker__arrow--${theme === GrafanaTheme.Light ? 'light' : 'dark'}`}
/>
);
};
return (
<PopperController content={popoverElement} hideAfter={300}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.pickerTriggerRef.current && (
<Popper
{...popperProps}
referenceElement={this.pickerTriggerRef.current}
wrapperClassName="ColorPicker"
renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)}
onMouseLeave={hidePopper}
onMouseEnter={showPopper}
/>
)}
{children ? (
React.cloneElement(children as JSX.Element, {
ref: this.pickerTriggerRef,
onClick: showPopper,
onMouseLeave: hidePopper,
})
) : (
<div
ref={this.pickerTriggerRef}
onClick={showPopper}
onMouseLeave={hidePopper}
className="sp-replacer sp-light"
>
<div className="sp-preview">
<div
className="sp-preview-inner"
style={{
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme),
}}
/>
</div>
</div>
)}
</>
);
}}
</PopperController>
);
}
};
};
export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker');
export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker');
@@ -0,0 +1,40 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { ColorPickerPopover } from './ColorPickerPopover';
import { withKnobs } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { getThemeKnob } from '../../utils/storybook/themeKnob';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
const ColorPickerPopoverStories = storiesOf('UI/ColorPicker/Popovers', module);
ColorPickerPopoverStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
ColorPickerPopoverStories.add('default', () => {
const selectedTheme = getThemeKnob();
return (
<ColorPickerPopover
color="#BC67E6"
onChange={color => {
console.log(color);
}}
theme={selectedTheme || undefined}
/>
);
});
ColorPickerPopoverStories.add('SeriesColorPickerPopover', () => {
const selectedTheme = getThemeKnob();
return (
<SeriesColorPickerPopover
color="#BC67E6"
onChange={color => {
console.log(color);
}}
theme={selectedTheme || undefined}
/>
);
});
@@ -0,0 +1,75 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { ColorPickerPopover } from './ColorPickerPopover';
import { getColorDefinitionByName, getNamedColorPalette } from '../../utils/namedColorsPalette';
import { ColorSwatch } from './NamedColorsGroup';
import { flatten } from 'lodash';
import { GrafanaTheme } from '../../types';
const allColors = flatten(Array.from(getNamedColorPalette().values()));
describe('ColorPickerPopover', () => {
const BasicGreen = getColorDefinitionByName('green');
const BasicBlue = getColorDefinitionByName('blue');
describe('rendering', () => {
it('should render provided color as selected if color provided by name', () => {
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} />);
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
expect(selectedSwatch.length).toBe(1);
expect(notSelectedSwatches.length).toBe(allColors.length - 1);
expect(selectedSwatch.prop('isSelected')).toBe(true);
});
it('should render provided color as selected if color provided by hex', () => {
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} />);
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
expect(selectedSwatch.length).toBe(1);
expect(notSelectedSwatches.length).toBe(allColors.length - 1);
expect(selectedSwatch.prop('isSelected')).toBe(true);
});
});
describe('named colors support', () => {
const onChangeSpy = jest.fn();
let wrapper: ReactWrapper;
afterEach(() => {
wrapper.unmount();
onChangeSpy.mockClear();
});
it('should pass hex color value to onChange prop by default', () => {
wrapper = mount(
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={GrafanaTheme.Light} />
);
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
basicBlueSwatch.simulate('click');
expect(onChangeSpy).toBeCalledTimes(1);
expect(onChangeSpy).toBeCalledWith(BasicBlue.variants.light);
});
it('should pass color name to onChange prop when named colors enabled', () => {
wrapper = mount(
<ColorPickerPopover
enableNamedColors
color={BasicGreen.variants.dark}
onChange={onChangeSpy}
theme={GrafanaTheme.Light}
/>
);
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
basicBlueSwatch.simulate('click');
expect(onChangeSpy).toBeCalledTimes(1);
expect(onChangeSpy).toBeCalledWith(BasicBlue.name);
});
});
});
@@ -0,0 +1,130 @@
import React from 'react';
import { NamedColorsPalette } from './NamedColorsPalette';
import { getColorName, getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker';
import { GrafanaTheme } from '../../types';
import { PopperContentProps } from '../Tooltip/PopperController';
import SpectrumPalette from './SpectrumPalette';
export interface Props<T> extends ColorPickerProps, PopperContentProps {
customPickers?: T;
}
type PickerType = 'palette' | 'spectrum';
interface CustomPickersDescriptor {
[key: string]: {
tabComponent: React.ComponentType<ColorPickerProps>;
name: string;
};
}
interface State<T> {
activePicker: PickerType | keyof T;
}
export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React.Component<Props<T>, State<T>> {
constructor(props: Props<T>) {
super(props);
this.state = {
activePicker: 'palette',
};
warnAboutColorPickerPropsDeprecation('ColorPickerPopover', props);
}
getTabClassName = (tabName: PickerType | keyof T) => {
const { activePicker } = this.state;
return `ColorPickerPopover__tab ${activePicker === tabName && 'ColorPickerPopover__tab--active'}`;
};
handleChange = (color: any) => {
const { onColorChange, onChange, enableNamedColors, theme } = this.props;
const changeHandler = onColorChange || onChange;
if (enableNamedColors) {
return changeHandler(color);
}
changeHandler(getColorFromHexRgbOrName(color, theme));
};
handleTabChange = (tab: PickerType | keyof T) => {
return () => this.setState({ activePicker: tab });
};
renderPicker = () => {
const { activePicker } = this.state;
const { color, theme } = this.props;
switch (activePicker) {
case 'spectrum':
return <SpectrumPalette color={color} onChange={this.handleChange} theme={theme} />;
case 'palette':
return <NamedColorsPalette color={getColorName(color, theme)} onChange={this.handleChange} theme={theme} />;
default:
return this.renderCustomPicker(activePicker);
}
};
renderCustomPicker = (tabKey: keyof T) => {
const { customPickers, color, theme } = this.props;
if (!customPickers) {
return null;
}
return React.createElement(customPickers[tabKey].tabComponent, {
color,
theme,
onChange: this.handleChange,
});
};
renderCustomPickerTabs = () => {
const { customPickers } = this.props;
if (!customPickers) {
return null;
}
return (
<>
{Object.keys(customPickers).map(key => {
return (
<div
className={this.getTabClassName(key)}
onClick={this.handleTabChange(key)}
key={key}
>
{customPickers[key].name}
</div>
);
})}
</>
);
};
render() {
const { theme } = this.props;
const colorPickerTheme = theme || GrafanaTheme.Dark;
return (
<div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
<div className="ColorPickerPopover__tabs">
<div
className={this.getTabClassName('palette')}
onClick={this.handleTabChange('palette')}
>
Colors
</div>
<div
className={this.getTabClassName('spectrum')}
onClick={this.handleTabChange('spectrum')}
>
Custom
</div>
{this.renderCustomPickerTabs()}
</div>
<div className="ColorPickerPopover__content">{this.renderPicker()}</div>
</div>
);
}
}
@@ -0,0 +1,110 @@
import React, { FunctionComponent } from 'react';
import { Themeable, GrafanaTheme } from '../../types';
import { ColorDefinition, getColorForTheme } from '../../utils/namedColorsPalette';
import { Color } from 'csstype';
import { find, upperFirst } from 'lodash';
type ColorChangeHandler = (color: ColorDefinition) => void;
export enum ColorSwatchVariant {
Small = 'small',
Large = 'large',
}
interface ColorSwatchProps extends Themeable, React.DOMAttributes<HTMLDivElement> {
color: string;
label?: string;
variant?: ColorSwatchVariant;
isSelected?: boolean;
}
export const ColorSwatch: FunctionComponent<ColorSwatchProps> = ({
color,
label,
variant = ColorSwatchVariant.Small,
isSelected,
theme,
...otherProps
}) => {
const isSmall = variant === ColorSwatchVariant.Small;
const swatchSize = isSmall ? '16px' : '32px';
const selectedSwatchBorder = theme === GrafanaTheme.Light ? '#ffffff' : '#1A1B1F';
const swatchStyles = {
width: swatchSize,
height: swatchSize,
borderRadius: '50%',
background: `${color}`,
marginRight: isSmall ? '0px' : '8px',
boxShadow: isSelected ? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${selectedSwatchBorder}` : 'none',
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
{...otherProps}
>
<div style={swatchStyles} />
{variant === ColorSwatchVariant.Large && <span>{label}</span>}
</div>
);
};
interface NamedColorsGroupProps extends Themeable {
colors: ColorDefinition[];
selectedColor?: Color;
onColorSelect: ColorChangeHandler;
key?: string;
}
const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
colors,
selectedColor,
onColorSelect,
theme,
...otherProps
}) => {
const primaryColor = find(colors, color => !!color.isPrimary);
return (
<div {...otherProps} style={{ display: 'flex', flexDirection: 'column' }}>
{primaryColor && (
<ColorSwatch
key={primaryColor.name}
isSelected={primaryColor.name === selectedColor}
variant={ColorSwatchVariant.Large}
color={getColorForTheme(primaryColor, theme)}
label={upperFirst(primaryColor.hue)}
onClick={() => onColorSelect(primaryColor)}
theme={theme}
/>
)}
<div
style={{
display: 'flex',
marginTop: '8px',
}}
>
{colors.map(
color =>
!color.isPrimary && (
<div key={color.name} style={{ marginRight: '4px' }}>
<ColorSwatch
key={color.name}
isSelected={color.name === selectedColor}
color={getColorForTheme(color, theme)}
onClick={() => onColorSelect(color)}
theme={theme}
/>
</div>
)
)}
</div>
</div>
);
};
export default NamedColorsGroup;
@@ -0,0 +1,52 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { NamedColorsPalette } from './NamedColorsPalette';
import { getColorName, getColorDefinitionByName } from '../../utils/namedColorsPalette';
import { withKnobs, select } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
const BasicGreen = getColorDefinitionByName('green');
const BasicBlue = getColorDefinitionByName('blue');
const LightBlue = getColorDefinitionByName('light-blue');
const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module);
NamedColorsPaletteStories.addDecorator(withKnobs).addDecorator(withCenteredStory);
NamedColorsPaletteStories.add('Named colors swatch - support for named colors', () => {
const selectedColor = select(
'Selected color',
{
Green: 'green',
Red: 'red',
'Light blue': 'light-blue',
},
'red'
);
return (
<UseState initialState={selectedColor}>
{(selectedColor, updateSelectedColor) => {
return <NamedColorsPalette color={selectedColor} onChange={updateSelectedColor} />;
}}
</UseState>
);
}).add('Named colors swatch - support for hex values', () => {
const selectedColor = select(
'Selected color',
{
Green: BasicGreen.variants.dark,
Red: BasicBlue.variants.dark,
'Light blue': LightBlue.variants.dark,
},
'red'
);
return (
<UseState initialState={selectedColor}>
{(selectedColor, updateSelectedColor) => {
return <NamedColorsPalette color={getColorName(selectedColor)} onChange={updateSelectedColor} />;
}}
</UseState>
);
});
@@ -0,0 +1,36 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { NamedColorsPalette } from './NamedColorsPalette';
import { ColorSwatch } from './NamedColorsGroup';
import { getColorDefinitionByName } from '../../utils';
import { GrafanaTheme } from '../../types';
describe('NamedColorsPalette', () => {
const BasicGreen = getColorDefinitionByName('green');
describe('theme support for named colors', () => {
let wrapper: ReactWrapper, selectedSwatch;
afterEach(() => {
wrapper.unmount();
});
it('should render provided color variant specific for theme', () => {
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Dark} onChange={() => {}} />);
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
wrapper.unmount();
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Light} onChange={() => {}} />);
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light);
});
it('should render dar variant of provided color when theme not provided', () => {
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} />);
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
});
});
});
@@ -0,0 +1,39 @@
import React from 'react';
import { Color, getNamedColorPalette } from '../../utils/namedColorsPalette';
import { Themeable } from '../../types/index';
import NamedColorsGroup from './NamedColorsGroup';
interface NamedColorsPaletteProps extends Themeable {
color?: Color;
onChange: (colorName: string) => void;
}
export const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPaletteProps) => {
const swatches: JSX.Element[] = [];
getNamedColorPalette().forEach((colors, hue) => {
swatches.push(
<NamedColorsGroup
key={hue}
theme={theme}
selectedColor={color}
colors={colors}
onColorSelect={color => {
onChange(color.name);
}}
/>
);
});
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridRowGap: '24px',
gridColumnGap: '24px',
}}
>
{swatches}
</div>
);
};
@@ -0,0 +1,87 @@
import React, { FunctionComponent } from 'react';
import { ColorPickerPopover } from './ColorPickerPopover';
import { ColorPickerProps } from './ColorPicker';
import { PopperContentProps } from '../Tooltip/PopperController';
import { Switch } from '../Switch/Switch';
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperContentProps {
yaxis?: number;
onToggleAxis?: () => void;
}
export const SeriesColorPickerPopover: FunctionComponent<SeriesColorPickerPopoverProps> = props => {
const { yaxis, onToggleAxis, color, ...colorPickerProps } = props;
return (
<ColorPickerPopover
{...colorPickerProps}
color={color || '#000000'}
customPickers={{
yaxis: {
name: 'Y-Axis',
tabComponent: () => (
<Switch
key="yaxisSwitch"
label="Use right y-axis"
className="ColorPicker__axisSwitch"
labelClass="ColorPicker__axisSwitchLabel"
checked={yaxis === 2}
onChange={() => {
if (onToggleAxis) {
onToggleAxis();
}
}}
/>
),
},
}}
/>
);
};
interface AxisSelectorProps {
yaxis: number;
onToggleAxis?: () => void;
}
interface AxisSelectorState {
yaxis: number;
}
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
constructor(props: AxisSelectorProps) {
super(props);
this.state = {
yaxis: this.props.yaxis,
};
this.onToggleAxis = this.onToggleAxis.bind(this);
}
onToggleAxis() {
this.setState({
yaxis: this.state.yaxis === 2 ? 1 : 2,
});
if (this.props.onToggleAxis) {
this.props.onToggleAxis();
}
}
render() {
const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse';
return (
<div className="p-b-1">
<label className="small p-r-1">Y Axis:</label>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
Left
</button>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
Right
</button>
</div>
);
}
}
@@ -0,0 +1,23 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
import SpectrumPalette from './SpectrumPalette';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
import { getThemeKnob } from '../../utils/storybook/themeKnob';
const SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalette', module);
SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
SpectrumPaletteStories.add('default', () => {
const selectedTheme = getThemeKnob();
return (
<UseState initialState="red">
{(selectedColor, updateSelectedColor) => {
return <SpectrumPalette theme={selectedTheme} color={selectedColor} onChange={updateSelectedColor} />;
}}
</UseState>
);
});
@@ -0,0 +1,100 @@
import React from 'react';
import { CustomPicker, ColorResult } from 'react-color';
import { Saturation, Hue, Alpha } from 'react-color/lib/components/common';
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
import tinycolor from 'tinycolor2';
import ColorInput from './ColorInput';
import { Themeable, GrafanaTheme } from '../../types';
import SpectrumPalettePointer, { SpectrumPalettePointerProps } from './SpectrumPalettePointer';
export interface SpectrumPaletteProps extends Themeable {
color: string;
onChange: (color: string) => void;
}
const renderPointer = (theme?: GrafanaTheme) => (props: SpectrumPalettePointerProps) => (
<SpectrumPalettePointer {...props} theme={theme} />
);
// @ts-ignore
const SpectrumPicker = CustomPicker<Themeable>(({ rgb, hsl, onChange, theme }) => {
return (
<div
style={{
display: 'flex',
width: '100%',
flexDirection: 'column',
}}
>
<div
style={{
display: 'flex',
}}
>
<div
style={{
display: 'flex',
flexGrow: 1,
flexDirection: 'column',
}}
>
<div
style={{
position: 'relative',
height: '100px',
width: '100%',
}}
>
{/*
// @ts-ignore */}
<Saturation onChange={onChange} hsl={hsl} hsv={tinycolor(hsl).toHsv()} />
</div>
<div
style={{
width: '100%',
height: '16px',
marginTop: '16px',
position: 'relative',
background: 'white',
}}
>
{/*
// @ts-ignore */}
<Alpha rgb={rgb} hsl={hsl} a={rgb.a} onChange={onChange} pointer={renderPointer(theme)} />
</div>
</div>
<div
style={{
position: 'relative',
width: '16px',
height: '100px',
marginLeft: '16px',
}}
>
{/*
// @ts-ignore */}
<Hue onChange={onChange} hsl={hsl} direction="vertical" pointer={renderPointer(theme)} />
</div>
</div>
</div>
);
});
const SpectrumPalette: React.FunctionComponent<SpectrumPaletteProps> = ({ color, onChange, theme }) => {
return (
<div>
<SpectrumPicker
color={tinycolor(getColorFromHexRgbOrName(color)).toRgb()}
onChange={(a: ColorResult) => {
onChange(tinycolor(a.rgb).toString());
}}
theme={theme}
/>
<ColorInput color={color} onChange={onChange} style={{ marginTop: '16px' }} />
</div>
);
};
export default SpectrumPalette;
@@ -0,0 +1,80 @@
import React from 'react';
import { GrafanaTheme, Themeable } from '../../types';
export interface SpectrumPalettePointerProps extends Themeable {
direction?: string;
}
const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProps> = ({
theme,
direction,
}) => {
const styles = {
picker: {
width: '16px',
height: '16px',
transform: direction === 'vertical' ? 'translate(0, -8px)' : 'translate(-8px, 0)',
},
};
const pointerColor = theme === GrafanaTheme.Light ? '#3F444D' : '#8E8E8E';
let pointerStyles: React.CSSProperties = {
position: 'absolute',
left: '6px',
width: '0',
height: '0',
borderStyle: 'solid',
background: 'none',
};
let topArrowStyles: React.CSSProperties = {
top: '-7px',
borderWidth: '6px 3px 0px 3px',
borderColor: `${pointerColor} transparent transparent transparent`,
};
let bottomArrowStyles: React.CSSProperties = {
bottom: '-7px',
borderWidth: '0px 3px 6px 3px',
borderColor: ` transparent transparent ${pointerColor} transparent`,
};
if (direction === 'vertical') {
pointerStyles = {
...pointerStyles,
left: 'auto',
};
topArrowStyles = {
borderWidth: '3px 0px 3px 6px',
borderColor: `transparent transparent transparent ${pointerColor}`,
left: '-7px',
top: '7px',
};
bottomArrowStyles = {
borderWidth: '3px 6px 3px 0px',
borderColor: `transparent ${pointerColor} transparent transparent`,
right: '-7px',
top: '7px',
};
}
return (
<div style={styles.picker}>
<div
style={{
...pointerStyles,
...topArrowStyles,
}}
/>
<div
style={{
...pointerStyles,
...bottomArrowStyles,
}}
/>
</div>
);
};
export default SpectrumPalettePointer;
@@ -0,0 +1,240 @@
$arrowSize: 15px;
.ColorPicker {
@extend .popper;
font-size: 12px;
}
.ColorPicker__arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 0px;
&[data-placement^='top'] {
border-width: $arrowSize $arrowSize 0 $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
bottom: -$arrowSize;
left: calc(50%-#{$arrowSize});
padding-top: $arrowSize;
}
&[data-placement^='bottom'] {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(50%-#{$arrowSize});
}
&[data-placement^='bottom-start'] {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: $arrowSize;
}
&[data-placement^='bottom-end'] & {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(100% -$arrowSize);
}
&[data-placement^='right'] {
border-width: $arrowSize $arrowSize $arrowSize 0;
border-left-color: transparent;
border-top-color: transparent;
border-bottom-color: transparent;
left: 0;
top: calc(50%-#{$arrowSize});
}
&[data-placement^='left'] {
border-width: $arrowSize 0 $arrowSize $arrowSize;
border-top-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
right: -$arrowSize;
top: calc(50%-#{$arrowSize});
}
}
.ColorPicker__arrow--light {
border-color: #ffffff;
}
.ColorPicker__arrow--dark {
border-color: #1e2028;
}
// Top
.ColorPicker[data-placement^='top'] {
padding-bottom: $arrowSize;
}
// Bottom
.ColorPicker[data-placement^='bottom'] {
padding-top: $arrowSize;
}
.ColorPicker[data-placement^='bottom-start'] {
padding-top: $arrowSize;
}
.ColorPicker[data-placement^='bottom-end'] {
padding-top: $arrowSize;
}
// Right
.ColorPicker[data-placement^='right'] {
padding-left: $arrowSize;
}
// Left
.ColorPicker[data-placement^='left'] {
padding-right: $arrowSize;
}
.ColorPickerPopover {
border-radius: 3px;
}
.ColorPickerPopover--light {
color: black;
background: linear-gradient(180deg, #ffffff 0%, #f7f8fa 104.25%);
box-shadow: 0px 2px 4px #dde4ed, 0px 0px 2px #dde4ed;
}
.ColorPickerPopover--dark {
color: #d8d9da;
background: linear-gradient(180deg, #1e2028 0%, #161719 104.25%);
box-shadow: 0px 2px 4px #000000, 0px 0px 2px #000000;
.ColorPickerPopover__tab {
background: #303133;
color: white;
cursor: pointer;
}
.ColorPickerPopover__tab--active {
background: none;
}
}
.ColorPickerPopover__content {
width: 336px;
min-height: 184px;
padding: 24px;
}
.ColorPickerPopover__tabs {
display: flex;
width: 100%;
border-radius: 3px 3px 0 0;
overflow: hidden;
}
.ColorPickerPopover__tab {
width: 50%;
text-align: center;
padding: 8px 0;
background: #dde4ed;
}
.ColorPickerPopover__tab--active {
background: white;
}
.ColorPicker__axisSwitch {
width: 100%;
}
.ColorPicker__axisSwitchLabel {
display: flex;
flex-grow: 1;
}
.sp-replacer {
background: inherit;
border: none;
color: inherit;
padding: 0;
border-radius: 10px;
}
.sp-replacer:hover,
.sp-replacer.sp-active {
border-color: inherit;
color: inherit;
}
.sp-container {
border-radius: 0;
background-color: $dropdownBackground;
border: none;
padding: 0;
}
.sp-palette-container,
.sp-picker-container {
border: none;
}
.sp-dd {
display: none;
}
.sp-preview {
position: relative;
width: 15px;
height: 15px;
border: none;
margin: 0;
float: left;
z-index: 0;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
}
.sp-preview-inner,
.sp-alpha-inner,
.sp-thumb-inner {
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.gf-color-picker__body {
padding-bottom: $arrowSize;
padding-left: 6px;
}
.drop-popover.gf-color-picker {
.drop-content {
width: 210px;
}
}
// TODO: Remove. This is a temporary solution until color picker popovers are used
// with Drop.js.
.drop-popover.drop-popover--transparent {
.drop-content {
border: none;
background: none;
padding: 0;
max-width: none;
&:before {
display: none;
}
}
}
@@ -0,0 +1,100 @@
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;
renderTrackHorizontal?: React.FunctionComponent<any>;
renderTrackVertical?: React.FunctionComponent<any>;
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,
renderTrackHorizontal,
renderTrackVertical,
} = 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={renderTrackHorizontal || (props => <div {...props} className="track-horizontal" />)}
renderTrackVertical={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, { FunctionComponent } from 'react';
import { storiesOf } from '@storybook/react';
import { DeleteButton } from '@grafana/ui';
const CenteredStory: FunctionComponent<{}> = ({ children }) => {
return (
<div
style={{
height: '100vh ',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{children}
</div>
);
};
storiesOf('UI/DeleteButton', module)
.addDecorator(story => <CenteredStory>{story()}</CenteredStory>)
.add('default', () => {
return <DeleteButton onConfirm={() => {}} />;
});
@@ -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();
});
});
@@ -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 };
@@ -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>
`;
@@ -0,0 +1,42 @@
import React, { FunctionComponent, ReactNode } from 'react';
import classNames from 'classnames';
import { Tooltip } from '../Tooltip/Tooltip';
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="top" content={tooltip} theme={"info"}>
<div className="gf-form-help-icon gf-form-help-icon--right-normal">
<i className="fa fa-info-circle" />
</div>
</Tooltip>
)}
</label>
);
};
@@ -0,0 +1,147 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Gauge, Props } from './Gauge';
import { TimeSeriesVMs } from '../../types/data';
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');
});
});
@@ -0,0 +1,220 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { ValueMapping, Threshold, BasicGaugeColor, TimeSeriesVMs, GrafanaTheme } from '../../types';
import { getMappedValue } from '../../utils/valueMappings';
import { getColorFromHexRgbOrName, getValueFormat } from '../../utils';
type TimeSeriesValue = string | number | null;
export interface Props {
decimals: number;
height: number;
valueMappings: ValueMapping[];
maxValue: number;
minValue: number;
prefix: string;
timeSeries: TimeSeriesVMs;
thresholds: Threshold[];
showThresholdMarkers: boolean;
showThresholdLabels: boolean;
stat: string;
suffix: string;
unit: string;
width: number;
theme?: GrafanaTheme;
}
const FONT_SCALE = 1;
export class Gauge extends PureComponent<Props> {
canvasElement: any;
static defaultProps = {
maxValue: 100,
valueMappings: [],
minValue: 0,
prefix: '',
showThresholdMarkers: true,
showThresholdLabels: false,
suffix: '',
thresholds: [],
unit: 'none',
stat: 'avg',
theme: GrafanaTheme.Dark,
};
componentDidMount() {
this.draw();
}
componentDidUpdate() {
this.draw();
}
formatValue(value: TimeSeriesValue) {
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
if (isNaN(value as number)) {
return value;
}
if (valueMappings.length > 0) {
const valueMappedValue = getMappedValue(valueMappings, value);
if (valueMappedValue) {
return `${prefix && prefix + ' '}${valueMappedValue.text}${suffix && ' ' + suffix}`;
}
}
const formatFunc = getValueFormat(unit);
const formattedValue = formatFunc(value as number, decimals);
const handleNoValueValue = formattedValue || 'no value';
return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
}
getFontColor(value: TimeSeriesValue) {
const { thresholds, theme } = this.props;
if (thresholds.length === 1) {
return getColorFromHexRgbOrName(thresholds[0].color, theme);
}
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
if (atThreshold) {
return getColorFromHexRgbOrName(atThreshold.color, theme);
}
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 getColorFromHexRgbOrName(nearestThreshold.color, theme);
}
return BasicGaugeColor.Red;
}
getFormattedThresholds() {
const { maxValue, minValue, thresholds, theme } = this.props;
const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
return [
...thresholdsSortedByIndex.map(threshold => {
if (threshold.index === 0) {
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme) };
}
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme) };
}),
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme) },
];
}
getFontScale(length: number): number {
if (length > 12) {
return FONT_SCALE - length * 5 / 120;
}
return FONT_SCALE - length * 5 / 105;
}
draw() {
const {
maxValue,
minValue,
timeSeries,
showThresholdLabels,
showThresholdMarkers,
width,
height,
stat,
theme,
} = this.props;
let value: TimeSeriesValue = '';
if (timeSeries[0]) {
value = timeSeries[0].stats[stat];
} else {
value = null;
}
const formattedValue = this.formatValue(value) as string;
const dimension = Math.min(width, height * 1.3);
const backgroundColor = theme === GrafanaTheme.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
const thresholdMarkersWidth = gaugeWidth / 5;
const fontSize =
Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1);
const thresholdLabelFontSize = fontSize / 2.5;
const options = {
series: {
gauges: {
gauge: {
min: minValue,
max: maxValue,
background: { color: backgroundColor },
border: { color: null },
shadow: { show: false },
width: gaugeWidth,
},
frame: { show: false },
label: { show: false },
layout: { margin: 0, thresholdWidth: 0 },
cell: { border: { width: 0 } },
threshold: {
values: this.getFormattedThresholds(),
label: {
show: showThresholdLabels,
margin: thresholdMarkersWidth + 1,
font: { size: thresholdLabelFontSize },
},
show: showThresholdMarkers,
width: thresholdMarkersWidth,
},
value: {
color: this.getFontColor(value),
formatter: () => {
return formattedValue;
},
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
},
show: true,
},
},
};
const plotSeries = { data: [[0, value]] };
try {
$.plot(this.canvasElement, [plotSeries], options);
} catch (err) {
console.log('Gauge rendering error', err, options, timeSeries);
}
}
render() {
const { height, width } = this.props;
return (
<div className="singlestat-panel">
<div
style={{
height: `${height * 0.9}px`,
width: `${Math.min(width, height * 1.3)}px`,
top: '10px',
margin: 'auto',
}}
ref={element => (this.canvasElement = element)}
/>
</div>
);
}
}
export default Gauge;
@@ -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,28 @@
.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;
border-radius: $border-radius $border-radius 0 0;
.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,13 @@
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 = {
// @ts-ignore
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 +20,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: () => ({}),
};
}
@@ -4,10 +4,11 @@ import _ from 'lodash';
export interface Props {
label: string;
checked: boolean;
className?: string;
labelClass?: string;
switchClass?: string;
transparent?: boolean;
onChange: (event) => any;
onChange: (event?: React.SyntheticEvent<HTMLInputElement>) => void;
}
export interface State {
@@ -19,20 +20,21 @@ export class Switch extends PureComponent<Props, State> {
id: _.uniqueId(),
};
internalOnChange = event => {
internalOnChange = (event: React.FormEvent<HTMLInputElement>) => {
event.stopPropagation();
this.props.onChange(event);
};
render() {
const { labelClass = '', switchClass = '', label, checked, transparent } = this.props;
const { labelClass = '', switchClass = '', label, checked, transparent, className } = this.props;
const labelId = `check-${this.state.id}`;
const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`;
const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`;
return (
<label htmlFor={labelId} className="gf-form gf-form-switch-container">
<label htmlFor={labelId} className={`gf-form gf-form-switch-container ${className}`}>
{label && <div className={labelClassName}>{label}</div>}
<div className={switchClassName}>
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
@@ -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' },
]);
});
});

Some files were not shown because too many files have changed in this diff Show More