diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 8086a6b86e5..00000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ -Read before posting: - -- Questions should be posted to https://community.grafana.com. Please search there and here on GitHub for similar issues before creating a new issue. -- Checkout FAQ: https://community.grafana.com/c/howto/faq -- Checkout How to troubleshoot metric query issues: https://community.grafana.com/t/how-to-troubleshoot-metric-query-issues/50 - -Please include this information: -### What Grafana version are you using? -### What datasource are you using? -### What OS are you running grafana on? -### What did you do? -### What was the expected result? -### What happened instead? -### If related to metric query / data viz: -### Include raw network request & response: get by opening Chrome Dev Tools (F12, Ctrl+Shift+I on windows, Cmd+Opt+I on Mac), go the network tab. - diff --git a/.github/ISSUE_TEMPLATE/1-bug_report.md b/.github/ISSUE_TEMPLATE/1-bug_report.md new file mode 100644 index 00000000000..eb87a7fc1c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Report a bug you found when using Grafana +labels: 'type: bug' +--- + + + +**What happened**: + +**What you expected to happen**: + +**How to reproduce it (as minimally and precisely as possible)**: + +**Anything else we need to know?**: + +**Environment**: +- Grafana version: +- Data source type & version: +- OS Grafana is installed on: +- User OS & Browser: +- Grafana plugins: +- Others: diff --git a/.github/ISSUE_TEMPLATE/2-feature_request.md b/.github/ISSUE_TEMPLATE/2-feature_request.md new file mode 100644 index 00000000000..ccb9f3cc391 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature_request.md @@ -0,0 +1,11 @@ +--- +name: Enhancement request +about: Suggest an enhancement or new feature for the Grafana project +labels: 'type: feature request' +--- + + + +**What would you like to be added**: + +**Why is this needed**: diff --git a/.github/ISSUE_TEMPLATE/3-accessibility.md b/.github/ISSUE_TEMPLATE/3-accessibility.md new file mode 100644 index 00000000000..51b3ecc255b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-accessibility.md @@ -0,0 +1,26 @@ +--- +name: Accessibility issue +about: Help make Grafana be better at keyboard navigation, screen-readable and accessible to all. +labels: 'type: accessibility' +--- + + + +**Steps to reproduce**: + +**Actual Result**: + +**Expected Result** + +**Relevant WCAG Criteria:** [#.#.# WCAG Criterion](link to https://www.w3.org/WAI/WCAG21/quickref/?versions=2.0) + +**Environment**: +- Grafana version: +- Data source type & version: +- User OS & Browser: +- Others: diff --git a/.github/ISSUE_TEMPLATE/4-question.md b/.github/ISSUE_TEMPLATE/4-question.md new file mode 100644 index 00000000000..3e3a20e49ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-question.md @@ -0,0 +1,14 @@ +--- +name: Support request +about: 'Question or support request relating to using Grafana' +title: '' +labels: '' +assignees: '' +--- + +STOP -- PLEASE READ! + +GitHub is not the right place for questions and support requests. + +Please ask questions on our community site: [https://community.grafana.com/](https://community.grafana.com/) + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 69ce3168730..22642808fa4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,27 @@ -* Follow the contribution guidelines in [`CONTRIBUTING.md`](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md) -* Rebase your PR if it gets out of sync with master -* Include `closes #` or a link to the issue in the description + + +**What this PR does / why we need it**: + +**Which issue(s) this PR fixes**: + +Fixes # + +**Special notes for your reviewer**: + +**Release note**: + +```release-note + +``` diff --git a/.prettierignore b/.prettierignore index b7a33870ddd..336d03e2551 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,5 @@ pkg/ node_modules public/vendor/ vendor/ +data/ diff --git a/CHANGELOG.md b/CHANGELOG.md index cb2a344b014..5483529f75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,42 @@ # 6.1.0 (unreleased) +### New Features +* **Prometheus**: adhoc filter support [#8253](https://github.com/grafana/grafana/issues/8253), thx [@mtanda](https://github.com/mtanda) + ### Minor * **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow) +* **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683) +* **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683) ### Bug Fixes * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506) * **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239) * **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739) * **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619) +* **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019) -# 6.0.1 (unreleased) +# 6.0.1 (2019-03-06) ### Bug Fixes * **Metrics**: Fixes broken usagestats metrics for /metrics [#15651](https://github.com/grafana/grafana/issues/15651) * **Dashboard**: Fixes kiosk mode should have &kiosk appended to the url [#15765](https://github.com/grafana/grafana/issues/15765) * **Dashboard**: Fixes kiosk=tv mode with autofitpanels should respect header [#15650](https://github.com/grafana/grafana/issues/15650) +* **Image rendering**: Fixed image rendering issue for dashboards with auto refresh, . [#15818](https://github.com/grafana/grafana/pull/15818), [@torkelo](https://github.com/torkelo) +* **Dashboard**: Fix only users that can edit a dashboard should be able to update panel json. [#15805](https://github.com/grafana/grafana/pull/15805), [@marefr](https://github.com/marefr) +* **LDAP**: fix allow anonymous initial bind for ldap search. [#15803](https://github.com/grafana/grafana/pull/15803), [@marefr](https://github.com/marefr) +* **UX**: Fixed scrollbar not visible initially (only after manual scroll). [#15798](https://github.com/grafana/grafana/pull/15798), [@torkelo](https://github.com/torkelo) +* **Datasource admin** TestData [#15793](https://github.com/grafana/grafana/pull/15793), [@hugohaggmark](https://github.com/hugohaggmark) +* **Dashboard**: Fixed scrolling issue that caused scroll to be locked to bottom. [#15792](https://github.com/grafana/grafana/pull/15792), [@torkelo](https://github.com/torkelo) +* **Explore**: Viewers with viewers_can_edit should be able to access /explore. [#15787](https://github.com/grafana/grafana/pull/15787), [@jschill](https://github.com/jschill) +* **Security** fix: limit access to org admin and alerting pages. [#15761](https://github.com/grafana/grafana/pull/15761), [@marefr](https://github.com/marefr) +* **Panel Edit** minInterval changes did not persist [#15757](https://github.com/grafana/grafana/pull/15757), [@hugohaggmark](https://github.com/hugohaggmark) +* **Teams**: Fixed bug when getting teams for user. [#15595](https://github.com/grafana/grafana/pull/15595), [@hugohaggmark](https://github.com/hugohaggmark) +* **Stackdriver**: fix for float64 bounds for distribution metrics [#14509](https://github.com/grafana/grafana/issues/14509) +* **Stackdriver**: no reducers available for distribution type [#15179](https://github.com/grafana/grafana/issues/15179) # 6.0.0 stable (2019-02-25) ### Bug Fixes -* **Stackdriver**: fix for float64 bounds for distribution metrics [#14509](https://github.com/grafana/grafana/issues/14509) -* **Stackdriver**: no reducers available for distribution type [#15179](https://github.com/grafana/grafana/issues/15179) * **Dashboard**: fixes click after scroll in series override menu [#15621](https://github.com/grafana/grafana/issues/15621) * **MySQL**: fix mysql query using _interval_ms variable throws error [#14507](https://github.com/grafana/grafana/issues/14507) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 677a12831dc..d21ef5232d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,10 +34,10 @@ To setup a local development environment we recommend reading [Building Grafana ### Pull requests with new features Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests). -Make sure to include `closes #` or `fixes #` in the pull request description. +Make sure to include `Closes #` or `Fixes #` in the pull request description. ### Pull requests with bug fixes -Please make all changes in one commit if possible. Include `closes #12345` in bottom of the commit message. +Please make all changes in one commit if possible. Include `Closes #` in bottom of the commit message. A commit message for a bug fix should look something like this. ``` @@ -48,7 +48,7 @@ provsioners each provisioner overwrite each other. filling up dashboard_versions quite fast if using default settings. -closes #12864 +Closes #12864 ``` -If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged. \ No newline at end of file +If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged. diff --git a/Gopkg.lock b/Gopkg.lock index dca36f1b3d0..235a315f1e8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,30 +2,39 @@ [[projects]] + digest = "1:f8ad8a53fa865a70efbe215b0ca34735523f50ea39e0efde319ab6fc80089b44" name = "cloud.google.com/go" packages = ["compute/metadata"] + pruneopts = "NUT" revision = "056a55f54a6cc77b440b31a56a5e7c3982d32811" version = "v0.22.0" [[projects]] + digest = "1:167b6f65a6656de568092189ae791253939f076df60231fdd64588ac703892a1" name = "github.com/BurntSushi/toml" packages = ["."] + pruneopts = "NUT" revision = "b26d9c308763d68093482582cea63d69be07a0f0" version = "v0.3.0" [[projects]] branch = "master" + digest = "1:7d23e6e1889b8bb4bbb37a564708fdab4497ce232c3a99d66406c975b642a6ff" name = "github.com/Unknwon/com" packages = ["."] + pruneopts = "NUT" revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520" [[projects]] branch = "master" + digest = "1:1610787cd9726e29d8fecc2a80e43e4fced008a1f560fec6688fc4d946f17835" name = "github.com/VividCortex/mysqlerr" packages = ["."] + pruneopts = "NUT" revision = "6c6b55f8796f578c870b7e19bafb16103bc40095" [[projects]] + digest = "1:ebe102b61c1615d2954734e3cfe1b6b06a5088c25a41055b38661d41ad7b8f27" name = "github.com/aws/aws-sdk-go" packages = [ "aws", @@ -69,399 +78,507 @@ "service/resourcegroupstaggingapi", "service/resourcegroupstaggingapi/resourcegroupstaggingapiiface", "service/s3", - "service/sts" + "service/sts", ] + pruneopts = "NUT" revision = "62936e15518acb527a1a9cb4a39d96d94d0fd9a2" version = "v1.16.15" [[projects]] branch = "master" + digest = "1:79cad073c7be02632d3fa52f62486848b089f560db1e94536de83a408c0f4726" name = "github.com/benbjohnson/clock" packages = ["."] + pruneopts = "NUT" revision = "7dc76406b6d3c05b5f71a86293cbcf3c4ea03b19" [[projects]] branch = "master" + digest = "1:707ebe952a8b3d00b343c01536c79c73771d100f63ec6babeaed5c79e2b8a8dd" name = "github.com/beorn7/perks" packages = ["quantile"] + pruneopts = "NUT" revision = "3a771d992973f24aa725d07868b467d1ddfceafb" [[projects]] branch = "master" + digest = "1:433a2ff0ef4e2f8634614aab3174783c5ff80120b487712db96cc3712f409583" name = "github.com/bmizerany/assert" packages = ["."] + pruneopts = "NUT" revision = "b7ed37b82869576c289d7d97fb2bbd8b64a0cb28" [[projects]] branch = "master" + digest = "1:d8f9145c361920507a4f85ffb7f70b96beaedacba2ce8c00aa663adb08689d3e" name = "github.com/bradfitz/gomemcache" packages = ["memcache"] + pruneopts = "NUT" revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b" [[projects]] branch = "master" + digest = "1:8ecb89af7dfe3ac401bdb0c9390b134ef96a97e85f732d2b0604fb7b3977839f" name = "github.com/codahale/hdrhistogram" packages = ["."] + pruneopts = "NUT" revision = "3a0bb77429bd3a61596f5e8a3172445844342120" [[projects]] + digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58" name = "github.com/codegangsta/cli" packages = ["."] + pruneopts = "NUT" revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" version = "v1.20.0" [[projects]] + digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "NUT" revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" [[projects]] + digest = "1:1b318d2dd6cea8a1a8d8ec70348852303bd3e491df74e8bca6e32eb5a4d06970" name = "github.com/denisenkom/go-mssqldb" packages = [ ".", - "internal/cp" + "internal/cp", ] + pruneopts = "NUT" revision = "270bc3860bb94dd3a3ffd047377d746c5e276726" [[projects]] branch = "master" + digest = "1:2da5f11ad66ff01a27a5c3dba4620b7eee2327be75b32c9ee9f87c9a8001ecbf" name = "github.com/facebookgo/inject" packages = ["."] + pruneopts = "NUT" revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b" [[projects]] branch = "master" + digest = "1:1108df7f658c90db041e0d6174d55be689aaeb0585913b9c3c7aab51a3a6b2b1" name = "github.com/facebookgo/structtag" packages = ["."] + pruneopts = "NUT" revision = "217e25fb96916cc60332e399c9aa63f5c422ceed" [[projects]] + digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45" name = "github.com/fatih/color" packages = ["."] + pruneopts = "NUT" revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4" version = "v1.7.0" -[[projects]] - name = "github.com/go-ini/ini" - packages = ["."] - revision = "6529cf7c58879c08d927016dde4477f18a0634cb" - version = "v1.36.0" - [[projects]] branch = "master" + digest = "1:682a0aca743a1a4a36697f3d7f86c0ed403c4e3a780db9935f633242855eac9c" name = "github.com/go-macaron/binding" packages = ["."] + pruneopts = "NUT" revision = "ac54ee249c27dca7e76fad851a4a04b73bd1b183" [[projects]] branch = "master" + digest = "1:6326b27f8e0c8e135c8674ddbc619fae879664ac832e8e6fa6a23ce0d279ed4d" name = "github.com/go-macaron/gzip" packages = ["."] + pruneopts = "NUT" revision = "cad1c6580a07c56f5f6bc52d66002a05985c5854" [[projects]] branch = "master" + digest = "1:fb8711b648d1ff03104fc1d9593a13cb1d5120be7ba2b01641c14ccae286a9e3" name = "github.com/go-macaron/inject" packages = ["."] + pruneopts = "NUT" revision = "d8a0b8677191f4380287cfebd08e462217bac7ad" [[projects]] branch = "master" + digest = "1:21577aafe885f088e8086a3415f154c63c0b7ce956a6994df2ac5776bc01b7e3" name = "github.com/go-macaron/session" packages = [ ".", "memcache", "postgres", - "redis" + "redis", ] + pruneopts = "NUT" revision = "068d408f9c54c7fa7fcc5e2bdd3241ab21280c9e" [[projects]] + digest = "1:fddd4bada6100d6fc49a9f32f18ba5718db45a58e4b00aa6377e1cfbf06af34f" name = "github.com/go-sql-driver/mysql" packages = ["."] + pruneopts = "NUT" revision = "2cc627ac8defc45d65066ae98f898166f580f9a4" [[projects]] + digest = "1:a1efdbc2762667c8a41cbf02b19a0549c846bf2c1d08cad4f445e3344089f1f0" name = "github.com/go-stack/stack" packages = ["."] + pruneopts = "NUT" revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc" version = "v1.7.0" [[projects]] + digest = "1:06d21295033f211588d0ad7ff391cc1b27e72b60cb6d4b7db0d70cffae4cf228" name = "github.com/go-xorm/builder" packages = ["."] - revision = "bad0a612f0d6277b953910822ab5dfb30dd18237" - version = "v0.2.0" + pruneopts = "NUT" + revision = "1d658d7596c25394aab557ef5b50ef35bf706384" + version = "v0.3.4" [[projects]] + digest = "1:b26928aab0fff92592e8728c5bc9d6e404fa2017d6a8e841ae5e60a42237f6fc" name = "github.com/go-xorm/core" packages = ["."] - revision = "da1adaf7a28ca792961721a34e6e04945200c890" - version = "v0.5.7" + pruneopts = "NUT" + revision = "ccc80c1adf1f6172bbc548877f50a1163041a40a" + version = "v0.6.2" [[projects]] + digest = "1:407316703b32d68ccf5d39bdae57d411b6954e253e07d0fff0988a3f39861f2f" name = "github.com/go-xorm/xorm" packages = ["."] - revision = "1933dd69e294c0a26c0266637067f24dbb25770c" - version = "v0.6.4" + pruneopts = "NUT" + revision = "1f39c590c64924f358c0d89016ac9b2bb84e9125" + version = "v0.7.1" [[projects]] branch = "master" + digest = "1:ffbb19fb66f140b5ea059428d1f84246a055d1bc3d9456c1e5c3d143611f03d0" name = "github.com/golang/protobuf" packages = [ "proto", "ptypes", "ptypes/any", "ptypes/duration", - "ptypes/timestamp" + "ptypes/timestamp", ] + pruneopts = "NUT" revision = "927b65914520a8b7d44f5c9057611cfec6b2e2d0" [[projects]] branch = "master" + digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795" name = "github.com/gopherjs/gopherjs" packages = ["js"] + pruneopts = "NUT" revision = "8dffc02ea1cb8398bb73f30424697c60fcf8d4c5" [[projects]] + digest = "1:3b708ebf63bfa9ba3313bedb8526bc0bb284e51474e65e958481476a9d4a12aa" name = "github.com/gorilla/websocket" packages = ["."] + pruneopts = "NUT" revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" version = "v1.2.0" [[projects]] + digest = "1:4e771d1c6e15ca4516ad971c34205c822b5cff2747179679d7b321e4e1bfe431" name = "github.com/gosimple/slug" packages = ["."] + pruneopts = "NUT" revision = "e9f42fa127660e552d0ad2b589868d403a9be7c6" version = "v1.1.1" [[projects]] branch = "master" + digest = "1:08e53c69cd267ef7d71eeae5d953153d0d2bc1b8e0b498731fe9acaead7001b6" name = "github.com/grafana/grafana-plugin-model" packages = [ "go/datasource", - "go/renderer" + "go/renderer", ] + pruneopts = "NUT" revision = "84176c64269d8060f99e750ee8aba6f062753336" [[projects]] branch = "master" + digest = "1:58ba5285227b0f635652cd4aa82c4cfd00b590191eadd823462f0c9f64e3ae07" name = "github.com/hashicorp/go-hclog" packages = ["."] + pruneopts = "NUT" revision = "69ff559dc25f3b435631604f573a5fa1efdb6433" [[projects]] + digest = "1:532090ffc3b05a7e4c0229dd2698d79149f2e0683df993224a8b202f607fb605" name = "github.com/hashicorp/go-plugin" packages = ["."] + pruneopts = "NUT" revision = "e8d22c780116115ae5624720c9af0c97afe4f551" [[projects]] branch = "master" + digest = "1:8925116d1edcd85fc0c014e1aa69ce12892489b48ee633a605c46d893b8c151f" name = "github.com/hashicorp/go-version" packages = ["."] + pruneopts = "NUT" revision = "23480c0665776210b5fbbac6eaaee40e3e6a96b7" [[projects]] branch = "master" + digest = "1:8deb0c5545c824dfeb0ac77ab8eb67a3d541eab76df5c85ce93064ef02d44cd0" name = "github.com/hashicorp/yamux" packages = ["."] + pruneopts = "NUT" revision = "7221087c3d281fda5f794e28c2ea4c6e4d5c4558" [[projects]] + digest = "1:efbe016b6d198cf44f1db0ed2fbdf1b36ebf1f6956cc9b76d6affa96f022d368" name = "github.com/inconshreveable/log15" packages = ["."] + pruneopts = "NUT" revision = "0decfc6c20d9ca0ad143b0e89dcaa20f810b4fb3" version = "v2.13" [[projects]] + digest = "1:1f2aebae7e7c856562355ec0198d8ca2fa222fb05e5b1b66632a1fce39631885" name = "github.com/jmespath/go-jmespath" packages = ["."] - revision = "0b12d6b5" + pruneopts = "NUT" + revision = "c2b33e84" [[projects]] + digest = "1:6ddab442e52381bab82fb6c07ef3f4b565ff7ec4b8fae96d8dd4b8573a460597" name = "github.com/jtolds/gls" packages = ["."] + pruneopts = "NUT" revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064" version = "v4.2.1" [[projects]] + digest = "1:1da1796a71eb70f1e3e085984d044f67840bb0326816ec8276231aa87b1b9fc3" name = "github.com/klauspost/compress" packages = [ "flate", - "gzip" + "gzip", ] + pruneopts = "NUT" revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf" version = "v1.2.1" [[projects]] + digest = "1:5e55a8699c9ff7aba1e4c8952aeda209685d88d4cb63a8766c338e333b8e65d6" name = "github.com/klauspost/cpuid" packages = ["."] + pruneopts = "NUT" revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da" version = "v1.1" [[projects]] + digest = "1:b95da1293525625ef6f07be79d537b9bf2ecd7901efcf9a92193edafbd55b9ef" name = "github.com/klauspost/crc32" packages = ["."] + pruneopts = "NUT" revision = "cb6bfca970f6908083f26f39a79009d608efd5cd" version = "v1.1" [[projects]] + digest = "1:7b21c7fc5551b46d1308b4ffa9e9e49b66c7a8b0ba88c0130474b0e7a20d859f" name = "github.com/kr/pretty" packages = ["."] + pruneopts = "NUT" revision = "73f6ac0b30a98e433b289500d779f50c1a6f0712" version = "v0.1.0" [[projects]] + digest = "1:c3a7836b5904db0f8b609595b619916a6831cb35b8b714aec39f96d00c6155d8" name = "github.com/kr/text" packages = ["."] + pruneopts = "NUT" revision = "e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f" version = "v0.1.0" [[projects]] branch = "master" + digest = "1:7a1e592f0349d56fac8ce47f28469e4e7f4ce637cb26f40c88da9dff25db1c98" name = "github.com/lib/pq" packages = [ ".", - "oid" + "oid", ] + pruneopts = "NUT" revision = "d34b9ff171c21ad295489235aec8b6626023cd04" [[projects]] + digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061" name = "github.com/mattn/go-colorable" packages = ["."] + pruneopts = "NUT" revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" version = "v0.0.9" [[projects]] + digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287" name = "github.com/mattn/go-isatty" packages = ["."] + pruneopts = "NUT" revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" [[projects]] + digest = "1:536979f1c56397dbf91c2785159b37dec37e35d3bffa3cd1cfe66d25f51f8088" name = "github.com/mattn/go-sqlite3" packages = ["."] + pruneopts = "NUT" revision = "323a32be5a2421b8c7087225079c6c900ec397cd" version = "v1.7.0" [[projects]] + digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] + pruneopts = "NUT" revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" version = "v1.0.0" [[projects]] branch = "master" + digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048" name = "github.com/mitchellh/go-testing-interface" packages = ["."] + pruneopts = "NUT" revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28" [[projects]] + digest = "1:3b517122f3aad1ecce45a630ea912b3092b4729f25532a911d0cb2935a1f9352" name = "github.com/oklog/run" packages = ["."] + pruneopts = "NUT" revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39" version = "v1.0.0" [[projects]] + digest = "1:7da29c22bcc5c2ffb308324377dc00b5084650348c2799e573ed226d8cc9faf0" name = "github.com/opentracing/opentracing-go" packages = [ ".", "ext", - "log" + "log", ] + pruneopts = "NUT" revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38" version = "v1.0.2" [[projects]] + digest = "1:748946761cf99c8b73cef5a3c0ee3e040859dd713a20cece0d0e0dc04e6ceca7" name = "github.com/patrickmn/go-cache" packages = ["."] + pruneopts = "NUT" revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0" version = "v2.1.0" [[projects]] + digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121" name = "github.com/pkg/errors" packages = ["."] + pruneopts = "NUT" revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" [[projects]] + digest = "1:4759bed95e3a52febc18c071db28790a5c6e9e106ee201a37add6f6a056f8f9c" name = "github.com/prometheus/client_golang" packages = [ "api", "api/prometheus/v1", "prometheus", - "prometheus/promhttp" + "prometheus/promhttp", ] + pruneopts = "NUT" revision = "967789050ba94deca04a5e84cce8ad472ce313c1" version = "v0.9.0-pre1" [[projects]] branch = "master" + digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b" name = "github.com/prometheus/client_model" packages = ["go"] + pruneopts = "NUT" revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" [[projects]] branch = "master" + digest = "1:768b555b86742de2f28beb37f1dedce9a75f91f871d75b5717c96399c1a78c08" name = "github.com/prometheus/common" packages = [ "expfmt", "internal/bitbucket.org/ww/goautoneg", - "model" + "model", ] + pruneopts = "NUT" revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c" [[projects]] branch = "master" + digest = "1:c4a213a8d73fbb0b13f717ba7996116602ef18ecb42b91d77405877914cb0349" name = "github.com/prometheus/procfs" packages = [ ".", "internal/util", "nfs", - "xfs" + "xfs", ] + pruneopts = "NUT" revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e" [[projects]] branch = "master" + digest = "1:16e2136a67ec44aa2d1d6b0fd65394b3c4a8b2a1b6730c77967f7b7b06b179b2" name = "github.com/rainycape/unidecode" packages = ["."] + pruneopts = "NUT" revision = "cb7f23ec59bec0d61b19c56cd88cee3d0cc1870c" [[projects]] + digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04" name = "github.com/sergi/go-diff" packages = ["diffmatchpatch"] + pruneopts = "NUT" revision = "1744e2970ca51c86172c8190fadad617561ed6e7" version = "v1.0.0" [[projects]] + digest = "1:1f0b284a6858827de4c27c66b49b2b25df3e16b031c2b57b7892273131e7dd2b" name = "github.com/smartystreets/assertions" packages = [ ".", "internal/go-render/render", - "internal/oglematchers" + "internal/oglematchers", ] + pruneopts = "NUT" revision = "7678a5452ebea5b7090a6b163f844c133f523da2" version = "1.8.3" [[projects]] + digest = "1:7efd0b2309cdd6468029fa30c808c50a820c9344df07e1a4bbdaf18f282907aa" name = "github.com/smartystreets/goconvey" packages = [ "convey", "convey/gotest", - "convey/reporting" + "convey/reporting", ] + pruneopts = "NUT" revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857" version = "1.6.3" [[projects]] branch = "master" + digest = "1:a66add8dd963bfc72649017c1b321198f596cb4958cb1a11ff91a1be8691020b" name = "github.com/teris-io/shortid" packages = ["."] + pruneopts = "NUT" revision = "771a37caa5cf0c81f585d7b6df4dfc77e0615b5c" [[projects]] + digest = "1:3d48c38e0eca8c66df62379c5ae7a83fb5cd839b94f241354c07ba077da7bc45" name = "github.com/uber/jaeger-client-go" packages = [ ".", @@ -479,45 +596,55 @@ "thrift-gen/jaeger", "thrift-gen/sampling", "thrift-gen/zipkincore", - "utils" + "utils", ] + pruneopts = "NUT" revision = "b043381d944715b469fd6b37addfd30145ca1758" version = "v2.14.0" [[projects]] + digest = "1:0f09db8429e19d57c8346ad76fbbc679341fa86073d3b8fb5ac919f0357d8f4c" name = "github.com/uber/jaeger-lib" packages = ["metrics"] + pruneopts = "NUT" revision = "ed3a127ec5fef7ae9ea95b01b542c47fbd999ce5" version = "v1.5.0" [[projects]] + digest = "1:4c7d12ad3ef47bb03892a52e2609dc9a9cff93136ca9c7d31c00b79fcbc23c7b" name = "github.com/yudai/gojsondiff" packages = [ ".", - "formatter" + "formatter", ] + pruneopts = "NUT" revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6" version = "1.0.0" [[projects]] branch = "master" + digest = "1:e50cbf8eba568d59b71e08c22c2a77809ed4646ae06ef4abb32b3d3d3fdb1a77" name = "github.com/yudai/golcs" packages = ["."] + pruneopts = "NUT" revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68" [[projects]] branch = "master" + digest = "1:758f363e0dff33cf00b234be2efb12f919d79b42d5ae3909ff9eb69ef2c3cca5" name = "golang.org/x/crypto" packages = [ "ed25519", "ed25519/internal/edwards25519", "md4", - "pbkdf2" + "pbkdf2", ] + pruneopts = "NUT" revision = "1a580b3eff7814fc9b40602fd35256c63b50f491" [[projects]] branch = "master" + digest = "1:0b3fee9c4472022a0982ee0d81e08b3cc3e595f50befd7a4b358b48540d9d8c5" name = "golang.org/x/net" packages = [ "context", @@ -527,35 +654,43 @@ "http2/hpack", "idna", "internal/timeseries", - "trace" + "trace", ] + pruneopts = "NUT" revision = "2491c5de3490fced2f6cff376127c667efeed857" [[projects]] branch = "master" + digest = "1:46bd4e66bfce5e77f08fc2e8dcacc3676e679241ce83d9c150ff0397d686dd44" name = "golang.org/x/oauth2" packages = [ ".", "google", "internal", "jws", - "jwt" + "jwt", ] + pruneopts = "NUT" revision = "cdc340f7c179dbbfa4afd43b7614e8fcadde4269" [[projects]] branch = "master" + digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239" name = "golang.org/x/sync" packages = ["errgroup"] + pruneopts = "NUT" revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" [[projects]] branch = "master" + digest = "1:ec21c5bf0572488865b93e30ffd9132afbf85bec0b20c2d6cbcf349cf2031ed5" name = "golang.org/x/sys" packages = ["unix"] + pruneopts = "NUT" revision = "7c87d13f8e835d2fb3a70a2912c811ed0c1d241b" [[projects]] + digest = "1:e7071ed636b5422cc51c0e3a6cebc229d6c9fffc528814b519a980641422d619" name = "golang.org/x/text" packages = [ "collate", @@ -571,12 +706,14 @@ "unicode/bidi", "unicode/cldr", "unicode/norm", - "unicode/rangetable" + "unicode/rangetable", ] + pruneopts = "NUT" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] + digest = "1:dbd5568923513ee74aa626d027e2a8a352cf8f35df41d19f4e34491d1858c38b" name = "google.golang.org/appengine" packages = [ ".", @@ -589,18 +726,22 @@ "internal/modules", "internal/remote_api", "internal/urlfetch", - "urlfetch" + "urlfetch", ] + pruneopts = "NUT" revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" version = "v1.0.0" [[projects]] branch = "master" + digest = "1:3c24554c312721e98fa6b76403e7100cf974eb46b1255ea7fc6471db9a9ce498" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] + pruneopts = "NUT" revision = "7bb2a897381c9c5ab2aeb8614f758d7766af68ff" [[projects]] + digest = "1:840b77b6eb539b830bb760b6e30b688ed2ff484bd83466fce2395835ed9367fe" name = "google.golang.org/grpc" packages = [ ".", @@ -627,78 +768,177 @@ "stats", "status", "tap", - "transport" + "transport", ] + pruneopts = "NUT" revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34" version = "v1.11.1" [[projects]] branch = "v3" + digest = "1:1244a9b3856f70d5ffb74bbfd780fc9d47f93f2049fa265c6fb602878f507bf8" name = "gopkg.in/alexcesaro/quotedprintable.v3" packages = ["."] + pruneopts = "NUT" revision = "2caba252f4dc53eaf6b553000885530023f54623" [[projects]] + digest = "1:aea6e9483c167cc6fdf1274c442558c5dda8fd3373372be04d98c79100868da1" name = "gopkg.in/asn1-ber.v1" packages = ["."] + pruneopts = "NUT" revision = "379148ca0225df7a432012b8df0355c2a2063ac0" version = "v1.2" [[projects]] + digest = "1:24bfc2e8bf971485cb5ba0f0e5b08a1b806cca5828134df76b32d1ea50f2ab49" name = "gopkg.in/bufio.v1" packages = ["."] + pruneopts = "NUT" revision = "567b2bfa514e796916c4747494d6ff5132a1dfce" version = "v1" [[projects]] + digest = "1:e05711632e1515319b014e8fe4cbe1d30ab024c473403f60cf0fdeb4c586a474" name = "gopkg.in/ini.v1" packages = ["."] + pruneopts = "NUT" revision = "6529cf7c58879c08d927016dde4477f18a0634cb" version = "v1.36.0" [[projects]] + digest = "1:c847b7fea4c7e6db5281a37dffc4620cb78c1227403a79e5aa290db517657ac1" name = "gopkg.in/ldap.v3" packages = ["."] + pruneopts = "NUT" revision = "5c2c0f997205c29de14cb6c35996370c2c5dfab1" version = "v3" [[projects]] + digest = "1:3b0cf3a465fd07f76e5fc1a9d0783c662dac0de9fc73d713ebe162768fd87b5f" name = "gopkg.in/macaron.v1" packages = ["."] + pruneopts = "NUT" revision = "c1be95e6d21e769e44e1ec33cec9da5837861c10" version = "v1.3.1" [[projects]] branch = "v2" + digest = "1:d52332f9e9f2c6343652e13aa3fd40cfd03353520c9a48d90f21215d3012d50f" name = "gopkg.in/mail.v2" packages = ["."] + pruneopts = "NUT" revision = "5bc5c8bb07bd8d2803831fbaf8cbd630fcde2c68" [[projects]] + digest = "1:00126f697efdcab42f07c89ac8bf0095fb2328aef6464e070055154088cea859" name = "gopkg.in/redis.v2" packages = ["."] + pruneopts = "NUT" revision = "e6179049628164864e6e84e973cfb56335748dea" version = "v2.3.2" [[projects]] + digest = "1:a50fabe7a46692dc7c656310add3d517abe7914df02afd151ef84da884605dc8" name = "gopkg.in/square/go-jose.v2" packages = [ ".", "cipher", - "json" + "json", ] + pruneopts = "NUT" revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8" version = "v2.1.9" [[projects]] branch = "v2" + digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "NUT" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "88f0eb826b9c154ba46ea3bb64767707d86db75449ec75199eb2b8cf2b337fd4" + input-imports = [ + "github.com/BurntSushi/toml", + "github.com/Unknwon/com", + "github.com/VividCortex/mysqlerr", + "github.com/aws/aws-sdk-go/aws", + "github.com/aws/aws-sdk-go/aws/awserr", + "github.com/aws/aws-sdk-go/aws/awsutil", + "github.com/aws/aws-sdk-go/aws/credentials", + "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds", + "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds", + "github.com/aws/aws-sdk-go/aws/defaults", + "github.com/aws/aws-sdk-go/aws/ec2metadata", + "github.com/aws/aws-sdk-go/aws/endpoints", + "github.com/aws/aws-sdk-go/aws/request", + "github.com/aws/aws-sdk-go/aws/session", + "github.com/aws/aws-sdk-go/service/cloudwatch", + "github.com/aws/aws-sdk-go/service/ec2", + "github.com/aws/aws-sdk-go/service/ec2/ec2iface", + "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi", + "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface", + "github.com/aws/aws-sdk-go/service/s3", + "github.com/aws/aws-sdk-go/service/sts", + "github.com/benbjohnson/clock", + "github.com/bmizerany/assert", + "github.com/codegangsta/cli", + "github.com/davecgh/go-spew/spew", + "github.com/denisenkom/go-mssqldb", + "github.com/facebookgo/inject", + "github.com/fatih/color", + "github.com/go-macaron/binding", + "github.com/go-macaron/gzip", + "github.com/go-macaron/session", + "github.com/go-macaron/session/memcache", + "github.com/go-macaron/session/postgres", + "github.com/go-macaron/session/redis", + "github.com/go-sql-driver/mysql", + "github.com/go-stack/stack", + "github.com/go-xorm/core", + "github.com/go-xorm/xorm", + "github.com/gorilla/websocket", + "github.com/gosimple/slug", + "github.com/grafana/grafana-plugin-model/go/datasource", + "github.com/grafana/grafana-plugin-model/go/renderer", + "github.com/hashicorp/go-hclog", + "github.com/hashicorp/go-plugin", + "github.com/hashicorp/go-version", + "github.com/inconshreveable/log15", + "github.com/lib/pq", + "github.com/mattn/go-isatty", + "github.com/mattn/go-sqlite3", + "github.com/opentracing/opentracing-go", + "github.com/opentracing/opentracing-go/ext", + "github.com/opentracing/opentracing-go/log", + "github.com/patrickmn/go-cache", + "github.com/pkg/errors", + "github.com/prometheus/client_golang/api", + "github.com/prometheus/client_golang/api/prometheus/v1", + "github.com/prometheus/client_golang/prometheus", + "github.com/prometheus/client_golang/prometheus/promhttp", + "github.com/prometheus/client_model/go", + "github.com/prometheus/common/expfmt", + "github.com/prometheus/common/model", + "github.com/smartystreets/goconvey/convey", + "github.com/teris-io/shortid", + "github.com/uber/jaeger-client-go/config", + "github.com/yudai/gojsondiff", + "github.com/yudai/gojsondiff/formatter", + "golang.org/x/net/context/ctxhttp", + "golang.org/x/oauth2", + "golang.org/x/oauth2/google", + "golang.org/x/oauth2/jwt", + "golang.org/x/sync/errgroup", + "gopkg.in/ini.v1", + "gopkg.in/ldap.v3", + "gopkg.in/macaron.v1", + "gopkg.in/mail.v2", + "gopkg.in/square/go-jose.v2", + "gopkg.in/yaml.v2", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 83e6890b0f4..d1bc0f55bae 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -81,11 +81,15 @@ ignored = [ [[constraint]] name = "github.com/go-xorm/core" - version = "=0.5.7" + version = "=0.6.2" + +[[override]] + name = "github.com/go-xorm/builder" + version = "=0.3.4" [[constraint]] name = "github.com/go-xorm/xorm" - version = "=0.6.4" + version = "=0.7.1" [[constraint]] name = "github.com/gorilla/websocket" diff --git a/README.md b/README.md index 550e7facfa8..8c84bfd0e87 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,6 @@ Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB. -![](https://www.grafanacon.org/2019/images/grafanacon_la_nav-logo.png) - -Join us Feb 25-26 in Los Angeles, California for GrafanaCon - a two-day event with talks focused on Grafana and the surrounding open source monitoring ecosystem. Get deep dives into Loki, the Explore workflow and all of the new features of Grafana 6, plus participate in hands on workshops to help you get the most out of your data. - -Time is running out - grab your ticket now! http://grafanacon.org - diff --git a/conf/defaults.ini b/conf/defaults.ini index df02e01235b..044d8e59a7a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -231,6 +231,7 @@ verify_email_enabled = false # Background text for the user field on the login page login_hint = email or username +password_hint = password # Default UI theme ("dark" or "light") default_theme = dark diff --git a/conf/sample.ini b/conf/sample.ini index 57ff82181de..dc1e4fbde8e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -211,6 +211,7 @@ log_queries = # Background text for the user field on the login page ;login_hint = email or username +;password_hint = password # Default UI theme ("dark" or "light") ;default_theme = dark diff --git a/devenv/dev-dashboards/panel_tests_multiseries_gauge.json b/devenv/dev-dashboards/panel_tests_multiseries_gauge.json new file mode 100644 index 00000000000..91313b7d439 --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_multiseries_gauge.json @@ -0,0 +1,296 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 6, + "links": [], + "options-gauge": { + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "options": { + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#1F78C1", + "index": 5, + "value": 96.875 + }, + { + "color": "#E24D42", + "index": 4, + "value": 93.75 + }, + { + "color": "#EF843C", + "index": 3, + "value": 87.5 + }, + { + "color": "#6ED0E0", + "index": 2, + "value": 75 + }, + { + "color": "#EAB839", + "index": 1, + "value": 50 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [ + { + "from": "50", + "id": 1, + "operator": "", + "text": "Hello :) ", + "to": "90", + "type": 2, + "value": "" + } + ] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk" + }, + { + "refId": "B", + "scenarioId": "random_walk" + }, + { + "refId": "C", + "scenarioId": "random_walk" + }, + { + "refId": "D", + "scenarioId": "random_walk" + }, + { + "refId": "E", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Horizontal with range variable", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 2, + "links": [], + "options-gauge": { + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "options": { + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [ + { + "color": "#EAB839", + "index": 1, + "value": 50 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk" + }, + { + "refId": "B", + "scenarioId": "random_walk" + }, + { + "refId": "C", + "scenarioId": "random_walk" + }, + { + "refId": "D", + "scenarioId": "random_walk" + }, + { + "refId": "E", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Repeat horizontal", + "type": "gauge" + }, + { + "datasource": "gdev-testdata", + "gridPos": { + "h": 14, + "w": 5, + "x": 0, + "y": 16 + }, + "id": 4, + "links": [], + "options-gauge": { + "decimals": 0, + "maxValue": "200", + "minValue": 0, + "options": { + "decimals": 0, + "maxValue": 100, + "minValue": 0, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "avg", + "suffix": "", + "thresholds": [], + "unit": "none", + "valueMappings": [] + }, + "prefix": "", + "showThresholdLabels": false, + "showThresholdMarkers": true, + "stat": "max", + "suffix": "", + "thresholds": [ + { + "color": "#6ED0E0", + "index": 2, + "value": 75 + }, + { + "color": "#EAB839", + "index": 1, + "value": 50 + }, + { + "color": "#7EB26D", + "index": 0, + "value": null + } + ], + "unit": "none", + "valueMappings": [] + }, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk" + }, + { + "refId": "B", + "scenarioId": "random_walk" + }, + { + "refId": "C", + "scenarioId": "random_walk" + }, + { + "refId": "D", + "scenarioId": "random_walk" + }, + { + "refId": "E", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Vertical", + "type": "gauge" + } + ], + "schemaVersion": 17, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "", + "title": "Multi series gauges", + "uid": "szkuR1umk", + "version": 7 +} diff --git a/devenv/docker/blocks/prometheus2/Dockerfile b/devenv/docker/blocks/prometheus2/Dockerfile index 03edf4c9ee2..c9a2327bd4a 100644 --- a/devenv/docker/blocks/prometheus2/Dockerfile +++ b/devenv/docker/blocks/prometheus2/Dockerfile @@ -1,3 +1,3 @@ -FROM prom/prometheus:v2.2.0 +FROM prom/prometheus:v2.7.2 ADD prometheus.yml /etc/prometheus/ ADD alert.rules /etc/prometheus/ diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 5327039341f..8aafdb0362c 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -83,7 +83,11 @@ or a bot integration via Slack Apps. Follow Slack's guide to set up a bot integr Setting | Description ---------- | ----------- -Recipient | allows you to override the Slack recipient. +Url | Slack incoming webhook url. +Username | Set the username for the bot's message. +Recipient | Allows you to override the Slack recipient. +Icon emoji | Provide an emoji to use as the icon for the bot's message. Ex :smile: +Icon URL | Provide a url to an image to use as the icon for the bot's message. Mention | make it possible to include a mention in the Slack notification sent by Grafana. Ex @here or @channel Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination. diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md index 1a83432b8f7..510776750f3 100644 --- a/docs/sources/auth/generic-oauth.md +++ b/docs/sources/auth/generic-oauth.md @@ -217,10 +217,10 @@ Some OAuth2 providers might not support `client_id` and `client_secret` passed v results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be send via POST body, which can be enabled via the following settings: - ```bash - [auth.generic_oauth] - send_client_credentials_via_post = true - ``` +```bash +[auth.generic_oauth] +send_client_credentials_via_post = true +```
diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 2b2704e0d81..0a5c087803f 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -68,6 +68,7 @@ provides the following functions you can use in the `Query` input field. Name | Description ---- | -------- +*label_names()* | Returns a list of label names. *label_values(label)* | Returns a list of label values for the `label` in every metric. *label_values(metric, label)* | Returns a list of label values for the `label` in the specified metric. *metrics(metric)* | Returns a list of metrics matching the specified `metric` regex. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index f0418ad31a6..3d1b25979c3 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -162,9 +162,9 @@ executed with working directory set to the installation path. ### enable_gzip -Set this option to `true` to enable HTTP compression, this can improve -transfer speed and bandwidth utilization. It is recommended that most -users set it to `true`. By default it is set to `false` for compatibility +Set this option to `true` to enable HTTP compression, this can improve +transfer speed and bandwidth utilization. It is recommended that most +users set it to `true`. By default it is set to `false` for compatibility reasons. ### cert_file @@ -342,6 +342,14 @@ options are `Admin` and `Editor`. e.g. : Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. Defaults to `false`. +### login_hint + +Text used as placeholder text on login page for login/username input. + +### password_hint + +Text used as placeholder text on login page for password input. +
## [auth] diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index e235f25b9e9..511d6117eb3 100644 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -120,7 +120,7 @@ If you're using systemd and have a large amount of annotations consider temporar ## Upgrading to v6.0 -If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitzied HTML. +If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitized HTML. Read more [here](/installation/configuration/#disable-sanitize-html) about this new setting. ### Authentication and security @@ -147,4 +147,4 @@ login_maximum_inactive_lifetime_days = 1 login_maximum_lifetime_days = 1 ``` -The default cookie name for storing the auth token is `grafana_session`. you can configure this with `login_cookie_name` in `[auth]` settings. \ No newline at end of file +The default cookie name for storing the auth token is `grafana_session`. you can configure this with `login_cookie_name` in `[auth]` settings. diff --git a/latest.json b/latest.json index 7e69b431a4d..e19a0f8550d 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "6.0.0", - "testing": "6.0.0" + "stable": "6.0.1", + "testing": "6.0.1" } diff --git a/package.json b/package.json index 04a6967fdb9..c951e5241c1 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.1.0", "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1", + "@types/angular": "^1.6.6", "@types/chalk": "^2.2.0", "@types/classnames": "^2.2.6", "@types/commander": "^2.12.2", @@ -123,10 +124,10 @@ }, "scripts": { "dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js", - "start": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme", - "start:hot": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --hot --theme", - "start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --hot", - "watch": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme -d watch,start", + "start": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --watchTheme", + "start:hot": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot --watchTheme", + "start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot", + "watch": "yarn start -d watch,start core:start --watchTheme ", "build": "grunt build", "test": "grunt test", "tslint": "tslint -c tslint.json --project tsconfig.json", @@ -136,8 +137,11 @@ "storybook": "cd packages/grafana-ui && yarn storybook", "themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts", "prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"", - "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --build", - "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --release" + "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build", + "gui:releasePrepare": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release", + "gui:publish": "cd packages/grafana-ui/dist && npm publish --access public", + "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p --createVersionCommit", + "cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts" }, "husky": { "hooks": { @@ -169,7 +173,6 @@ "angular-native-dragdrop": "1.2.2", "angular-route": "1.6.6", "angular-sanitize": "1.6.6", - "ansicolor": "1.1.78", "baron": "^3.0.3", "brace": "^0.10.0", "classnames": "^2.2.6", diff --git a/packages/grafana-ui/README.md b/packages/grafana-ui/README.md index fa482003253..935124e99ba 100644 --- a/packages/grafana-ui/README.md +++ b/packages/grafana-ui/README.md @@ -12,6 +12,36 @@ See [package source](https://github.com/grafana/grafana/tree/master/packages/gra `npm install @grafana/ui` +## Development + +For development purposes we suggest using `yarn link` that will create symlink to @grafana/ui lib. To do so navigate to `packages/grafana-ui` and run `yarn link`. Then, navigate to your project and run `yarn link @grafana/ui` to use the linked version of the lib. To unlink follow the same procedure, but use `yarn unlink` instead. + +## Building @grafana/ui +To build @grafana/ui run `npm run gui:build` script *from Grafana repository root*. The build will be created in `packages/grafana-ui/dist` directory. Following steps from [Development](#development) you can test built package. + +## Releasing new version +To release new version run `npm run gui:release` script *from Grafana repository root*. The script will prepare the distribution package as well as prompt you to bump library version and publish it to the NPM registry. + +### Automatic version bump +When running `npm run gui:release` package.json file will be automatically updated. Also, package.json file will be commited and pushed to upstream branch. + +### Manual version bump +To use `package.json` defined version run `npm run gui:release --usePackageJsonVersion` *from Grafana repository root*. + +### Preparing release package without publishing to NPM registry +For testing purposes there is `npm run gui:releasePrepare` task that prepares distribution package without publishing it to the NPM registry. + +### V1 release process overview +1. Package is compiled with TSC. Typings are created in `/dist` directory, and the compiled js lands in `/compiled` dir +2. Rollup creates a CommonJS package based on compiled sources, and outputs it to `/dist` directory +3. Readme, changelog and index.js files are moved to `/dist` directory +4. Package version is bumped in both `@grafana/ui` package dir and in dist directory. +5. Version commit is created and pushed to master branch +5. Package is published to npm + + ## Versioning To limit the confusion related to @grafana/ui and Grafana versioning we decided to keep the major version in sync between those two. This means, that first version of @grafana/ui is taged with 6.0.0-alpha.0 to keep version in sync with Grafana 6.0 release. + + diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 43758d84c58..d9db01b32f8 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -25,6 +25,7 @@ "jquery": "^3.2.1", "lodash": "^4.17.10", "moment": "^2.22.2", + "papaparse": "^4.6.3", "react": "^16.6.3", "react-color": "^2.17.0", "react-custom-scrollbars": "^4.2.1", @@ -48,6 +49,7 @@ "@types/jquery": "^1.10.35", "@types/lodash": "^4.14.119", "@types/node": "^10.12.18", + "@types/papaparse": "^4.5.9", "@types/react": "^16.7.6", "@types/react-custom-scrollbars": "^4.0.5", "@types/react-test-renderer": "^16.0.3", diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx new file mode 100644 index 00000000000..c7a53af5ccf --- /dev/null +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx @@ -0,0 +1,54 @@ +import { storiesOf } from '@storybook/react'; +import { number, text } from '@storybook/addon-knobs'; +import { BarGauge } from './BarGauge'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; + +const getKnobs = () => { + return { + value: number('value', 70), + minValue: number('minValue', 0), + maxValue: number('maxValue', 100), + threshold1Value: number('threshold1Value', 40), + threshold1Color: text('threshold1Color', 'orange'), + threshold2Value: number('threshold2Value', 60), + threshold2Color: text('threshold2Color', 'red'), + unit: text('unit', 'ms'), + decimals: number('decimals', 1), + }; +}; + +const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module); + +BarGaugeStories.addDecorator(withCenteredStory); + +BarGaugeStories.add('Vertical, with basic thresholds', () => { + const { + value, + minValue, + maxValue, + threshold1Color, + threshold2Color, + threshold1Value, + threshold2Value, + unit, + decimals, + } = getKnobs(); + + return renderComponentWithTheme(BarGauge, { + width: 200, + height: 400, + value: value, + minValue: minValue, + maxValue: maxValue, + unit: unit, + prefix: '', + postfix: '', + decimals: decimals, + thresholds: [ + { index: 0, value: -Infinity, color: 'green' }, + { index: 1, value: threshold1Value, color: threshold1Color }, + { index: 1, value: threshold2Value, color: threshold2Color }, + ], + }); +}); diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx new file mode 100644 index 00000000000..8fa0b2846a5 --- /dev/null +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { BarGauge, Props } from './BarGauge'; +import { VizOrientation } from '../../types'; +import { getTheme } from '../../themes'; + +jest.mock('jquery', () => ({ + plot: jest.fn(), +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + maxValue: 100, + valueMappings: [], + minValue: 0, + prefix: '', + suffix: '', + thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }], + unit: 'none', + height: 300, + width: 300, + value: 25, + decimals: 0, + theme: getTheme(), + orientation: VizOrientation.Horizontal, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as BarGauge; + + return { + instance, + wrapper, + }; +}; + +describe('Get font color', () => { + it('should get first threshold color when only one threshold', () => { + const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] }); + + expect(instance.getValueColors().value).toEqual('#7EB26D'); + }); + + it('should get the threshold color if value is same as a threshold', () => { + const { instance } = setup({ + thresholds: [ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 10, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ], + }); + + expect(instance.getValueColors().value).toEqual('#EAB839'); + }); +}); + +describe('Render BarGauge with basic options', () => { + it('should render', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx new file mode 100644 index 00000000000..97cc4792785 --- /dev/null +++ b/packages/grafana-ui/src/components/BarGauge/BarGauge.tsx @@ -0,0 +1,239 @@ +// Library +import React, { PureComponent, CSSProperties } from 'react'; +import tinycolor from 'tinycolor2'; + +// Utils +import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils'; + +// Types +import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types'; + +const BAR_SIZE_RATIO = 0.8; + +export interface Props extends Themeable { + height: number; + unit: string; + width: number; + thresholds: Threshold[]; + valueMappings: ValueMapping[]; + value: TimeSeriesValue; + maxValue: number; + minValue: number; + orientation: VizOrientation; + prefix?: string; + suffix?: string; + decimals?: number; +} + +/* + * This visualization is still in POC state, needed more tests & better structure + */ +export class BarGauge extends PureComponent { + static defaultProps: Partial = { + maxValue: 100, + minValue: 0, + value: 100, + unit: 'none', + orientation: VizOrientation.Horizontal, + thresholds: [], + valueMappings: [], + }; + + getNumericValue(): number { + if (Number.isFinite(this.props.value as number)) { + return this.props.value as number; + } + return 0; + } + + getValueColors(): BarColors { + const { thresholds, theme, value } = this.props; + + const activeThreshold = getThresholdForValue(thresholds, value); + + if (activeThreshold !== null) { + const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type); + + return { + value: color, + border: color, + bar: tinycolor(color) + .setAlpha(0.3) + .toRgbString(), + }; + } + + return { + value: getColorFromHexRgbOrName('gray', theme.type), + bar: getColorFromHexRgbOrName('gray', theme.type), + border: getColorFromHexRgbOrName('gray', theme.type), + }; + } + + getCellColor(positionValue: TimeSeriesValue): string { + const { thresholds, theme, value } = this.props; + const activeThreshold = getThresholdForValue(thresholds, positionValue); + + if (activeThreshold !== null) { + const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type); + + // if we are past real value the cell is not "on" + if (value === null || (positionValue !== null && positionValue > value)) { + return tinycolor(color) + .setAlpha(0.15) + .toRgbString(); + } else { + return tinycolor(color) + .setAlpha(0.7) + .toRgbString(); + } + } + + return 'gray'; + } + + getValueStyles(value: string, color: string, width: number): CSSProperties { + const guess = width / (value.length * 1.1); + const fontSize = Math.min(Math.max(guess, 14), 40); + + return { + color: color, + fontSize: fontSize + 'px', + }; + } + + renderVerticalBar(valueFormatted: string, valuePercent: number) { + const { height, width } = this.props; + + const maxHeight = height * BAR_SIZE_RATIO; + const barHeight = Math.max(valuePercent * maxHeight, 0); + const colors = this.getValueColors(); + const valueStyles = this.getValueStyles(valueFormatted, colors.value, width); + + const containerStyles: CSSProperties = { + width: `${width}px`, + height: `${height}px`, + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', + }; + + const barStyles: CSSProperties = { + height: `${barHeight}px`, + width: `${width}px`, + backgroundColor: colors.bar, + borderTop: `1px solid ${colors.border}`, + }; + + return ( +
+
+ {valueFormatted} +
+
+
+ ); + } + + renderHorizontalBar(valueFormatted: string, valuePercent: number) { + const { height, width } = this.props; + + const maxWidth = width * BAR_SIZE_RATIO; + const barWidth = Math.max(valuePercent * maxWidth, 0); + const colors = this.getValueColors(); + const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO)); + + valueStyles.marginLeft = '8px'; + + const containerStyles: CSSProperties = { + width: `${width}px`, + height: `${height}px`, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }; + + const barStyles = { + height: `${height}px`, + width: `${barWidth}px`, + backgroundColor: colors.bar, + borderRight: `1px solid ${colors.border}`, + }; + + return ( +
+
+
+ {valueFormatted} +
+
+ ); + } + + renderHorizontalLCD(valueFormatted: string, valuePercent: number) { + const { height, width, maxValue, minValue } = this.props; + + const valueRange = maxValue - minValue; + const maxWidth = width * BAR_SIZE_RATIO; + const cellSpacing = 4; + const cellCount = 30; + const cellWidth = (maxWidth - cellSpacing * cellCount) / cellCount; + const colors = this.getValueColors(); + const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO)); + valueStyles.marginLeft = '8px'; + + const containerStyles: CSSProperties = { + width: `${width}px`, + height: `${height}px`, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }; + + const cells: JSX.Element[] = []; + + for (let i = 0; i < cellCount; i++) { + const currentValue = (valueRange / cellCount) * i; + const cellColor = this.getCellColor(currentValue); + const cellStyles: CSSProperties = { + width: `${cellWidth}px`, + backgroundColor: cellColor, + marginRight: '4px', + height: `${height}px`, + borderRadius: '2px', + }; + + cells.push(
); + } + + return ( +
+ {cells} +
+ {valueFormatted} +
+
+ ); + } + + render() { + const { maxValue, minValue, orientation, unit, decimals } = this.props; + + const numericValue = this.getNumericValue(); + const valuePercent = Math.min(numericValue / (maxValue - minValue), 1); + + const formatFunc = getValueFormat(unit); + const valueFormatted = formatFunc(numericValue, decimals); + const vertical = orientation === 'vertical'; + + return vertical + ? this.renderVerticalBar(valueFormatted, valuePercent) + : this.renderHorizontalLCD(valueFormatted, valuePercent); + } +} + +interface BarColors { + value: string; + bar: string; + border: string; +} diff --git a/packages/grafana-ui/src/components/BarGauge/_BarGauge.scss b/packages/grafana-ui/src/components/BarGauge/_BarGauge.scss new file mode 100644 index 00000000000..9e43439f0a2 --- /dev/null +++ b/packages/grafana-ui/src/components/BarGauge/_BarGauge.scss @@ -0,0 +1,9 @@ +.bar-gauge { + display: flex; + flex-direction: column; + justify-content: flex-end; +} + +.bar-gauge__value { + text-align: center; +} diff --git a/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap new file mode 100644 index 00000000000..65c647bd90c --- /dev/null +++ b/packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap @@ -0,0 +1,358 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render BarGauge with basic options should render 1`] = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 25 +
+
+`; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx index 9b5f5b98432..c67407f2e44 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx @@ -39,7 +39,7 @@ class ColorInput extends React.PureComponent { this.props.onChange(color); }; - handleChange = (event: React.SyntheticEvent) => { + onChange = (event: React.SyntheticEvent) => { const newColor = tinycolor(event.currentTarget.value); this.setState({ @@ -51,7 +51,7 @@ class ColorInput extends React.PureComponent { } }; - handleBlur = () => { + onBlur = () => { const newColor = tinycolor(this.state.value); if (!newColor.isValid()) { @@ -84,7 +84,7 @@ class ColorInput extends React.PureComponent { flexGrow: 1, }} > - +
); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx index f7fa71391ae..c4f2a848029 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx @@ -50,7 +50,16 @@ ColorPickerStories.add('Series color picker', () => { color={selectedColor} onChange={color => updateSelectedColor(color)} > -
Open color picker
+ {({ ref, showColorPicker, hideColorPicker }) => ( +
+ Open color picker +
+ )} ); }} diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx new file mode 100644 index 00000000000..8bf41f307f3 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { ColorPicker } from './ColorPicker'; +import { ColorPickerTrigger } from './ColorPickerTrigger'; + +describe('ColorPicker', () => { + it('renders ColorPickerTrigger component by default', () => { + expect( + renderer.create( {}} />).root.findByType(ColorPickerTrigger) + ).toBeTruthy(); + }); + + it('renders custom trigger when supplied', () => { + const div = renderer + .create( + {}}> + {() =>
Custom trigger
} +
+ ) + .root.findByType('div'); + expect(div.children[0]).toBe('Custom trigger'); + }); +}); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 183cbfed67f..fd5f81c3f33 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -1,4 +1,5 @@ import React, { Component, createRef } from 'react'; +import { omit } from 'lodash'; import { PopperController } from '../Tooltip/PopperController'; import { Popper } from '../Tooltip/Popper'; import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover'; @@ -6,16 +7,31 @@ import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; import { withTheme } from '../../themes/ThemeContext'; +import { ColorPickerTrigger } from './ColorPickerTrigger'; + +/** + * If you need custom trigger for the color picker you can do that with a render prop pattern and supply a function + * as a child. You will get show/hide function which you can map to desired interaction (like onClick or onMouseLeave) + * and a ref which needs to be passed to an HTMLElement for correct positioning. If you want to use class or functional + * component as a custom trigger you will need to forward the reference to first HTMLElement child. + */ +type ColorPickerTriggerRenderer = (props: { + // This should be a React.RefObject but due to how object refs are defined you cannot downcast from that + // to a specific type like React.RefObject even though it would be fine in runtime. + ref: React.RefObject; + showColorPicker: () => void; + hideColorPicker: () => void; +}) => React.ReactNode; export const colorPickerFactory = ( popover: React.ComponentType, displayName = 'ColorPicker' ) => { - return class ColorPicker extends Component { + return class ColorPicker extends Component { static displayName = displayName; - pickerTriggerRef = createRef(); + pickerTriggerRef = createRef(); - handleColorChange = (color: string) => { + onColorChange = (color: string) => { const { onColorChange, onChange } = this.props; const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler; @@ -23,11 +39,11 @@ export const colorPickerFactory = ( }; render() { - const popoverElement = React.createElement(popover, { - ...this.props, - onChange: this.handleColorChange, - }); const { theme, children } = this.props; + const popoverElement = React.createElement(popover, { + ...omit(this.props, 'children'), + onChange: this.onColorChange, + }); return ( @@ -45,27 +61,21 @@ export const colorPickerFactory = ( )} {children ? ( - React.cloneElement(children as JSX.Element, { + // Children have a bit weird type due to intersection used in the definition so we need to cast here, + // but the definition is correct and should not allow to pass a children that does not conform to + // ColorPickerTriggerRenderer type. + (children as ColorPickerTriggerRenderer)({ ref: this.pickerTriggerRef, - onClick: showPopper, - onMouseLeave: hidePopper, + showColorPicker: showPopper, + hideColorPicker: hidePopper, }) ) : ( -
-
-
-
-
+ color={getColorFromHexRgbOrName(this.props.color || '#000000', theme.type)} + /> )} ); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index ce9ca5130d4..934bc56a550 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -17,8 +17,8 @@ export interface ColorPickerProps extends Themeable { */ onColorChange?: ColorPickerChangeHandler; enableNamedColors?: boolean; - children?: JSX.Element; } + export interface Props extends ColorPickerProps, PopperContentProps { customPickers?: T; } @@ -60,7 +60,7 @@ export class ColorPickerPopover extends React changeHandler(getColorFromHexRgbOrName(color, theme.type)); }; - handleTabChange = (tab: PickerType | keyof T) => { + onTabChange = (tab: PickerType | keyof T) => { return () => this.setState({ activePicker: tab }); }; @@ -104,7 +104,7 @@ export class ColorPickerPopover extends React <> {Object.keys(customPickers).map(key => { return ( -
+
{customPickers[key].name}
); @@ -119,10 +119,10 @@ export class ColorPickerPopover extends React return (
-
+
Colors
-
+
Custom
{this.renderCustomPickerTabs()} diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx new file mode 100644 index 00000000000..0445fd465e3 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx @@ -0,0 +1,56 @@ +import React, { forwardRef } from 'react'; + +interface ColorPickerTriggerProps { + onClick: () => void; + onMouseLeave: () => void; + color: string; +} + +export const ColorPickerTrigger = forwardRef(function ColorPickerTrigger( + props: ColorPickerTriggerProps, + ref: React.Ref +) { + return ( +
+
+
+
+
+ ); +}); diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx index f4901b28bfd..3e5639172be 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx @@ -8,7 +8,7 @@ import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; import { UseState } from '../../utils/storybook/UseState'; const BasicGreen = getColorDefinitionByName('green'); -const BasicBlue = getColorDefinitionByName('blue'); +const BasicRed = getColorDefinitionByName('red'); const LightBlue = getColorDefinitionByName('light-blue'); const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module); @@ -41,7 +41,7 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors', 'Selected color', { Green: BasicGreen.variants.dark, - Red: BasicBlue.variants.dark, + Red: BasicRed.variants.dark, 'Light blue': LightBlue.variants.dark, }, 'red' diff --git a/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss b/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss index b07fe2433c9..626a03538bb 100644 --- a/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss +++ b/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss @@ -161,59 +161,6 @@ $arrowSize: 15px; flex-grow: 1; } -.sp-replacer { - background: inherit; - border: none; - color: inherit; - padding: 0; - border-radius: 10px; - cursor: pointer; -} - -.sp-replacer:hover, -.sp-replacer.sp-active { - border-color: inherit; - color: inherit; -} - -.sp-container { - border-radius: 0; - background-color: $dropdownBackground; - border: none; - padding: 0; -} - -.sp-palette-container, -.sp-picker-container { - border: none; -} - -.sp-dd { - display: none; -} - -.sp-preview { - position: relative; - width: 15px; - height: 15px; - border: none; - margin: 0; - float: left; - z-index: 0; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==); -} - -.sp-preview-inner, -.sp-alpha-inner, -.sp-thumb-inner { - display: block; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; -} - .gf-color-picker__body { padding-bottom: $arrowSize; padding-left: 6px; diff --git a/packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts b/packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts index b8919c682e2..fb4e454d2f9 100644 --- a/packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts +++ b/packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts @@ -1,9 +1,9 @@ -import propDeprecationWarning from '../../utils/propDeprecationWarning'; +import deprecationWarning from '../../utils/deprecationWarning'; import { ColorPickerProps } from './ColorPickerPopover'; export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => { const { onColorChange } = props; if (onColorChange) { - propDeprecationWarning(componentName, 'onColorChange', 'onChange'); + deprecationWarning(componentName, 'onColorChange', 'onChange'); } }; diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 1bc1fd42792..945e3d825d8 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -15,6 +15,7 @@ interface Props { scrollTop?: number; setScrollTop: (event: any) => void; autoHeightMin?: number | string; + updateAfterMountMs?: number; } /** @@ -48,6 +49,20 @@ export class CustomScrollbar extends Component { componentDidMount() { this.updateScroll(); + + // this logic is to make scrollbar visible when content is added body after mount + if (this.props.updateAfterMountMs) { + setTimeout(() => this.updateAfterMount(), this.props.updateAfterMountMs); + } + } + + updateAfterMount() { + if (this.ref && this.ref.current) { + const scrollbar = this.ref.current as any; + if (scrollbar.update) { + scrollbar.update(); + } + } } componentDidUpdate() { diff --git a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx index ccbccea9c5c..0f5e85414eb 100644 --- a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx +++ b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx @@ -1,24 +1,17 @@ -import React, { FunctionComponent } from 'react'; +import React from 'react'; import { storiesOf } from '@storybook/react'; import { DeleteButton } from './DeleteButton'; - -const CenteredStory: FunctionComponent<{}> = ({ children }) => { - return ( -
- {children} -
- ); -}; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { action } from '@storybook/addon-actions'; storiesOf('UI/DeleteButton', module) - .addDecorator(story => {story()}) + .addDecorator(withCenteredStory) .add('default', () => { - return {}} />; + return ( + { + action('Delete Confirmed')('delete!'); + }} + /> + ); }); diff --git a/packages/grafana-ui/src/components/FormField/_FormField.scss b/packages/grafana-ui/src/components/FormField/_FormField.scss index 36955e2fca6..0c69e67f82a 100644 --- a/packages/grafana-ui/src/components/FormField/_FormField.scss +++ b/packages/grafana-ui/src/components/FormField/_FormField.scss @@ -1,5 +1,5 @@ .form-field { - margin-bottom: $gf-form-margin; + margin-bottom: $space-xxs; display: flex; flex-direction: row; align-items: center; diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index d04daae3dab..3d1567d4937 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -1,12 +1,12 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { getMappedValue } from '../../utils/valueMappings'; -import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; -import { Themeable, GrafanaThemeType } from '../../types/theme'; -import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel'; -import { getValueFormat } from '../../utils/valueFormats/valueFormats'; -type TimeSeriesValue = string | number | null; +import { ValueMapping, Threshold, GrafanaThemeType } from '../../types'; +import { getMappedValue } from '../../utils/valueMappings'; +import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils'; +import { Themeable } from '../../index'; + +type GaugeValue = string | number | null; export interface Props extends Themeable { decimals?: number | null; @@ -30,7 +30,7 @@ const FONT_SCALE = 1; export class Gauge extends PureComponent { canvasElement: any; - static defaultProps = { + static defaultProps: Partial = { maxValue: 100, valueMappings: [], minValue: 0, @@ -41,7 +41,6 @@ export class Gauge extends PureComponent { thresholds: [], unit: 'none', stat: 'avg', - theme: GrafanaThemeType.Dark, }; componentDidMount() { @@ -52,7 +51,7 @@ export class Gauge extends PureComponent { this.draw(); } - formatValue(value: TimeSeriesValue) { + formatValue(value: GaugeValue) { const { decimals, valueMappings, prefix, suffix, unit } = this.props; if (isNaN(value as number)) { @@ -73,26 +72,16 @@ export class Gauge extends PureComponent { return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`; } - getFontColor(value: TimeSeriesValue) { + getFontColor(value: GaugeValue): string { const { thresholds, theme } = this.props; - if (thresholds.length === 1) { - return getColorFromHexRgbOrName(thresholds[0].color, theme.type); + const activeThreshold = getThresholdForValue(thresholds, value); + + if (activeThreshold !== null) { + return getColorFromHexRgbOrName(activeThreshold.color, theme.type); } - const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0]; - if (atThreshold) { - return getColorFromHexRgbOrName(atThreshold.color, theme.type); - } - - const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value); - - if (belowThreshold.length > 0) { - const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0]; - return getColorFromHexRgbOrName(nearestThreshold.color, theme.type); - } - - return BasicGaugeColor.Red; + return ''; } getFormattedThresholds() { @@ -134,7 +123,7 @@ export class Gauge extends PureComponent { Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1); const thresholdLabelFontSize = fontSize / 2.5; - const options = { + const options: any = { series: { gauges: { gauge: { @@ -184,19 +173,15 @@ export class Gauge extends PureComponent { const { height, width } = this.props; return ( -
-
(this.canvasElement = element)} - /> -
+
(this.canvasElement = element)} + /> ); } } - -export default Gauge; diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss index 4ce9c5264ea..2b1a401d1e4 100644 --- a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -53,7 +53,7 @@ } .panel-options-group__title { - font-size: 1.1rem; + font-size: 16px; position: relative; top: 1px; } diff --git a/packages/grafana-ui/src/components/Select/_Select.scss b/packages/grafana-ui/src/components/Select/_Select.scss index bc18ed9d369..f0270b1422e 100644 --- a/packages/grafana-ui/src/components/Select/_Select.scss +++ b/packages/grafana-ui/src/components/Select/_Select.scss @@ -3,7 +3,7 @@ $select-input-bg-disabled: $input-bg-disabled; @mixin select-control() { width: 100%; - margin-right: $gf-form-margin; + margin-right: $space-xs; @include border-radius($input-border-radius-sm); background-color: $input-bg; } diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx new file mode 100644 index 00000000000..03765730343 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -0,0 +1,99 @@ +// import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { Table } from './Table'; +import { getTheme } from '../../themes'; + +import { migratedTestTable, migratedTestStyles, simpleTable } from './examples'; +import { ScopedVars, TableData, GrafanaThemeType } from '../../types/index'; +import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory'; +import { number, boolean } from '@storybook/addon-knobs'; + +const replaceVariables = (value: string, scopedVars?: ScopedVars) => { + if (scopedVars) { + // For testing variables replacement in link + for (const key in scopedVars) { + const val = scopedVars[key]; + value = value.replace('$' + key, val.value); + } + } + return value; +}; + +export function columnIndexToLeter(column: number) { + const A = 'A'.charCodeAt(0); + const c1 = Math.floor(column / 26); + const c2 = column % 26; + if (c1 > 0) { + return String.fromCharCode(A + c1 - 1) + String.fromCharCode(A + c2); + } + return String.fromCharCode(A + c2); +} + +export function makeDummyTable(columnCount: number, rowCount: number): TableData { + return { + columns: Array.from(new Array(columnCount), (x, i) => { + return { + text: columnIndexToLeter(i), + }; + }), + rows: Array.from(new Array(rowCount), (x, rowId) => { + const suffix = (rowId + 1).toString(); + return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix); + }), + type: 'table', + columnMap: {}, + }; +} + +storiesOf('Alpha/Table', module) + .add('Basic Table', () => { + // NOTE: This example does not seem to survice rotate & + // Changing fixed headers... but the next one does? + // perhaps `simpleTable` is static and reused? + + const showHeader = boolean('Show Header', true); + const fixedHeader = boolean('Fixed Header', true); + const fixedColumns = number('Fixed Columns', 0, { min: 0, max: 50, step: 1, range: false }); + const rotate = boolean('Rotate', false); + + return withFullSizeStory(Table, { + styles: [], + data: simpleTable, + replaceVariables, + showHeader, + fixedHeader, + fixedColumns, + rotate, + theme: getTheme(GrafanaThemeType.Light), + }); + }) + .add('Variable Size', () => { + const columnCount = number('Column Count', 15, { min: 2, max: 50, step: 1, range: false }); + const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false }); + + const showHeader = boolean('Show Header', true); + const fixedHeader = boolean('Fixed Header', true); + const fixedColumns = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false }); + const rotate = boolean('Rotate', false); + + return withFullSizeStory(Table, { + styles: [], + data: makeDummyTable(columnCount, rowCount), + replaceVariables, + showHeader, + fixedHeader, + fixedColumns, + rotate, + theme: getTheme(GrafanaThemeType.Light), + }); + }) + .add('Test Config (migrated)', () => { + return withFullSizeStory(Table, { + styles: migratedTestStyles, + data: migratedTestTable, + replaceVariables, + showHeader: true, + rotate: true, + theme: getTheme(GrafanaThemeType.Light), + }); + }); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx new file mode 100644 index 00000000000..a551d6d221d --- /dev/null +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -0,0 +1,287 @@ +// Libraries +import _ from 'lodash'; +import React, { Component, ReactElement } from 'react'; +import { + SortDirectionType, + SortIndicator, + MultiGrid, + CellMeasurerCache, + CellMeasurer, + GridCellProps, +} from 'react-virtualized'; +import { Themeable } from '../../types/theme'; + +import { sortTableData } from '../../utils/processTableData'; + +import { TableData, InterpolateFunction } from '@grafana/ui'; +import { + TableCellBuilder, + ColumnStyle, + getCellBuilder, + TableCellBuilderOptions, + simpleCellBuilder, +} from './TableCellBuilder'; +import { stringToJsRegex } from '../../utils/index'; + +export interface Props extends Themeable { + data: TableData; + + showHeader: boolean; + fixedHeader: boolean; + fixedColumns: number; + rotate: boolean; + styles: ColumnStyle[]; + + replaceVariables: InterpolateFunction; + width: number; + height: number; + isUTC?: boolean; +} + +interface State { + sortBy?: number; + sortDirection?: SortDirectionType; + data: TableData; +} + +interface ColumnRenderInfo { + header: string; + builder: TableCellBuilder; +} + +interface DataIndex { + column: number; + row: number; // -1 is the header! +} + +export class Table extends Component { + renderer: ColumnRenderInfo[]; + measurer: CellMeasurerCache; + scrollToTop = false; + + static defaultProps = { + showHeader: true, + fixedHeader: true, + fixedColumns: 0, + rotate: false, + }; + + constructor(props: Props) { + super(props); + + this.state = { + data: props.data, + }; + + this.renderer = this.initColumns(props); + this.measurer = new CellMeasurerCache({ + defaultHeight: 30, + defaultWidth: 150, + }); + } + + componentDidUpdate(prevProps: Props, prevState: State) { + const { data, styles, showHeader } = this.props; + const { sortBy, sortDirection } = this.state; + const dataChanged = data !== prevProps.data; + const configsChanged = + showHeader !== prevProps.showHeader || + this.props.rotate !== prevProps.rotate || + this.props.fixedColumns !== prevProps.fixedColumns || + this.props.fixedHeader !== prevProps.fixedHeader; + + // Reset the size cache + if (dataChanged || configsChanged) { + this.measurer.clearAll(); + } + + // Update the renderer if options change + // We only *need* do to this if the header values changes, but this does every data update + if (dataChanged || styles !== prevProps.styles) { + this.renderer = this.initColumns(this.props); + } + + // Update the data when data or sort changes + if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { + this.scrollToTop = true; + this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') }); + } + } + + /** Given the configuration, setup how each column gets rendered */ + initColumns(props: Props): ColumnRenderInfo[] { + const { styles, data } = props; + + return data.columns.map((col, index) => { + let title = col.text; + let style: ColumnStyle | null = null; // ColumnStyle + + // Find the style based on the text + for (let i = 0; i < styles.length; i++) { + const s = styles[i]; + const regex = stringToJsRegex(s.pattern); + if (title.match(regex)) { + style = s; + if (s.alias) { + title = title.replace(regex, s.alias); + } + break; + } + } + + return { + header: title, + builder: getCellBuilder(col, style, this.props), + }; + }); + } + + //---------------------------------------------------------------------- + //---------------------------------------------------------------------- + + doSort = (columnIndex: number) => { + let sort: any = this.state.sortBy; + let dir = this.state.sortDirection; + if (sort !== columnIndex) { + dir = 'DESC'; + sort = columnIndex; + } else if (dir === 'DESC') { + dir = 'ASC'; + } else { + sort = null; + } + this.setState({ sortBy: sort, sortDirection: dir }); + }; + + /** Converts the grid coordinates to TableData coordinates */ + getCellRef = (rowIndex: number, columnIndex: number): DataIndex => { + const { showHeader, rotate } = this.props; + const rowOffset = showHeader ? -1 : 0; + + if (rotate) { + return { column: rowIndex, row: columnIndex + rowOffset }; + } else { + return { column: columnIndex, row: rowIndex + rowOffset }; + } + }; + + onCellClick = (rowIndex: number, columnIndex: number) => { + const { row, column } = this.getCellRef(rowIndex, columnIndex); + if (row < 0) { + this.doSort(column); + } else { + const values = this.state.data.rows[row]; + const value = values[column]; + console.log('CLICK', value, row); + } + }; + + headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => { + const { data, sortBy, sortDirection } = this.state; + const { columnIndex, rowIndex, style } = cell.props; + const { column } = this.getCellRef(rowIndex, columnIndex); + + let col = data.columns[column]; + const sorting = sortBy === column; + if (!col) { + col = { + text: '??' + columnIndex + '???', + }; + } + + return ( +
this.onCellClick(rowIndex, columnIndex)}> + {col.text} + {sorting && } +
+ ); + }; + + getTableCellBuilder = (column: number): TableCellBuilder => { + const render = this.renderer[column]; + if (render && render.builder) { + return render.builder; + } + return simpleCellBuilder; // the default + }; + + cellRenderer = (props: GridCellProps): React.ReactNode => { + const { rowIndex, columnIndex, key, parent } = props; + const { row, column } = this.getCellRef(rowIndex, columnIndex); + const { data } = this.state; + + const isHeader = row < 0; + const rowData = isHeader ? data.columns : data.rows[row]; + const value = rowData ? rowData[column] : ''; + const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column); + + return ( + + {builder({ + value, + row: rowData, + column: data.columns[column], + table: this, + props, + })} + + ); + }; + + render() { + const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props; + const { data } = this.state; + + let columnCount = data.columns.length; + let rowCount = data.rows.length + (showHeader ? 1 : 0); + + let fixedColumnCount = Math.min(fixedColumns, columnCount); + let fixedRowCount = showHeader && fixedHeader ? 1 : 0; + + if (rotate) { + const temp = columnCount; + columnCount = rowCount; + rowCount = temp; + + fixedRowCount = 0; + fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0); + } + + // Called after sort or the data changes + const scroll = this.scrollToTop ? 1 : -1; + const scrollToRow = rotate ? -1 : scroll; + const scrollToColumn = rotate ? scroll : -1; + if (this.scrollToTop) { + this.scrollToTop = false; + } + + return ( + + ); + } +} + +export default Table; diff --git a/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx new file mode 100644 index 00000000000..b8cb91053d8 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableCellBuilder.tsx @@ -0,0 +1,291 @@ +// Libraries +import _ from 'lodash'; +import React, { ReactElement } from 'react'; +import { GridCellProps } from 'react-virtualized'; +import { Table, Props } from './Table'; +import moment from 'moment'; +import { ValueFormatter } from '../../utils/index'; +import { GrafanaTheme } from '../../types/theme'; +import { getValueFormat, getColorFromHexRgbOrName, Column } from '@grafana/ui'; +import { InterpolateFunction } from '../../types/panel'; + +export interface TableCellBuilderOptions { + value: any; + column?: Column; + row?: any[]; + table?: Table; + className?: string; + props: GridCellProps; +} + +export type TableCellBuilder = (cell: TableCellBuilderOptions) => ReactElement<'div'>; + +/** Simplest cell that just spits out the value */ +export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOptions) => { + const { props, value, className } = cell; + const { style } = props; + + return ( +
+ {value} +
+ ); +}; + +// *************************************************************************** +// HERE BE DRAGONS!!! +// *************************************************************************** +// +// The following code has been migrated blindy two times from the angular +// table panel. I don't understand all the options nor do I know if they +// are correct! +// +// *************************************************************************** + +// Made to match the existing (untyped) settings in the angular table +export interface ColumnStyle { + pattern: string; + + alias?: string; + colorMode?: 'cell' | 'value'; + colors?: any[]; + decimals?: number; + thresholds?: any[]; + type?: 'date' | 'number' | 'string' | 'hidden'; + unit?: string; + dateFormat?: string; + sanitize?: boolean; // not used in react + mappingType?: any; + valueMaps?: any; + rangeMaps?: any; + + link?: any; + linkUrl?: any; + linkTooltip?: any; + linkTargetBlank?: boolean; + + preserveFormat?: boolean; +} + +// private mapper:ValueMapper, +// private style:ColumnStyle, +// private theme:GrafanaTheme, +// private column:Column, +// private replaceVariables: InterpolateFunction, +// private fmt?:ValueFormatter) { + +export function getCellBuilder(schema: Column, style: ColumnStyle | null, props: Props): TableCellBuilder { + if (!style) { + return simpleCellBuilder; + } + + if (style.type === 'hidden') { + // TODO -- for hidden, we either need to: + // 1. process the Table and remove hidden fields + // 2. do special math to pick the right column skipping hidden fields + throw new Error('hidden not supported!'); + } + + if (style.type === 'date') { + return new CellBuilderWithStyle( + (v: any) => { + if (v === undefined || v === null) { + return '-'; + } + + if (_.isArray(v)) { + v = v[0]; + } + let date = moment(v); + if (false) { + // TODO?????? this.props.isUTC) { + date = date.utc(); + } + return date.format(style.dateFormat); + }, + style, + props.theme, + schema, + props.replaceVariables + ).build; + } + + if (style.type === 'string') { + return new CellBuilderWithStyle( + (v: any) => { + if (_.isArray(v)) { + v = v.join(', '); + } + return v; + }, + style, + props.theme, + schema, + props.replaceVariables + ).build; + // TODO!!!! all the mapping stuff!!!! + } + + if (style.type === 'number') { + const valueFormatter = getValueFormat(style.unit || schema.unit || 'none'); + return new CellBuilderWithStyle( + (v: any) => { + if (v === null || v === void 0) { + return '-'; + } + return v; + }, + style, + props.theme, + schema, + props.replaceVariables, + valueFormatter + ).build; + } + + return simpleCellBuilder; +} + +type ValueMapper = (value: any) => any; + +// Runs the value through a formatter and adds colors to the cell properties +class CellBuilderWithStyle { + constructor( + private mapper: ValueMapper, + private style: ColumnStyle, + private theme: GrafanaTheme, + private column: Column, + private replaceVariables: InterpolateFunction, + private fmt?: ValueFormatter + ) { + // + console.log('COLUMN', column.text, theme); + } + + getColorForValue = (value: any): string | null => { + const { thresholds, colors } = this.style; + if (!thresholds || !colors) { + return null; + } + + for (let i = thresholds.length; i > 0; i--) { + if (value >= thresholds[i - 1]) { + return getColorFromHexRgbOrName(colors[i], this.theme.type); + } + } + return getColorFromHexRgbOrName(_.first(colors), this.theme.type); + }; + + build = (cell: TableCellBuilderOptions) => { + let { props } = cell; + let value = this.mapper(cell.value); + + if (_.isNumber(value)) { + if (this.fmt) { + value = this.fmt(value, this.style.decimals); + } + + // For numeric values set the color + const { colorMode } = this.style; + if (colorMode) { + const color = this.getColorForValue(Number(value)); + if (color) { + if (colorMode === 'cell') { + props = { + ...props, + style: { + ...props.style, + backgroundColor: color, + color: 'white', + }, + }; + } else if (colorMode === 'value') { + props = { + ...props, + style: { + ...props.style, + color: color, + }, + }; + } + } + } + } + + const cellClasses = []; + if (this.style.preserveFormat) { + cellClasses.push('table-panel-cell-pre'); + } + + if (this.style.link) { + // Render cell as link + const { row } = cell; + + const scopedVars: any = {}; + if (row) { + for (let i = 0; i < row.length; i++) { + scopedVars[`__cell_${i}`] = { value: row[i] }; + } + } + scopedVars['__cell'] = { value: value }; + + const cellLink = this.replaceVariables(this.style.linkUrl, scopedVars, encodeURIComponent); + const cellLinkTooltip = this.replaceVariables(this.style.linkTooltip, scopedVars); + const cellTarget = this.style.linkTargetBlank ? '_blank' : ''; + + cellClasses.push('table-panel-cell-link'); + value = ( + + {value} + + ); + } + + // ??? I don't think this will still work! + if (this.column.filterable) { + cellClasses.push('table-panel-cell-filterable'); + value = ( + <> + {value} + + + + + + + + + + ); + } + + let className; + if (cellClasses.length) { + className = cellClasses.join(' '); + } + + return simpleCellBuilder({ value, props, className }); + }; +} diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx b/packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx new file mode 100644 index 00000000000..604eefca7a2 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { storiesOf } from '@storybook/react'; +import TableInputCSV from './TableInputCSV'; +import { action } from '@storybook/addon-actions'; +import { TableData } from '../../types/data'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; + +const TableInputStories = storiesOf('UI/Table/Input', module); + +TableInputStories.addDecorator(withCenteredStory); + +TableInputStories.add('default', () => { + return ( +
+ { + console.log('Table', table, text); + action('Table')(table, text); + }} + /> +
+ ); +}); diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx b/packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx new file mode 100644 index 00000000000..9e2dfbf80c3 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import renderer from 'react-test-renderer'; +import TableInputCSV from './TableInputCSV'; +import { TableData } from '../../types/data'; + +describe('TableInputCSV', () => { + it('renders correctly', () => { + const tree = renderer + .create( + { + // console.log('Table:', table, 'from:', text); + }} + /> + ) + .toJSON(); + //expect(tree).toMatchSnapshot(); + expect(tree).toBeDefined(); + }); +}); diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.tsx b/packages/grafana-ui/src/components/Table/TableInputCSV.tsx new file mode 100644 index 00000000000..14bc68550c3 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableInputCSV.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import debounce from 'lodash/debounce'; +import { parseCSV, TableParseOptions, TableParseDetails } from '../../utils/processTableData'; +import { TableData } from '../../types/data'; +import { AutoSizer } from 'react-virtualized'; + +interface Props { + options?: TableParseOptions; + text: string; + onTableParsed: (table: TableData, text: string) => void; +} + +interface State { + text: string; + table: TableData; + details: TableParseDetails; +} + +/** + * Expects the container div to have size set and will fill it 100% + */ +class TableInputCSV extends React.PureComponent { + constructor(props: Props) { + super(props); + + // Shoud this happen in onComponentMounted? + const { text, options, onTableParsed } = props; + const details = {}; + const table = parseCSV(text, options, details); + this.state = { + text, + table, + details, + }; + onTableParsed(table, text); + } + + readCSV = debounce(() => { + const details = {}; + const table = parseCSV(this.state.text, this.props.options, details); + this.setState({ table, details }); + }, 150); + + componentDidUpdate(prevProps: Props, prevState: State) { + const { text } = this.state; + if (text !== prevState.text || this.props.options !== prevProps.options) { + this.readCSV(); + } + // If the props text has changed, replace our local version + if (this.props.text !== prevProps.text && this.props.text !== text) { + this.setState({ text: this.props.text }); + } + + if (this.state.table !== prevState.table) { + this.props.onTableParsed(this.state.table, this.state.text); + } + } + + onFooterClicked = (event: any) => { + console.log('Errors', this.state); + const message = this.state.details + .errors!.map(err => { + return err.message; + }) + .join('\n'); + alert('CSV Parsing Errors:\n' + message); + }; + + onTextChange = (event: any) => { + this.setState({ text: event.target.value }); + }; + + render() { + const { table, details } = this.state; + + const hasErrors = details.errors && details.errors.length > 0; + const footerClassNames = hasErrors ? 'gf-table-input-csv-err' : ''; + + return ( + + {({ height, width }) => ( +
+