mirror of
https://github.com/grafana/grafana.git
synced 2025-12-20 19:44:55 +08:00
Compare commits
54 Commits
sriram/pos
...
v6.4.x
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62b53c3eb7 | ||
|
|
4c305bf2e2 | ||
|
|
092e51408a | ||
|
|
3a2bfb7e38 | ||
|
|
80431256ca | ||
|
|
af364fb91d | ||
|
|
90fe6c5a9f | ||
|
|
399dd583da | ||
|
|
475a0baee1 | ||
|
|
f3e7f878d6 | ||
|
|
a364a86855 | ||
|
|
223f30c71f | ||
|
|
f53c6ebb65 | ||
|
|
bcbe45a745 | ||
|
|
1e32d7bced | ||
|
|
fed38ea617 | ||
|
|
e40dd982c6 | ||
|
|
443a0ba78e | ||
|
|
21bbb7530c | ||
|
|
3478088482 | ||
|
|
d17fb21ecc | ||
|
|
d94eaea64e | ||
|
|
45971205b0 | ||
|
|
3d95eea6ba | ||
|
|
54a092e0a1 | ||
|
|
4437f8af26 | ||
|
|
35213f192c | ||
|
|
1006650ae4 | ||
|
|
f1225330e2 | ||
|
|
4f888d9660 | ||
|
|
969c60e87c | ||
|
|
931dd93d91 | ||
|
|
34a172e133 | ||
|
|
9b764e3a20 | ||
|
|
23aa9b6e45 | ||
|
|
4ba8388f3a | ||
|
|
c3b3ad4380 | ||
|
|
bef64b046c | ||
|
|
4edafb7c8c | ||
|
|
3c0268d671 | ||
|
|
126296826b | ||
|
|
dd75bb67bb | ||
|
|
c31f39ca11 | ||
|
|
e17af53428 | ||
|
|
4d1617c1dd | ||
|
|
3cb8b896dd | ||
|
|
943f661a75 | ||
|
|
b2c1473e59 | ||
|
|
052ea8f63b | ||
|
|
38e88083a3 | ||
|
|
6232cfcdda | ||
|
|
aa7659d1dd | ||
|
|
199031a6e2 | ||
|
|
10d47ab095 |
@@ -19,7 +19,7 @@ version: 2
|
||||
jobs:
|
||||
mysql-integration-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.9
|
||||
- image: circleci/golang:1.12.10
|
||||
- image: circleci/mysql:5.6-ram
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
postgres-integration-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.9
|
||||
- image: circleci/golang:1.12.10
|
||||
- image: circleci/postgres:9.3-ram
|
||||
environment:
|
||||
POSTGRES_USER: grafanatest
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
cache-server-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.9
|
||||
- image: circleci/golang:1.12.10
|
||||
- image: circleci/redis:4-alpine
|
||||
- image: memcached
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
|
||||
lint-go:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.9
|
||||
- image: circleci/golang:1.12.10
|
||||
environment:
|
||||
# we need CGO because of go-sqlite3
|
||||
CGO_ENABLED: 1
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
|
||||
test-backend:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.9
|
||||
- image: circleci/golang:1.12.10
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
|
||||
build-all:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.8
|
||||
- image: grafana/build-container:1.2.9
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -214,15 +214,15 @@ jobs:
|
||||
- run:
|
||||
name: build and package grafana
|
||||
command: './scripts/build/build-all.sh'
|
||||
- run:
|
||||
name: Prepare GPG private key
|
||||
command: './scripts/build/prepare_signing_key.sh'
|
||||
- run:
|
||||
name: sign packages
|
||||
command: './scripts/build/sign_packages.sh'
|
||||
command: './scripts/build/sign_packages.sh dist/*.rpm'
|
||||
- run:
|
||||
name: verify signed packages
|
||||
command: |
|
||||
mkdir -p ~/.rpmdb/pubkeys
|
||||
curl -s https://packages.grafana.com/gpg.key > ~/.rpmdb/pubkeys/grafana.key
|
||||
./scripts/build/verify_signed_packages.sh dist/*.rpm
|
||||
command: './scripts/build/verify_signed_packages.sh dist/*.rpm'
|
||||
- run:
|
||||
name: sha-sum packages
|
||||
command: 'go run build.go sha-dist'
|
||||
@@ -239,7 +239,7 @@ jobs:
|
||||
|
||||
build:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.8
|
||||
- image: grafana/build-container:1.2.9
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -249,9 +249,12 @@ jobs:
|
||||
- run:
|
||||
name: build and package grafana
|
||||
command: './scripts/build/build.sh'
|
||||
- run:
|
||||
name: Prepare GPG private key
|
||||
command: './scripts/build/prepare_signing_key.sh'
|
||||
- run:
|
||||
name: sign packages
|
||||
command: './scripts/build/sign_packages.sh'
|
||||
command: './scripts/build/sign_packages.sh dist/*.rpm'
|
||||
- run:
|
||||
name: sha-sum packages
|
||||
command: 'go run build.go sha-dist'
|
||||
@@ -265,7 +268,7 @@ jobs:
|
||||
|
||||
build-fast-backend:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.8
|
||||
- image: grafana/build-container:1.2.9
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -282,7 +285,7 @@ jobs:
|
||||
|
||||
build-fast-frontend:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.8
|
||||
- image: grafana/build-container:1.2.9
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -306,7 +309,7 @@ jobs:
|
||||
|
||||
build-fast-package:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.8
|
||||
- image: grafana/build-container:1.2.9
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -333,7 +336,7 @@ jobs:
|
||||
|
||||
build-fast-save:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.8
|
||||
- image: grafana/build-container:1.2.9
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -360,9 +363,12 @@ jobs:
|
||||
- run:
|
||||
name: package grafana
|
||||
command: './scripts/build/build.sh --fast --package-only'
|
||||
- run:
|
||||
name: Prepare GPG private key
|
||||
command: './scripts/build/prepare_signing_key.sh'
|
||||
- run:
|
||||
name: sign packages
|
||||
command: './scripts/build/sign_packages.sh'
|
||||
command: './scripts/build/sign_packages.sh dist/*.rpm'
|
||||
- run:
|
||||
name: sha-sum packages
|
||||
command: 'go run build.go sha-dist'
|
||||
@@ -419,7 +425,7 @@ jobs:
|
||||
|
||||
build-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.8
|
||||
- image: grafana/build-container:1.2.9
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -435,9 +441,12 @@ jobs:
|
||||
- run:
|
||||
name: build and package enterprise
|
||||
command: './scripts/build/build.sh -enterprise'
|
||||
- run:
|
||||
name: Prepare GPG private key
|
||||
command: './scripts/build/prepare_signing_key.sh'
|
||||
- run:
|
||||
name: sign packages
|
||||
command: './scripts/build/sign_packages.sh'
|
||||
command: './scripts/build/sign_packages.sh dist/*.rpm'
|
||||
- run:
|
||||
name: sha-sum packages
|
||||
command: 'go run build.go sha-dist'
|
||||
@@ -451,7 +460,7 @@ jobs:
|
||||
|
||||
build-all-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.8
|
||||
- image: grafana/build-container:1.2.9
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -476,15 +485,15 @@ jobs:
|
||||
- run:
|
||||
name: build and package grafana
|
||||
command: './scripts/build/build-all.sh -enterprise'
|
||||
- run:
|
||||
name: Prepare GPG private key
|
||||
command: './scripts/build/prepare_signing_key.sh'
|
||||
- run:
|
||||
name: sign packages
|
||||
command: './scripts/build/sign_packages.sh'
|
||||
command: './scripts/build/sign_packages.sh dist/*.rpm'
|
||||
- run:
|
||||
name: verify signed packages
|
||||
command: |
|
||||
mkdir -p ~/.rpmdb/pubkeys
|
||||
curl -s https://packages.grafana.com/gpg.key > ~/.rpmdb/pubkeys/grafana.key
|
||||
./scripts/build/verify_signed_packages.sh dist/*.rpm
|
||||
command: './scripts/build/verify_signed_packages.sh dist/*.rpm'
|
||||
- run:
|
||||
name: sha-sum packages
|
||||
command: 'go run build.go sha-dist'
|
||||
@@ -537,15 +546,24 @@ jobs:
|
||||
- run:
|
||||
name: Deploy to Grafana.com
|
||||
command: './scripts/build/publish.sh --enterprise'
|
||||
- run:
|
||||
name: Prepare GPG private key
|
||||
command: './scripts/build/prepare_signing_key.sh'
|
||||
- run:
|
||||
name: Load GPG private key
|
||||
command: './scripts/build/load-signing-key.sh'
|
||||
command: './scripts/build/update_repo/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: Publish Debian repository
|
||||
command: './scripts/build/update_repo/publish-deb.sh "enterprise"'
|
||||
- run:
|
||||
name: Update RPM repository
|
||||
command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"'
|
||||
- run:
|
||||
name: Publish RPM repository
|
||||
command: './scripts/build/update_repo/publish-rpm.sh "enterprise" "$CIRCLE_TAG"'
|
||||
|
||||
|
||||
deploy-master:
|
||||
@@ -591,15 +609,24 @@ jobs:
|
||||
- run:
|
||||
name: Deploy to Grafana.com
|
||||
command: './scripts/build/publish.sh'
|
||||
- run:
|
||||
name: Prepare GPG private key
|
||||
command: './scripts/build/prepare_signing_key.sh'
|
||||
- run:
|
||||
name: Load GPG private key
|
||||
command: './scripts/build/load-signing-key.sh'
|
||||
command: './scripts/build/update_repo/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: Publish Debian repository
|
||||
command: './scripts/build/update_repo/publish-deb.sh "oss"'
|
||||
- run:
|
||||
name: Update RPM repository
|
||||
command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"'
|
||||
- run:
|
||||
name: Publish RPM repository
|
||||
command: './scripts/build/update_repo/publish-rpm.sh "oss" "$CIRCLE_TAG"'
|
||||
|
||||
build-oss-msi:
|
||||
docker:
|
||||
|
||||
@@ -43,7 +43,7 @@ Whether you are contributing or doing code review, first read and understand htt
|
||||
### Low-level checks
|
||||
|
||||
- [ ] The pull request contains a title that explains it. It follows [PR and commit messages guidelines](#Pull-Requests-titles-and-message).
|
||||
- [ ] The pull request contains necessary links to issues.
|
||||
- [ ] The pull request contains necessary links to issues.
|
||||
- [ ] The pull request contains commits with messages that are small and understandable. It follows [PR and commit messages guidelines](#Pull-Requests-titles-and-message).
|
||||
- [ ] The pull request does not contain magic strings or numbers that could be replaced with an `Enum` or `const` instead.
|
||||
|
||||
@@ -59,6 +59,8 @@ Whether you are contributing or doing code review, first read and understand htt
|
||||
- [ ] The pull request does not contain uses of `any` or `{}` without comments describing why.
|
||||
- [ ] The pull request does not contain large React components that could easily be split into several smaller components.
|
||||
- [ ] The pull request does not contain back end calls directly from components, use actions and Redux instead.
|
||||
- [ ] The pull request follows our [styling with Emotion convention](./style_guides/styling.md)
|
||||
> We still use a lot of SASS, but any new CSS work should be using or migrating existing code to Emotion
|
||||
|
||||
#### Redux specific checks (skip if your pull request does not contain Redux changes)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Golang build container
|
||||
FROM golang:1.12.9-alpine
|
||||
FROM golang:1.12.10-alpine
|
||||
|
||||
RUN apk add --no-cache gcc g++
|
||||
|
||||
@@ -62,7 +62,8 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
RUN apk add --no-cache ca-certificates bash
|
||||
RUN apk add --no-cache ca-certificates bash tzdata && \
|
||||
apk add --no-cache --upgrade --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main openssl musl-utils
|
||||
|
||||
COPY conf ./conf
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,10 +38,7 @@ Just add it as a datasource and you are ready to query your log data in [Explore
|
||||
|
||||
## Querying Logs
|
||||
|
||||
Querying and displaying log data from Loki is available via [Explore](/features/explore).
|
||||
Select the Loki data source, and then enter a log query to display your logs.
|
||||
|
||||
> Viewing Loki data in dashboard panels is not supported yet, but is being worked on.
|
||||
Querying and displaying log data from Loki is available via [Explore](/features/explore), and with the [logs panel](/features/panels/logs/) in dashboards. Select the Loki data source, and then enter a log query to display your logs.
|
||||
|
||||
### Log Queries
|
||||
|
||||
|
||||
39
docs/sources/features/panels/logs.md
Normal file
39
docs/sources/features/panels/logs.md
Normal file
@@ -0,0 +1,39 @@
|
||||
+++
|
||||
title = "Logs Panel"
|
||||
keywords = ["grafana", "dashboard", "documentation", "panels", "logs panel"]
|
||||
type = "docs"
|
||||
aliases = ["/reference/logs/"]
|
||||
[menu.docs]
|
||||
name = "Logs"
|
||||
parent = "panels"
|
||||
weight = 2
|
||||
+++
|
||||
|
||||
# Logs Panel
|
||||
|
||||
<img class="screenshot" src="/assets/img/features/logs-panel.png">
|
||||
|
||||
> Logs panel is only available in Grafana v6.4+
|
||||
|
||||
The logs panel shows log lines from datasources that support logs, e.g., Elastic, Influx, and Loki.
|
||||
Typically you would use this panel next to a graph panel to display the log output of a related process.
|
||||
|
||||
## Querying Data
|
||||
|
||||
The logs panel will show the result of queries that are specified in the **Queries** tab.
|
||||
The results of multiple queries will be merged and sorted by time.
|
||||
Note that you can scroll inside the panel in case the datasource returns more lines than can be displayed at any one time.
|
||||
|
||||
### Query Options
|
||||
|
||||
To limit the number of lines rendered, you can use the queries-wide **Max data points** setting. If it is not set, the datasource will usually enforce a limit.
|
||||
|
||||
## Visualization Options
|
||||
|
||||
### Columns
|
||||
|
||||
1. **Time**: Show/hide the time column. This is the timestamp associated with the log line as reported from the datasource.
|
||||
2. **Order**: Set to **Ascending** to show the oldest log lines first.
|
||||
|
||||
|
||||
<div class="clearfix"></div>
|
||||
9
go.mod
9
go.mod
@@ -11,10 +11,9 @@ require (
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
|
||||
github.com/codegangsta/cli v1.20.0
|
||||
github.com/crewjam/saml v0.0.0-20190521120225-344d075952c9
|
||||
github.com/crewjam/saml v0.0.0-20191031171751-c42136edf9b1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/inject v0.0.0-20180706035515-f23751cae28b
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
@@ -38,7 +37,6 @@ require (
|
||||
github.com/hashicorp/go-version v1.1.0
|
||||
github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
|
||||
github.com/jonboulle/clockwork v0.1.0 // indirect
|
||||
github.com/jung-kurt/gofpdf v1.10.1
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||
github.com/klauspost/compress v1.4.1 // indirect
|
||||
@@ -55,10 +53,9 @@ require (
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/robfig/cron/v3 v3.0.0
|
||||
github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 // indirect
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
|
||||
github.com/ua-parser/uap-go v0.0.0-20190826212731-daf92ba38329
|
||||
github.com/uber-go/atomic v1.3.2 // indirect
|
||||
@@ -81,5 +78,5 @@ require (
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/redis.v5 v5.2.9
|
||||
gopkg.in/square/go-jose.v2 v2.3.0
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
gopkg.in/yaml.v2 v2.2.4
|
||||
)
|
||||
|
||||
17
go.sum
17
go.sum
@@ -8,6 +8,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/aws/aws-sdk-go v1.18.5 h1:S6j4o4AoJpq98DRc7wQrQsPZg73NyntGtUj6K6NPnuY=
|
||||
github.com/aws/aws-sdk-go v1.18.5/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/beevik/etree v1.0.1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 h1:wOysYcIdqv3WnvwqFFzrYCFALPED7qkUGaLXu359GSc=
|
||||
@@ -26,12 +27,13 @@ github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkb
|
||||
github.com/couchbase/gomemcached v0.0.0-20190515232915-c4b4ca0eb21d/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
|
||||
github.com/couchbase/goutils v0.0.0-20190315194238-f9d42b11473b/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
|
||||
github.com/couchbaselabs/go-couchbase v0.0.0-20190708161019-23e7ca2ce2b7/go.mod h1:mby/05p8HE5yHEAKiIH/555NoblMs7PtW6NrYshDruc=
|
||||
github.com/crewjam/saml v0.0.0-20190521120225-344d075952c9 h1:+cz/lCIhz+eg8+jC8cWk5LBLbbpH39IKyHliN6GZyUE=
|
||||
github.com/crewjam/saml v0.0.0-20190521120225-344d075952c9/go.mod h1:w5eu+HNtubx+kRpQL6QFT2F3yIFfYVe6+EzOFVU7Hko=
|
||||
github.com/crewjam/saml v0.0.0-20191031171751-c42136edf9b1 h1:PKeiHI5SxrkdEtI8FVdk1ubBl2wjnOmHQf5D4ZJOKFE=
|
||||
github.com/crewjam/saml v0.0.0-20191031171751-c42136edf9b1/go.mod h1:pzACCdpqjQKTvpPZs5P3FzFNQ+RSOJX5StwHwh7ZUgw=
|
||||
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853 h1:tTngnoO/B6HQnJ+pK8tN7kEAhmhIfaJOutqq/A4/JTM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20190315220205-a8ed825ac853/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
|
||||
@@ -229,6 +231,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
|
||||
@@ -250,6 +254,7 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3Ifn
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/zenazn/goji v0.9.1-0.20160507202103-64eb34159fe5/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||
@@ -260,6 +265,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -291,6 +298,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M=
|
||||
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
@@ -319,6 +328,8 @@ gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfz
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
|
||||
@@ -340,3 +351,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "6.4.0-pre"
|
||||
"version": "6.4.5"
|
||||
}
|
||||
|
||||
14
package.json
14
package.json
@@ -3,7 +3,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"name": "grafana",
|
||||
"version": "6.4.0-pre",
|
||||
"version": "6.4.5",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@@ -52,7 +52,9 @@
|
||||
"@types/redux-logger": "3.0.7",
|
||||
"@types/redux-mock-store": "1.0.1",
|
||||
"@types/reselect": "2.2.0",
|
||||
"@types/slate": "0.44.11",
|
||||
"@types/slate": "0.47.1",
|
||||
"@types/slate-plain-serializer": "0.6.1",
|
||||
"@types/slate-react": "0.22.5",
|
||||
"@types/tinycolor2": "1.4.2",
|
||||
"angular-mocks": "1.6.6",
|
||||
"autoprefixer": "9.5.0",
|
||||
@@ -121,6 +123,7 @@
|
||||
"redux-mock-store": "1.5.3",
|
||||
"regexp-replace-loader": "1.0.1",
|
||||
"rimraf": "2.6.3",
|
||||
"rxjs-spy": "^7.5.1",
|
||||
"sass-lint": "1.12.1",
|
||||
"sass-loader": "7.1.0",
|
||||
"sinon": "1.17.6",
|
||||
@@ -193,6 +196,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "7.2.5",
|
||||
"@grafana/slate-react": "0.22.9-grafana",
|
||||
"@torkelo/react-select": "2.4.1",
|
||||
"angular": "1.6.6",
|
||||
"angular-bindonce": "0.3.1",
|
||||
@@ -243,10 +247,8 @@
|
||||
"rst2html": "github:thoward/rst2html#990cb89",
|
||||
"rxjs": "6.4.0",
|
||||
"search-query-parser": "1.5.2",
|
||||
"slate": "0.33.8",
|
||||
"slate-plain-serializer": "0.5.41",
|
||||
"slate-prism": "0.5.0",
|
||||
"slate-react": "0.12.11",
|
||||
"slate": "0.47.8",
|
||||
"slate-plain-serializer": "0.7.10",
|
||||
"tether": "1.4.5",
|
||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||
"tinycolor2": "1.4.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/data",
|
||||
"version": "6.4.0-pre",
|
||||
"version": "6.4.5",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('FieldCache', () => {
|
||||
it('should get the first field with a duplicate name', () => {
|
||||
const field = ext.getFieldByName('value');
|
||||
expect(field!.name).toEqual('value');
|
||||
expect(field!.values.toJSON()).toEqual([1, 2, 3]);
|
||||
expect(field!.values.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return index of the field', () => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
isDataFrame,
|
||||
toLegacyResponseData,
|
||||
isTableData,
|
||||
toDataFrame,
|
||||
guessFieldTypes,
|
||||
guessFieldTypeFromValue,
|
||||
guessFieldTypes,
|
||||
isDataFrame,
|
||||
isTableData,
|
||||
sortDataFrame,
|
||||
toDataFrame,
|
||||
toLegacyResponseData,
|
||||
} from './processDataFrame';
|
||||
import { FieldType, TimeSeries, TableData, DataFrameDTO } from '../types/index';
|
||||
import { DataFrameDTO, FieldType, TableData, TimeSeries } from '../types/index';
|
||||
import { dateTime } from '../datetime/moment_wrapper';
|
||||
import { MutableDataFrame } from './MutableDataFrame';
|
||||
|
||||
@@ -103,6 +103,34 @@ describe('toDataFrame', () => {
|
||||
expect(norm.fields[2].type).toBe(FieldType.other);
|
||||
expect(norm.fields[3].type).toBe(FieldType.time); // based on name
|
||||
});
|
||||
|
||||
it('converts JSON document data to series', () => {
|
||||
const input1 = {
|
||||
datapoints: [
|
||||
{
|
||||
_id: 'W5rvjW0BKe0cA-E1aHvr',
|
||||
_type: '_doc',
|
||||
_index: 'logs-2019.10.02',
|
||||
'@message': 'Deployed website',
|
||||
'@timestamp': [1570044340458],
|
||||
tags: ['deploy', 'website-01'],
|
||||
description: 'Torkel deployed website',
|
||||
coordinates: { latitude: 12, longitude: 121, level: { depth: 3, coolnes: 'very' } },
|
||||
'unescaped-content': 'breaking <br /> the <br /> row',
|
||||
},
|
||||
],
|
||||
filterable: true,
|
||||
target: 'docs',
|
||||
total: 206,
|
||||
type: 'docs',
|
||||
};
|
||||
const dataFrame = toDataFrame(input1);
|
||||
expect(dataFrame.fields[0].name).toBe(input1.target);
|
||||
|
||||
const v0 = dataFrame.fields[0].values;
|
||||
expect(v0.length).toEqual(1);
|
||||
expect(v0.get(0)).toEqual(input1.datapoints[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SerisData backwards compatibility', () => {
|
||||
@@ -124,11 +152,13 @@ describe('SerisData backwards compatibility', () => {
|
||||
const table = {
|
||||
columns: [],
|
||||
rows: [],
|
||||
type: 'table',
|
||||
};
|
||||
|
||||
const series = toDataFrame(table);
|
||||
const roundtrip = toLegacyResponseData(series) as TableData;
|
||||
expect(roundtrip.columns.length).toBe(0);
|
||||
expect(roundtrip.type).toBe('table');
|
||||
});
|
||||
|
||||
it('converts TableData to series and back again', () => {
|
||||
@@ -176,6 +206,37 @@ describe('SerisData backwards compatibility', () => {
|
||||
const names = table.columns.map(c => c.text);
|
||||
expect(names).toEqual(['T', 'N', 'S']);
|
||||
});
|
||||
|
||||
it('can convert TimeSeries to JSON document and back again', () => {
|
||||
const timeseries = {
|
||||
datapoints: [
|
||||
{
|
||||
_id: 'W5rvjW0BKe0cA-E1aHvr',
|
||||
_type: '_doc',
|
||||
_index: 'logs-2019.10.02',
|
||||
'@message': 'Deployed website',
|
||||
'@timestamp': [1570044340458],
|
||||
tags: ['deploy', 'website-01'],
|
||||
description: 'Torkel deployed website',
|
||||
coordinates: { latitude: 12, longitude: 121, level: { depth: 3, coolnes: 'very' } },
|
||||
'unescaped-content': 'breaking <br /> the <br /> row',
|
||||
},
|
||||
],
|
||||
filterable: true,
|
||||
target: 'docs',
|
||||
total: 206,
|
||||
type: 'docs',
|
||||
};
|
||||
const series = toDataFrame(timeseries);
|
||||
expect(isDataFrame(timeseries)).toBeFalsy();
|
||||
expect(isDataFrame(series)).toBeTruthy();
|
||||
|
||||
const roundtrip = toLegacyResponseData(series) as any;
|
||||
expect(isDataFrame(roundtrip)).toBeFalsy();
|
||||
expect(roundtrip.type).toBe('docs');
|
||||
expect(roundtrip.target).toBe('docs');
|
||||
expect(roundtrip.filterable).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorted DataFrame', () => {
|
||||
@@ -189,14 +250,14 @@ describe('sorted DataFrame', () => {
|
||||
it('Should sort numbers', () => {
|
||||
const sorted = sortDataFrame(frame, 0, true);
|
||||
expect(sorted.length).toEqual(3);
|
||||
expect(sorted.fields[0].values.toJSON()).toEqual([3, 2, 1]);
|
||||
expect(sorted.fields[1].values.toJSON()).toEqual(['c', 'b', 'a']);
|
||||
expect(sorted.fields[0].values.toArray()).toEqual([3, 2, 1]);
|
||||
expect(sorted.fields[1].values.toArray()).toEqual(['c', 'b', 'a']);
|
||||
});
|
||||
|
||||
it('Should sort strings', () => {
|
||||
const sorted = sortDataFrame(frame, 1, true);
|
||||
expect(sorted.length).toEqual(3);
|
||||
expect(sorted.fields[0].values.toJSON()).toEqual([3, 2, 1]);
|
||||
expect(sorted.fields[1].values.toJSON()).toEqual(['c', 'b', 'a']);
|
||||
expect(sorted.fields[0].values.toArray()).toEqual([3, 2, 1]);
|
||||
expect(sorted.fields[1].values.toArray()).toEqual(['c', 'b', 'a']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,6 +127,33 @@ function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame {
|
||||
};
|
||||
}
|
||||
|
||||
function convertJSONDocumentDataToDataFrame(timeSeries: TimeSeries): DataFrame {
|
||||
const fields = [
|
||||
{
|
||||
name: timeSeries.target,
|
||||
type: FieldType.other,
|
||||
config: {
|
||||
unit: timeSeries.unit,
|
||||
filterable: (timeSeries as any).filterable,
|
||||
},
|
||||
values: new ArrayVector(),
|
||||
},
|
||||
];
|
||||
|
||||
for (const point of timeSeries.datapoints) {
|
||||
fields[0].values.buffer.push(point);
|
||||
}
|
||||
|
||||
return {
|
||||
name: timeSeries.target,
|
||||
labels: timeSeries.tags,
|
||||
refId: timeSeries.target,
|
||||
meta: { json: true },
|
||||
fields,
|
||||
length: timeSeries.datapoints.length,
|
||||
};
|
||||
}
|
||||
|
||||
// PapaParse Dynamic Typing regex:
|
||||
// https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998
|
||||
const NUMBER = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i;
|
||||
@@ -241,6 +268,11 @@ export const toDataFrame = (data: any): DataFrame => {
|
||||
return new MutableDataFrame(data as DataFrameDTO);
|
||||
}
|
||||
|
||||
// Handle legacy docs/json type
|
||||
if (data.hasOwnProperty('type') && data.type === 'docs') {
|
||||
return convertJSONDocumentDataToDataFrame(data);
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('datapoints')) {
|
||||
return convertTimeSeriesToDataFrame(data);
|
||||
}
|
||||
@@ -288,6 +320,16 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
|
||||
}
|
||||
}
|
||||
|
||||
if (frame.meta && frame.meta.json) {
|
||||
return {
|
||||
alias: fields[0].name || frame.name,
|
||||
target: fields[0].name || frame.name,
|
||||
datapoints: fields[0].values.toArray(),
|
||||
filterable: fields[0].config ? fields[0].config.filterable : undefined,
|
||||
type: 'docs',
|
||||
} as TimeSeries;
|
||||
}
|
||||
|
||||
return {
|
||||
columns: fields.map(f => {
|
||||
const { name, config } = f;
|
||||
@@ -299,6 +341,7 @@ export const toLegacyResponseData = (frame: DataFrame): TimeSeries | TableData =
|
||||
}
|
||||
return { text: name };
|
||||
}),
|
||||
type: 'table',
|
||||
refId: frame.refId,
|
||||
meta: frame.meta,
|
||||
rows,
|
||||
@@ -401,7 +444,7 @@ export function toDataFrameDTO(data: DataFrame): DataFrameDTO {
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
config: f.config,
|
||||
values: f.values.toJSON(),
|
||||
values: f.values.toArray(),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
import * as dateMath from './datemath';
|
||||
import * as rangeUtil from './rangeutil';
|
||||
export * from './moment_wrapper';
|
||||
export * from './timezones';
|
||||
export { dateMath, rangeUtil };
|
||||
|
||||
390
packages/grafana-data/src/datetime/timezones.ts
Normal file
390
packages/grafana-data/src/datetime/timezones.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
// List taken from https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
|
||||
|
||||
export const getTimeZoneGroups = () => {
|
||||
const europeZones = [
|
||||
'Europe/Amsterdam',
|
||||
'Europe/Andorra',
|
||||
'Europe/Astrakhan',
|
||||
'Europe/Athens',
|
||||
'Europe/Belgrade',
|
||||
'Europe/Berlin',
|
||||
'Europe/Brussels',
|
||||
'Europe/Bucharest',
|
||||
'Europe/Budapest',
|
||||
'Europe/Chisinau',
|
||||
'Europe/Copenhagen',
|
||||
'Europe/Dublin',
|
||||
'Europe/Gibraltar',
|
||||
'Europe/Helsinki',
|
||||
'Europe/Istanbul',
|
||||
'Europe/Kaliningrad',
|
||||
'Europe/Kiev',
|
||||
'Europe/Kirov',
|
||||
'Europe/Lisbon',
|
||||
'Europe/London',
|
||||
'Europe/Luxembourg',
|
||||
'Europe/Madrid',
|
||||
'Europe/Malta',
|
||||
'Europe/Minsk',
|
||||
'Europe/Monaco',
|
||||
'Europe/Moscow',
|
||||
'Europe/Oslo',
|
||||
'Europe/Paris',
|
||||
'Europe/Prague',
|
||||
'Europe/Riga',
|
||||
'Europe/Rome',
|
||||
'Europe/Samara',
|
||||
'Europe/Saratov',
|
||||
'Europe/Simferopol',
|
||||
'Europe/Sofia',
|
||||
'Europe/Stockholm',
|
||||
'Europe/Tallinn',
|
||||
'Europe/Tirane',
|
||||
'Europe/Ulyanovsk',
|
||||
'Europe/Uzhgorod',
|
||||
'Europe/Vienna',
|
||||
'Europe/Vilnius',
|
||||
'Europe/Volgograd',
|
||||
'Europe/Warsaw',
|
||||
'Europe/Zaporozhye',
|
||||
'Europe/Zurich',
|
||||
];
|
||||
|
||||
const africaZones = [
|
||||
'Africa/Abidjan',
|
||||
'Africa/Accra',
|
||||
'Africa/Algiers',
|
||||
'Africa/Bissau',
|
||||
'Africa/Cairo',
|
||||
'Africa/Casablanca',
|
||||
'Africa/Ceuta',
|
||||
'Africa/El_Aaiun',
|
||||
'Africa/Johannesburg',
|
||||
'Africa/Juba',
|
||||
'Africa/Khartoum',
|
||||
'Africa/Lagos',
|
||||
'Africa/Maputo',
|
||||
'Africa/Monrovia',
|
||||
'Africa/Nairobi',
|
||||
'Africa/Ndjamena',
|
||||
'Africa/Sao_Tome',
|
||||
'Africa/Tripoli',
|
||||
'Africa/Tunis',
|
||||
'Africa/Windhoek',
|
||||
];
|
||||
|
||||
const asiaZones = [
|
||||
'Asia/Almaty',
|
||||
'Asia/Amman',
|
||||
'Asia/Anadyr',
|
||||
'Asia/Aqtau',
|
||||
'Asia/Aqtobe',
|
||||
'Asia/Ashgabat',
|
||||
'Asia/Atyrau',
|
||||
'Asia/Baghdad',
|
||||
'Asia/Baku',
|
||||
'Asia/Bangkok',
|
||||
'Asia/Barnaul',
|
||||
'Asia/Beirut',
|
||||
'Asia/Bishkek',
|
||||
'Asia/Brunei',
|
||||
'Asia/Chita',
|
||||
'Asia/Choibalsan',
|
||||
'Asia/Colombo',
|
||||
'Asia/Damascus',
|
||||
'Asia/Dhaka',
|
||||
'Asia/Dili',
|
||||
'Asia/Dubai',
|
||||
'Asia/Dushanbe',
|
||||
'Asia/Famagusta',
|
||||
'Asia/Gaza',
|
||||
'Asia/Hebron',
|
||||
'Asia/Ho_Chi_Minh',
|
||||
'Asia/Hong_Kong',
|
||||
'Asia/Hovd',
|
||||
'Asia/Irkutsk',
|
||||
'Asia/Jakarta',
|
||||
'Asia/Jayapura',
|
||||
'Asia/Jerusalem',
|
||||
'Asia/Kabul',
|
||||
'Asia/Kamchatka',
|
||||
'Asia/Karachi',
|
||||
'Asia/Kathmandu',
|
||||
'Asia/Khandyga',
|
||||
'Asia/Kolkata',
|
||||
'Asia/Krasnoyarsk',
|
||||
'Asia/Kuala_Lumpur',
|
||||
'Asia/Kuching',
|
||||
'Asia/Macau',
|
||||
'Asia/Magadan',
|
||||
'Asia/Makassar',
|
||||
'Asia/Manila',
|
||||
'Asia/Nicosia',
|
||||
'Asia/Novokuznetsk',
|
||||
'Asia/Novosibirsk',
|
||||
'Asia/Omsk',
|
||||
'Asia/Oral',
|
||||
'Asia/Pontianak',
|
||||
'Asia/Pyongyang',
|
||||
'Asia/Qatar',
|
||||
'Asia/Qostanay',
|
||||
'Asia/Qyzylorda',
|
||||
'Asia/Riyadh',
|
||||
'Asia/Sakhalin',
|
||||
'Asia/Samarkand',
|
||||
'Asia/Seoul',
|
||||
'Asia/Shanghai',
|
||||
'Asia/Singapore',
|
||||
'Asia/Srednekolymsk',
|
||||
'Asia/Taipei',
|
||||
'Asia/Tashkent',
|
||||
'Asia/Tbilisi',
|
||||
'Asia/Tehran',
|
||||
'Asia/Thimphu',
|
||||
'Asia/Tokyo',
|
||||
'Asia/Tomsk',
|
||||
'Asia/Ulaanbaatar',
|
||||
'Asia/Urumqi',
|
||||
'Asia/Ust-Nera',
|
||||
'Asia/Vladivostok',
|
||||
'Asia/Yakutsk',
|
||||
'Asia/Yangon',
|
||||
'Asia/Yekaterinburg',
|
||||
'Asia/Yerevan',
|
||||
];
|
||||
|
||||
const antarcticaZones = [
|
||||
'Antarctica/Casey',
|
||||
'Antarctica/Davis',
|
||||
'Antarctica/DumontDUrville',
|
||||
'Antarctica/Macquarie',
|
||||
'Antarctica/Mawson',
|
||||
'Antarctica/Palmer',
|
||||
'Antarctica/Rothera',
|
||||
'Antarctica/Syowa',
|
||||
'Antarctica/Troll',
|
||||
'Antarctica/Vostok',
|
||||
];
|
||||
|
||||
const americaZones = [
|
||||
'America/Adak',
|
||||
'America/Anchorage',
|
||||
'America/Araguaina',
|
||||
'America/Argentina/Buenos_Aires',
|
||||
'America/Argentina/Catamarca',
|
||||
'America/Argentina/Cordoba',
|
||||
'America/Argentina/Jujuy',
|
||||
'America/Argentina/La_Rioja',
|
||||
'America/Argentina/Mendoza',
|
||||
'America/Argentina/Rio_Gallegos',
|
||||
'America/Argentina/Salta',
|
||||
'America/Argentina/San_Juan',
|
||||
'America/Argentina/San_Luis',
|
||||
'America/Argentina/Tucuman',
|
||||
'America/Argentina/Ushuaia',
|
||||
'America/Asuncion',
|
||||
'America/Atikokan',
|
||||
'America/Bahia',
|
||||
'America/Bahia_Banderas',
|
||||
'America/Barbados',
|
||||
'America/Belem',
|
||||
'America/Belize',
|
||||
'America/Blanc-Sablon',
|
||||
'America/Boa_Vista',
|
||||
'America/Bogota',
|
||||
'America/Boise',
|
||||
'America/Cambridge_Bay',
|
||||
'America/Campo_Grande',
|
||||
'America/Cancun',
|
||||
'America/Caracas',
|
||||
'America/Cayenne',
|
||||
'America/Chicago',
|
||||
'America/Chihuahua',
|
||||
'America/Costa_Rica',
|
||||
'America/Creston',
|
||||
'America/Cuiaba',
|
||||
'America/Curacao',
|
||||
'America/Danmarkshavn',
|
||||
'America/Dawson',
|
||||
'America/Dawson_Creek',
|
||||
'America/Denver',
|
||||
'America/Detroit',
|
||||
'America/Edmonton',
|
||||
'America/Eirunepe',
|
||||
'America/El_Salvador',
|
||||
'America/Fort_Nelson',
|
||||
'America/Fortaleza',
|
||||
'America/Glace_Bay',
|
||||
'America/Godthab',
|
||||
'America/Goose_Bay',
|
||||
'America/Grand_Turk',
|
||||
'America/Guatemala',
|
||||
'America/Guayaquil',
|
||||
'America/Guyana',
|
||||
'America/Halifax',
|
||||
'America/Havana',
|
||||
'America/Hermosillo',
|
||||
'America/Indiana/Indianapolis',
|
||||
'America/Indiana/Knox',
|
||||
'America/Indiana/Marengo',
|
||||
'America/Indiana/Petersburg',
|
||||
'America/Indiana/Tell_City',
|
||||
'America/Indiana/Vevay',
|
||||
'America/Indiana/Vincennes',
|
||||
'America/Indiana/Winamac',
|
||||
'America/Inuvik',
|
||||
'America/Iqaluit',
|
||||
'America/Jamaica',
|
||||
'America/Juneau',
|
||||
'America/Kentucky/Louisville',
|
||||
'America/Kentucky/Monticello',
|
||||
'America/La_Paz',
|
||||
'America/Lima',
|
||||
'America/Los_Angeles',
|
||||
'America/Maceio',
|
||||
'America/Managua',
|
||||
'America/Manaus',
|
||||
'America/Martinique',
|
||||
'America/Matamoros',
|
||||
'America/Mazatlan',
|
||||
'America/Menominee',
|
||||
'America/Merida',
|
||||
'America/Metlakatla',
|
||||
'America/Mexico_City',
|
||||
'America/Miquelon',
|
||||
'America/Moncton',
|
||||
'America/Monterrey',
|
||||
'America/Montevideo',
|
||||
'America/Nassau',
|
||||
'America/New_York',
|
||||
'America/Nipigon',
|
||||
'America/Nome',
|
||||
'America/Noronha',
|
||||
'America/North_Dakota/Beulah',
|
||||
'America/North_Dakota/Center',
|
||||
'America/North_Dakota/New_Salem',
|
||||
'America/Ojinaga',
|
||||
'America/Panama',
|
||||
'America/Pangnirtung',
|
||||
'America/Paramaribo',
|
||||
'America/Phoenix',
|
||||
'America/Port-au-Prince',
|
||||
'America/Port_of_Spain',
|
||||
'America/Porto_Velho',
|
||||
'America/Puerto_Rico',
|
||||
'America/Punta_Arenas',
|
||||
'America/Rainy_River',
|
||||
'America/Rankin_Inlet',
|
||||
'America/Recife',
|
||||
'America/Regina',
|
||||
'America/Resolute',
|
||||
'America/Rio_Branco',
|
||||
'America/Santarem',
|
||||
'America/Santiago',
|
||||
'America/Santo_Domingo',
|
||||
'America/Sao_Paulo',
|
||||
'America/Scoresbysund',
|
||||
'America/Sitka',
|
||||
'America/St_Johns',
|
||||
'America/Swift_Current',
|
||||
'America/Tegucigalpa',
|
||||
'America/Thule',
|
||||
'America/Thunder_Bay',
|
||||
'America/Tijuana',
|
||||
'America/Toronto',
|
||||
'America/Vancouver',
|
||||
'America/Whitehorse',
|
||||
'America/Winnipeg',
|
||||
'America/Yakutat',
|
||||
'America/Yellowknife',
|
||||
];
|
||||
|
||||
const pacificZones = [
|
||||
'Pacific/Apia',
|
||||
'Pacific/Auckland',
|
||||
'Pacific/Bougainville',
|
||||
'Pacific/Chatham',
|
||||
'Pacific/Chuuk',
|
||||
'Pacific/Easter',
|
||||
'Pacific/Efate',
|
||||
'Pacific/Enderbury',
|
||||
'Pacific/Fakaofo',
|
||||
'Pacific/Fiji',
|
||||
'Pacific/Funafuti',
|
||||
'Pacific/Galapagos',
|
||||
'Pacific/Gambier',
|
||||
'Pacific/Guadalcanal',
|
||||
'Pacific/Guam',
|
||||
'Pacific/Honolulu',
|
||||
'Pacific/Kiritimati',
|
||||
'Pacific/Kosrae',
|
||||
'Pacific/Kwajalein',
|
||||
'Pacific/Majuro',
|
||||
'Pacific/Marquesas',
|
||||
'Pacific/Nauru',
|
||||
'Pacific/Niue',
|
||||
'Pacific/Norfolk',
|
||||
'Pacific/Noumea',
|
||||
'Pacific/Pago_Pago',
|
||||
'Pacific/Palau',
|
||||
'Pacific/Pitcairn',
|
||||
'Pacific/Pohnpei',
|
||||
'Pacific/Port_Moresby',
|
||||
'Pacific/Rarotonga',
|
||||
'Pacific/Tahiti',
|
||||
'Pacific/Tarawa',
|
||||
'Pacific/Tongatapu',
|
||||
'Pacific/Wake',
|
||||
'Pacific/Wallis',
|
||||
];
|
||||
|
||||
const australiaZones = [
|
||||
'Australia/Adelaide',
|
||||
'Australia/Brisbane',
|
||||
'Australia/Broken_Hill',
|
||||
'Australia/Currie',
|
||||
'Australia/Darwin',
|
||||
'Australia/Eucla',
|
||||
'Australia/Hobart',
|
||||
'Australia/Lindeman',
|
||||
'Australia/Lord_Howe',
|
||||
'Australia/Melbourne',
|
||||
'Australia/Perth',
|
||||
'Australia/Sydney',
|
||||
];
|
||||
|
||||
const atlanticZones = [
|
||||
'Atlantic/Azores',
|
||||
'Atlantic/Bermuda',
|
||||
'Atlantic/Canary',
|
||||
'Atlantic/Cape_Verde',
|
||||
'Atlantic/Faroe',
|
||||
'Atlantic/Madeira',
|
||||
'Atlantic/Reykjavik',
|
||||
'Atlantic/South_Georgia',
|
||||
'Atlantic/Stanley',
|
||||
];
|
||||
|
||||
const indianZones = [
|
||||
'Indian/Chagos',
|
||||
'Indian/Christmas',
|
||||
'Indian/Cocos',
|
||||
'Indian/Kerguelen',
|
||||
'Indian/Mahe',
|
||||
'Indian/Maldives',
|
||||
'Indian/Mauritius',
|
||||
'Indian/Reunion',
|
||||
];
|
||||
|
||||
return [
|
||||
{ label: 'Africa', options: africaZones },
|
||||
{ label: 'America', options: americaZones },
|
||||
{ label: 'Antarctica', options: antarcticaZones },
|
||||
{ label: 'Asia', options: asiaZones },
|
||||
{ label: 'Atlantic', options: atlanticZones },
|
||||
{ label: 'Australia', options: australiaZones },
|
||||
{ label: 'Europe', options: europeZones },
|
||||
{ label: 'Indian', options: indianZones },
|
||||
{ label: 'Pacific', options: pacificZones },
|
||||
];
|
||||
};
|
||||
@@ -116,11 +116,11 @@ describe('Stats Calculators', () => {
|
||||
},
|
||||
{
|
||||
data: [null, null, null], // All null
|
||||
result: undefined,
|
||||
result: null,
|
||||
},
|
||||
{
|
||||
data: [undefined, undefined, undefined], // Empty row
|
||||
result: undefined,
|
||||
result: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -236,8 +236,8 @@ function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean
|
||||
mean: null,
|
||||
last: null,
|
||||
first: null,
|
||||
lastNotNull: undefined,
|
||||
firstNotNull: undefined,
|
||||
lastNotNull: null,
|
||||
firstNotNull: null,
|
||||
count: 0,
|
||||
nonNullCount: 0,
|
||||
allIsNull: true,
|
||||
@@ -272,8 +272,8 @@ function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean
|
||||
}
|
||||
}
|
||||
|
||||
if (currentValue !== null) {
|
||||
const isFirst = calcs.firstNotNull === undefined;
|
||||
if (currentValue !== null && currentValue !== undefined) {
|
||||
const isFirst = calcs.firstNotNull === null;
|
||||
if (isFirst) {
|
||||
calcs.firstNotNull = currentValue;
|
||||
}
|
||||
@@ -366,11 +366,11 @@ function calculateFirstNotNull(field: Field, ignoreNulls: boolean, nullAsZero: b
|
||||
const data = field.values;
|
||||
for (let idx = 0; idx < data.length; idx++) {
|
||||
const v = data.get(idx);
|
||||
if (v != null) {
|
||||
if (v != null && v !== undefined) {
|
||||
return { firstNotNull: v };
|
||||
}
|
||||
}
|
||||
return { firstNotNull: undefined };
|
||||
return { firstNotNull: null };
|
||||
}
|
||||
|
||||
function calculateLast(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||
@@ -383,11 +383,11 @@ function calculateLastNotNull(field: Field, ignoreNulls: boolean, nullAsZero: bo
|
||||
let idx = data.length - 1;
|
||||
while (idx >= 0) {
|
||||
const v = data.get(idx--);
|
||||
if (v != null) {
|
||||
if (v != null && v !== undefined) {
|
||||
return { lastNotNull: v };
|
||||
}
|
||||
}
|
||||
return { lastNotNull: undefined };
|
||||
return { lastNotNull: null };
|
||||
}
|
||||
|
||||
function calculateChangeCount(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface TableData extends QueryResultBase {
|
||||
name?: string;
|
||||
columns: Column[];
|
||||
rows: any[][];
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export type TimeSeriesValue = number | null;
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './displayValue';
|
||||
export * from './graph';
|
||||
export * from './ScopedVars';
|
||||
export * from './transformations';
|
||||
export * from './vector';
|
||||
|
||||
@@ -41,3 +41,9 @@ export interface TimeOptions {
|
||||
export type TimeFragment = string | DateTime;
|
||||
|
||||
export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
export const DefaultTimeRange: TimeRange = {
|
||||
from: {} as DateTime,
|
||||
to: {} as DateTime,
|
||||
raw: { from: '6h', to: 'now' },
|
||||
};
|
||||
|
||||
@@ -10,11 +10,6 @@ export interface Vector<T = any> {
|
||||
* Get the resutls as an array.
|
||||
*/
|
||||
toArray(): T[];
|
||||
|
||||
/**
|
||||
* Return the values as a simple array for json serialization
|
||||
*/
|
||||
toJSON(): any; // same results as toArray()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/runtime",
|
||||
"version": "6.4.0-pre",
|
||||
"version": "6.4.5",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -21,8 +21,8 @@
|
||||
"build": "grafana-toolkit package:build --scope=runtime"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "^6.4.0-alpha",
|
||||
"@grafana/ui": "^6.4.0-alpha",
|
||||
"@grafana/data": "6.4.5",
|
||||
"@grafana/ui": "6.4.5",
|
||||
"systemjs": "0.20.19",
|
||||
"systemjs-plugin-css": "0.1.37"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# 6.4.0 (unreleased)
|
||||
|
||||
# 6.4.0-beta1 (2019-09-17)
|
||||
First release, see [Readme](https://github.com/grafana/grafana/blob/v6.4.0-beta1/packages/grafana-toolkit/README.md) for details.
|
||||
|
||||
@@ -2,23 +2,27 @@
|
||||
> **@grafana/toolkit is currently in ALPHA**. Core API is unstable and can be a subject of breaking changes!
|
||||
|
||||
# grafana-toolkit
|
||||
grafana-toolkit is CLI that enables efficient development of Grafana extensions
|
||||
grafana-toolkit is CLI that enables efficient development of Grafana plugins
|
||||
|
||||
|
||||
## Rationale
|
||||
Historically, creating Grafana extension was an exercise of reverse engineering and ceremony around testing, developing and eventually building the plugin. We want to help our community to focus on the core value of their plugins rather than all the setup required to develop an extension.
|
||||
Historically, creating Grafana plugin was an exercise of reverse engineering and ceremony around testing, developing and eventually building the plugin. We want to help our community to focus on the core value of their plugins rather than all the setup required to develop them.
|
||||
|
||||
## Installation
|
||||
## Getting started
|
||||
|
||||
You can either add grafana-toolkit to your extension's `package.json` file by running
|
||||
`yarn add @grafana/toolkit` or `npm instal @grafana/toolkit`, or use one of our extension templates:
|
||||
- [React Panel](https://github.com/grafana/simple-react-panel)
|
||||
- [Angular Panel](https://github.com/grafana/simple-angular-panel)
|
||||
Setup new plugin with `grafana-toolkit plugin:create` command:
|
||||
|
||||
### Updating your extension to use grafana-toolkit
|
||||
In order to start using grafana-toolkit in your extension you need to follow the steps below:
|
||||
1. Add `@grafana/toolkit` package to your project
|
||||
2. Create `tsconfig.json` file in the root dir of your extension and paste the code below:
|
||||
```sh
|
||||
npx grafana-toolkit plugin:create my-grafana-plugin
|
||||
cd my-grafana-plugin
|
||||
yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Updating your plugin to use grafana-toolkit
|
||||
In order to start using grafana-toolkit in your existing plugin you need to follow the steps below:
|
||||
1. Add `@grafana/toolkit` package to your project by running `yarn add @grafana/toolkit` or `npm install @grafana/toolkit`
|
||||
2. Create `tsconfig.json` file in the root dir of your plugin and paste the code below:
|
||||
```json
|
||||
{
|
||||
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json",
|
||||
@@ -31,7 +35,7 @@ In order to start using grafana-toolkit in your extension you need to follow the
|
||||
}
|
||||
```
|
||||
|
||||
3. Create `.prettierrc.js` file in the root dir of your extension and paste the code below:
|
||||
3. Create `.prettierrc.js` file in the root dir of your plugin and paste the code below:
|
||||
```js
|
||||
module.exports = {
|
||||
...require("./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json"),
|
||||
@@ -49,13 +53,21 @@ module.exports = {
|
||||
```
|
||||
|
||||
## Usage
|
||||
With grafana-toolkit we put in your hands a CLI that addresses common tasks performed when working on Grafana extension:
|
||||
- `grafana-toolkit plugin:test`
|
||||
With grafana-toolkit we put in your hands a CLI that addresses common tasks performed when working on Grafana plugin:
|
||||
- `grafana-toolkit plugin:create`
|
||||
- `grafana-toolkit plugin:dev`
|
||||
- `grafana-toolkit plugin:test`
|
||||
- `grafana-toolkit plugin:build`
|
||||
|
||||
|
||||
### Developing extensions
|
||||
### Creating plugin
|
||||
`grafana-toolkit plugin:create plugin-name`
|
||||
|
||||
Creates new Grafana plugin from template.
|
||||
|
||||
If `plugin-name` is provided, the template will be downloaded to `./plugin-name` directory. Otherwise, it will be downloaded to current directory.
|
||||
|
||||
### Developing plugin
|
||||
`grafana-toolkit plugin:dev`
|
||||
|
||||
Creates development build that's easy to play with and debug using common browser tooling
|
||||
@@ -63,7 +75,7 @@ Creates development build that's easy to play with and debug using common browse
|
||||
Available options:
|
||||
- `-w`, `--watch` - run development task in a watch mode
|
||||
|
||||
### Testing extensions
|
||||
### Testing plugin
|
||||
`grafana-toolkit plugin:test`
|
||||
|
||||
Runs Jest against your codebase
|
||||
@@ -76,26 +88,29 @@ Available options:
|
||||
- `--testPathPattern=<regex>` - runs test with paths that match provided regex (https://jestjs.io/docs/en/cli#testpathpattern-regex)
|
||||
|
||||
|
||||
### Building extensions
|
||||
### Building plugin
|
||||
`grafana-toolkit plugin:build`
|
||||
|
||||
Creates production ready build of your extension
|
||||
Creates production ready build of your plugin
|
||||
|
||||
## FAQ
|
||||
|
||||
### Which version should I use?
|
||||
Please refer to [Grafana packages versioning guide](https://github.com/grafana/grafana/blob/master/packages/README.md#versioning)
|
||||
### What tools does grafana-toolkit use?
|
||||
grafana-toolkit comes with Typescript, TSLint, Prettier, Jest, CSS and SASS support.
|
||||
|
||||
### How to start using grafana-toolkit in my extension?
|
||||
See [Updating your extension to use grafana-toolkit](#updating-your-extension-to-use-grafana-toolkit)
|
||||
### Can I use Typescript to develop Grafana extensions?
|
||||
### How to start using grafana-toolkit in my plugin?
|
||||
See [Updating your plugin to use grafana-toolkit](#updating-your-plugin-to-use-grafana-toolkit)
|
||||
|
||||
### Can I use Typescript to develop Grafana plugins?
|
||||
Yes! grafana-toolkit supports Typescript by default.
|
||||
|
||||
|
||||
### How can I test my extension?
|
||||
### How can I test my plugin?
|
||||
grafana-toolkit comes with Jest as a test runner.
|
||||
|
||||
Internally at Grafana we use Enzyme. If you are developing React extension and you want to configure Enzyme as a testing utility, you need to configure `enzyme-adapter-react`. To do so create `<YOUR_EXTENSION>/config/jest-setup.ts` file that will provide necessary setup. Copy the following code into that file to get Enzyme working with React:
|
||||
Internally at Grafana we use Enzyme. If you are developing React plugin and you want to configure Enzyme as a testing utility, you need to configure `enzyme-adapter-react`. To do so create `<YOUR_PLUGIN_DIR>/config/jest-setup.ts` file that will provide necessary setup. Copy the following code into that file to get Enzyme working with React:
|
||||
|
||||
```ts
|
||||
import { configure } from 'enzyme';
|
||||
@@ -104,7 +119,7 @@ import Adapter from 'enzyme-adapter-react-16';
|
||||
configure({ adapter: new Adapter() });
|
||||
```
|
||||
|
||||
You can also setup Jest with shims of your needs by creating `jest-shim.ts` file in the same directory: `<YOUR_EXTENSION>/config/jest-shim.ts`
|
||||
You can also setup Jest with shims of your needs by creating `jest-shim.ts` file in the same directory: `<YOUR_PLUGIN_DIR_>/config/jest-shim.ts`
|
||||
|
||||
### Can I provide custom setup for Jest?
|
||||
|
||||
@@ -114,7 +129,7 @@ Currently we support following Jest config properties:
|
||||
- [`snapshotSerializers`](https://jest-bot.github.io/jest/docs/configuration.html#snapshotserializers-array-string)
|
||||
- [`moduleNameMapper`](https://jestjs.io/docs/en/configuration#modulenamemapper-object-string-string)
|
||||
|
||||
### How can I style my extension?
|
||||
### How can I style my plugin?
|
||||
We support pure CSS, SASS and CSS-in-JS approach (via [Emotion](https://emotion.sh/)).
|
||||
|
||||
#### Single CSS or SASS file
|
||||
@@ -132,18 +147,18 @@ The styles will be injected via `style` tag during runtime.
|
||||
|
||||
If you want to provide different stylesheets for dark/light theme, create `dark.[css|scss]` and `light.[css|scss]` files in `src/styles` directory of your plugin. grafana-toolkit will generate theme specific stylesheets that will end up in `dist/styles` directory.
|
||||
|
||||
In order for Grafana to pickup up you theme stylesheets you need to use `loadPluginCss` from `@grafana/runtime` package. Typically you would do that in the entrypoint of your extension:
|
||||
In order for Grafana to pickup up you theme stylesheets you need to use `loadPluginCss` from `@grafana/runtime` package. Typically you would do that in the entrypoint of your plugin:
|
||||
|
||||
```ts
|
||||
import { loadPluginCss } from '@grafana/runtime';
|
||||
|
||||
loadPluginCss({
|
||||
dark: 'plugins/<YOUR-EXTENSION-NAME>/styles/dark.css',
|
||||
light: 'plugins/<YOUR-EXTENSION-NAME>/styles/light.css',
|
||||
dark: 'plugins/<YOUR-PLUGIN-ID>/styles/dark.css',
|
||||
light: 'plugins/<YOUR-PLUGIN-ID>/styles/light.css',
|
||||
});
|
||||
```
|
||||
|
||||
You need to add `@grafana/runtime` to your extension dependencies by running `yarn add @grafana/runtime` or `npm instal @grafana/runtime`
|
||||
You need to add `@grafana/runtime` to your plugin dependencies by running `yarn add @grafana/runtime` or `npm instal @grafana/runtime`
|
||||
|
||||
> Note that in this case static files (png, svg, json, html) are all copied to dist directory when the plugin is bundled. Relative paths to those files does not change!
|
||||
|
||||
@@ -194,7 +209,7 @@ grafana-toolkit comes with [default config for TSLint](https://github.com/grafan
|
||||
|
||||
|
||||
### How is Prettier integrated into grafana-toolkit workflow?
|
||||
When building extension with [`grafana-toolkit plugin:build`](#building-extensions) task, grafana-toolkit performs Prettier check. If the check detects any Prettier issues, the build will not pass. To avoid such situation we suggest developing plugin with [`grafana-toolkit plugin:dev --watch`](#developing-extensions) task running. This task tries to fix Prettier issues automatically.
|
||||
When building plugin with [`grafana-toolkit plugin:build`](#building-plugin) task, grafana-toolkit performs Prettier check. If the check detects any Prettier issues, the build will not pass. To avoid such situation we suggest developing plugin with [`grafana-toolkit plugin:dev --watch`](#developing-plugin) task running. This task tries to fix Prettier issues automatically.
|
||||
|
||||
### My editor does not respect Prettier config, what should I do?
|
||||
In order for your editor to pickup our Prettier config you need to create `.prettierrc.js` file in the root directory of your plugin with following content:
|
||||
|
||||
@@ -11,4 +11,5 @@ require('ts-node').register({
|
||||
transpileOnly: true
|
||||
});
|
||||
|
||||
|
||||
require('../src/cli/index.ts').run(true);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "6.4.0-pre",
|
||||
"version": "6.4.5",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -28,6 +28,9 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.4.5",
|
||||
"@babel/preset-env": "7.4.5",
|
||||
"@grafana/data": "6.4.5",
|
||||
"@grafana/ui": "6.4.5",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/execa": "^0.9.0",
|
||||
"@types/expect-puppeteer": "3.3.1",
|
||||
"@types/inquirer": "^6.0.3",
|
||||
@@ -40,12 +43,11 @@
|
||||
"@types/tmp": "^0.1.0",
|
||||
"@types/webpack": "4.4.34",
|
||||
"aws-sdk": "^2.495.0",
|
||||
"@grafana/data": "^6.4.0-alpha",
|
||||
"@grafana/ui": "^6.4.0-alpha",
|
||||
"axios": "0.19.0",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-plugin-angularjs-annotate": "0.10.0",
|
||||
"chalk": "^2.4.2",
|
||||
"command-exists": "^1.2.8",
|
||||
"commander": "^2.20.0",
|
||||
"concurrently": "4.1.0",
|
||||
"copy-webpack-plugin": "5.0.3",
|
||||
@@ -98,6 +100,5 @@
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"puppeteer": "node_modules/puppeteer-core"
|
||||
},
|
||||
"types": "src/index.ts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ciPluginReportTask,
|
||||
} from './tasks/plugin.ci';
|
||||
import { buildPackageTask } from './tasks/package.build';
|
||||
import { pluginCreateTask } from './tasks/plugin.create';
|
||||
|
||||
export const run = (includeInternalScripts = false) => {
|
||||
if (includeInternalScripts) {
|
||||
@@ -61,6 +62,7 @@ export const run = (includeInternalScripts = false) => {
|
||||
|
||||
await execTask(changelogTask)({
|
||||
milestone: cmd.milestone,
|
||||
silent: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,8 +91,7 @@ export const run = (includeInternalScripts = false) => {
|
||||
.command('toolkit:build')
|
||||
.description('Prepares grafana/toolkit dist package')
|
||||
.action(async cmd => {
|
||||
// @ts-ignore
|
||||
await execTask(toolkitBuildTask)();
|
||||
await execTask(toolkitBuildTask)({});
|
||||
});
|
||||
|
||||
program
|
||||
@@ -117,11 +118,18 @@ export const run = (includeInternalScripts = false) => {
|
||||
});
|
||||
}
|
||||
|
||||
program
|
||||
.command('plugin:create [name]')
|
||||
.description('Creates plugin from template')
|
||||
.action(async cmd => {
|
||||
await execTask(pluginCreateTask)({ name: cmd, silent: true });
|
||||
});
|
||||
|
||||
program
|
||||
.command('plugin:build')
|
||||
.description('Prepares plugin dist package')
|
||||
.action(async cmd => {
|
||||
await execTask(pluginBuildTask)({ coverage: false });
|
||||
await execTask(pluginBuildTask)({ coverage: false, silent: true });
|
||||
});
|
||||
|
||||
program
|
||||
@@ -133,6 +141,7 @@ export const run = (includeInternalScripts = false) => {
|
||||
await execTask(pluginDevTask)({
|
||||
watch: !!cmd.watch,
|
||||
yarnlink: !!cmd.yarnlink,
|
||||
silent: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,6 +160,7 @@ export const run = (includeInternalScripts = false) => {
|
||||
watch: !!cmd.watch,
|
||||
testPathPattern: cmd.testPathPattern,
|
||||
testNamePattern: cmd.testNamePattern,
|
||||
silent: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,67 +2,84 @@
|
||||
import * as _ from 'lodash';
|
||||
import { Task, TaskRunner } from './task';
|
||||
import GithubClient from '../utils/githubClient';
|
||||
import difference from 'lodash/difference';
|
||||
import chalk from 'chalk';
|
||||
import { useSpinner } from '../utils/useSpinner';
|
||||
|
||||
interface ChangelogOptions {
|
||||
milestone: string;
|
||||
}
|
||||
|
||||
const changelogTaskRunner: TaskRunner<ChangelogOptions> = async ({ milestone }) => {
|
||||
const githubClient = new GithubClient();
|
||||
const client = githubClient.client;
|
||||
const filterBugs = (item: any) => {
|
||||
if (item.title.match(/fix|fixes/i)) {
|
||||
return true;
|
||||
}
|
||||
if (item.labels.find((label: any) => label.name === 'type/bug')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!/^\d+$/.test(milestone)) {
|
||||
console.log('Use milestone number not title, find number in milestone url');
|
||||
return;
|
||||
const getPackageChangelog = (packageName: string, issues: any[]) => {
|
||||
if (issues.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const res = await client.get('/issues', {
|
||||
params: {
|
||||
state: 'closed',
|
||||
per_page: 100,
|
||||
labels: 'add to changelog',
|
||||
milestone: milestone,
|
||||
},
|
||||
});
|
||||
|
||||
const issues = res.data;
|
||||
|
||||
const bugs = _.sortBy(
|
||||
issues.filter((item: any) => {
|
||||
if (item.title.match(/fix|fixes/i)) {
|
||||
return true;
|
||||
}
|
||||
if (item.labels.find((label: any) => label.name === 'type/bug')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
'title'
|
||||
);
|
||||
|
||||
const notBugs = _.sortBy(issues.filter((item: any) => !bugs.find((bug: any) => bug === item)), 'title');
|
||||
|
||||
let markdown = '';
|
||||
let markdown = chalk.bold.yellow(`\n\n/*** ${packageName} changelog ***/\n\n`);
|
||||
const bugs = _.sortBy(issues.filter(filterBugs), 'title');
|
||||
const notBugs = _.sortBy(difference(issues, bugs), 'title');
|
||||
|
||||
if (notBugs.length > 0) {
|
||||
markdown = '### Features / Enhancements\n';
|
||||
}
|
||||
|
||||
for (const item of notBugs) {
|
||||
markdown += getMarkdownLineForIssue(item);
|
||||
markdown += '### Features / Enhancements\n';
|
||||
for (const item of notBugs) {
|
||||
markdown += getMarkdownLineForIssue(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (bugs.length > 0) {
|
||||
markdown += '\n### Bug Fixes\n';
|
||||
for (const item of bugs) {
|
||||
markdown += getMarkdownLineForIssue(item);
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of bugs) {
|
||||
markdown += getMarkdownLineForIssue(item);
|
||||
}
|
||||
|
||||
console.log(markdown);
|
||||
return markdown;
|
||||
};
|
||||
|
||||
const changelogTaskRunner: TaskRunner<ChangelogOptions> = useSpinner<ChangelogOptions>(
|
||||
'Generating changelog',
|
||||
async ({ milestone }) => {
|
||||
const githubClient = new GithubClient();
|
||||
const client = githubClient.client;
|
||||
|
||||
if (!/^\d+$/.test(milestone)) {
|
||||
console.log('Use milestone number not title, find number in milestone url');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await client.get('/issues', {
|
||||
params: {
|
||||
state: 'closed',
|
||||
per_page: 100,
|
||||
labels: 'add to changelog',
|
||||
milestone: milestone,
|
||||
},
|
||||
});
|
||||
|
||||
const issues = res.data;
|
||||
const toolkitIssues = issues.filter((item: any) =>
|
||||
item.labels.find((label: any) => label.name === 'area/grafana/toolkit')
|
||||
);
|
||||
|
||||
let markdown = '';
|
||||
|
||||
markdown += getPackageChangelog('Grafana', issues);
|
||||
markdown += getPackageChangelog('grafana-toolkit', toolkitIssues);
|
||||
|
||||
console.log(markdown);
|
||||
}
|
||||
);
|
||||
|
||||
function getMarkdownLineForIssue(item: any) {
|
||||
const githubGrafanaUrl = 'https://github.com/grafana/grafana';
|
||||
let markdown = '';
|
||||
|
||||
@@ -10,7 +10,10 @@ const cherryPickRunner: TaskRunner<CherryPickOptions> = async () => {
|
||||
const res = await client.get('/issues', {
|
||||
params: {
|
||||
state: 'closed',
|
||||
per_page: 100,
|
||||
labels: 'cherry-pick needed',
|
||||
sort: 'closed',
|
||||
direction: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -152,9 +152,11 @@ export const lintPlugin = useSpinner<Fixable>('Linting', async ({ fix }) => {
|
||||
failures.forEach(f => {
|
||||
// tslint:disable-next-line
|
||||
console.log(
|
||||
`${f.getRuleSeverity() === 'warning' ? 'WARNING' : 'ERROR'}: ${f.getFileName().split('src')[1]}[${
|
||||
f.getStartPosition().getLineAndCharacter().line
|
||||
}:${f.getStartPosition().getLineAndCharacter().character}]: ${f.getFailure()}`
|
||||
`${f.getRuleSeverity() === 'warning' ? 'WARNING' : 'ERROR'}: ${
|
||||
f.getFileName().split('src')[1]
|
||||
}[${f.getStartPosition().getLineAndCharacter().line + 1}:${
|
||||
f.getStartPosition().getLineAndCharacter().character
|
||||
}]: ${f.getFailure()}`
|
||||
);
|
||||
});
|
||||
console.log('\n');
|
||||
|
||||
47
packages/grafana-toolkit/src/cli/tasks/plugin.create.ts
Normal file
47
packages/grafana-toolkit/src/cli/tasks/plugin.create.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { prompt } from 'inquirer';
|
||||
import path from 'path';
|
||||
|
||||
import { Task, TaskRunner } from './task';
|
||||
import { promptConfirm } from '../utils/prompt';
|
||||
import {
|
||||
getPluginIdFromName,
|
||||
verifyGitExists,
|
||||
promptPluginType,
|
||||
fetchTemplate,
|
||||
promptPluginDetails,
|
||||
formatPluginDetails,
|
||||
prepareJsonFiles,
|
||||
removeGitFiles,
|
||||
} from './plugin/create';
|
||||
|
||||
interface PluginCreateOptions {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const pluginCreateRunner: TaskRunner<PluginCreateOptions> = async ({ name }) => {
|
||||
const destPath = path.resolve(process.cwd(), getPluginIdFromName(name || ''));
|
||||
let pluginDetails;
|
||||
|
||||
// 1. Verifying if git exists in user's env as templates are cloned from git templates
|
||||
await verifyGitExists();
|
||||
|
||||
// 2. Prompt plugin template
|
||||
const { type } = await promptPluginType();
|
||||
|
||||
// 3. Fetch plugin template from Github
|
||||
await fetchTemplate({ type, dest: destPath });
|
||||
|
||||
// 4. Prompt plugin details
|
||||
do {
|
||||
pluginDetails = await promptPluginDetails(name);
|
||||
formatPluginDetails(pluginDetails);
|
||||
} while ((await prompt<{ confirm: boolean }>(promptConfirm('confirm', 'Is that ok?'))).confirm === false);
|
||||
|
||||
// 5. Update json files (package.json, src/plugin.json)
|
||||
await prepareJsonFiles({ pluginDetails, pluginPath: destPath });
|
||||
|
||||
// 6. Remove cloned repository .git dir
|
||||
await removeGitFiles(destPath);
|
||||
};
|
||||
|
||||
export const pluginCreateTask = new Task<PluginCreateOptions>('plugin:create task', pluginCreateRunner);
|
||||
153
packages/grafana-toolkit/src/cli/tasks/plugin/create.ts
Normal file
153
packages/grafana-toolkit/src/cli/tasks/plugin/create.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import commandExists from 'command-exists';
|
||||
import { readFileSync, promises as fs } from 'fs';
|
||||
import { prompt } from 'inquirer';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import path from 'path';
|
||||
import gitPromise from 'simple-git/promise';
|
||||
|
||||
import { useSpinner } from '../../utils/useSpinner';
|
||||
import { rmdir } from '../../utils/rmdir';
|
||||
import { promptInput, promptConfirm } from '../../utils/prompt';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const simpleGit = gitPromise(process.cwd());
|
||||
|
||||
interface PluginDetails {
|
||||
name: string;
|
||||
org: string;
|
||||
description: string;
|
||||
author: boolean | string;
|
||||
url: string;
|
||||
keywords: string;
|
||||
}
|
||||
|
||||
type PluginType = 'angular-panel' | 'react-panel' | 'datasource-plugin';
|
||||
|
||||
const RepositoriesPaths = {
|
||||
'angular-panel': 'https://github.com/grafana/simple-angular-panel.git',
|
||||
'react-panel': 'https://github.com/grafana/simple-react-panel.git',
|
||||
'datasource-plugin': 'https://github.com/grafana/simple-datasource.git',
|
||||
};
|
||||
|
||||
export const getGitUsername = async () => {
|
||||
const name = await simpleGit.raw(['config', '--global', 'user.name']);
|
||||
return name || '';
|
||||
};
|
||||
export const getPluginIdFromName = (name: string) => kebabCase(name);
|
||||
export const getPluginId = (pluginDetails: PluginDetails) =>
|
||||
`${kebabCase(pluginDetails.org)}-${getPluginIdFromName(pluginDetails.name)}`;
|
||||
|
||||
export const getPluginKeywords = (pluginDetails: PluginDetails) =>
|
||||
pluginDetails.keywords
|
||||
.split(',')
|
||||
.map(k => k.trim())
|
||||
.filter(k => k !== '');
|
||||
|
||||
export const verifyGitExists = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
commandExists('git', (err, exists) => {
|
||||
if (exists) {
|
||||
resolve(true);
|
||||
}
|
||||
reject(new Error('git is not installed'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const promptPluginType = async () =>
|
||||
prompt<{ type: PluginType }>([
|
||||
{
|
||||
type: 'list',
|
||||
message: 'Select plugin type',
|
||||
name: 'type',
|
||||
choices: [
|
||||
{ name: 'Angular panel', value: 'angular-panel' },
|
||||
{ name: 'React panel', value: 'react-panel' },
|
||||
{ name: 'Datasource plugin', value: 'datasource-plugin' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export const promptPluginDetails = async (name?: string) => {
|
||||
const username = (await getGitUsername()).trim();
|
||||
const responses = await prompt<PluginDetails>([
|
||||
promptInput('name', 'Plugin name', true, name),
|
||||
promptInput('org', 'Organization (used as part of plugin ID)', true),
|
||||
promptInput('description', 'Description'),
|
||||
promptInput('keywords', 'Keywords (separated by comma)'),
|
||||
// Try using git specified username
|
||||
promptConfirm('author', `Author (${username})`, username, username !== ''),
|
||||
// Prompt for manual author entry if no git user.name specifed
|
||||
promptInput('author', `Author`, true, undefined, answers => !answers.author || username === ''),
|
||||
promptInput('url', 'Your URL (i.e. organisation url)'),
|
||||
]);
|
||||
|
||||
return {
|
||||
...responses,
|
||||
author: responses.author === true ? username : responses.author,
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchTemplate = useSpinner<{ type: PluginType; dest: string }>(
|
||||
'Fetching plugin template...',
|
||||
async ({ type, dest }) => {
|
||||
const url = RepositoriesPaths[type];
|
||||
if (!url) {
|
||||
throw new Error('Unknown plugin type');
|
||||
}
|
||||
|
||||
await simpleGit.clone(url, dest);
|
||||
}
|
||||
);
|
||||
|
||||
export const prepareJsonFiles = useSpinner<{ pluginDetails: PluginDetails; pluginPath: string }>(
|
||||
'Saving package.json and plugin.json files',
|
||||
async ({ pluginDetails, pluginPath }) => {
|
||||
const packageJsonPath = path.resolve(pluginPath, 'package.json');
|
||||
const pluginJsonPath = path.resolve(pluginPath, 'src/plugin.json');
|
||||
const packageJson: any = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
const pluginJson: any = JSON.parse(readFileSync(pluginJsonPath, 'utf8'));
|
||||
|
||||
const pluginId = `${kebabCase(pluginDetails.org)}-${getPluginIdFromName(pluginDetails.name)}`;
|
||||
packageJson.name = pluginId;
|
||||
packageJson.author = pluginDetails.author;
|
||||
packageJson.description = pluginDetails.description;
|
||||
|
||||
pluginJson.name = pluginDetails.name;
|
||||
pluginJson.id = pluginId;
|
||||
pluginJson.info = {
|
||||
...pluginJson.info,
|
||||
description: pluginDetails.description,
|
||||
author: {
|
||||
name: pluginDetails.author,
|
||||
url: pluginDetails.url,
|
||||
},
|
||||
keywords: getPluginKeywords(pluginDetails),
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
[packageJson, pluginJson].map((f, i) => {
|
||||
const filePath = i === 0 ? packageJsonPath : pluginJsonPath;
|
||||
return fs.writeFile(filePath, JSON.stringify(f, null, 2));
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const removeGitFiles = useSpinner('Cleaning', async pluginPath => rmdir(`${path.resolve(pluginPath, '.git')}`));
|
||||
|
||||
export const formatPluginDetails = (details: PluginDetails) => {
|
||||
console.group();
|
||||
console.log();
|
||||
console.log(chalk.bold.yellow('Your plugin details'));
|
||||
console.log('---');
|
||||
console.log(chalk.bold('Name: '), details.name);
|
||||
console.log(chalk.bold('ID: '), getPluginId(details));
|
||||
console.log(chalk.bold('Description: '), details.description);
|
||||
console.log(chalk.bold('Keywords: '), getPluginKeywords(details));
|
||||
console.log(chalk.bold('Author: '), details.author);
|
||||
console.log(chalk.bold('Organisation: '), details.org);
|
||||
console.log(chalk.bold('Website: '), details.url);
|
||||
console.log();
|
||||
console.groupEnd();
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import * as fs from 'fs';
|
||||
import chalk from 'chalk';
|
||||
import { useSpinner } from '../utils/useSpinner';
|
||||
import { Task, TaskRunner } from './task';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
@@ -105,7 +104,9 @@ const copySassFiles = () => {
|
||||
})();
|
||||
};
|
||||
|
||||
const toolkitBuildTaskRunner: TaskRunner<void> = async () => {
|
||||
interface ToolkitBuildOptions {}
|
||||
|
||||
const toolkitBuildTaskRunner: TaskRunner<ToolkitBuildOptions> = async () => {
|
||||
cwd = path.resolve(__dirname, '../../../');
|
||||
distDir = `${cwd}/dist`;
|
||||
const pkg = require(`${cwd}/package.json`);
|
||||
@@ -118,21 +119,6 @@ const toolkitBuildTaskRunner: TaskRunner<void> = async () => {
|
||||
fs.mkdirSync('./dist/sass');
|
||||
await copyFiles();
|
||||
await copySassFiles();
|
||||
|
||||
// RYAN HACK HACK HACK
|
||||
// when Dominik is back from vacation, we can find a better way
|
||||
// This moves the index to the root so plugin e2e tests can import them
|
||||
console.warn('hacking an index.js file for toolkit. Help!');
|
||||
const index = `${distDir}/src/index.js`;
|
||||
fs.readFile(index, 'utf8', (err, data) => {
|
||||
const pattern = 'require("./';
|
||||
const js = data.replace(new RegExp(escapeRegExp(pattern), 'g'), 'require("./src/');
|
||||
fs.writeFile(`${distDir}/index.js`, js, err => {
|
||||
if (err) {
|
||||
throw new Error('Error writing index: ' + err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const toolkitBuildTask = new Task<void>('@grafana/toolkit build', toolkitBuildTaskRunner);
|
||||
export const toolkitBuildTask = new Task<ToolkitBuildOptions>('@grafana/toolkit build', toolkitBuildTaskRunner);
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Task } from '../tasks/task';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export const execTask = <TOptions>(task: Task<TOptions>) => async (options: TOptions) => {
|
||||
console.log(chalk.yellow(`Running ${chalk.bold(task.name)} task`));
|
||||
interface TaskBasicOptions {
|
||||
// Don't print task details when running
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
export const execTask = <TOptions>(task: Task<TOptions>) => async (options: TOptions & TaskBasicOptions) => {
|
||||
if (!options.silent) {
|
||||
console.log(chalk.yellow(`Running ${chalk.bold(task.name)} task`));
|
||||
}
|
||||
task.setOptions(options);
|
||||
try {
|
||||
console.group();
|
||||
|
||||
58
packages/grafana-toolkit/src/cli/utils/prompt.ts
Normal file
58
packages/grafana-toolkit/src/cli/utils/prompt.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Question,
|
||||
InputQuestion,
|
||||
CheckboxQuestion,
|
||||
NumberQuestion,
|
||||
PasswordQuestion,
|
||||
EditorQuestion,
|
||||
ConfirmQuestion,
|
||||
} from 'inquirer';
|
||||
|
||||
type QuestionWithValidation<A = any> =
|
||||
| InputQuestion<A>
|
||||
| CheckboxQuestion<A>
|
||||
| NumberQuestion<A>
|
||||
| PasswordQuestion<A>
|
||||
| EditorQuestion<A>;
|
||||
|
||||
export const answerRequired = (question: QuestionWithValidation): Question<any> => {
|
||||
return {
|
||||
...question,
|
||||
validate: (answer: any) => answer.trim() !== '' || `${question.name} is required`,
|
||||
};
|
||||
};
|
||||
|
||||
export const promptInput = <A>(
|
||||
name: string,
|
||||
message: string | ((answers: A) => string),
|
||||
required = false,
|
||||
def: any = undefined,
|
||||
when: boolean | ((answers: A) => boolean | Promise<boolean>) = true
|
||||
) => {
|
||||
const model: InputQuestion<A> = {
|
||||
type: 'input',
|
||||
name,
|
||||
message,
|
||||
default: def,
|
||||
when,
|
||||
};
|
||||
|
||||
return required ? answerRequired(model) : model;
|
||||
};
|
||||
|
||||
export const promptConfirm = <A>(
|
||||
name: string,
|
||||
message: string | ((answers: A) => string),
|
||||
def: any = undefined,
|
||||
when: boolean | ((answers: A) => boolean | Promise<boolean>) = true
|
||||
) => {
|
||||
const model: ConfirmQuestion<A> = {
|
||||
type: 'confirm',
|
||||
name,
|
||||
message,
|
||||
default: def,
|
||||
when,
|
||||
};
|
||||
|
||||
return model;
|
||||
};
|
||||
23
packages/grafana-toolkit/src/cli/utils/rmdir.ts
Normal file
23
packages/grafana-toolkit/src/cli/utils/rmdir.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fs = require('fs');
|
||||
import path = require('path');
|
||||
|
||||
/**
|
||||
* Remove directory recursively
|
||||
* Ref https://stackoverflow.com/a/42505874
|
||||
*/
|
||||
export const rmdir = (dirPath: string) => {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readdirSync(dirPath).forEach(entry => {
|
||||
const entryPath = path.join(dirPath, entry);
|
||||
if (fs.lstatSync(entryPath).isDirectory()) {
|
||||
rmdir(entryPath);
|
||||
} else {
|
||||
fs.unlinkSync(entryPath);
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmdirSync(dirPath);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import ora from 'ora';
|
||||
|
||||
type FnToSpin<T> = (options: T) => Promise<void>;
|
||||
|
||||
export const useSpinner = <T>(spinnerLabel: string, fn: FnToSpin<T>, killProcess = true) => {
|
||||
export const useSpinner = <T = any>(spinnerLabel: string, fn: FnToSpin<T>, killProcess = true) => {
|
||||
return async (options: T) => {
|
||||
const spinner = ora(spinnerLabel);
|
||||
spinner.start();
|
||||
|
||||
11
packages/grafana-toolkit/src/config/utils/getPluginId.ts
Normal file
11
packages/grafana-toolkit/src/config/utils/getPluginId.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import path from 'path';
|
||||
|
||||
let PLUGIN_ID: string;
|
||||
|
||||
export const getPluginId = () => {
|
||||
if (!PLUGIN_ID) {
|
||||
const pluginJson = require(path.resolve(process.cwd(), 'src/plugin.json'));
|
||||
PLUGIN_ID = pluginJson.id;
|
||||
}
|
||||
return PLUGIN_ID;
|
||||
};
|
||||
@@ -149,7 +149,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
|
||||
'emotion',
|
||||
'prismjs',
|
||||
'slate-plain-serializer',
|
||||
'slate-react',
|
||||
'@grafana/slate-react',
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-redux',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getPluginId } from '../utils/getPluginId';
|
||||
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
@@ -139,5 +140,14 @@ export const getFileLoaders = () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
publicPath: `/public/plugins/${getPluginId()}/fonts`,
|
||||
outputPath: 'fonts',
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './e2e';
|
||||
|
||||
// Namespace for Plugins
|
||||
import * as plugins from './plugins';
|
||||
|
||||
export { plugins };
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Browser, Page } from 'puppeteer-core';
|
||||
|
||||
import { e2eScenario, takeScreenShot, plugins, pages } from '@grafana/toolkit';
|
||||
import { e2eScenario, takeScreenShot, pages } from '@grafana/toolkit/src/e2e';
|
||||
import { getEndToEndSettings } from '@grafana/toolkit/src/plugins';
|
||||
|
||||
// ****************************************************************
|
||||
// NOTE, This file is copied to plugins at runtime, it is not run locally
|
||||
@@ -11,7 +12,7 @@ const sleep = (milliseconds: number) => {
|
||||
};
|
||||
|
||||
e2eScenario('Common Plugin Test', 'should pass', async (browser: Browser, page: Page) => {
|
||||
const settings = plugins.getEndToEndSettings();
|
||||
const settings = getEndToEndSettings();
|
||||
const pluginPage = pages.getPluginPage(settings.plugin.id);
|
||||
await pluginPage.init(page);
|
||||
await pluginPage.navigateTo();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"rootDirs": ["."],
|
||||
"outDir": "dist/src",
|
||||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"declarationDir": "dist/src",
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2015", "es2017.string", "dom"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/ui",
|
||||
"version": "6.4.0-pre",
|
||||
"version": "6.4.5",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -25,11 +25,13 @@
|
||||
"build": "grafana-toolkit package:build --scope=ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "^6.4.0-alpha",
|
||||
"@grafana/data": "6.4.5",
|
||||
"@grafana/slate-react": "0.22.9-grafana",
|
||||
"@torkelo/react-select": "2.1.1",
|
||||
"@types/react-color": "2.17.0",
|
||||
"classnames": "2.2.6",
|
||||
"d3": "5.9.1",
|
||||
"immutable": "3.8.2",
|
||||
"jquery": "3.4.1",
|
||||
"lodash": "4.17.15",
|
||||
"moment": "2.24.0",
|
||||
@@ -45,6 +47,7 @@
|
||||
"react-storybook-addon-props-combinations": "1.1.0",
|
||||
"react-transition-group": "2.6.1",
|
||||
"react-virtualized": "9.21.0",
|
||||
"slate": "0.47.8",
|
||||
"tinycolor2": "1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -65,6 +68,8 @@
|
||||
"@types/react-custom-scrollbars": "4.0.5",
|
||||
"@types/react-test-renderer": "16.8.1",
|
||||
"@types/react-transition-group": "2.0.16",
|
||||
"@types/slate": "0.47.1",
|
||||
"@types/slate-react": "0.22.5",
|
||||
"@types/storybook__addon-actions": "3.4.2",
|
||||
"@types/storybook__addon-info": "4.1.1",
|
||||
"@types/storybook__addon-knobs": "4.0.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import sourceMaps from 'rollup-plugin-sourcemaps';
|
||||
// import sourceMaps from 'rollup-plugin-sourcemaps';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
||||
const pkg = require('./package.json');
|
||||
@@ -47,19 +47,20 @@ const buildCjsPackage = ({ env }) => {
|
||||
],
|
||||
'../../node_modules/react-color/lib/components/common': ['Saturation', 'Hue', 'Alpha'],
|
||||
'../../node_modules/immutable/dist/immutable.js': [
|
||||
'Record',
|
||||
'Set',
|
||||
'Map',
|
||||
'List',
|
||||
'OrderedSet',
|
||||
'is',
|
||||
'Stack',
|
||||
'Record',
|
||||
],
|
||||
'node_modules/immutable/dist/immutable.js': ['Record', 'Set', 'Map', 'List', 'OrderedSet', 'is', 'Stack'],
|
||||
'../../node_modules/esrever/esrever.js': ['reverse'],
|
||||
},
|
||||
}),
|
||||
resolve(),
|
||||
sourceMaps(),
|
||||
// sourceMaps(),
|
||||
env === 'production' && terser(),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getColorFromHexRgbOrName } from '../../utils';
|
||||
|
||||
// Types
|
||||
import { Themeable } from '../../types';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
|
||||
export interface BigValueSparkline {
|
||||
data: any[][]; // [[number,number]]
|
||||
@@ -31,6 +32,33 @@ export interface Props extends Themeable {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
return {
|
||||
wrapper: css`
|
||||
position: 'relative';
|
||||
display: 'table';
|
||||
`,
|
||||
title: css`
|
||||
line-height: 1;
|
||||
text-align: 'center';
|
||||
z-index: 1;
|
||||
display: 'block';
|
||||
width: '100%';
|
||||
position: 'absolute';
|
||||
`,
|
||||
value: css`
|
||||
line-height: 1;
|
||||
text-align: 'center';
|
||||
z-index: 1;
|
||||
display: 'table-cell';
|
||||
vertical-align: 'middle';
|
||||
position: 'relative';
|
||||
font-size: '3em';
|
||||
font-weight: 500;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
/*
|
||||
* This visualization is still in POC state, needed more tests & better structure
|
||||
*/
|
||||
@@ -122,46 +150,12 @@ export class BigValue extends PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
const { height, width, value, prefix, suffix, sparkline, backgroundColor, onClick, className } = this.props;
|
||||
|
||||
const styles = getStyles();
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
css({
|
||||
position: 'relative',
|
||||
display: 'table',
|
||||
}),
|
||||
className
|
||||
)}
|
||||
style={{ width, height, backgroundColor }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{value.title && (
|
||||
<div
|
||||
className={css({
|
||||
lineHeight: 1,
|
||||
textAlign: 'center',
|
||||
zIndex: 1,
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
})}
|
||||
>
|
||||
{value.title}
|
||||
</div>
|
||||
)}
|
||||
<div className={cx(styles.wrapper, className)} style={{ width, height, backgroundColor }} onClick={onClick}>
|
||||
{value.title && <div className={styles.title}>{value.title}</div>}
|
||||
|
||||
<span
|
||||
className={css({
|
||||
lineHeight: 1,
|
||||
textAlign: 'center',
|
||||
zIndex: 1,
|
||||
display: 'table-cell',
|
||||
verticalAlign: 'middle',
|
||||
position: 'relative',
|
||||
fontSize: '3em',
|
||||
fontWeight: 500, // TODO: $font-weight-semi-bold
|
||||
})}
|
||||
>
|
||||
<span className={styles.value}>
|
||||
{this.renderText(prefix, '0px 2px 0px 0px')}
|
||||
{this.renderText(value)}
|
||||
{this.renderText(suffix)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import tinycolor from 'tinycolor2';
|
||||
import { css, cx } from 'emotion';
|
||||
import { Themeable, GrafanaTheme } from '../../types';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
|
||||
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent';
|
||||
|
||||
@@ -19,7 +20,9 @@ export interface CommonButtonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface LinkButtonProps extends CommonButtonProps, AnchorHTMLAttributes<HTMLAnchorElement> {}
|
||||
export interface LinkButtonProps extends CommonButtonProps, AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
disabled?: boolean;
|
||||
}
|
||||
export interface ButtonProps extends CommonButtonProps, ButtonHTMLAttributes<HTMLButtonElement> {}
|
||||
|
||||
interface AbstractButtonProps extends CommonButtonProps, Themeable {
|
||||
@@ -47,7 +50,13 @@ const buttonVariantStyles = (
|
||||
}
|
||||
`;
|
||||
|
||||
const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonVariant, withIcon: boolean) => {
|
||||
interface StyleDeps {
|
||||
theme: GrafanaTheme;
|
||||
size: ButtonSize;
|
||||
variant: ButtonVariant;
|
||||
withIcon: boolean;
|
||||
}
|
||||
const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: StyleDeps) => {
|
||||
const borderRadius = theme.border.radius.sm;
|
||||
let padding,
|
||||
background,
|
||||
@@ -153,7 +162,7 @@ const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonV
|
||||
filter: brightness(100);
|
||||
`,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
|
||||
renderAs,
|
||||
@@ -165,7 +174,7 @@ export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
|
||||
children,
|
||||
...otherProps
|
||||
}) => {
|
||||
const buttonStyles = getButtonStyles(theme, size, variant, !!icon);
|
||||
const buttonStyles = getButtonStyles({ theme, size, variant, withIcon: !!icon });
|
||||
const nonHtmlProps = {
|
||||
theme,
|
||||
size,
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Themeable, GrafanaTheme } from '../../types/theme';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
import { css, cx } from 'emotion';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
export interface CallToActionCardProps extends Themeable {
|
||||
message?: string | JSX.Element;
|
||||
@@ -10,7 +11,7 @@ export interface CallToActionCardProps extends Themeable {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const getCallToActionCardStyles = (theme: GrafanaTheme) => ({
|
||||
const getCallToActionCardStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
wrapper: css`
|
||||
label: call-to-action-card;
|
||||
padding: ${theme.spacing.lg};
|
||||
@@ -28,7 +29,7 @@ const getCallToActionCardStyles = (theme: GrafanaTheme) => ({
|
||||
footer: css`
|
||||
margin-top: ${theme.spacing.lg};
|
||||
`,
|
||||
});
|
||||
}));
|
||||
|
||||
export const CallToActionCard: React.FunctionComponent<CallToActionCardProps> = ({
|
||||
message,
|
||||
|
||||
@@ -3,9 +3,10 @@ import { css, cx } from 'emotion';
|
||||
|
||||
import { GrafanaTheme } from '../../types/theme';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
import { ThemeContext } from '../../themes/index';
|
||||
import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
collapse: css`
|
||||
label: collapse;
|
||||
margin-top: ${theme.spacing.sm};
|
||||
@@ -79,7 +80,7 @@ const getStyles = (theme: GrafanaTheme) => ({
|
||||
font-size: ${theme.typography.heading.h6};
|
||||
box-shadow: ${selectThemeVariant({ light: 'none', dark: '1px 1px 4px rgb(45, 45, 45)' }, theme.type)};
|
||||
`,
|
||||
});
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import React, { useContext, useRef, useState, useLayoutEffect } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import useClickAway from 'react-use/lib/useClickAway';
|
||||
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
import { Portal, List } from '../index';
|
||||
import { LinkTarget } from '@grafana/data';
|
||||
|
||||
@@ -26,7 +27,7 @@ export interface ContextMenuProps {
|
||||
renderHeader?: () => JSX.Element;
|
||||
}
|
||||
|
||||
const getContextMenuStyles = (theme: GrafanaTheme) => {
|
||||
const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const linkColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.dark2,
|
||||
@@ -106,6 +107,7 @@ const getContextMenuStyles = (theme: GrafanaTheme) => {
|
||||
z-index: 1;
|
||||
box-shadow: 0 2px 5px 0 ${wrapperShadow};
|
||||
min-width: 200px;
|
||||
display: inline-block;
|
||||
border-radius: ${theme.border.radius.sm};
|
||||
`,
|
||||
link: css`
|
||||
@@ -146,11 +148,31 @@ const getContextMenuStyles = (theme: GrafanaTheme) => {
|
||||
top: 4px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClose, items, renderHeader }) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const menuRef = useRef(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [positionStyles, setPositionStyles] = useState({});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const menuElement = menuRef.current;
|
||||
if (menuElement) {
|
||||
const rect = menuElement.getBoundingClientRect();
|
||||
const OFFSET = 5;
|
||||
const collisions = {
|
||||
right: window.innerWidth < x + rect.width,
|
||||
bottom: window.innerHeight < rect.bottom + rect.height + OFFSET,
|
||||
};
|
||||
|
||||
setPositionStyles({
|
||||
position: 'fixed',
|
||||
left: collisions.right ? x - rect.width - OFFSET : x - OFFSET,
|
||||
top: collisions.bottom ? y - rect.height - OFFSET : y + OFFSET,
|
||||
});
|
||||
}
|
||||
}, [menuRef.current]);
|
||||
|
||||
useClickAway(menuRef, () => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
@@ -158,18 +180,9 @@ export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClo
|
||||
});
|
||||
|
||||
const styles = getContextMenuStyles(theme);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: x - 5,
|
||||
top: y + 5,
|
||||
}}
|
||||
className={styles.wrapper}
|
||||
>
|
||||
<div ref={menuRef} style={positionStyles} className={styles.wrapper}>
|
||||
{renderHeader && <div className={styles.header}>{renderHeader()}</div>}
|
||||
<List
|
||||
items={items || []}
|
||||
|
||||
@@ -3,25 +3,38 @@ import { DataLink } from '@grafana/data';
|
||||
import { FormField, Switch } from '../index';
|
||||
import { VariableSuggestion } from './DataLinkSuggestions';
|
||||
import { css } from 'emotion';
|
||||
import { ThemeContext } from '../../themes/index';
|
||||
import { ThemeContext, stylesFactory } from '../../themes/index';
|
||||
import { DataLinkInput } from './DataLinkInput';
|
||||
import { GrafanaTheme } from '../../types';
|
||||
|
||||
interface DataLinkEditorProps {
|
||||
index: number;
|
||||
isLast: boolean;
|
||||
value: DataLink;
|
||||
suggestions: VariableSuggestion[];
|
||||
onChange: (index: number, link: DataLink) => void;
|
||||
onChange: (index: number, link: DataLink, callback?: () => void) => void;
|
||||
onRemove: (link: DataLink) => void;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
listItem: css`
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
infoText: css`
|
||||
padding-bottom: ${theme.spacing.md};
|
||||
margin-left: 66px;
|
||||
color: ${theme.colors.textWeak};
|
||||
`,
|
||||
}));
|
||||
|
||||
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
||||
({ index, value, onChange, onRemove, suggestions, isLast }) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const styles = getStyles(theme);
|
||||
const [title, setTitle] = useState(value.title);
|
||||
|
||||
const onUrlChange = (url: string) => {
|
||||
onChange(index, { ...value, url });
|
||||
const onUrlChange = (url: string, callback?: () => void) => {
|
||||
onChange(index, { ...value, url }, callback);
|
||||
};
|
||||
const onTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(event.target.value);
|
||||
@@ -39,18 +52,8 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
||||
onChange(index, { ...value, targetBlank: !value.targetBlank });
|
||||
};
|
||||
|
||||
const listItemStyle = css`
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`;
|
||||
|
||||
const infoTextStyle = css`
|
||||
padding-bottom: ${theme.spacing.md};
|
||||
margin-left: 66px;
|
||||
color: ${theme.colors.textWeak};
|
||||
`;
|
||||
|
||||
return (
|
||||
<div className={listItemStyle}>
|
||||
<div className={styles.listItem}>
|
||||
<div className="gf-form gf-form--inline">
|
||||
<FormField
|
||||
className="gf-form--grow"
|
||||
@@ -76,7 +79,7 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
||||
`}
|
||||
/>
|
||||
{isLast && (
|
||||
<div className={infoTextStyle}>
|
||||
<div className={styles.infoText}>
|
||||
With data links you can reference data variables like series name, labels and values. Type CMD+Space,
|
||||
CTRL+Space, or $ to open variable suggestions.
|
||||
</div>
|
||||
|
||||
@@ -1,153 +1,121 @@
|
||||
import React, { useState, useMemo, useCallback, useContext } from 'react';
|
||||
import React, { useState, useMemo, useContext, useRef, RefObject, memo, useEffect } from 'react';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
|
||||
import { makeValue, ThemeContext, DataLinkBuiltInVars } from '../../index';
|
||||
import { ThemeContext, DataLinkBuiltInVars, makeValue } from '../../index';
|
||||
import { SelectionReference } from './SelectionReference';
|
||||
import { Portal } from '../index';
|
||||
// @ts-ignore
|
||||
import { Editor } from 'slate-react';
|
||||
// @ts-ignore
|
||||
import { Value, Change, Document } from 'slate';
|
||||
// @ts-ignore
|
||||
|
||||
import { Editor } from '@grafana/slate-react';
|
||||
import { Value } from 'slate';
|
||||
import Plain from 'slate-plain-serializer';
|
||||
import { Popper as ReactPopper } from 'react-popper';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { css, cx } from 'emotion';
|
||||
// @ts-ignore
|
||||
import PluginPrism from 'slate-prism';
|
||||
|
||||
import { SlatePrism } from '../../slate-plugins';
|
||||
import { SCHEMA } from '../../utils/slate';
|
||||
import { stylesFactory } from '../../themes';
|
||||
import { GrafanaTheme } from '../../types';
|
||||
|
||||
const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
|
||||
|
||||
interface DataLinkInputProps {
|
||||
value: string;
|
||||
onChange: (url: string) => void;
|
||||
onChange: (url: string, callback?: () => void) => void;
|
||||
suggestions: VariableSuggestion[];
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
PluginPrism({
|
||||
SlatePrism({
|
||||
onlyIn: (node: any) => node.type === 'code_block',
|
||||
getSyntax: () => 'links',
|
||||
}),
|
||||
];
|
||||
|
||||
export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, suggestions }) => {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
editor: css`
|
||||
.token.builtInVariable {
|
||||
color: ${theme.colors.queryGreen};
|
||||
}
|
||||
.token.variable {
|
||||
color: ${theme.colors.queryKeyword};
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
// This memoised also because rerendering the slate editor grabs focus which created problem in some cases this
|
||||
// was used and changes to different state were propagated here.
|
||||
export const DataLinkInput: React.FC<DataLinkInputProps> = memo(({ value, onChange, suggestions }) => {
|
||||
const editorRef = useRef<Editor>() as RefObject<Editor>;
|
||||
const theme = useContext(ThemeContext);
|
||||
const styles = getStyles(theme);
|
||||
const [showingSuggestions, setShowingSuggestions] = useState(false);
|
||||
const [suggestionsIndex, setSuggestionsIndex] = useState(0);
|
||||
const [usedSuggestions, setUsedSuggestions] = useState(
|
||||
suggestions.filter(suggestion => {
|
||||
return value.indexOf(suggestion.value) > -1;
|
||||
})
|
||||
);
|
||||
// Using any here as TS has problem pickung up `change` method existance on Value
|
||||
// According to code and documentation `change` is an instance method on Value in slate 0.33.8 that we use
|
||||
// https://github.com/ianstormtaylor/slate/blob/slate%400.33.8/docs/reference/slate/value.md#change
|
||||
const [linkUrl, setLinkUrl] = useState<any>(makeValue(value));
|
||||
const [linkUrl, setLinkUrl] = useState<Value>(makeValue(value));
|
||||
const prevLinkUrl = usePrevious<Value>(linkUrl);
|
||||
|
||||
const getStyles = useCallback(() => {
|
||||
return {
|
||||
editor: css`
|
||||
.token.builtInVariable {
|
||||
color: ${theme.colors.queryGreen};
|
||||
}
|
||||
.token.variable {
|
||||
color: ${theme.colors.queryKeyword};
|
||||
}
|
||||
`,
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
const currentSuggestions = useMemo(
|
||||
() =>
|
||||
suggestions.filter(suggestion => {
|
||||
return usedSuggestions.map(s => s.value).indexOf(suggestion.value) === -1;
|
||||
}),
|
||||
[usedSuggestions, suggestions]
|
||||
);
|
||||
// Workaround for https://github.com/ianstormtaylor/slate/issues/2927
|
||||
const stateRef = useRef({ showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange });
|
||||
stateRef.current = { showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange };
|
||||
|
||||
// SelectionReference is used to position the variables suggestion relatively to current DOM selection
|
||||
const selectionRef = useMemo(() => new SelectionReference(), [setShowingSuggestions]);
|
||||
const selectionRef = useMemo(() => new SelectionReference(), [setShowingSuggestions, linkUrl]);
|
||||
|
||||
// Keep track of variables that has been used already
|
||||
const updateUsedSuggestions = () => {
|
||||
const currentLink = Plain.serialize(linkUrl);
|
||||
const next = usedSuggestions.filter(suggestion => {
|
||||
return currentLink.indexOf(suggestion.value) > -1;
|
||||
});
|
||||
if (next.length !== usedSuggestions.length) {
|
||||
setUsedSuggestions(next);
|
||||
}
|
||||
};
|
||||
|
||||
useDebounce(updateUsedSuggestions, 250, [linkUrl]);
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Backspace' || event.key === 'Escape') {
|
||||
setShowingSuggestions(false);
|
||||
setSuggestionsIndex(0);
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (showingSuggestions) {
|
||||
onVariableSelect(currentSuggestions[suggestionsIndex]);
|
||||
const onKeyDown = React.useCallback((event: KeyboardEvent, next: () => any) => {
|
||||
if (!stateRef.current.showingSuggestions) {
|
||||
if (event.key === '=' || event.key === '$' || (event.keyCode === 32 && event.ctrlKey)) {
|
||||
return setShowingSuggestions(true);
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
if (showingSuggestions) {
|
||||
if (event.key === 'ArrowDown') {
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
case 'Escape':
|
||||
setShowingSuggestions(false);
|
||||
return setSuggestionsIndex(0);
|
||||
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
setSuggestionsIndex(index => {
|
||||
return (index + 1) % currentSuggestions.length;
|
||||
});
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
return onVariableSelect(stateRef.current.suggestions[stateRef.current.suggestionsIndex]);
|
||||
|
||||
case 'ArrowDown':
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
setSuggestionsIndex(index => {
|
||||
const nextIndex = index - 1 < 0 ? currentSuggestions.length - 1 : (index - 1) % currentSuggestions.length;
|
||||
return nextIndex;
|
||||
});
|
||||
}
|
||||
const direction = event.key === 'ArrowDown' ? 1 : -1;
|
||||
return setSuggestionsIndex(index => modulo(index + direction, stateRef.current.suggestions.length));
|
||||
default:
|
||||
return next();
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (event.key === '?' || event.key === '&' || event.key === '$' || (event.keyCode === 32 && event.ctrlKey)) {
|
||||
setShowingSuggestions(true);
|
||||
useEffect(() => {
|
||||
// Update the state of the link in the parent. This is basically done on blur but we need to do it after
|
||||
// our state have been updated. The duplicity of state is done for perf reasons and also because local
|
||||
// state also contains things like selection and formating.
|
||||
if (prevLinkUrl && prevLinkUrl.selection.isFocused && !linkUrl.selection.isFocused) {
|
||||
stateRef.current.onChange(Plain.serialize(linkUrl));
|
||||
}
|
||||
}, [linkUrl, prevLinkUrl]);
|
||||
|
||||
if (event.key === 'Enter' && showingSuggestions) {
|
||||
// Preventing entering a new line
|
||||
// As of https://github.com/ianstormtaylor/slate/issues/1345#issuecomment-340508289
|
||||
return false;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onUrlChange = ({ value }: Change) => {
|
||||
const onUrlChange = React.useCallback(({ value }: { value: Value }) => {
|
||||
setLinkUrl(value);
|
||||
};
|
||||
|
||||
const onUrlBlur = () => {
|
||||
onChange(Plain.serialize(linkUrl));
|
||||
};
|
||||
|
||||
const onVariableSelect = (item: VariableSuggestion) => {
|
||||
const includeDollarSign = Plain.serialize(linkUrl).slice(-1) !== '$';
|
||||
|
||||
const change = linkUrl.change();
|
||||
}, []);
|
||||
|
||||
const onVariableSelect = (item: VariableSuggestion, editor = editorRef.current!) => {
|
||||
const includeDollarSign = Plain.serialize(editor.value).slice(-1) !== '$';
|
||||
if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) {
|
||||
change.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
|
||||
editor.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
|
||||
} else {
|
||||
change.insertText(`var-${item.value}=$\{${item.value}}`);
|
||||
editor.insertText(`var-${item.value}=$\{${item.value}}`);
|
||||
}
|
||||
|
||||
setLinkUrl(change.value);
|
||||
setLinkUrl(editor.value);
|
||||
setShowingSuggestions(false);
|
||||
setUsedSuggestions((previous: VariableSuggestion[]) => {
|
||||
return [...previous, item];
|
||||
});
|
||||
|
||||
setSuggestionsIndex(0);
|
||||
onChange(Plain.serialize(change.value));
|
||||
onChange(Plain.serialize(editor.value));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
@@ -163,7 +131,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
||||
<Portal>
|
||||
<ReactPopper
|
||||
referenceElement={selectionRef}
|
||||
placement="auto-end"
|
||||
placement="top-end"
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: true, boundariesElement: 'window' },
|
||||
arrow: { enabled: false },
|
||||
@@ -174,7 +142,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
||||
return (
|
||||
<div ref={ref} style={style} data-placement={placement}>
|
||||
<DataLinkSuggestions
|
||||
suggestions={currentSuggestions}
|
||||
suggestions={stateRef.current.suggestions}
|
||||
onSuggestionSelect={onVariableSelect}
|
||||
onClose={() => setShowingSuggestions(false)}
|
||||
activeIndex={suggestionsIndex}
|
||||
@@ -186,17 +154,18 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
|
||||
</Portal>
|
||||
)}
|
||||
<Editor
|
||||
schema={SCHEMA}
|
||||
ref={editorRef}
|
||||
placeholder="http://your-grafana.com/d/000000010/annotations"
|
||||
value={linkUrl}
|
||||
value={stateRef.current.linkUrl}
|
||||
onChange={onUrlChange}
|
||||
onBlur={onUrlBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)}
|
||||
plugins={plugins}
|
||||
className={getStyles().editor}
|
||||
className={styles.editor}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
DataLinkInput.displayName = 'DataLinkInput';
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { useRef, useContext, useMemo } from 'react';
|
||||
import useClickAway from 'react-use/lib/useClickAway';
|
||||
import { List } from '../index';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
export enum VariableOrigin {
|
||||
Series = 'series',
|
||||
@@ -28,7 +29,7 @@ interface DataLinkSuggestionsProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const wrapperBg = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.white,
|
||||
@@ -129,7 +130,7 @@ const getStyles = (theme: GrafanaTheme) => {
|
||||
color: ${itemDocsColor};
|
||||
`,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
export const DataLinkSuggestions: React.FC<DataLinkSuggestionsProps> = ({ suggestions, ...otherProps }) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { VariableSuggestion } from './DataLinkSuggestions';
|
||||
|
||||
interface DataLinksEditorProps {
|
||||
value: DataLink[];
|
||||
onChange: (links: DataLink[]) => void;
|
||||
onChange: (links: DataLink[], callback?: () => void) => void;
|
||||
suggestions: VariableSuggestion[];
|
||||
maxLinks?: number;
|
||||
}
|
||||
@@ -30,14 +30,15 @@ export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(({ value, on
|
||||
onChange(value ? [...value, { url: '', title: '' }] : [{ url: '', title: '' }]);
|
||||
};
|
||||
|
||||
const onLinkChanged = (linkIndex: number, newLink: DataLink) => {
|
||||
const onLinkChanged = (linkIndex: number, newLink: DataLink, callback?: () => void) => {
|
||||
onChange(
|
||||
value.map((item, listIndex) => {
|
||||
if (linkIndex === listIndex) {
|
||||
return newLink;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
}),
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import { Alert } from '../Alert/Alert';
|
||||
import { css } from 'emotion';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
interface ErrorInfo {
|
||||
componentStack: string;
|
||||
@@ -44,12 +45,12 @@ export class ErrorBoundary extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
function getAlertPageStyle() {
|
||||
const getStyles = stylesFactory(() => {
|
||||
return css`
|
||||
width: 500px;
|
||||
margin: 64px auto;
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
interface WithAlertBoxProps {
|
||||
title?: string;
|
||||
@@ -85,7 +86,7 @@ export class ErrorBoundaryAlert extends PureComponent<WithAlertBoxProps> {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={getAlertPageStyle()}>
|
||||
<div className={getStyles()}>
|
||||
<h2>{title}</h2>
|
||||
<details style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{error && error.toString()}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { LegendItem } from '../Legend/Legend';
|
||||
import { SeriesColorChangeHandler } from './GraphWithLegend';
|
||||
import { LegendStatsList } from '../Legend/LegendStatsList';
|
||||
import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes';
|
||||
import { GrafanaTheme } from '../../types';
|
||||
|
||||
export interface GraphLegendItemProps {
|
||||
key?: React.Key;
|
||||
@@ -56,6 +58,32 @@ export const GraphLegendListItem: React.FunctionComponent<GraphLegendItemProps>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
row: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
td {
|
||||
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
label: css`
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
itemWrapper: css`
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
value: css`
|
||||
text-align: right;
|
||||
`,
|
||||
yAxisLabel: css`
|
||||
color: ${theme.colors.gray2};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps> = ({
|
||||
item,
|
||||
onSeriesColorChange,
|
||||
@@ -64,27 +92,11 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
|
||||
className,
|
||||
}) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<tr
|
||||
className={cx(
|
||||
css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
td {
|
||||
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<tr className={cx(styles.row, className)}>
|
||||
<td>
|
||||
<span
|
||||
className={css`
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
`}
|
||||
>
|
||||
<span className={styles.itemWrapper}>
|
||||
<LegendSeriesIcon
|
||||
disabled={!!onSeriesColorChange}
|
||||
color={item.color}
|
||||
@@ -102,33 +114,16 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
|
||||
onLabelClick(item, event);
|
||||
}
|
||||
}}
|
||||
className={css`
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
`}
|
||||
className={styles.label}
|
||||
>
|
||||
{item.label}{' '}
|
||||
{item.yAxis === 2 && (
|
||||
<span
|
||||
className={css`
|
||||
color: ${theme.colors.gray2};
|
||||
`}
|
||||
>
|
||||
(right y-axis)
|
||||
</span>
|
||||
)}
|
||||
{item.label} {item.yAxis === 2 && <span className={styles.yAxisLabel}>(right y-axis)</span>}
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
{item.displayValues &&
|
||||
item.displayValues.map((stat, index) => {
|
||||
return (
|
||||
<td
|
||||
className={css`
|
||||
text-align: right;
|
||||
`}
|
||||
key={`${stat.title}-${index}`}
|
||||
>
|
||||
<td className={styles.value} key={`${stat.title}-${index}`}>
|
||||
{stat.text}
|
||||
</td>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Graph, GraphProps } from './Graph';
|
||||
import { LegendRenderOptions, LegendItem, LegendDisplayMode } from '../Legend/Legend';
|
||||
import { GraphLegend } from './GraphLegend';
|
||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
export type SeriesOptionChangeHandler<TOption> = (label: string, option: TOption) => void;
|
||||
export type SeriesColorChangeHandler = SeriesOptionChangeHandler<string>;
|
||||
@@ -24,7 +25,7 @@ export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions {
|
||||
onToggleSort: (sortBy: string) => void;
|
||||
}
|
||||
|
||||
const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({
|
||||
const getGraphWithLegendStyles = stylesFactory(({ placement }: GraphWithLegendProps) => ({
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
flex-direction: ${placement === 'under' ? 'column' : 'row'};
|
||||
@@ -38,7 +39,7 @@ const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({
|
||||
padding: 10px 0;
|
||||
max-height: ${placement === 'under' ? '35%' : 'none'};
|
||||
`,
|
||||
});
|
||||
}));
|
||||
|
||||
const shouldHideLegendItem = (data: GraphSeriesValue[][], hideEmpty = false, hideZero = false) => {
|
||||
const isZeroOnlySeries = data.reduce((acc, current) => acc + (current[1] || 0), 0) === 0;
|
||||
|
||||
@@ -4,6 +4,30 @@ import { InlineList } from '../List/InlineList';
|
||||
import { List } from '../List/List';
|
||||
import { css, cx } from 'emotion';
|
||||
import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes';
|
||||
import { GrafanaTheme } from '../../types';
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
item: css`
|
||||
padding-left: 10px;
|
||||
display: flex;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
white-space: nowrap;
|
||||
`,
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`,
|
||||
section: css`
|
||||
display: flex;
|
||||
`,
|
||||
sectionRight: css`
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
`,
|
||||
}));
|
||||
|
||||
export const LegendList: React.FunctionComponent<LegendComponentProps> = ({
|
||||
items,
|
||||
@@ -12,45 +36,16 @@ export const LegendList: React.FunctionComponent<LegendComponentProps> = ({
|
||||
className,
|
||||
}) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const renderItem = (item: LegendItem, index: number) => {
|
||||
return (
|
||||
<span
|
||||
className={css`
|
||||
padding-left: 10px;
|
||||
display: flex;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
white-space: nowrap;
|
||||
`}
|
||||
>
|
||||
{itemRenderer ? itemRenderer(item, index) : item.label}
|
||||
</span>
|
||||
);
|
||||
return <span className={styles.item}>{itemRenderer ? itemRenderer(item, index) : item.label}</span>;
|
||||
};
|
||||
|
||||
const getItemKey = (item: LegendItem) => `${item.label}`;
|
||||
|
||||
const styles = {
|
||||
wrapper: cx(
|
||||
css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`,
|
||||
className
|
||||
),
|
||||
section: css`
|
||||
display: flex;
|
||||
`,
|
||||
sectionRight: css`
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
`,
|
||||
};
|
||||
|
||||
return placement === 'under' ? (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={cx(styles.wrapper, className)}>
|
||||
<div className={styles.section}>
|
||||
<InlineList items={items.filter(item => item.yAxis === 1)} renderItem={renderItem} getItemKey={getItemKey} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { cx, css } from 'emotion';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
export interface ListProps<T> {
|
||||
items: T[];
|
||||
@@ -12,32 +13,27 @@ interface AbstractListProps<T> extends ListProps<T> {
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((inlineList = false) => ({
|
||||
list: css`
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`,
|
||||
|
||||
item: css`
|
||||
display: ${(inlineList && 'inline-block') || 'block'};
|
||||
`,
|
||||
}));
|
||||
|
||||
export class AbstractList<T> extends React.PureComponent<AbstractListProps<T>> {
|
||||
constructor(props: AbstractListProps<T>) {
|
||||
super(props);
|
||||
this.getListStyles = this.getListStyles.bind(this);
|
||||
}
|
||||
|
||||
getListStyles() {
|
||||
const { inline, className } = this.props;
|
||||
return {
|
||||
list: cx([
|
||||
css`
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`,
|
||||
className,
|
||||
]),
|
||||
item: css`
|
||||
display: ${(inline && 'inline-block') || 'block'};
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, renderItem, getItemKey, className } = this.props;
|
||||
const styles = this.getListStyles();
|
||||
const { items, renderItem, getItemKey, className, inline } = this.props;
|
||||
const styles = getStyles(inline);
|
||||
|
||||
return (
|
||||
<ul className={cx(styles.list, className)}>
|
||||
{items.map((item, i) => {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
exports[`AbstractList allows custom item key 1`] = `
|
||||
<ul
|
||||
className="css-9xf0yn"
|
||||
className="css-1ld8h5b"
|
||||
>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="item1"
|
||||
>
|
||||
<div>
|
||||
@@ -18,7 +18,7 @@ exports[`AbstractList allows custom item key 1`] = `
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="item2"
|
||||
>
|
||||
<div>
|
||||
@@ -31,7 +31,7 @@ exports[`AbstractList allows custom item key 1`] = `
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="item3"
|
||||
>
|
||||
<div>
|
||||
@@ -48,10 +48,10 @@ exports[`AbstractList allows custom item key 1`] = `
|
||||
|
||||
exports[`AbstractList renders items using renderItem prop function 1`] = `
|
||||
<ul
|
||||
className="css-9xf0yn"
|
||||
className="css-1ld8h5b"
|
||||
>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="0"
|
||||
>
|
||||
<div>
|
||||
@@ -64,7 +64,7 @@ exports[`AbstractList renders items using renderItem prop function 1`] = `
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="1"
|
||||
>
|
||||
<div>
|
||||
@@ -77,7 +77,7 @@ exports[`AbstractList renders items using renderItem prop function 1`] = `
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="2"
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -83,9 +83,9 @@ $select-input-bg-disabled: $input-bg-disabled;
|
||||
.gf-form-select-box__multi-value__remove {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
height: 14px;
|
||||
vertical-align: middle;
|
||||
margin-left: 2px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.gf-form-select-box__multi-value__label {
|
||||
@@ -111,6 +111,10 @@ $select-input-bg-disabled: $input-bg-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__placeholder {
|
||||
color: $input-color-placeholder;
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--is-focused .gf-form-select-box__placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export function sharedSingleStatPanelChangedHandler(
|
||||
}
|
||||
|
||||
// Convert value mappings
|
||||
const mappings = convertOldAngulrValueMapping(panel);
|
||||
const mappings = convertOldAngularValueMapping(panel);
|
||||
if (mappings && mappings.length) {
|
||||
defaults.mappings = mappings;
|
||||
}
|
||||
@@ -192,7 +192,7 @@ export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefine
|
||||
/**
|
||||
* Convert the angular single stat mapping to new react style
|
||||
*/
|
||||
export function convertOldAngulrValueMapping(panel: any): ValueMapping[] {
|
||||
export function convertOldAngularValueMapping(panel: any): ValueMapping[] {
|
||||
const mappings: ValueMapping[] = [];
|
||||
|
||||
// Guess the right type based on options
|
||||
|
||||
@@ -5,5 +5,5 @@ export {
|
||||
SingleStatBaseOptions,
|
||||
sharedSingleStatPanelChangedHandler,
|
||||
sharedSingleStatMigrationHandler,
|
||||
convertOldAngulrValueMapping,
|
||||
convertOldAngularValueMapping,
|
||||
} from './SingleStatBaseOptions';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { TimeZonePicker } from './TimeZonePicker';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
|
||||
const TimeZonePickerStories = storiesOf('UI/TimeZonePicker', module);
|
||||
|
||||
TimeZonePickerStories.addDecorator(withCenteredStory);
|
||||
|
||||
TimeZonePickerStories.add('default', () => {
|
||||
return (
|
||||
<UseState
|
||||
initialState={{
|
||||
value: 'europe/stockholm',
|
||||
}}
|
||||
>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<TimeZonePicker
|
||||
value={value.value}
|
||||
onChange={newValue => {
|
||||
action('on selected')(newValue);
|
||||
updateValue({ value: newValue });
|
||||
}}
|
||||
width={20}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { FC } from 'react';
|
||||
import { getTimeZoneGroups, SelectableValue } from '@grafana/data';
|
||||
import { Select } from '..';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
width?: number;
|
||||
|
||||
onChange: (newValue: string) => void;
|
||||
}
|
||||
|
||||
export const TimeZonePicker: FC<Props> = ({ onChange, value, width }) => {
|
||||
const timeZoneGroups = getTimeZoneGroups();
|
||||
|
||||
const groupOptions = timeZoneGroups.map(group => {
|
||||
const options = group.options.map(timeZone => {
|
||||
return {
|
||||
label: timeZone,
|
||||
value: timeZone,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
label: group.label,
|
||||
options,
|
||||
};
|
||||
});
|
||||
|
||||
const selectedValue = groupOptions.map(group => {
|
||||
return group.options.find(option => option.value === value);
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={groupOptions}
|
||||
value={selectedValue}
|
||||
onChange={(newValue: SelectableValue) => onChange(newValue.value)}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,7 @@ export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransfor
|
||||
}) => {
|
||||
return (
|
||||
<StatsPicker
|
||||
width={12}
|
||||
width={25}
|
||||
placeholder="Choose Stat"
|
||||
allowMultiple
|
||||
stats={options.reducers || []}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './components';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './themes';
|
||||
export * from './slate-plugins';
|
||||
|
||||
1
packages/grafana-ui/src/slate-plugins/index.ts
Normal file
1
packages/grafana-ui/src/slate-plugins/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SlatePrism } from './slate-prism';
|
||||
@@ -0,0 +1,3 @@
|
||||
const TOKEN_MARK = 'prism-token';
|
||||
|
||||
export default TOKEN_MARK;
|
||||
160
packages/grafana-ui/src/slate-plugins/slate-prism/index.ts
Normal file
160
packages/grafana-ui/src/slate-plugins/slate-prism/index.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import Prism from 'prismjs';
|
||||
import { Block, Text, Decoration } from 'slate';
|
||||
import { Plugin } from '@grafana/slate-react';
|
||||
import Options, { OptionsFormat } from './options';
|
||||
import TOKEN_MARK from './TOKEN_MARK';
|
||||
|
||||
/**
|
||||
* A Slate plugin to highlight code syntax.
|
||||
*/
|
||||
export function SlatePrism(optsParam: OptionsFormat = {}): Plugin {
|
||||
const opts: Options = new Options(optsParam);
|
||||
|
||||
return {
|
||||
decorateNode: (node, editor, next) => {
|
||||
if (!opts.onlyIn(node)) {
|
||||
return next();
|
||||
}
|
||||
return decorateNode(opts, Block.create(node as Block));
|
||||
},
|
||||
|
||||
renderDecoration: (props, editor, next) =>
|
||||
opts.renderDecoration(
|
||||
{
|
||||
children: props.children,
|
||||
decoration: props.decoration,
|
||||
},
|
||||
editor as any,
|
||||
next
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decoration for a node
|
||||
*/
|
||||
function decorateNode(opts: Options, block: Block) {
|
||||
const grammarName = opts.getSyntax(block);
|
||||
const grammar = Prism.languages[grammarName];
|
||||
if (!grammar) {
|
||||
// Grammar not loaded
|
||||
return [];
|
||||
}
|
||||
|
||||
// Tokenize the whole block text
|
||||
const texts = block.getTexts();
|
||||
const blockText = texts.map(text => text && text.getText()).join('\n');
|
||||
const tokens = Prism.tokenize(blockText, grammar);
|
||||
|
||||
// The list of decorations to return
|
||||
const decorations: Decoration[] = [];
|
||||
let textStart = 0;
|
||||
let textEnd = 0;
|
||||
|
||||
texts.forEach(text => {
|
||||
textEnd = textStart + text!.getText().length;
|
||||
|
||||
let offset = 0;
|
||||
function processToken(token: string | Prism.Token, accu?: string | number) {
|
||||
if (typeof token === 'string') {
|
||||
if (accu) {
|
||||
const decoration = createDecoration({
|
||||
text: text!,
|
||||
textStart,
|
||||
textEnd,
|
||||
start: offset,
|
||||
end: offset + token.length,
|
||||
className: `prism-token token ${accu}`,
|
||||
block,
|
||||
});
|
||||
if (decoration) {
|
||||
decorations.push(decoration);
|
||||
}
|
||||
}
|
||||
offset += token.length;
|
||||
} else {
|
||||
accu = `${accu} ${token.type} ${token.alias || ''}`;
|
||||
|
||||
if (typeof token.content === 'string') {
|
||||
const decoration = createDecoration({
|
||||
text: text!,
|
||||
textStart,
|
||||
textEnd,
|
||||
start: offset,
|
||||
end: offset + token.content.length,
|
||||
className: `prism-token token ${accu}`,
|
||||
block,
|
||||
});
|
||||
if (decoration) {
|
||||
decorations.push(decoration);
|
||||
}
|
||||
|
||||
offset += token.content.length;
|
||||
} else {
|
||||
// When using token.content instead of token.matchedStr, token can be deep
|
||||
for (let i = 0; i < token.content.length; i += 1) {
|
||||
// @ts-ignore
|
||||
processToken(token.content[i], accu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokens.forEach(processToken);
|
||||
textStart = textEnd + 1; // account for added `\n`
|
||||
});
|
||||
|
||||
return decorations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a decoration range for the given text.
|
||||
*/
|
||||
function createDecoration({
|
||||
text,
|
||||
textStart,
|
||||
textEnd,
|
||||
start,
|
||||
end,
|
||||
className,
|
||||
block,
|
||||
}: {
|
||||
text: Text; // The text being decorated
|
||||
textStart: number; // Its start position in the whole text
|
||||
textEnd: number; // Its end position in the whole text
|
||||
start: number; // The position in the whole text where the token starts
|
||||
end: number; // The position in the whole text where the token ends
|
||||
className: string; // The prism token classname
|
||||
block: Block;
|
||||
}): Decoration | null {
|
||||
if (start >= textEnd || end <= textStart) {
|
||||
// Ignore, the token is not in the text
|
||||
return null;
|
||||
}
|
||||
|
||||
// Shrink to this text boundaries
|
||||
start = Math.max(start, textStart);
|
||||
end = Math.min(end, textEnd);
|
||||
|
||||
// Now shift offsets to be relative to this text
|
||||
start -= textStart;
|
||||
end -= textStart;
|
||||
|
||||
const myDec = block.createDecoration({
|
||||
object: 'decoration',
|
||||
anchor: {
|
||||
key: text.key,
|
||||
offset: start,
|
||||
object: 'point',
|
||||
},
|
||||
focus: {
|
||||
key: text.key,
|
||||
offset: end,
|
||||
object: 'point',
|
||||
},
|
||||
type: TOKEN_MARK,
|
||||
data: { className },
|
||||
});
|
||||
|
||||
return myDec;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { Mark, Node, Decoration } from 'slate';
|
||||
import { Editor } from '@grafana/slate-react';
|
||||
import { Record } from 'immutable';
|
||||
|
||||
import TOKEN_MARK from './TOKEN_MARK';
|
||||
|
||||
export interface OptionsFormat {
|
||||
// Determine which node should be highlighted
|
||||
onlyIn?: (node: Node) => boolean;
|
||||
// Returns the syntax for a node that should be highlighted
|
||||
getSyntax?: (node: Node) => string;
|
||||
// Render a highlighting mark in a highlighted node
|
||||
renderMark?: ({ mark, children }: { mark: Mark; children: React.ReactNode }) => void | React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter for code blocks
|
||||
*/
|
||||
function defaultOnlyIn(node: Node): boolean {
|
||||
return node.object === 'block' && node.type === 'code_block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Default getter for syntax
|
||||
*/
|
||||
function defaultGetSyntax(node: Node): string {
|
||||
return 'javascript';
|
||||
}
|
||||
|
||||
/**
|
||||
* Default rendering for decorations
|
||||
*/
|
||||
function defaultRenderDecoration(
|
||||
props: { children: React.ReactNode; decoration: Decoration },
|
||||
editor: Editor,
|
||||
next: () => any
|
||||
): void | React.ReactNode {
|
||||
const { decoration } = props;
|
||||
if (decoration.type !== TOKEN_MARK) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const className = decoration.data.get('className');
|
||||
return <span className={className}>{props.children}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin options
|
||||
*/
|
||||
class Options
|
||||
extends Record({
|
||||
onlyIn: defaultOnlyIn,
|
||||
getSyntax: defaultGetSyntax,
|
||||
renderDecoration: defaultRenderDecoration,
|
||||
})
|
||||
implements OptionsFormat {
|
||||
readonly onlyIn!: (node: Node) => boolean;
|
||||
readonly getSyntax!: (node: Node) => string;
|
||||
readonly renderDecoration!: (
|
||||
{
|
||||
decoration,
|
||||
children,
|
||||
}: {
|
||||
decoration: Decoration;
|
||||
children: React.ReactNode;
|
||||
},
|
||||
editor: Editor,
|
||||
next: () => any
|
||||
) => void | React.ReactNode;
|
||||
|
||||
constructor(props: OptionsFormat) {
|
||||
super(props);
|
||||
}
|
||||
}
|
||||
|
||||
export default Options;
|
||||
@@ -193,6 +193,7 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
|
||||
|
||||
// sidemenu
|
||||
$side-menu-width: 60px;
|
||||
$navbar-padding: 20px;
|
||||
|
||||
// dashboard
|
||||
$dashboard-padding: $space-md;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ThemeContext, withTheme, useTheme } from './ThemeContext';
|
||||
import { getTheme, mockTheme } from './getTheme';
|
||||
import { selectThemeVariant } from './selectThemeVariant';
|
||||
|
||||
export { stylesFactory } from './stylesFactory';
|
||||
export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme };
|
||||
|
||||
31
packages/grafana-ui/src/themes/stylesFactory.test.ts
Normal file
31
packages/grafana-ui/src/themes/stylesFactory.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { stylesFactory } from './stylesFactory';
|
||||
|
||||
interface FakeProps {
|
||||
theme: {
|
||||
a: string;
|
||||
};
|
||||
}
|
||||
describe('Stylesheet creation', () => {
|
||||
it('memoizes results', () => {
|
||||
const spy = jest.fn();
|
||||
|
||||
const getStyles = stylesFactory(({ theme }: FakeProps) => {
|
||||
spy();
|
||||
return {
|
||||
className: `someClass${theme.a}`,
|
||||
};
|
||||
});
|
||||
|
||||
const props: FakeProps = { theme: { a: '-interpolated' } };
|
||||
const changedProps: FakeProps = { theme: { a: '-interpolatedChanged' } };
|
||||
const styles = getStyles(props);
|
||||
getStyles(props);
|
||||
|
||||
expect(spy).toBeCalledTimes(1);
|
||||
expect(styles.className).toBe('someClass-interpolated');
|
||||
|
||||
const styles2 = getStyles(changedProps);
|
||||
expect(spy).toBeCalledTimes(2);
|
||||
expect(styles2.className).toBe('someClass-interpolatedChanged');
|
||||
});
|
||||
});
|
||||
12
packages/grafana-ui/src/themes/stylesFactory.ts
Normal file
12
packages/grafana-ui/src/themes/stylesFactory.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import memoizeOne from 'memoize-one';
|
||||
// import { KeyValue } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* Creates memoized version of styles creator
|
||||
* @param stylesCreator function accepting dependencies based on which styles are created
|
||||
*/
|
||||
export function stylesFactory<ResultFn extends (this: any, ...newArgs: any[]) => ReturnType<ResultFn>>(
|
||||
stylesCreator: ResultFn
|
||||
) {
|
||||
return memoizeOne(stylesCreator);
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export interface PanelData {
|
||||
series: DataFrame[];
|
||||
request?: DataQueryRequest;
|
||||
error?: DataQueryError;
|
||||
// Contains the range from the request or a shifted time range if a request uses relative time
|
||||
timeRange: TimeRange;
|
||||
}
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
@@ -35,7 +37,11 @@ export interface PanelProps<T = any> {
|
||||
|
||||
export interface PanelEditorProps<T = any> {
|
||||
options: T;
|
||||
onOptionsChange: (options: T) => void;
|
||||
onOptionsChange: (
|
||||
options: T,
|
||||
// callback can be used to run something right after update.
|
||||
callback?: () => void
|
||||
) => void;
|
||||
data: PanelData;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export enum PluginType {
|
||||
panel = 'panel',
|
||||
datasource = 'datasource',
|
||||
app = 'app',
|
||||
renderer = 'renderer',
|
||||
}
|
||||
|
||||
export interface PluginMeta<T extends {} = KeyValue> {
|
||||
|
||||
@@ -50,13 +50,13 @@ function getTitleTemplate(title: string | undefined, stats: string[], data?: Dat
|
||||
|
||||
const parts: string[] = [];
|
||||
if (stats.length > 1) {
|
||||
parts.push('$' + VAR_CALC);
|
||||
parts.push('${' + VAR_CALC + '}');
|
||||
}
|
||||
if (data.length > 1) {
|
||||
parts.push('${' + VAR_SERIES_NAME + '}');
|
||||
}
|
||||
if (fieldCount > 1 || !parts.length) {
|
||||
parts.push('$' + VAR_FIELD_NAME);
|
||||
parts.push('${' + VAR_FIELD_NAME + '}');
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// @ts-ignore
|
||||
import { Block, Document, Text, Value } from 'slate';
|
||||
import { Block, Document, Text, Value, SchemaProperties } from 'slate';
|
||||
|
||||
const SCHEMA = {
|
||||
blocks: {
|
||||
paragraph: 'paragraph',
|
||||
codeblock: 'code_block',
|
||||
codeline: 'code_line',
|
||||
export const SCHEMA: SchemaProperties = {
|
||||
document: {
|
||||
nodes: [
|
||||
{
|
||||
match: [{ type: 'paragraph' }, { type: 'code_block' }, { type: 'code_line' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
inlines: {},
|
||||
marks: {},
|
||||
};
|
||||
|
||||
export const makeFragment = (text: string, syntax?: string) => {
|
||||
export const makeFragment = (text: string, syntax?: string): Document => {
|
||||
const lines = text.split('\n').map(line =>
|
||||
Block.create({
|
||||
type: 'code_line',
|
||||
nodes: [Text.create(line)],
|
||||
} as any)
|
||||
})
|
||||
);
|
||||
|
||||
const block = Block.create({
|
||||
@@ -25,18 +25,17 @@ export const makeFragment = (text: string, syntax?: string) => {
|
||||
},
|
||||
type: 'code_block',
|
||||
nodes: lines,
|
||||
} as any);
|
||||
});
|
||||
|
||||
return Document.create({
|
||||
nodes: [block],
|
||||
});
|
||||
};
|
||||
|
||||
export const makeValue = (text: string, syntax?: string) => {
|
||||
export const makeValue = (text: string, syntax?: string): Value => {
|
||||
const fragment = makeFragment(text, syntax);
|
||||
|
||||
return Value.create({
|
||||
document: fragment,
|
||||
SCHEMA,
|
||||
} as any);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -154,7 +154,7 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{ name: 'gigabytes/sec', id: 'GBs', fn: decimalSIPrefix('Bs', 3) },
|
||||
{ name: 'gigabits/sec', id: 'Gbits', fn: decimalSIPrefix('bps', 3) },
|
||||
{ name: 'terabytes/sec', id: 'TBs', fn: decimalSIPrefix('Bs', 4) },
|
||||
{ name: 'terabits/sec', id: 'Gbits', fn: decimalSIPrefix('bps', 4) },
|
||||
{ name: 'terabits/sec', id: 'Tbits', fn: decimalSIPrefix('bps', 4) },
|
||||
{ name: 'petabytes/sec', id: 'PBs', fn: decimalSIPrefix('Bs', 5) },
|
||||
{ name: 'petabits/sec', id: 'Pbits', fn: decimalSIPrefix('bps', 5) },
|
||||
],
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { toFixed, getValueFormat } from './valueFormats';
|
||||
|
||||
describe('valueFormats', () => {
|
||||
describe('toFixed with edge cases', () => {
|
||||
it('should handle non number input gracefully', () => {
|
||||
expect(toFixed(NaN)).toBe('NaN');
|
||||
expect(toFixed(Number.NEGATIVE_INFINITY)).toBe('-Inf');
|
||||
expect(toFixed(Number.POSITIVE_INFINITY)).toBe('Inf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toFixed and negative decimals', () => {
|
||||
it('should treat as zero decimals', () => {
|
||||
const str = toFixed(186.123, -2);
|
||||
|
||||
@@ -33,6 +33,12 @@ export function toFixed(value: number, decimals?: DecimalCount): string {
|
||||
if (value === null) {
|
||||
return '';
|
||||
}
|
||||
if (value === Number.NEGATIVE_INFINITY) {
|
||||
return '-Inf';
|
||||
}
|
||||
if (value === Number.POSITIVE_INFINITY) {
|
||||
return 'Inf';
|
||||
}
|
||||
|
||||
const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1;
|
||||
const formatted = String(Math.round(value * factor) / factor);
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
"compilerOptions": {
|
||||
"rootDirs": [".", "stories"],
|
||||
"typeRoots": ["./node_modules/@types", "types"],
|
||||
"baseUrl": "./node_modules/@types",
|
||||
"paths": {
|
||||
"@grafana/slate-react": ["slate-react"]
|
||||
},
|
||||
"declarationDir": "dist",
|
||||
"outDir": "compiled"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
RUN apk add --no-cache ca-certificates bash
|
||||
RUN apk add --no-cache ca-certificates bash tzdata && \
|
||||
apk add --no-cache --upgrade --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main openssl musl-utils
|
||||
|
||||
# PhantomJS
|
||||
RUN if [ `arch` = "x86_64" ]; then \
|
||||
|
||||
@@ -135,3 +135,15 @@ func Respond(status int, body interface{}) *NormalResponse {
|
||||
header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
type RedirectResponse struct {
|
||||
location string
|
||||
}
|
||||
|
||||
func (r *RedirectResponse) WriteTo(ctx *m.ReqContext) {
|
||||
ctx.Redirect(r.location)
|
||||
}
|
||||
|
||||
func Redirect(location string) *RedirectResponse {
|
||||
return &RedirectResponse{location: location}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ type LDAPAttribute struct {
|
||||
}
|
||||
|
||||
// RoleDTO is a serializer for mapped roles from LDAP
|
||||
type RoleDTO struct {
|
||||
type LDAPRoleDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
OrgName string `json:"orgName"`
|
||||
OrgRole models.RoleType `json:"orgRole"`
|
||||
@@ -49,7 +49,7 @@ type LDAPUserDTO struct {
|
||||
Username *LDAPAttribute `json:"login"`
|
||||
IsGrafanaAdmin *bool `json:"isGrafanaAdmin"`
|
||||
IsDisabled bool `json:"isDisabled"`
|
||||
OrgRoles []RoleDTO `json:"roles"`
|
||||
OrgRoles []LDAPRoleDTO `json:"roles"`
|
||||
Teams []models.TeamOrgGroupDTO `json:"teams"`
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ func (user *LDAPUserDTO) FetchOrgs() error {
|
||||
}
|
||||
|
||||
for i, orgDTO := range user.OrgRoles {
|
||||
if orgDTO.OrgId < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
orgName := orgNamesById[orgDTO.OrgId]
|
||||
|
||||
if orgName != "" {
|
||||
@@ -256,7 +260,7 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response {
|
||||
user, serverConfig, err := ldap.User(username)
|
||||
|
||||
if user == nil {
|
||||
return Error(http.StatusNotFound, "No user was found on the LDAP server(s)", err)
|
||||
return Error(http.StatusNotFound, "No user was found in the LDAP server(s) with that username", err)
|
||||
}
|
||||
|
||||
logger.Debug("user found", "user", user)
|
||||
@@ -272,22 +276,37 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response {
|
||||
IsDisabled: user.IsDisabled,
|
||||
}
|
||||
|
||||
orgRoles := []RoleDTO{}
|
||||
orgRoles := []LDAPRoleDTO{}
|
||||
|
||||
for _, g := range serverConfig.Groups {
|
||||
role := &RoleDTO{}
|
||||
// Need to iterate based on the config groups as only the first match for an org is used
|
||||
// We are showing all matches as that should help in understanding why one match wins out
|
||||
// over another.
|
||||
for _, configGroup := range serverConfig.Groups {
|
||||
for _, userGroup := range user.Groups {
|
||||
if configGroup.GroupDN == userGroup {
|
||||
r := &LDAPRoleDTO{GroupDN: configGroup.GroupDN, OrgId: configGroup.OrgId, OrgRole: configGroup.OrgRole}
|
||||
orgRoles = append(orgRoles, *r)
|
||||
break
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
if isMatchToLDAPGroup(user, g) {
|
||||
role.OrgId = g.OrgID
|
||||
role.OrgRole = user.OrgRoles[g.OrgID]
|
||||
role.GroupDN = g.GroupDN
|
||||
// Then, we find what we did not match by inspecting the list of groups returned from
|
||||
// LDAP against what we have already matched above.
|
||||
for _, userGroup := range user.Groups {
|
||||
var matched bool
|
||||
|
||||
orgRoles = append(orgRoles, *role)
|
||||
} else {
|
||||
role.OrgId = g.OrgID
|
||||
role.GroupDN = g.GroupDN
|
||||
for _, orgRole := range orgRoles {
|
||||
if orgRole.GroupDN == userGroup { // we already matched it
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
orgRoles = append(orgRoles, *role)
|
||||
if !matched {
|
||||
r := &LDAPRoleDTO{GroupDN: userGroup}
|
||||
orgRoles = append(orgRoles, *r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,12 +331,6 @@ func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response {
|
||||
return JSON(200, u)
|
||||
}
|
||||
|
||||
// isMatchToLDAPGroup determines if we were able to match an LDAP group to an organization+role.
|
||||
// Since we allow one role per organization. If it's set, we were able to match it.
|
||||
func isMatchToLDAPGroup(user *models.ExternalUserInfo, groupConfig *ldap.GroupToOrgRole) bool {
|
||||
return user.OrgRoles[groupConfig.OrgID] == groupConfig.OrgRole
|
||||
}
|
||||
|
||||
// splitName receives the full name of a user and splits it into two parts: A name and a surname.
|
||||
func splitName(name string) (string, string) {
|
||||
names := util.SplitString(name)
|
||||
|
||||
@@ -94,7 +94,7 @@ func TestGetUserFromLDAPApiEndpoint_UserNotFound(t *testing.T) {
|
||||
sc := getUserFromLDAPContext(t, "/api/admin/ldap/user-that-does-not-exist")
|
||||
|
||||
require.Equal(t, sc.resp.Code, http.StatusNotFound)
|
||||
assert.JSONEq(t, "{\"message\":\"No user was found on the LDAP server(s)\"}", sc.resp.Body.String())
|
||||
assert.JSONEq(t, "{\"message\":\"No user was found in the LDAP server(s) with that username\"}", sc.resp.Body.String())
|
||||
}
|
||||
|
||||
func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
|
||||
@@ -103,6 +103,7 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
|
||||
Name: "John Doe",
|
||||
Email: "john.doe@example.com",
|
||||
Login: "johndoe",
|
||||
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org"},
|
||||
OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN, 2: models.ROLE_VIEWER},
|
||||
IsGrafanaAdmin: &isAdmin,
|
||||
}
|
||||
@@ -117,12 +118,12 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
|
||||
Groups: []*ldap.GroupToOrgRole{
|
||||
{
|
||||
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
|
||||
OrgID: 1,
|
||||
OrgId: 1,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
},
|
||||
{
|
||||
GroupDN: "cn=admins,ou=groups,dc=grafana2,dc=org",
|
||||
OrgID: 2,
|
||||
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
|
||||
OrgId: 2,
|
||||
OrgRole: models.ROLE_VIEWER,
|
||||
},
|
||||
},
|
||||
@@ -147,7 +148,7 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
|
||||
|
||||
sc := getUserFromLDAPContext(t, "/api/admin/ldap/johndoe")
|
||||
|
||||
require.Equal(t, sc.resp.Code, http.StatusBadRequest)
|
||||
require.Equal(t, http.StatusBadRequest, sc.resp.Code)
|
||||
|
||||
expected := `
|
||||
{
|
||||
@@ -164,6 +165,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
|
||||
Name: "John Doe",
|
||||
Email: "john.doe@example.com",
|
||||
Login: "johndoe",
|
||||
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org", "another-group-not-matched"},
|
||||
OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN},
|
||||
IsGrafanaAdmin: &isAdmin,
|
||||
}
|
||||
@@ -178,7 +180,12 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
|
||||
Groups: []*ldap.GroupToOrgRole{
|
||||
{
|
||||
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
|
||||
OrgID: 1,
|
||||
OrgId: 1,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
},
|
||||
{
|
||||
GroupDN: "cn=admins2,ou=groups,dc=grafana,dc=org",
|
||||
OrgId: 1,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
},
|
||||
},
|
||||
@@ -203,7 +210,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
|
||||
|
||||
sc := getUserFromLDAPContext(t, "/api/admin/ldap/johndoe")
|
||||
|
||||
require.Equal(t, sc.resp.Code, http.StatusOK)
|
||||
assert.Equal(t, sc.resp.Code, http.StatusOK)
|
||||
|
||||
expected := `
|
||||
{
|
||||
@@ -222,7 +229,8 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
|
||||
"isGrafanaAdmin": true,
|
||||
"isDisabled": false,
|
||||
"roles": [
|
||||
{ "orgId": 1, "orgRole": "Admin", "orgName": "Main Org.", "groupDN": "cn=admins,ou=groups,dc=grafana,dc=org" }
|
||||
{ "orgId": 1, "orgRole": "Admin", "orgName": "Main Org.", "groupDN": "cn=admins,ou=groups,dc=grafana,dc=org" },
|
||||
{ "orgId": 0, "orgRole": "", "orgName": "", "groupDN": "another-group-not-matched" }
|
||||
],
|
||||
"teams": null
|
||||
}
|
||||
@@ -237,6 +245,7 @@ func TestGetUserFromLDAPApiEndpoint_WithTeamHandler(t *testing.T) {
|
||||
Name: "John Doe",
|
||||
Email: "john.doe@example.com",
|
||||
Login: "johndoe",
|
||||
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org"},
|
||||
OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN},
|
||||
IsGrafanaAdmin: &isAdmin,
|
||||
}
|
||||
@@ -251,7 +260,7 @@ func TestGetUserFromLDAPApiEndpoint_WithTeamHandler(t *testing.T) {
|
||||
Groups: []*ldap.GroupToOrgRole{
|
||||
{
|
||||
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
|
||||
OrgID: 1,
|
||||
OrgId: 1,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -495,6 +495,7 @@ func TestDSRouteRule(t *testing.T) {
|
||||
createAuthTest(m.DS_ES, AUTHTYPE_BASIC, AUTHCHECK_HEADER, true),
|
||||
}
|
||||
for _, test := range tests {
|
||||
m.ClearDSDecryptionCache()
|
||||
runDatasourceAuthTest(test)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -157,32 +157,34 @@ func latestSupportedVersion(plugin *m.Plugin) *m.Version {
|
||||
|
||||
// SelectVersion returns latest version if none is specified or the specified version. If the version string is not
|
||||
// matched to existing version it errors out. It also errors out if version that is matched is not available for current
|
||||
// os and platform.
|
||||
// os and platform. It expects plugin.Versions to be sorted so the newest version is first.
|
||||
func SelectVersion(plugin *m.Plugin, version string) (*m.Version, error) {
|
||||
var ver *m.Version
|
||||
if version == "" {
|
||||
ver = &plugin.Versions[0]
|
||||
}
|
||||
|
||||
for _, v := range plugin.Versions {
|
||||
if v.Version == version {
|
||||
ver = &v
|
||||
}
|
||||
}
|
||||
|
||||
if ver == nil {
|
||||
return nil, xerrors.New("Could not find the version you're looking for")
|
||||
}
|
||||
var ver m.Version
|
||||
|
||||
latestForArch := latestSupportedVersion(plugin)
|
||||
if latestForArch == nil {
|
||||
return nil, xerrors.New("Plugin is not supported on your architecture and os.")
|
||||
}
|
||||
|
||||
if latestForArch.Version == ver.Version {
|
||||
return ver, nil
|
||||
if version == "" {
|
||||
return latestForArch, nil
|
||||
}
|
||||
return nil, xerrors.Errorf("Version you want is not supported on your architecture and os. Latest suitable version is %v", latestForArch.Version)
|
||||
for _, v := range plugin.Versions {
|
||||
if v.Version == version {
|
||||
ver = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(ver.Version) == 0 {
|
||||
return nil, xerrors.New("Could not find the version you're looking for")
|
||||
}
|
||||
|
||||
if !supportsCurrentArch(&ver) {
|
||||
return nil, xerrors.Errorf("Version you want is not supported on your architecture and os. Latest suitable version is %v", latestForArch.Version)
|
||||
}
|
||||
|
||||
return &ver, nil
|
||||
}
|
||||
|
||||
func RemoveGitBuildFromName(pluginName, filename string) string {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFoldernameReplacement(t *testing.T) {
|
||||
func TestFolderNameReplacement(t *testing.T) {
|
||||
Convey("path containing git commit path", t, func() {
|
||||
pluginName := "datasource-plugin-kairosdb"
|
||||
|
||||
@@ -134,7 +134,68 @@ func TestIsPathSafe(t *testing.T) {
|
||||
assert.False(t, isPathSafe("../../", dest))
|
||||
assert.False(t, isPathSafe("../../test", dest))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSelectVersion(t *testing.T) {
|
||||
t.Run("Should return error when requested version does not exist", func(t *testing.T) {
|
||||
_, err := SelectVersion(
|
||||
makePluginWithVersions(versionArg{Version: "version"}),
|
||||
"1.1.1",
|
||||
)
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should return error when no version supports current arch", func(t *testing.T) {
|
||||
_, err := SelectVersion(
|
||||
makePluginWithVersions(versionArg{Version: "version", Arch: []string{"non-existent"}}),
|
||||
"",
|
||||
)
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should return error when requested version does not support current arch", func(t *testing.T) {
|
||||
_, err := SelectVersion(
|
||||
makePluginWithVersions(
|
||||
versionArg{Version: "2.0.0"},
|
||||
versionArg{Version: "1.1.1", Arch: []string{"non-existent"}},
|
||||
),
|
||||
"1.1.1",
|
||||
)
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should return latest available for arch when no version specified", func(t *testing.T) {
|
||||
ver, err := SelectVersion(
|
||||
makePluginWithVersions(
|
||||
versionArg{Version: "2.0.0", Arch: []string{"non-existent"}},
|
||||
versionArg{Version: "1.0.0"},
|
||||
),
|
||||
"",
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "1.0.0", ver.Version)
|
||||
})
|
||||
|
||||
t.Run("Should return latest version when no version specified", func(t *testing.T) {
|
||||
ver, err := SelectVersion(
|
||||
makePluginWithVersions(versionArg{Version: "2.0.0"}, versionArg{Version: "1.0.0"}),
|
||||
"",
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "2.0.0", ver.Version)
|
||||
})
|
||||
|
||||
t.Run("Should return requested version", func(t *testing.T) {
|
||||
ver, err := SelectVersion(
|
||||
makePluginWithVersions(
|
||||
versionArg{Version: "2.0.0"},
|
||||
versionArg{Version: "1.0.0"},
|
||||
),
|
||||
"1.0.0",
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "1.0.0", ver.Version)
|
||||
})
|
||||
}
|
||||
|
||||
func setupPluginInstallCmd(t *testing.T, pluginDir string) utils.CommandLine {
|
||||
@@ -199,3 +260,35 @@ func skipWindows(t *testing.T) {
|
||||
t.Skip("Skipping test on Windows")
|
||||
}
|
||||
}
|
||||
|
||||
type versionArg struct {
|
||||
Version string
|
||||
Arch []string
|
||||
}
|
||||
|
||||
func makePluginWithVersions(versions ...versionArg) *models.Plugin {
|
||||
plugin := &models.Plugin{
|
||||
Id: "",
|
||||
Category: "",
|
||||
Versions: []models.Version{},
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
ver := models.Version{
|
||||
Version: version.Version,
|
||||
Commit: fmt.Sprintf("commit_%s", version.Version),
|
||||
Url: fmt.Sprintf("url_%s", version.Version),
|
||||
}
|
||||
if version.Arch != nil {
|
||||
ver.Arch = map[string]models.ArchMeta{}
|
||||
for _, arch := range version.Arch {
|
||||
ver.Arch[arch] = models.ArchMeta{
|
||||
Md5: fmt.Sprintf("md5_%s", arch),
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.Versions = append(plugin.Versions, ver)
|
||||
}
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (ds *DataSource) DecryptedPassword() string {
|
||||
|
||||
// decryptedValue returns decrypted value from secureJsonData
|
||||
func (ds *DataSource) decryptedValue(field string, fallback string) string {
|
||||
if value, ok := ds.SecureJsonData.DecryptedValue(field); ok {
|
||||
if value, ok := ds.DecryptedValue(field); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
|
||||
@@ -110,3 +110,49 @@ func (ds *DataSource) GetTLSConfig() (*tls.Config, error) {
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
type cachedDecryptedJSON struct {
|
||||
updated time.Time
|
||||
json map[string]string
|
||||
}
|
||||
|
||||
type secureJSONDecryptionCache struct {
|
||||
cache map[int64]cachedDecryptedJSON
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
var dsDecryptionCache = secureJSONDecryptionCache{
|
||||
cache: make(map[int64]cachedDecryptedJSON),
|
||||
}
|
||||
|
||||
// DecryptedValues returns cached decrypted values from secureJsonData.
|
||||
func (ds *DataSource) DecryptedValues() map[string]string {
|
||||
dsDecryptionCache.Lock()
|
||||
defer dsDecryptionCache.Unlock()
|
||||
|
||||
if item, present := dsDecryptionCache.cache[ds.Id]; present && ds.Updated.Equal(item.updated) {
|
||||
return item.json
|
||||
}
|
||||
|
||||
json := ds.SecureJsonData.Decrypt()
|
||||
dsDecryptionCache.cache[ds.Id] = cachedDecryptedJSON{
|
||||
updated: ds.Updated,
|
||||
json: json,
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
|
||||
// DecryptedValue returns cached decrypted value from cached secureJsonData.
|
||||
func (ds *DataSource) DecryptedValue(key string) (string, bool) {
|
||||
value, exists := ds.DecryptedValues()[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// ClearDSDecryptionCache clears the datasource decryption cache.
|
||||
func ClearDSDecryptionCache() {
|
||||
dsDecryptionCache.Lock()
|
||||
defer dsDecryptionCache.Unlock()
|
||||
|
||||
dsDecryptionCache.cache = make(map[int64]cachedDecryptedJSON)
|
||||
}
|
||||
|
||||
@@ -6,15 +6,16 @@ import (
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
//nolint:goconst
|
||||
func TestDataSourceCache(t *testing.T) {
|
||||
func TestDataSourceProxyCache(t *testing.T) {
|
||||
Convey("When caching a datasource proxy", t, func() {
|
||||
clearCache()
|
||||
clearDSProxyCache()
|
||||
ds := DataSource{
|
||||
Id: 1,
|
||||
Url: "http://k8s:8001",
|
||||
@@ -36,13 +37,13 @@ func TestDataSourceCache(t *testing.T) {
|
||||
Convey("Should have no TLS client certificate configured", func() {
|
||||
So(len(t1.TLSClientConfig.Certificates), ShouldEqual, 0)
|
||||
})
|
||||
Convey("Should have no user-supplied TLS CA onfigured", func() {
|
||||
Convey("Should have no user-supplied TLS CA configured", func() {
|
||||
So(t1.TLSClientConfig.RootCAs, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When caching a datasource proxy then updating it", t, func() {
|
||||
clearCache()
|
||||
clearDSProxyCache()
|
||||
setting.SecretKey = "password"
|
||||
|
||||
json := simplejson.New()
|
||||
@@ -84,7 +85,7 @@ func TestDataSourceCache(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When caching a datasource proxy with TLS client authentication enabled", t, func() {
|
||||
clearCache()
|
||||
clearDSProxyCache()
|
||||
setting.SecretKey = "password"
|
||||
|
||||
json := simplejson.New()
|
||||
@@ -118,7 +119,7 @@ func TestDataSourceCache(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When caching a datasource proxy with a user-supplied TLS CA", t, func() {
|
||||
clearCache()
|
||||
clearDSProxyCache()
|
||||
setting.SecretKey = "password"
|
||||
|
||||
json := simplejson.New()
|
||||
@@ -147,7 +148,7 @@ func TestDataSourceCache(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When caching a datasource proxy when user skips TLS verification", t, func() {
|
||||
clearCache()
|
||||
clearDSProxyCache()
|
||||
|
||||
json := simplejson.New()
|
||||
json.Set("tlsSkipVerify", true)
|
||||
@@ -168,7 +169,64 @@ func TestDataSourceCache(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func clearCache() {
|
||||
func TestDataSourceDecryptionCache(t *testing.T) {
|
||||
Convey("When datasource hasn't been updated, encrypted JSON should be fetched from cache", t, func() {
|
||||
ClearDSDecryptionCache()
|
||||
|
||||
ds := DataSource{
|
||||
Id: 1,
|
||||
Type: DS_INFLUXDB_08,
|
||||
JsonData: simplejson.New(),
|
||||
User: "user",
|
||||
SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{
|
||||
"password": "password",
|
||||
}),
|
||||
}
|
||||
|
||||
// Populate cache
|
||||
password, ok := ds.DecryptedValue("password")
|
||||
So(password, ShouldEqual, "password")
|
||||
So(ok, ShouldBeTrue)
|
||||
|
||||
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
|
||||
"password": "",
|
||||
})
|
||||
|
||||
password, ok = ds.DecryptedValue("password")
|
||||
So(password, ShouldEqual, "password")
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When datasource is updated, encrypted JSON should not be fetched from cache", t, func() {
|
||||
ClearDSDecryptionCache()
|
||||
|
||||
ds := DataSource{
|
||||
Id: 1,
|
||||
Type: DS_INFLUXDB_08,
|
||||
JsonData: simplejson.New(),
|
||||
User: "user",
|
||||
SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{
|
||||
"password": "password",
|
||||
}),
|
||||
}
|
||||
|
||||
// Populate cache
|
||||
password, ok := ds.DecryptedValue("password")
|
||||
So(password, ShouldEqual, "password")
|
||||
So(ok, ShouldBeTrue)
|
||||
|
||||
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
|
||||
"password": "",
|
||||
})
|
||||
ds.Updated = time.Now()
|
||||
|
||||
password, ok = ds.DecryptedValue("password")
|
||||
So(password, ShouldEqual, "")
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func clearDSProxyCache() {
|
||||
ptc.Lock()
|
||||
defer ptc.Unlock()
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
|
||||
|
||||
// External plugins need a module.js file for SystemJS to load
|
||||
if !strings.HasPrefix(pluginJsonFilePath, setting.StaticRootPath) {
|
||||
if !strings.HasPrefix(pluginJsonFilePath, setting.StaticRootPath) && !scanner.IsBackendOnlyPlugin(pluginCommon.Type) {
|
||||
module := filepath.Join(filepath.Dir(pluginJsonFilePath), "module.js")
|
||||
if _, err := os.Stat(module); os.IsNotExist(err) {
|
||||
plog.Warn("Plugin missing module.js",
|
||||
@@ -231,6 +231,10 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
return loader.Load(jsonParser, currentDir)
|
||||
}
|
||||
|
||||
func (scanner *PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
|
||||
return pluginType == "renderer"
|
||||
}
|
||||
|
||||
func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
plug, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
|
||||
@@ -42,4 +42,18 @@ func TestPluginScans(t *testing.T) {
|
||||
So(Apps["test-app"].Info.Screenshots[1].Path, ShouldEqual, "public/plugins/test-app/img/screenshot2.png")
|
||||
})
|
||||
|
||||
Convey("When checking if renderer is backend only plugin", t, func() {
|
||||
pluginScanner := &PluginScanner{}
|
||||
result := pluginScanner.IsBackendOnlyPlugin("renderer")
|
||||
|
||||
So(result, ShouldEqual, true)
|
||||
})
|
||||
|
||||
Convey("When checking if app is backend only plugin", t, func() {
|
||||
pluginScanner := &PluginScanner{}
|
||||
result := pluginScanner.IsBackendOnlyPlugin("app")
|
||||
|
||||
So(result, ShouldEqual, false)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ type notificationService struct {
|
||||
func (n *notificationService) SendIfNeeded(context *EvalContext) error {
|
||||
notifierStates, err := n.getNeededNotifiers(context.Rule.OrgID, context.Rule.Notifications, context)
|
||||
if err != nil {
|
||||
n.log.Error("Failed to get alert notifiers", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -109,10 +110,11 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi
|
||||
err := n.sendNotification(evalContext, notifierState)
|
||||
if err != nil {
|
||||
n.log.Error("failed to send notification", "uid", notifierState.notifier.GetNotifierUID(), "error", err)
|
||||
return err
|
||||
if evalContext.IsTestRun {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,13 @@ func (pn *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
pn.log.Info("Notifying Pagerduty", "event_type", eventType)
|
||||
|
||||
payloadJSON := simplejson.New()
|
||||
payloadJSON.Set("summary", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
|
||||
|
||||
summary := evalContext.Rule.Name + " - " + evalContext.Rule.Message
|
||||
if len(summary) > 1024 {
|
||||
summary = summary[0:1024]
|
||||
}
|
||||
payloadJSON.Set("summary", summary)
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
payloadJSON.Set("source", hostname)
|
||||
}
|
||||
|
||||
@@ -408,12 +408,12 @@ func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserIn
|
||||
|
||||
for _, group := range server.Config.Groups {
|
||||
// only use the first match for each org
|
||||
if extUser.OrgRoles[group.OrgID] != "" {
|
||||
if extUser.OrgRoles[group.OrgId] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if isMemberOf(memberOf, group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgID] = group.OrgRole
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package ldap
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
)
|
||||
|
||||
func TestLDAPPrivateMethods(t *testing.T) {
|
||||
@@ -124,7 +123,7 @@ func TestLDAPPrivateMethods(t *testing.T) {
|
||||
Config: &ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{
|
||||
OrgID: 1,
|
||||
OrgId: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -162,7 +161,7 @@ func TestLDAPPrivateMethods(t *testing.T) {
|
||||
Config: &ServerConfig{
|
||||
Groups: []*GroupToOrgRole{
|
||||
{
|
||||
OrgID: 1,
|
||||
OrgId: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user