Compare commits
422 Commits
v6.0.0-bet
...
v6.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a71b199ca | ||
|
|
e50b7b3279 | ||
|
|
99df7b87c0 | ||
|
|
6d11999e20 | ||
|
|
5195954681 | ||
|
|
18615a3357 | ||
|
|
afc2efa56d | ||
|
|
8e93b68e6d | ||
|
|
b920ee0ea3 | ||
|
|
b93cdf56fb | ||
|
|
c332e106a2 | ||
|
|
e4e42fcd08 | ||
|
|
c4fa64e6dc | ||
|
|
93f1a48641 | ||
|
|
4408817e65 | ||
|
|
1c364b57b5 | ||
|
|
931bece16e | ||
|
|
962815169e | ||
|
|
ac345312a4 | ||
|
|
56b35354c7 | ||
|
|
52fe6b0316 | ||
|
|
8769b7aa57 | ||
|
|
757a98257d | ||
|
|
1f0c7727f4 | ||
|
|
63f465f0ac | ||
|
|
9472d7e600 | ||
|
|
a7c44c2ce7 | ||
|
|
82e330a1c5 | ||
|
|
e53f41e511 | ||
|
|
22d64a8c2a | ||
|
|
c13e302cde | ||
|
|
5e6c746c9b | ||
|
|
77ba734491 | ||
|
|
c92cd73bd8 | ||
|
|
9570394c49 | ||
|
|
a1cd550df4 | ||
|
|
13f21fffc4 | ||
|
|
1f7a1f807e | ||
|
|
0b74860f55 | ||
|
|
b14958edef | ||
|
|
b9c36e5301 | ||
|
|
519dfd0899 | ||
|
|
a4dd63e224 | ||
|
|
1b22ceabbe | ||
|
|
6f16f70456 | ||
|
|
37a73b6b35 | ||
|
|
ba2f698b81 | ||
|
|
9485c67827 | ||
|
|
d970c3a686 | ||
|
|
5af1bd657f | ||
|
|
5dc864b47f | ||
|
|
7ce18ec4f7 | ||
|
|
a54484638d | ||
|
|
b780b6377a | ||
|
|
217eb6310e | ||
|
|
8f6ccce16f | ||
|
|
f73f0e69e0 | ||
|
|
3ce99bca66 | ||
|
|
f39fef2a02 | ||
|
|
784d4fb70d | ||
|
|
2c8c4729a8 | ||
|
|
396a5a947f | ||
|
|
a402a3713f | ||
|
|
2a2b242eb0 | ||
|
|
e75e69a709 | ||
|
|
1cff59731c | ||
|
|
3bf0a5ffc6 | ||
|
|
9e0c795228 | ||
|
|
58e57a1669 | ||
|
|
9565e48f03 | ||
|
|
c34021344a | ||
|
|
5eea85a3a3 | ||
|
|
75f89ecf1f | ||
|
|
be11da5b31 | ||
|
|
06972144d2 | ||
|
|
7b761f0a28 | ||
|
|
41217ea110 | ||
|
|
bd48408e2d | ||
|
|
d6904ba9b4 | ||
|
|
60327953a2 | ||
|
|
9ab06c6eef | ||
|
|
3e583707ca | ||
|
|
295bc425ff | ||
|
|
f38e64cc5d | ||
|
|
a0729b9b50 | ||
|
|
452c4f5b9b | ||
|
|
b94de101cd | ||
|
|
b816f35c41 | ||
|
|
105879ab5d | ||
|
|
716db35fae | ||
|
|
a5e5db20e1 | ||
|
|
d2aed7e075 | ||
|
|
2987a47a9b | ||
|
|
bd6cefa53f | ||
|
|
89c153d44b | ||
|
|
2e050d3337 | ||
|
|
748cb44911 | ||
|
|
169732997d | ||
|
|
10194df112 | ||
|
|
db8c74c4c3 | ||
|
|
1bc2a0af70 | ||
|
|
0e228d582d | ||
|
|
3d5ae3dca3 | ||
|
|
3e317b7d20 | ||
|
|
5436c28448 | ||
|
|
7e03913d0d | ||
|
|
71576a634e | ||
|
|
bb8bec5aaa | ||
|
|
b32d420a75 | ||
|
|
21a1507c77 | ||
|
|
13d9acb1ef | ||
|
|
0bd39e5426 | ||
|
|
0f96cf8662 | ||
|
|
df17f7dc45 | ||
|
|
2be60887ca | ||
|
|
e080983147 | ||
|
|
9c18aa8684 | ||
|
|
b4267eafb8 | ||
|
|
6e7941d396 | ||
|
|
0cc9cbcb54 | ||
|
|
c71904e326 | ||
|
|
78ea7ae783 | ||
|
|
ac50d2b31f | ||
|
|
b545f51820 | ||
|
|
8f0e9b674d | ||
|
|
e7917ce4e0 | ||
|
|
5ba3b0aa2c | ||
|
|
00a676ff86 | ||
|
|
8c65430ea3 | ||
|
|
2eca4caa5d | ||
|
|
89d69a6f21 | ||
|
|
61e9148eed | ||
|
|
5f808ddf22 | ||
|
|
487e7b5ea6 | ||
|
|
170783c292 | ||
|
|
8b080f0511 | ||
|
|
4bd94b8aba | ||
|
|
3555997f98 | ||
|
|
1a140ee199 | ||
|
|
6caae9167e | ||
|
|
7edc3fdd5c | ||
|
|
baeec495a2 | ||
|
|
961695a61f | ||
|
|
a4841a72d9 | ||
|
|
dd0afd0a0b | ||
|
|
ee132c1091 | ||
|
|
8574dca081 | ||
|
|
836501186f | ||
|
|
dc6b27d123 | ||
|
|
1e4c6b4b52 | ||
|
|
7762d72ae3 | ||
|
|
8ae066ab5d | ||
|
|
8678620730 | ||
|
|
43b5eba8ee | ||
|
|
eb879062f9 | ||
|
|
a60124a88c | ||
|
|
16e3c193ec | ||
|
|
0e7b420d6a | ||
|
|
3ef4d20f77 | ||
|
|
27a1a9e8c5 | ||
|
|
7eb2558fc5 | ||
|
|
809d4b040a | ||
|
|
afa87e6ab4 | ||
|
|
a53c3b45fc | ||
|
|
6848fe0edf | ||
|
|
e58f3a678d | ||
|
|
e4446f0340 | ||
|
|
08a86250be | ||
|
|
865d1567fc | ||
|
|
4caea91164 | ||
|
|
c47c2528aa | ||
|
|
9c64e3b4b9 | ||
|
|
85ef2ca738 | ||
|
|
1fbdd02464 | ||
|
|
44275d9660 | ||
|
|
d8658a765c | ||
|
|
0be43948e2 | ||
|
|
e87ff5a06d | ||
|
|
4de9e3598b | ||
|
|
9483506590 | ||
|
|
871c84d195 | ||
|
|
0915f931ae | ||
|
|
80d0943d9d | ||
|
|
3c2fd02bc0 | ||
|
|
1d1b617cee | ||
|
|
749320002b | ||
|
|
3e129dffa0 | ||
|
|
4d2cff41ff | ||
|
|
26df20082b | ||
|
|
2196b4f1c2 | ||
|
|
28aafcd789 | ||
|
|
fa977ce090 | ||
|
|
6fb76b7c9b | ||
|
|
fa32198831 | ||
|
|
80ccea3d85 | ||
|
|
096751b658 | ||
|
|
6b1390b972 | ||
|
|
1bc007e29c | ||
|
|
a0bd022186 | ||
|
|
c19baaffaa | ||
|
|
9959bbfdfb | ||
|
|
f5249d6033 | ||
|
|
1ecd70e2dd | ||
|
|
e42b670f5c | ||
|
|
a344091d82 | ||
|
|
0302c7afa7 | ||
|
|
e2ffaef88a | ||
|
|
6dd1a8e688 | ||
|
|
49a597fcd0 | ||
|
|
a624c9713a | ||
|
|
6d874dd1f1 | ||
|
|
e103143634 | ||
|
|
f5431f5210 | ||
|
|
260b6f5de8 | ||
|
|
d68df9d704 | ||
|
|
097396c517 | ||
|
|
ae0b027d69 | ||
|
|
da53103281 | ||
|
|
aa2bf07c71 | ||
|
|
08925ffad8 | ||
|
|
310ee5674f | ||
|
|
9ba98b8703 | ||
|
|
bbc5dff7bd | ||
|
|
2802569529 | ||
|
|
181b4f9e80 | ||
|
|
139fb65fa9 | ||
|
|
fd1ef0a2be | ||
|
|
0642c52693 | ||
|
|
4b5bfd3da5 | ||
|
|
04f190c3e3 | ||
|
|
6eb69d37de | ||
|
|
44cef757e5 | ||
|
|
bfdfb215f3 | ||
|
|
e4c92ae124 | ||
|
|
3d5ca99d53 | ||
|
|
28fc27c4ae | ||
|
|
275800cca9 | ||
|
|
693bb43452 | ||
|
|
3b0a04c627 | ||
|
|
2c255fd85a | ||
|
|
a7a964ec19 | ||
|
|
d53e64a32c | ||
|
|
7cd3cd6cd4 | ||
|
|
d29e1278dc | ||
|
|
57457e2aa4 | ||
|
|
e4dad78045 | ||
|
|
7d5becceb5 | ||
|
|
70974c01f2 | ||
|
|
fb3c510178 | ||
|
|
3b1da3758d | ||
|
|
60d7d9c691 | ||
|
|
d978a66ef6 | ||
|
|
23ac9405c1 | ||
|
|
cfd8eb5167 | ||
|
|
3baaf2c3e4 | ||
|
|
7626ce9922 | ||
|
|
9ab5eeb7f3 | ||
|
|
99ff8e68ff | ||
|
|
f6b46f7a34 | ||
|
|
dd8ca70151 | ||
|
|
b9c58d88dc | ||
|
|
d4ecba5235 | ||
|
|
3ceb436462 | ||
|
|
fdeea9144c | ||
|
|
7162583396 | ||
|
|
648bec1807 | ||
|
|
f74ebdade6 | ||
|
|
bc21c9520f | ||
|
|
a0c4837eb5 | ||
|
|
f695975f65 | ||
|
|
7634e04231 | ||
|
|
34dd1a22ab | ||
|
|
b58a3c939c | ||
|
|
ae768193e3 | ||
|
|
96aef3bab8 | ||
|
|
09708dfe20 | ||
|
|
d433ca7d40 | ||
|
|
ad821cf629 | ||
|
|
df9ecc6816 | ||
|
|
0c3657da7e | ||
|
|
f2d2712a95 | ||
|
|
efa48390b7 | ||
|
|
5e2b9e40a2 | ||
|
|
217468074f | ||
|
|
eb8dfefb23 | ||
|
|
7436723686 | ||
|
|
6b98b05976 | ||
|
|
c61e905434 | ||
|
|
c15ab1b2bc | ||
|
|
fdd5ac1895 | ||
|
|
f5084045f2 | ||
|
|
d9578bc485 | ||
|
|
1f5bb76718 | ||
|
|
2d0fd96621 | ||
|
|
acea1d7f00 | ||
|
|
43f8098981 | ||
|
|
48c8ff8899 | ||
|
|
883f7a164b | ||
|
|
0324de37d2 | ||
|
|
1f3fafb198 | ||
|
|
d7151e5c88 | ||
|
|
4a8effddf5 | ||
|
|
09efa24f28 | ||
|
|
2cb1733c59 | ||
|
|
cba2ca5531 | ||
|
|
8dec74689d | ||
|
|
83937f59c0 | ||
|
|
d86e773c75 | ||
|
|
60f700a1d2 | ||
|
|
9ac960a803 | ||
|
|
a4d35e2fbc | ||
|
|
9e33f8b7c4 | ||
|
|
e2c958eb43 | ||
|
|
cab6a872cf | ||
|
|
bd6fed54de | ||
|
|
35693d3b23 | ||
|
|
16f30664f5 | ||
|
|
59dfe794f5 | ||
|
|
3a5f03c772 | ||
|
|
3f1fd6c6f2 | ||
|
|
116e70740c | ||
|
|
7d958080c1 | ||
|
|
1a0b21b8d1 | ||
|
|
24ccfb9ccc | ||
|
|
3be1deea44 | ||
|
|
4b03e7c31f | ||
|
|
3c358e406e | ||
|
|
609129c039 | ||
|
|
025d37e9a2 | ||
|
|
d1d5bbf697 | ||
|
|
2a4b10a5c8 | ||
|
|
740e1a0540 | ||
|
|
2ddccb4a21 | ||
|
|
6d03766ace | ||
|
|
ab1ae1b2a7 | ||
|
|
9a3f4def98 | ||
|
|
bdd59de877 | ||
|
|
cf60ae79c3 | ||
|
|
6ab9355146 | ||
|
|
ce2209585c | ||
|
|
a6bd2c73a0 | ||
|
|
6e0b873739 | ||
|
|
57596462a4 | ||
|
|
a1b3986532 | ||
|
|
68ae17e4a4 | ||
|
|
3d78cb4f8c | ||
|
|
82f0388af6 | ||
|
|
aeaac7480b | ||
|
|
c4f55fecbe | ||
|
|
efec897fd4 | ||
|
|
ac0140d596 | ||
|
|
e54689a964 | ||
|
|
bd83078025 | ||
|
|
dd5a8275f1 | ||
|
|
91bd908e03 | ||
|
|
5739ad3cfc | ||
|
|
11c4967bdc | ||
|
|
f9bab9585a | ||
|
|
0442a86400 | ||
|
|
1e0de188b0 | ||
|
|
c7b5fca885 | ||
|
|
88ca54eba9 | ||
|
|
43ac79685a | ||
|
|
59dc91ada3 | ||
|
|
ed0f5b71c7 | ||
|
|
7def403018 | ||
|
|
53331772ef | ||
|
|
06a9c7bacc | ||
|
|
a2cba6685c | ||
|
|
6c1f8a9cfe | ||
|
|
a43c00ce70 | ||
|
|
e2c2b70a61 | ||
|
|
88cb38adde | ||
|
|
6a4777eafc | ||
|
|
7e1b6f59fe | ||
|
|
ddfccec2ea | ||
|
|
7e64ee8225 | ||
|
|
aafd4a339a | ||
|
|
6663b2fab9 | ||
|
|
474185c977 | ||
|
|
99b500c740 | ||
|
|
ab812e73f6 | ||
|
|
6a84a85a80 | ||
|
|
08cfa5e32a | ||
|
|
ca7afc10a9 | ||
|
|
bcae94f8ca | ||
|
|
36a2b9c015 | ||
|
|
f643e2abe6 | ||
|
|
17ef221c68 | ||
|
|
2f47b225a0 | ||
|
|
f13018ce05 | ||
|
|
d3815beb1c | ||
|
|
e83831904a | ||
|
|
e3472f6d81 | ||
|
|
b8427ba379 | ||
|
|
6fd60c639f | ||
|
|
4b47e857f2 | ||
|
|
d784accdec | ||
|
|
2236a1a36d | ||
|
|
94ce065f74 | ||
|
|
65fb77ce73 | ||
|
|
0ddaa95d0e | ||
|
|
ae0b9692be | ||
|
|
62341ffe56 | ||
|
|
0de861a3a8 | ||
|
|
e4244d8bf8 | ||
|
|
f157c19e16 | ||
|
|
7df5e3cebf | ||
|
|
6584ca20d2 | ||
|
|
02083d71c8 | ||
|
|
f31fe495e9 | ||
|
|
7db848f153 | ||
|
|
ec0fe11939 | ||
|
|
6435df0a22 | ||
|
|
9c086be591 | ||
|
|
f7170829ce | ||
|
|
7d6c1dd825 | ||
|
|
b888aeec0d | ||
|
|
1e94127070 | ||
|
|
594051b3fb | ||
|
|
eeb80a43f4 | ||
|
|
16ac02f9c7 |
@@ -333,6 +333,7 @@ jobs:
|
||||
docker:
|
||||
- image: grafana/grafana-ci-deploy:1.2.0
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,6 +46,7 @@ devenv/docker-compose.yaml
|
||||
/conf/provisioning/**/custom.yaml
|
||||
/conf/provisioning/**/dev.yaml
|
||||
/conf/ldap_dev.toml
|
||||
/conf/ldap_freeipa.toml
|
||||
profile.cov
|
||||
/grafana
|
||||
/local
|
||||
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,4 +1,35 @@
|
||||
# 6.0.0-beta1 (unreleased)
|
||||
# 6.0.0-beta2 (unreleased)
|
||||
|
||||
### New Features
|
||||
* **AzureMonitor**: Enable alerting by converting Azure Monitor API to Go [#14623](https://github.com/grafana/grafana/issues/14623)
|
||||
|
||||
### Minor
|
||||
* **Alerting**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae)
|
||||
* **Graphite/InfluxDB/OpenTSDB**: Fix always take dashboard timezone into consideration when handle custom time ranges [#15284](https://github.com/grafana/grafana/issues/15284)
|
||||
* **Stackdriver**: Template variables in filters using globbing format [#15182](https://github.com/grafana/grafana/issues/15182)
|
||||
* **Cloudwatch**: Add `resource_arns` template variable query function [#8207](https://github.com/grafana/grafana/issues/8207), thx [@jeroenvollenbrock](https://github.com/jeroenvollenbrock)
|
||||
* **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson)
|
||||
* **Cloudwatch**: Add AWS/EC2/API metrics [#14233](https://github.com/grafana/grafana/issues/14233), thx [@tcpatterson](https://github.com/tcpatterson)
|
||||
* **Cloudwatch**: Add AWS RDS ServerlessDatabaseCapacity metric [#15265](https://github.com/grafana/grafana/pull/15265), thx [@larsjoergensen](https://github.com/larsjoergensen)
|
||||
* **MySQL**: Adds datasource SSL CA/client certificates support [#8570](https://github.com/grafana/grafana/issues/8570), thx [@bugficks](https://github.com/bugficks)
|
||||
* **MSSQL**: Timerange are now passed for template variable queries [#13324](https://github.com/grafana/grafana/issues/13324), thx [@thatsparesh](https://github.com/thatsparesh)
|
||||
* **Annotations**: Support PATCH verb in annotations http api [#12546](https://github.com/grafana/grafana/issues/12546), thx [@SamuelToh](https://github.com/SamuelToh)
|
||||
* **Templating**: Add json formatting to variable interpolation [#15291](https://github.com/grafana/grafana/issues/15291), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Login**: Anonymous usage stats for token auth [#15288](https://github.com/grafana/grafana/issues/15288)
|
||||
* **AzureMonitor**: improve autocomplete for Log Analytics and App Insights editor [#15131](https://github.com/grafana/grafana/issues/15131)
|
||||
* **LDAP**: Fix IPA/FreeIPA v4.6.4 does not allow LDAP searches with empty attributes [#14432](https://github.com/grafana/grafana/issues/14432)
|
||||
|
||||
### 6.0.0-beta1 fixes
|
||||
|
||||
* **Postgres**: Fix default port not added when port not configured [#15189](https://github.com/grafana/grafana/issues/15189)
|
||||
* **Alerting**: Fixes crash bug when alert notifier folders are missing [#15295](https://github.com/grafana/grafana/issues/15295)
|
||||
* **Dashboard**: Fix save provisioned dashboard modal [#15219](https://github.com/grafana/grafana/pull/15219)
|
||||
* **Dashboard**: Fix having a long query in prometheus dashboard query editor blocks 30% of the query field when on OSX and having native scrollbars [#15122](https://github.com/grafana/grafana/issues/15122)
|
||||
* **Explore**: Fix issue with wrapping on long queries [#15222](https://github.com/grafana/grafana/issues/15222)
|
||||
* **Explore**: Fix cut & paste adds newline before and after selection [#15223](https://github.com/grafana/grafana/issues/15223)
|
||||
* **Dataproxy**: Fix global datasource proxy timeout not added to correct http client [#15258](https://github.com/grafana/grafana/issues/15258) [#5699](https://github.com/grafana/grafana/issues/5699)
|
||||
|
||||
# 6.0.0-beta1 (2019-01-30)
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -11,6 +42,7 @@
|
||||
|
||||
### Minor
|
||||
|
||||
* **Templating**: Built in time range variables `$__from` and `$__to`, [#1909](https://github.com/grafana/grafana/issues/1909)
|
||||
* **Alerting**: Use separate timeouts for alert evals and notifications [#14701](https://github.com/grafana/grafana/issues/14701), thx [@sharkpc0813](https://github.com/sharkpc0813)
|
||||
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
|
||||
* **Elasticsearch**: Add support for moving average and derivative using doc count (metric count) [#8843](https://github.com/grafana/grafana/issues/8843) [#11175](https://github.com/grafana/grafana/issues/11175)
|
||||
@@ -75,7 +107,7 @@
|
||||
* **Stackdriver**: Fixes issue with data proxy and Authorization header [#14262](https://github.com/grafana/grafana/issues/14262)
|
||||
* **Units**: fixedUnit for Flow:l/min and mL/min [#14294](https://github.com/grafana/grafana/issues/14294), thx [@flopp999](https://github.com/flopp999).
|
||||
* **Logging**: Fix for issue where data proxy logged a secret when debug logging was enabled, now redacted. [#14319](https://github.com/grafana/grafana/issues/14319)
|
||||
* **InfluxDB**: Add support for alerting on InfluxDB queries that use the cumulative_sum function. [#14314](https://github.com/grafana/grafana/pull/14314), thx [@nitti](https://github.com/nitti)
|
||||
* TSDB**: Fix always take dashboard timezone into consideration when handle custom time ranges**: Add support for alerting on InfluxDB queries that use the cumulative_sum function. [#14314](https://github.com/grafana/grafana/pull/14314), thx [@nitti](https://github.com/nitti)
|
||||
* **Plugins**: Panel plugins should no receive the panel-initialized event again as usual.
|
||||
* **Embedded Graphs**: Iframe graph panels should now work as usual. [#14284](https://github.com/grafana/grafana/issues/14284)
|
||||
* **Postgres**: Improve PostgreSQL Query Editor if using different Schemas, [#14313](
|
||||
@@ -1010,7 +1042,7 @@ Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472)
|
||||
* **Docs**: Added some details about Sessions in Postgres [#7694](https://github.com/grafana/grafana/pull/7694) thx [@rickard-von-essen](https://github.com/rickard-von-essen)
|
||||
* **Influxdb**: Allow commas in template variables [#7681](https://github.com/grafana/grafana/issues/7681) thx [@thuck](https://github.com/thuck)
|
||||
* **Cloudwatch**: stop using deprecated session.New() [#7736](https://github.com/grafana/grafana/issues/7736) thx [@mtanda](https://github.com/mtanda)
|
||||
* **OpenTSDB**: Pass dropcounter rate option if no max counter and no reset value or reset value as 0 is specified [#7743](https://github.com/grafana/grafana/pull/7743) thx [@r4um](https://github.com/r4um)
|
||||
*TSDB**: Fix always take dashboard timezone into consideration when handle custom time ranges**: Pass dropcounter rate option if no max counter and no reset value or reset value as 0 is specified [#7743](https://github.com/grafana/grafana/pull/7743) thx [@r4um](https://github.com/r4um)
|
||||
* **Templating**: support full resolution for $interval variable [#7696](https://github.com/grafana/grafana/pull/7696) thx [@mtanda](https://github.com/mtanda)
|
||||
* **Elasticsearch**: Unique Count on string fields in ElasticSearch [#3536](https://github.com/grafana/grafana/issues/3536), thx [@pyro2927](https://github.com/pyro2927)
|
||||
* **Templating**: Data source template variable that refers to other variable in regex filter [#6365](https://github.com/grafana/grafana/issues/6365) thx [@rlodge](https://github.com/rlodge)
|
||||
|
||||
@@ -64,6 +64,7 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
useradd -r -u $GF_UID -g grafana grafana && \
|
||||
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
|
||||
"$GF_PATHS_PROVISIONING/dashboards" \
|
||||
"$GF_PATHS_PROVISIONING/notifiers" \
|
||||
"$GF_PATHS_LOGS" \
|
||||
"$GF_PATHS_PLUGINS" \
|
||||
"$GF_PATHS_DATA" && \
|
||||
|
||||
12
Gopkg.lock
generated
12
Gopkg.lock
generated
@@ -37,6 +37,7 @@
|
||||
"aws/credentials",
|
||||
"aws/credentials/ec2rolecreds",
|
||||
"aws/credentials/endpointcreds",
|
||||
"aws/credentials/processcreds",
|
||||
"aws/credentials/stscreds",
|
||||
"aws/csm",
|
||||
"aws/defaults",
|
||||
@@ -45,13 +46,18 @@
|
||||
"aws/request",
|
||||
"aws/session",
|
||||
"aws/signer/v4",
|
||||
"internal/ini",
|
||||
"internal/s3err",
|
||||
"internal/sdkio",
|
||||
"internal/sdkrand",
|
||||
"internal/sdkuri",
|
||||
"internal/shareddefaults",
|
||||
"private/protocol",
|
||||
"private/protocol/ec2query",
|
||||
"private/protocol/eventstream",
|
||||
"private/protocol/eventstream/eventstreamapi",
|
||||
"private/protocol/json/jsonutil",
|
||||
"private/protocol/jsonrpc",
|
||||
"private/protocol/query",
|
||||
"private/protocol/query/queryutil",
|
||||
"private/protocol/rest",
|
||||
@@ -60,11 +66,13 @@
|
||||
"service/cloudwatch",
|
||||
"service/ec2",
|
||||
"service/ec2/ec2iface",
|
||||
"service/resourcegroupstaggingapi",
|
||||
"service/resourcegroupstaggingapi/resourcegroupstaggingapiiface",
|
||||
"service/s3",
|
||||
"service/sts"
|
||||
]
|
||||
revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
|
||||
version = "v1.14.12"
|
||||
revision = "62936e15518acb527a1a9cb4a39d96d94d0fd9a2"
|
||||
version = "v1.16.15"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
||||
67
README.md
67
README.md
@@ -7,13 +7,18 @@
|
||||
Grafana is an open source, feature rich metrics dashboard and graph editor for
|
||||
Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
<!---
|
||||

|
||||
-->
|
||||
|
||||
## Installation
|
||||
Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get)
|
||||
the latest release.
|
||||
|
||||
If you have any problems please read the [troubleshooting guide](http://docs.grafana.org/installation/troubleshooting/).
|
||||
Head to [docs.grafana.org](http://docs.grafana.org/installation/) for documentation or [download](https://grafana.com/get) to get the latest release.
|
||||
|
||||
## Documentation & Support
|
||||
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
|
||||
@@ -25,49 +30,71 @@ the latest master builds [here](https://grafana.com/grafana/download)
|
||||
### Dependencies
|
||||
|
||||
- Go (Latest Stable)
|
||||
- bra [`go get github.com/Unknwon/bra`]
|
||||
- Node.js LTS
|
||||
- yarn [`npm install -g yarn`]
|
||||
|
||||
### Get the project
|
||||
|
||||
**The project located in the go-path will be your working directory.**
|
||||
|
||||
### Building the backend
|
||||
```bash
|
||||
go get github.com/grafana/grafana
|
||||
cd $GOPATH/src/github.com/grafana/grafana
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
#### The backend
|
||||
|
||||
```bash
|
||||
go run build.go setup
|
||||
go run build.go build
|
||||
```
|
||||
|
||||
### Building frontend assets
|
||||
#### Frontend assets
|
||||
|
||||
For this you need Node.js (LTS version).
|
||||
*For this you need Node.js (LTS version).*
|
||||
|
||||
To build the assets, rebuild on file change, and serve them by Grafana's webserver (http://localhost:3000):
|
||||
```bash
|
||||
npm install -g yarn
|
||||
yarn install --pure-lockfile
|
||||
```
|
||||
|
||||
### Run and rebuild on source change
|
||||
|
||||
#### Backend
|
||||
|
||||
To run the backend and rebuild on source change:
|
||||
|
||||
```bash
|
||||
$GOPATH/bin/bra run
|
||||
```
|
||||
|
||||
#### Frontend
|
||||
|
||||
Rebuild on file change, and serve them by Grafana's webserver (http://localhost:3000):
|
||||
|
||||
```bash
|
||||
yarn watch
|
||||
```
|
||||
|
||||
Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333):
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
# OR set a theme
|
||||
env GRAFANA_THEME=light yarn start
|
||||
```
|
||||
Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.
|
||||
|
||||
Run tests
|
||||
*Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload.*
|
||||
|
||||
Run tests and rebuild on source change:
|
||||
|
||||
```bash
|
||||
yarn jest
|
||||
```
|
||||
|
||||
### Recompile backend on source change
|
||||
|
||||
To rebuild on source change.
|
||||
```bash
|
||||
go get github.com/Unknwon/bra
|
||||
bra run
|
||||
```
|
||||
|
||||
Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
|
||||
**Open grafana in your browser (default: e.g. `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).**
|
||||
|
||||
### Building a Docker image
|
||||
|
||||
|
||||
@@ -106,22 +106,6 @@ path = grafana.db
|
||||
# For "sqlite3" only. cache mode setting used for connecting to the database
|
||||
cache_mode = private
|
||||
|
||||
#################################### Login ###############################
|
||||
|
||||
[login]
|
||||
|
||||
# Login cookie name
|
||||
cookie_name = grafana_session
|
||||
|
||||
# How many days an session can be unused before we inactivate it
|
||||
login_remember_days = 7
|
||||
|
||||
# How often should the login token be rotated. default to '10m'
|
||||
rotate_token_minutes = 10
|
||||
|
||||
# How long should Grafana keep expired tokens before deleting them
|
||||
delete_expired_token_after_days = 30
|
||||
|
||||
#################################### Session #############################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
|
||||
@@ -203,8 +187,11 @@ data_source_proxy_whitelist =
|
||||
# disable protection against brute force login attempts
|
||||
disable_brute_force_login_protection = false
|
||||
|
||||
# set cookies as https only. default is false
|
||||
https_flag_cookies = false
|
||||
# set to true if you host Grafana behind HTTPS. default is false.
|
||||
cookie_secure = false
|
||||
|
||||
# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict" and "none"
|
||||
cookie_samesite = lax
|
||||
|
||||
#################################### Snapshots ###########################
|
||||
[snapshots]
|
||||
@@ -257,6 +244,18 @@ external_manage_info =
|
||||
viewers_can_edit = false
|
||||
|
||||
[auth]
|
||||
# Login cookie name
|
||||
login_cookie_name = grafana_session
|
||||
|
||||
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days.
|
||||
login_maximum_inactive_lifetime_days = 7
|
||||
|
||||
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
|
||||
login_maximum_lifetime_days = 30
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
token_rotation_interval_minutes = 10
|
||||
|
||||
# Set to true to disable (hide) the login form, useful if you use OAuth
|
||||
disable_login_form = false
|
||||
|
||||
|
||||
@@ -102,22 +102,6 @@ log_queries =
|
||||
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
|
||||
;cache_mode = private
|
||||
|
||||
#################################### Login ###############################
|
||||
|
||||
[login]
|
||||
|
||||
# Login cookie name
|
||||
;cookie_name = grafana_session
|
||||
|
||||
# How many days an session can be unused before we inactivate it
|
||||
;login_remember_days = 7
|
||||
|
||||
# How often should the login token be rotated. default to '10'
|
||||
;rotate_token_minutes = 10
|
||||
|
||||
# How long should Grafana keep expired tokens before deleting them
|
||||
;delete_expired_token_after_days = 30
|
||||
|
||||
#################################### Session ####################################
|
||||
[session]
|
||||
# Either "memory", "file", "redis", "mysql", "postgres", default is "file"
|
||||
@@ -190,8 +174,11 @@ log_queries =
|
||||
# disable protection against brute force login attempts
|
||||
;disable_brute_force_login_protection = false
|
||||
|
||||
# set cookies as https only. default is false
|
||||
;https_flag_cookies = false
|
||||
# set to true if you host Grafana behind HTTPS. default is false.
|
||||
;cookie_secure = false
|
||||
|
||||
# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict" and "none"
|
||||
;cookie_samesite = lax
|
||||
|
||||
#################################### Snapshots ###########################
|
||||
[snapshots]
|
||||
@@ -237,6 +224,18 @@ log_queries =
|
||||
;viewers_can_edit = false
|
||||
|
||||
[auth]
|
||||
# Login cookie name
|
||||
;login_cookie_name = grafana_session
|
||||
|
||||
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days,
|
||||
;login_maximum_inactive_lifetime_days = 7
|
||||
|
||||
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
|
||||
;login_maximum_lifetime_days = 30
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
;token_rotation_interval_minutes = 10
|
||||
|
||||
# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
|
||||
;disable_login_form = false
|
||||
|
||||
@@ -250,7 +249,7 @@ log_queries =
|
||||
# This setting is ignored if multiple OAuth providers are configured.
|
||||
;oauth_auto_login = false
|
||||
|
||||
#################################### Anonymous Auth ##########################
|
||||
#################################### Anonymous Auth ######################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
;enabled = false
|
||||
|
||||
54
devenv/docker/blocks/freeipa/docker-compose.yaml
Normal file
54
devenv/docker/blocks/freeipa/docker-compose.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
version: '3'
|
||||
|
||||
volumes:
|
||||
freeipa_data: {}
|
||||
|
||||
services:
|
||||
freeipa:
|
||||
image: freeipa/freeipa-server:fedora-29
|
||||
container_name: freeipa
|
||||
stdin_open: true
|
||||
tty: true
|
||||
sysctls:
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
hostname: ipa.example.test
|
||||
environment:
|
||||
# - DEBUG_TRACE=1
|
||||
- IPA_SERVER_IP=172.17.0.2
|
||||
- DEBUG_NO_EXIT=1
|
||||
- IPA_SERVER_HOSTNAME=ipa.example.test
|
||||
- PASSWORD=Secret123
|
||||
- HOSTNAME=ipa.example.test
|
||||
command:
|
||||
- --admin-password=Secret123
|
||||
- --ds-password=Secret123
|
||||
- -U
|
||||
- --realm=EXAMPLE.TEST
|
||||
ports:
|
||||
# FreeIPA WebUI
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
# Kerberos
|
||||
- "88:88/udp"
|
||||
- "88:88"
|
||||
- "464:464/udp"
|
||||
- "464:464"
|
||||
# LDAP
|
||||
- "389:389"
|
||||
- "636:636"
|
||||
# DNS
|
||||
# - "53:53/udp"
|
||||
# - "53:53"
|
||||
# NTP
|
||||
- "123:123/udp"
|
||||
# other
|
||||
- "7389:7389"
|
||||
- "9443:9443"
|
||||
- "9444:9444"
|
||||
- "9445:9445"
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
volumes:
|
||||
- freeipa_data:/data:Z
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:ro
|
||||
74
devenv/docker/blocks/freeipa/ldap_freeipa.toml
Normal file
74
devenv/docker/blocks/freeipa/ldap_freeipa.toml
Normal file
@@ -0,0 +1,74 @@
|
||||
# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
|
||||
# [log]
|
||||
# filters = ldap:debug
|
||||
|
||||
[[servers]]
|
||||
# Ldap server host (specify multiple hosts space separated)
|
||||
host = "172.17.0.1"
|
||||
# Default port is 389 or 636 if use_ssl = true
|
||||
port = 389
|
||||
# Set to true if ldap server supports TLS
|
||||
use_ssl = false
|
||||
# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
|
||||
start_tls = false
|
||||
# set to true if you want to skip ssl cert validation
|
||||
ssl_skip_verify = false
|
||||
# set to the path to your root CA certificate or leave unset to use system defaults
|
||||
# root_ca_cert = "/path/to/certificate.crt"
|
||||
|
||||
# Search user bind dn
|
||||
bind_dn = "uid=admin,cn=users,cn=accounts,dc=example,dc=test"
|
||||
# Search user bind password
|
||||
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
|
||||
bind_password = 'Secret123'
|
||||
|
||||
# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
|
||||
search_filter = "(uid=%s)"
|
||||
|
||||
# An array of base dns to search through
|
||||
search_base_dns = ["cn=users,cn=accounts,dc=example,dc=test"]
|
||||
|
||||
# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
|
||||
# This is done by enabling group_search_filter below. You must also set member_of= "cn"
|
||||
# in [servers.attributes] below.
|
||||
|
||||
# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
|
||||
# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
|
||||
# below in such a way that the user's recursive group membership is considered.
|
||||
#
|
||||
# Nested Groups + Active Directory (AD) Example:
|
||||
#
|
||||
# AD groups store the Distinguished Names (DNs) of members, so your filter must
|
||||
# recursively search your groups for the authenticating user's DN. For example:
|
||||
#
|
||||
# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
|
||||
# group_search_filter_user_attribute = "distinguishedName"
|
||||
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
|
||||
#
|
||||
# [servers.attributes]
|
||||
# ...
|
||||
# member_of = "distinguishedName"
|
||||
|
||||
## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
|
||||
# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
|
||||
## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
|
||||
## Defaults to the value of username in [server.attributes]
|
||||
## Valid options are any of your values in [servers.attributes]
|
||||
## If you are using nested groups you probably want to set this and member_of in
|
||||
## [servers.attributes] to "distinguishedName"
|
||||
# group_search_filter_user_attribute = "distinguishedName"
|
||||
## An array of the base DNs to search through for groups. Typically uses ou=groups
|
||||
# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
|
||||
|
||||
# Specify names of the ldap attributes your ldap uses
|
||||
[servers.attributes]
|
||||
name = "givenName"
|
||||
username = "uid"
|
||||
member_of = "memberOf"
|
||||
# surname = "sn"
|
||||
# email = "mail"
|
||||
|
||||
[[servers.group_mappings]]
|
||||
# If you want to match all (or no ldap groups) then you can use wildcard
|
||||
group_dn = "*"
|
||||
org_role = "Viewer"
|
||||
32
devenv/docker/blocks/freeipa/notes.md
Normal file
32
devenv/docker/blocks/freeipa/notes.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Notes on FreeIPA LDAP Docker Block
|
||||
|
||||
Users have to be created manually. The docker-compose up command takes a few minutes to run.
|
||||
|
||||
## Create a user
|
||||
|
||||
`docker exec -it freeipa /bin/bash`
|
||||
|
||||
To create a user with username: `ldap-viewer` and password: `grafana123`
|
||||
|
||||
```bash
|
||||
kinit admin
|
||||
```
|
||||
|
||||
Log in with password `Secret123`
|
||||
|
||||
```bash
|
||||
ipa user-add ldap-viewer --first ldap --last viewer
|
||||
ipa passwd ldap-viewer
|
||||
ldappasswd -D uid=ldap-viewer,cn=users,cn=accounts,dc=example,dc=org -w test -a test -s grafana123
|
||||
```
|
||||
|
||||
## Enabling FreeIPA LDAP in Grafana
|
||||
|
||||
Copy the ldap_freeipa.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block:
|
||||
|
||||
```ini
|
||||
[auth.ldap]
|
||||
enabled = true
|
||||
config_file = conf/ldap_freeipa.toml
|
||||
; allow_sign_up = true
|
||||
```
|
||||
27
devenv/docker/blocks/loki/config.yaml
Normal file
27
devenv/docker/blocks/loki/config.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
server:
|
||||
http_listen_port: 9080
|
||||
grpc_listen_port: 0
|
||||
|
||||
positions:
|
||||
filename: /tmp/positions.yaml
|
||||
|
||||
client:
|
||||
url: http://loki:3100/api/prom/push
|
||||
|
||||
scrape_configs:
|
||||
- job_name: system
|
||||
entry_parser: raw
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: varlogs
|
||||
__path__: /var/log/*log
|
||||
- job_name: grafana
|
||||
entry_parser: raw
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost
|
||||
labels:
|
||||
job: grafana
|
||||
__path__: /var/log/grafana/*log
|
||||
@@ -1,22 +1,14 @@
|
||||
version: "3"
|
||||
|
||||
networks:
|
||||
loki:
|
||||
|
||||
services:
|
||||
loki:
|
||||
image: grafana/loki:master
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
networks:
|
||||
- loki
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:master
|
||||
volumes:
|
||||
- ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml
|
||||
- /var/log:/var/log
|
||||
- ../data/log:/var/log/grafana
|
||||
command:
|
||||
-config.file=/etc/promtail/docker-config.yaml
|
||||
networks:
|
||||
- loki
|
||||
|
||||
@@ -15,6 +15,7 @@ services:
|
||||
MYSQL_DATABASE: grafana
|
||||
MYSQL_USER: grafana
|
||||
MYSQL_PASSWORD: password
|
||||
command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all, --max-connections=1001]
|
||||
ports:
|
||||
- 3306
|
||||
healthcheck:
|
||||
@@ -22,6 +23,16 @@ services:
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
mysqld-exporter:
|
||||
image: prom/mysqld-exporter
|
||||
environment:
|
||||
- DATA_SOURCE_NAME=root:rootpass@(db:3306)/
|
||||
ports:
|
||||
- 9104
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
# db:
|
||||
# image: postgres:9.3
|
||||
# environment:
|
||||
@@ -47,6 +58,7 @@ services:
|
||||
- GF_DATABASE_PASSWORD=password
|
||||
- GF_DATABASE_TYPE=mysql
|
||||
- GF_DATABASE_HOST=db:3306
|
||||
- GF_DATABASE_MAX_OPEN_CONN=300
|
||||
- GF_SESSION_PROVIDER=mysql
|
||||
- GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(db:3306)/grafana?allowNativePasswords=true
|
||||
# - GF_DATABASE_TYPE=postgres
|
||||
@@ -55,7 +67,7 @@ services:
|
||||
# - GF_SESSION_PROVIDER=postgres
|
||||
# - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
|
||||
- GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug
|
||||
- GF_LOGIN_ROTATE_TOKEN_MINUTES=2
|
||||
- GF_AUTH_TOKEN_ROTATION_INTERVAL_MINUTES=2
|
||||
ports:
|
||||
- 3000
|
||||
depends_on:
|
||||
@@ -70,10 +82,3 @@ services:
|
||||
- VIRTUAL_HOST=prometheus.loc
|
||||
ports:
|
||||
- 9090
|
||||
|
||||
# mysqld-exporter:
|
||||
# image: prom/mysqld-exporter
|
||||
# environment:
|
||||
# - DATA_SOURCE_NAME=grafana:password@(mysql:3306)/
|
||||
# ports:
|
||||
# - 9104
|
||||
|
||||
@@ -6,3 +6,9 @@ providers:
|
||||
type: file
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards/alerts
|
||||
|
||||
- name: 'MySQL'
|
||||
folder: 'MySQL'
|
||||
type: file
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards/mysql
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,10 +30,10 @@ scrape_configs:
|
||||
port: 3000
|
||||
refresh_interval: 10s
|
||||
|
||||
# - job_name: 'mysql'
|
||||
# dns_sd_configs:
|
||||
# - names:
|
||||
# - 'mysqld-exporter'
|
||||
# type: 'A'
|
||||
# port: 9104
|
||||
# refresh_interval: 10s
|
||||
- job_name: 'mysql'
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- 'mysqld-exporter'
|
||||
type: 'A'
|
||||
port: 9104
|
||||
refresh_interval: 10s
|
||||
@@ -8,7 +8,7 @@ Docker
|
||||
|
||||
## Run
|
||||
|
||||
Run load test for 15 minutes:
|
||||
Run load test for 15 minutes using 2 virtual users and targeting http://localhost:3000.
|
||||
|
||||
```bash
|
||||
$ ./run.sh
|
||||
@@ -20,6 +20,18 @@ Run load test for custom duration:
|
||||
$ ./run.sh -d 10s
|
||||
```
|
||||
|
||||
Run load test for custom target url:
|
||||
|
||||
```bash
|
||||
$ ./run.sh -u http://grafana.loc
|
||||
```
|
||||
|
||||
Run load test for 10 virtual users:
|
||||
|
||||
```bash
|
||||
$ ./run.sh -v 10
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -65,7 +65,7 @@ export default (data) => {
|
||||
}
|
||||
});
|
||||
|
||||
sleep(1)
|
||||
sleep(5)
|
||||
}
|
||||
|
||||
export const teardown = (data) => {}
|
||||
|
||||
@@ -5,8 +5,9 @@ PWD=$(pwd)
|
||||
run() {
|
||||
duration='15m'
|
||||
url='http://localhost:3000'
|
||||
vus='2'
|
||||
|
||||
while getopts ":d:u:" o; do
|
||||
while getopts ":d:u:v:" o; do
|
||||
case "${o}" in
|
||||
d)
|
||||
duration=${OPTARG}
|
||||
@@ -14,11 +15,14 @@ run() {
|
||||
u)
|
||||
url=${OPTARG}
|
||||
;;
|
||||
v)
|
||||
vus=${OPTARG}
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus 2 --duration $duration src/auth_token_test.js
|
||||
docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus $vus --duration $duration src/auth_token_test.js
|
||||
}
|
||||
|
||||
run "$@"
|
||||
|
||||
@@ -36,6 +36,35 @@ Grafana of course has a built in user authentication system with password authen
|
||||
disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth
|
||||
provider (listed above). There is also options for allowing self sign up.
|
||||
|
||||
### Login and short-lived tokens
|
||||
|
||||
> The followung applies when using Grafana's built in user authentication, LDAP (without Auth proxy) or OAuth integration.
|
||||
|
||||
Grafana are using short-lived tokens as a mechanism for verifying authenticated users.
|
||||
These short-lived tokens are rotated each `token_rotation_interval_minutes` for an active authenticated user.
|
||||
|
||||
An active authenticated user that gets it token rotated will extend the `login_maximum_inactive_lifetime_days` time from "now" that Grafana will remember the user.
|
||||
This means that a user can close its browser and come back before `now + login_maximum_inactive_lifetime_days` and still being authenticated.
|
||||
This is true as long as the time since user login is less than `login_maximum_lifetime_days`.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
[auth]
|
||||
|
||||
# Login cookie name
|
||||
login_cookie_name = grafana_session
|
||||
|
||||
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days.
|
||||
login_maximum_inactive_lifetime_days = 7
|
||||
|
||||
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
|
||||
login_maximum_lifetime_days = 30
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
token_rotation_interval_minutes = 10
|
||||
```
|
||||
|
||||
### Anonymous authentication
|
||||
|
||||
You can make Grafana accessible without any login required by enabling anonymous access in the configuration file.
|
||||
|
||||
@@ -74,6 +74,12 @@ Here is a minimal policy example:
|
||||
"ec2:DescribeRegions"
|
||||
],
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "AllowReadingResourcesForTags",
|
||||
"Effect" : "Allow",
|
||||
"Action" : "tag:GetResources",
|
||||
"Resource" : "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -128,6 +134,7 @@ Name | Description
|
||||
*dimension_values(region, namespace, metric, dimension_key, [filters])* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric`, `dimension_key` or you can use dimension `filters` to get more specific result as well.
|
||||
*ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`.
|
||||
*ec2_instance_attribute(region, attribute_name, filters)* | Returns a list of attributes matching the specified `region`, `attribute_name`, `filters`.
|
||||
*resource_arns(region, resource_type, tags)* | Returns a list of ARNs matching the specified `region`, `resource_type` and `tags`.
|
||||
|
||||
For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
|
||||
|
||||
@@ -143,6 +150,8 @@ Query | Service
|
||||
*dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS
|
||||
*dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)* | S3
|
||||
*dimension_values(us-east-1,CWAgent,disk_used_percent,device,{"InstanceId":"$instance_id"})* | CloudWatch Agent
|
||||
*resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})* | ELB
|
||||
*resource_arns(eu-west-1,ec2:instance,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})* | EC2
|
||||
|
||||
## ec2_instance_attribute examples
|
||||
|
||||
@@ -205,6 +214,16 @@ Example `ec2_instance_attribute()` query
|
||||
ec2_instance_attribute(us-east-1, Tags.Name, { "tag:Team": [ "sysops" ] })
|
||||
```
|
||||
|
||||
## Using json format template variables
|
||||
|
||||
Some of query takes JSON format filter. Grafana support to interpolate template variable to JSON format string, it can use as filter string.
|
||||
|
||||
If `env = 'production', 'staging'`, following query will return ARNs of EC2 instances which `Environment` tag is `production` or `staging`.
|
||||
|
||||
```
|
||||
resource_arns(us-east-1, ec2:instance, {"Environment":${env:json}})
|
||||
```
|
||||
|
||||
## Cost
|
||||
|
||||
Amazon provides 1 million CloudWatch API requests each month at no additional charge. Past this,
|
||||
|
||||
@@ -99,7 +99,14 @@ The Logs Explorer (the `Log labels` button) next to the query field shows a list
|
||||
|
||||
Once the result is returned, the log panel shows a list of log rows and a bar chart where the x-axis shows the time and the y-axis shows the frequency/count.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/explore_loki.png" class="docs-image--no-shadow" caption="Explore Loki Log Streams" >}}
|
||||
<div class="medium-6 columns">
|
||||
<video width="800" height="500" controls>
|
||||
<source src="/assets/videos/explore_loki.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
#### Log Stream Selector
|
||||
|
||||
|
||||
@@ -14,15 +14,19 @@ weight = -11
|
||||
|
||||
This update to Grafana introduces a new way of exploring your data, support for log data and tons of other features.
|
||||
|
||||
Grafana v6.0 is out in **Beta**, [Download Now!](https://grafana.com/grafana/download/beta)
|
||||
|
||||
The main highlights are:
|
||||
|
||||
- [Explore]({{< relref "#explore" >}}) - A new query focused workflow for ad hoc data exploration and troubleshooting.
|
||||
- [Explore]({{< relref "#explore" >}}) - A new query focused workflow for ad-hoc data exploration and troubleshooting.
|
||||
- [Grafana Loki]({{< relref "#explore-and-grafana-loki" >}}) - Integration with the new open source log aggregation system from Grafana Labs.
|
||||
- [Gauge Panel]({{< relref "#gauge-panel" >}}) - A new standalone panel for gauges.
|
||||
- [New Panel Editor UX]({{< relref "#easily-switch-visualization-with-panel-edit-ux-update" >}}) improves panel editing
|
||||
and enables easy switch between different visualizations.
|
||||
- [New Panel Editor UX]({{< relref "#new-panel-editor" >}}) improves panel editing
|
||||
and enables easy switching between different visualizations.
|
||||
- [Google Stackdriver Datasource]({{< relref "#google-stackdriver-datasource" >}}) is out of beta and is officially released.
|
||||
- [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource
|
||||
- [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins.
|
||||
- [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker.
|
||||
|
||||
## Explore
|
||||
|
||||
@@ -36,7 +40,7 @@ For infrastructure monitoring and incident response, you no longer need to switc
|
||||
of observability - metrics and logs. Explore works with every datasource but for Prometheus we have customized the
|
||||
query editor and the experience to provide the best possible exploration UX.
|
||||
|
||||
#### Explore and Prometheus
|
||||
### Explore and Prometheus
|
||||
|
||||
Explore features a new [Prometheus query editor](/features/explore/#prometheus-specific-features). This new editor has improved autocomplete, metric tree selector,
|
||||
integrations with the Explore table view for easy label filtering and useful query hints that can automatically apply
|
||||
@@ -49,6 +53,8 @@ Explore supports splitting the view so you can compare different queries, differ
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/explore_split.png" max-width="800px" caption="Screenshot of the new Explore option in the panel menu" >}}
|
||||
|
||||
<br />
|
||||
|
||||
### Explore and Grafana Loki
|
||||
|
||||
The log exploration & visualization features in Explore are available to any data source but are currently only implemented by the new open source log
|
||||
@@ -68,6 +74,8 @@ for other log sources to Explore and the next planned integration is Elasticsear
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## New Panel Editor
|
||||
|
||||
Grafana v6.0 has a completely redesigned UX around editing panels. You can now resize the visualization area if you want
|
||||
@@ -103,6 +111,7 @@ source** plugins can be written in React using our published `@grafana/ui` sdk l
|
||||
will be shared closer to or just after release.
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/react_panels.png" max-width="600px" caption="React Panel" >}}
|
||||
<br />
|
||||
|
||||
### Google Stackdriver Datasource
|
||||
|
||||
@@ -127,15 +136,23 @@ If you are using `Auth proxy` for authentication the session storage will still
|
||||
|
||||
This release will force all users to log in again since their previous token is not valid anymore.
|
||||
|
||||
### Named Colors
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}}
|
||||
|
||||
We have updated the color picker to show named colors and primary colors. We hope this will improve accessibility and
|
||||
helps making colors more consistent across dashboards. We hope to do more in this color picker in the future, like show
|
||||
colors used in the dashboard.
|
||||
|
||||
Named colors also enables Grafana to adapt colors to the current theme.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
### Other features
|
||||
|
||||
- The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per bucket computations like the difference or ratio between two metrics.
|
||||
|
||||
- Support for Google Hangouts Chat alert notifications
|
||||
|
||||
#### Technical Work - moving from Angular to React
|
||||
|
||||
The Grafana team is putting a huge amount of work into converting the frontend code in Grafana from Angular to React. Currently, all external plugins for Grafana are written in Angular but we are planning to also support plugins written in React very soon.
|
||||
- New built in template variables for the current time range in `$__from` and `$__to`
|
||||
|
||||
## Changelog
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ Creates an annotation in the Grafana database. The `dashboardId` and `panelId` f
|
||||
|
||||
## Update Annotation
|
||||
|
||||
Content-Type: application/json
|
||||
`PUT /api/annotations/:id`
|
||||
|
||||
Updates all properties of an annotation that matches the specified id. To only update certain property, consider using the [Patch Annotation](#patch-annotation) operation.
|
||||
|
||||
@@ -115,7 +115,7 @@ Content-Type: application/json
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
"id": 1
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
@@ -135,7 +135,7 @@ format (string with multiple tags being separated by a space).
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
Content-Type: application/json
|
||||
|
||||
"tags":["tag3","tag4","tag5"]
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
@@ -150,7 +150,7 @@ Content-Type: application/json
|
||||
`DELETE /api/annotations/:id`
|
||||
|
||||
Deletes the annotation that matches the specified id.
|
||||
Content-Type: application/json
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
@@ -164,11 +164,14 @@ Content-Type: application/json
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id.
|
||||
## Delete Annotation By RegionId
|
||||
|
||||
`DELETE /api/annotations/region/:id`
|
||||
|
||||
Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id.
|
||||
|
||||
**Example Request**:
|
||||
@@ -180,6 +183,50 @@ Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
"message":"Annotation updated"
|
||||
}
|
||||
```
|
||||
|
||||
## Patch Annotation
|
||||
|
||||
`PATCH /api/annotations/:id`
|
||||
|
||||
Updates one or more properties of an annotation that matches the specified id.
|
||||
|
||||
This operation currently supports updating of the `text`, `tags`, `time` and `timeEnd` properties. It does not handle updating of the `isRegion` and `regionId` properties. To make an annotation regional or vice versa, consider using the [Update Annotation](#update-annotation) operation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PATCH /api/annotations/1145 HTTP/1.1
|
||||
Accept: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"text":"New Annotation Description",
|
||||
"tags":["tag6","tag7","tag8"]
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message":"Annotation patched"
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Annotation By Id
|
||||
|
||||
`DELETE /api/annotations/:id`
|
||||
@@ -201,7 +248,9 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Annotation deleted"}
|
||||
{
|
||||
"message":"Annotation deleted"
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Annotation By RegionId
|
||||
@@ -225,5 +274,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Annotation region deleted"}
|
||||
{
|
||||
"message":"Annotation region deleted"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -287,6 +287,14 @@ Default is `false`.
|
||||
|
||||
Define a white list of allowed ips/domains to use in data sources. Format: `ip_or_domain:port` separated by spaces
|
||||
|
||||
### cookie_secure
|
||||
|
||||
Set to `true` if you host Grafana behind HTTPS. Default is `false`.
|
||||
|
||||
### cookie_samesite
|
||||
|
||||
Sets the `SameSite` cookie attribute and prevents the browser from sending this cookie along with cross-site requests. The main goal is mitigate the risk of cross-origin information leakage. It also provides some protection against cross-site request forgery attacks (CSRF), [read more here](https://www.owasp.org/index.php/SameSite). Valid values are `lax`, `strict` and `none`. Default is `lax`.
|
||||
|
||||
<hr />
|
||||
|
||||
## [users]
|
||||
@@ -393,9 +401,7 @@ Analytics ID here. By default this feature is disabled.
|
||||
|
||||
### check_for_updates
|
||||
|
||||
Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used
|
||||
in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor
|
||||
send any sensitive information.
|
||||
Set to false to disable all checks to https://grafana.com for new versions of installed plugins and to the Grafana GitHub repository to check for a newer version of Grafana. The version information is used in some UI views to notify that a new Grafana update or a plugin update exists. This option does not cause any auto updates, nor send any sensitive information. The check is run every 10 minutes.
|
||||
|
||||
<hr />
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ Filter Option | Example | Raw | Interpolated | Description
|
||||
`regex` | ${servers:regex} | `'test.', 'test2'` | <code>(test\.|test2)</code> | Formats multi-value variable into a regex string
|
||||
`pipe` | ${servers:pipe} | `'test.', 'test2'` | <code>test.|test2</code> | Formats multi-value variable into a pipe-separated string
|
||||
`csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
|
||||
`json`| ${servers:json} | `'test1', 'test2'` | `["test1","test2"]` | Formats multi-value variable as a JSON string
|
||||
`distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB.
|
||||
`lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression.
|
||||
`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "6.0.0-beta1",
|
||||
"version": "6.0.0-beta2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@@ -27,6 +27,7 @@
|
||||
"@types/react-dom": "^16.0.9",
|
||||
"@types/react-grid-layout": "^0.16.6",
|
||||
"@types/react-select": "^2.0.4",
|
||||
"@types/react-transition-group": "^2.0.15",
|
||||
"@types/react-virtualized": "^9.18.12",
|
||||
"angular-mocks": "1.6.6",
|
||||
"autoprefixer": "^6.4.0",
|
||||
@@ -67,7 +68,7 @@
|
||||
"husky": "^0.14.3",
|
||||
"jest": "^23.6.0",
|
||||
"jest-date-mock": "^1.0.6",
|
||||
"lint-staged": "^6.0.0",
|
||||
"lint-staged": "^8.1.3",
|
||||
"load-grunt-tasks": "3.5.2",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"mocha": "^4.0.1",
|
||||
@@ -85,6 +86,7 @@
|
||||
"prettier": "1.9.2",
|
||||
"react-hot-loader": "^4.3.6",
|
||||
"react-test-renderer": "^16.5.0",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"regexp-replace-loader": "^1.0.1",
|
||||
"sass-lint": "^1.10.2",
|
||||
"sass-loader": "^7.0.1",
|
||||
@@ -118,7 +120,8 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"jest": "jest --notify --watch",
|
||||
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||
"precommit": "grunt precommit"
|
||||
"precommit": "grunt precommit",
|
||||
"storybook": "cd packages/grafana-ui && yarn storybook"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"tslint": "echo \"Nothing to do\"",
|
||||
"typecheck": "echo \"Nothing to do\""
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { configure } from '@storybook/react';
|
||||
import { configure, addDecorator } from '@storybook/react';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
import { withTheme } from '../src/utils/storybook/withTheme';
|
||||
|
||||
import '../../../public/sass/grafana.light.scss';
|
||||
|
||||
// automatically import all files ending in *.stories.tsx
|
||||
const req = require.context('../src/components', true, /.story.tsx$/);
|
||||
|
||||
addDecorator(withKnobs);
|
||||
addDecorator(withTheme);
|
||||
|
||||
function loadStories() {
|
||||
req.keys().forEach(req);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = (baseConfig, env, config) => {
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.(ts|tsx)$/,
|
||||
use: [
|
||||
@@ -33,7 +32,12 @@ module.exports = (baseConfig, env, config) => {
|
||||
config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' },
|
||||
},
|
||||
},
|
||||
{ loader: 'sass-loader', options: { sourceMap: false } },
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: false
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -52,5 +56,9 @@ module.exports = (baseConfig, env, config) => {
|
||||
});
|
||||
|
||||
config.resolve.extensions.push('.ts', '.tsx');
|
||||
|
||||
// Remove pure js loading rules as Storybook's Babel config is causing problems when mixing ES6 and CJS
|
||||
// More about the problem we encounter: https://github.com/webpack/webpack/issues/4039
|
||||
config.module.rules = config.module.rules.filter(rule => rule.test.toString() !== /\.(mjs|jsx?)$/.toString());
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"storybook": "start-storybook -p 9001 -c .storybook -s ../../public"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@torkelo/react-select": "2.1.1",
|
||||
"@types/react-color": "^2.14.0",
|
||||
|
||||
@@ -1,46 +1,43 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { withKnobs, boolean } from '@storybook/addon-knobs';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { SeriesColorPicker, ColorPicker } from './ColorPicker';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { getThemeKnob } from '../../utils/storybook/themeKnob';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
|
||||
const getColorPickerKnobs = () => {
|
||||
return {
|
||||
selectedTheme: getThemeKnob(),
|
||||
enableNamedColors: boolean('Enable named colors', false),
|
||||
};
|
||||
};
|
||||
|
||||
const ColorPickerStories = storiesOf('UI/ColorPicker/Pickers', module);
|
||||
|
||||
ColorPickerStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
|
||||
ColorPickerStories.addDecorator(withCenteredStory);
|
||||
|
||||
ColorPickerStories.add('default', () => {
|
||||
const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
|
||||
const { enableNamedColors } = getColorPickerKnobs();
|
||||
|
||||
return (
|
||||
<UseState initialState="#00ff00">
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return (
|
||||
<ColorPicker
|
||||
enableNamedColors={enableNamedColors}
|
||||
color={selectedColor}
|
||||
onChange={color => {
|
||||
action('Color changed')(color);
|
||||
updateSelectedColor(color);
|
||||
}}
|
||||
theme={selectedTheme || undefined}
|
||||
/>
|
||||
);
|
||||
return renderComponentWithTheme(ColorPicker, {
|
||||
enableNamedColors,
|
||||
color: selectedColor,
|
||||
onChange: (color: any) => {
|
||||
action('Color changed')(color);
|
||||
updateSelectedColor(color);
|
||||
},
|
||||
});
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
});
|
||||
|
||||
ColorPickerStories.add('Series color picker', () => {
|
||||
const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
|
||||
const { enableNamedColors } = getColorPickerKnobs();
|
||||
|
||||
return (
|
||||
<UseState initialState="#00ff00">
|
||||
@@ -52,7 +49,6 @@ ColorPickerStories.add('Series color picker', () => {
|
||||
onToggleAxis={() => {}}
|
||||
color={selectedColor}
|
||||
onChange={color => updateSelectedColor(color)}
|
||||
theme={selectedTheme || undefined}
|
||||
>
|
||||
<div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div>
|
||||
</SeriesColorPicker>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { Component, createRef } from 'react';
|
||||
import PopperController from '../Tooltip/PopperController';
|
||||
import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper';
|
||||
import Popper from '../Tooltip/Popper';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { Themeable, GrafanaTheme } from '../../types';
|
||||
import { Themeable } from '../../types';
|
||||
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
|
||||
import propDeprecationWarning from '../../utils/propDeprecationWarning';
|
||||
|
||||
import { withTheme } from '../../themes/ThemeContext';
|
||||
type ColorPickerChangeHandler = (color: string) => void;
|
||||
|
||||
export interface ColorPickerProps extends Themeable {
|
||||
@@ -18,7 +18,6 @@ export interface ColorPickerProps extends Themeable {
|
||||
*/
|
||||
onColorChange?: ColorPickerChangeHandler;
|
||||
enableNamedColors?: boolean;
|
||||
withArrow?: boolean;
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
@@ -32,7 +31,6 @@ export const warnAboutColorPickerPropsDeprecation = (componentName: string, prop
|
||||
export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
popover: React.ComponentType<T>,
|
||||
displayName = 'ColorPicker',
|
||||
renderPopoverArrowFunction?: RenderPopperArrowFn
|
||||
) => {
|
||||
return class ColorPicker extends Component<T, any> {
|
||||
static displayName = displayName;
|
||||
@@ -50,17 +48,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
...this.props,
|
||||
onChange: this.handleColorChange,
|
||||
});
|
||||
const { theme, withArrow, children } = this.props;
|
||||
|
||||
const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => {
|
||||
return (
|
||||
<div
|
||||
{...arrowProps}
|
||||
data-placement={placement}
|
||||
className={`ColorPicker__arrow ColorPicker__arrow--${theme === GrafanaTheme.Light ? 'light' : 'dark'}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const { theme, children } = this.props;
|
||||
|
||||
return (
|
||||
<PopperController content={popoverElement} hideAfter={300}>
|
||||
@@ -72,7 +60,6 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
{...popperProps}
|
||||
referenceElement={this.pickerTriggerRef.current}
|
||||
wrapperClassName="ColorPicker"
|
||||
renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)}
|
||||
onMouseLeave={hidePopper}
|
||||
onMouseEnter={showPopper}
|
||||
/>
|
||||
@@ -95,7 +82,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
<div
|
||||
className="sp-preview-inner"
|
||||
style={{
|
||||
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme),
|
||||
backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme.type),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -110,5 +97,5 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
};
|
||||
};
|
||||
|
||||
export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker');
|
||||
export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker');
|
||||
export const ColorPicker = withTheme(colorPickerFactory(ColorPickerPopover, 'ColorPicker'));
|
||||
export const SeriesColorPicker = withTheme(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker'));
|
||||
|
||||
@@ -1,40 +1,27 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { getThemeKnob } from '../../utils/storybook/themeKnob';
|
||||
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
|
||||
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
const ColorPickerPopoverStories = storiesOf('UI/ColorPicker/Popovers', module);
|
||||
|
||||
ColorPickerPopoverStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
|
||||
ColorPickerPopoverStories.addDecorator(withCenteredStory);
|
||||
|
||||
ColorPickerPopoverStories.add('default', () => {
|
||||
const selectedTheme = getThemeKnob();
|
||||
|
||||
return (
|
||||
<ColorPickerPopover
|
||||
color="#BC67E6"
|
||||
onChange={color => {
|
||||
console.log(color);
|
||||
}}
|
||||
theme={selectedTheme || undefined}
|
||||
/>
|
||||
);
|
||||
return renderComponentWithTheme(ColorPickerPopover, {
|
||||
color: '#BC67E6',
|
||||
onChange: (color: any) => {
|
||||
console.log(color);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
ColorPickerPopoverStories.add('SeriesColorPickerPopover', () => {
|
||||
const selectedTheme = getThemeKnob();
|
||||
|
||||
return (
|
||||
<SeriesColorPickerPopover
|
||||
color="#BC67E6"
|
||||
onChange={color => {
|
||||
console.log(color);
|
||||
}}
|
||||
theme={selectedTheme || undefined}
|
||||
/>
|
||||
);
|
||||
return renderComponentWithTheme(SeriesColorPickerPopover, {
|
||||
color: '#BC67E6',
|
||||
onChange: (color: any) => {
|
||||
console.log(color);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@ import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { getColorDefinitionByName, getNamedColorPalette } from '../../utils/namedColorsPalette';
|
||||
import { ColorSwatch } from './NamedColorsGroup';
|
||||
import { flatten } from 'lodash';
|
||||
import { GrafanaTheme } from '../../types';
|
||||
import { GrafanaThemeType } from '../../types';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
const allColors = flatten(Array.from(getNamedColorPalette().values()));
|
||||
|
||||
@@ -14,7 +15,7 @@ describe('ColorPickerPopover', () => {
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render provided color as selected if color provided by name', () => {
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} />);
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
|
||||
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
|
||||
|
||||
@@ -24,7 +25,7 @@ describe('ColorPickerPopover', () => {
|
||||
});
|
||||
|
||||
it('should render provided color as selected if color provided by hex', () => {
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} />);
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />);
|
||||
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
|
||||
|
||||
@@ -45,7 +46,7 @@ describe('ColorPickerPopover', () => {
|
||||
|
||||
it('should pass hex color value to onChange prop by default', () => {
|
||||
wrapper = mount(
|
||||
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={GrafanaTheme.Light} />
|
||||
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={getTheme(GrafanaThemeType.Light)} />
|
||||
);
|
||||
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
|
||||
|
||||
@@ -61,7 +62,7 @@ describe('ColorPickerPopover', () => {
|
||||
enableNamedColors
|
||||
color={BasicGreen.variants.dark}
|
||||
onChange={onChangeSpy}
|
||||
theme={GrafanaTheme.Light}
|
||||
theme={getTheme(GrafanaThemeType.Light)}
|
||||
/>
|
||||
);
|
||||
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import { NamedColorsPalette } from './NamedColorsPalette';
|
||||
import { getColorName, getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||
import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker';
|
||||
import { GrafanaTheme } from '../../types';
|
||||
import { PopperContentProps } from '../Tooltip/PopperController';
|
||||
import SpectrumPalette from './SpectrumPalette';
|
||||
import { GrafanaThemeType } from '@grafana/ui';
|
||||
|
||||
export interface Props<T> extends ColorPickerProps, PopperContentProps {
|
||||
customPickers?: T;
|
||||
@@ -43,7 +43,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
if (enableNamedColors) {
|
||||
return changeHandler(color);
|
||||
}
|
||||
changeHandler(getColorFromHexRgbOrName(color, theme));
|
||||
changeHandler(getColorFromHexRgbOrName(color, theme.type));
|
||||
};
|
||||
|
||||
handleTabChange = (tab: PickerType | keyof T) => {
|
||||
@@ -58,7 +58,9 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
case 'spectrum':
|
||||
return <SpectrumPalette color={color} onChange={this.handleChange} theme={theme} />;
|
||||
case 'palette':
|
||||
return <NamedColorsPalette color={getColorName(color, theme)} onChange={this.handleChange} theme={theme} />;
|
||||
return (
|
||||
<NamedColorsPalette color={getColorName(color, theme.type)} onChange={this.handleChange} theme={theme} />
|
||||
);
|
||||
default:
|
||||
return this.renderCustomPicker(activePicker);
|
||||
}
|
||||
@@ -88,11 +90,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
<>
|
||||
{Object.keys(customPickers).map(key => {
|
||||
return (
|
||||
<div
|
||||
className={this.getTabClassName(key)}
|
||||
onClick={this.handleTabChange(key)}
|
||||
key={key}
|
||||
>
|
||||
<div className={this.getTabClassName(key)} onClick={this.handleTabChange(key)} key={key}>
|
||||
{customPickers[key].name}
|
||||
</div>
|
||||
);
|
||||
@@ -103,21 +101,14 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
|
||||
render() {
|
||||
const { theme } = this.props;
|
||||
const colorPickerTheme = theme || GrafanaTheme.Dark;
|
||||
|
||||
const colorPickerTheme = theme.type || GrafanaThemeType.Dark;
|
||||
return (
|
||||
<div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
|
||||
<div className="ColorPickerPopover__tabs">
|
||||
<div
|
||||
className={this.getTabClassName('palette')}
|
||||
onClick={this.handleTabChange('palette')}
|
||||
>
|
||||
<div className={this.getTabClassName('palette')} onClick={this.handleTabChange('palette')}>
|
||||
Colors
|
||||
</div>
|
||||
<div
|
||||
className={this.getTabClassName('spectrum')}
|
||||
onClick={this.handleTabChange('spectrum')}
|
||||
>
|
||||
<div className={this.getTabClassName('spectrum')} onClick={this.handleTabChange('spectrum')}>
|
||||
Custom
|
||||
</div>
|
||||
{this.renderCustomPickerTabs()}
|
||||
@@ -128,3 +119,4 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { Themeable, GrafanaTheme } from '../../types';
|
||||
import { Themeable } from '../../types';
|
||||
import { ColorDefinition, getColorForTheme } from '../../utils/namedColorsPalette';
|
||||
import { Color } from 'csstype';
|
||||
import { find, upperFirst } from 'lodash';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
|
||||
type ColorChangeHandler = (color: ColorDefinition) => void;
|
||||
|
||||
@@ -28,7 +29,15 @@ export const ColorSwatch: FunctionComponent<ColorSwatchProps> = ({
|
||||
}) => {
|
||||
const isSmall = variant === ColorSwatchVariant.Small;
|
||||
const swatchSize = isSmall ? '16px' : '32px';
|
||||
const selectedSwatchBorder = theme === GrafanaTheme.Light ? '#ffffff' : '#1A1B1F';
|
||||
|
||||
const selectedSwatchBorder = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.white,
|
||||
dark: theme.colors.black,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
const swatchStyles = {
|
||||
width: swatchSize,
|
||||
height: swatchSize,
|
||||
@@ -76,7 +85,7 @@ const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
|
||||
key={primaryColor.name}
|
||||
isSelected={primaryColor.name === selectedColor}
|
||||
variant={ColorSwatchVariant.Large}
|
||||
color={getColorForTheme(primaryColor, theme)}
|
||||
color={getColorForTheme(primaryColor, theme.type)}
|
||||
label={upperFirst(primaryColor.hue)}
|
||||
onClick={() => onColorSelect(primaryColor)}
|
||||
theme={theme}
|
||||
@@ -95,7 +104,7 @@ const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
|
||||
<ColorSwatch
|
||||
key={color.name}
|
||||
isSelected={color.name === selectedColor}
|
||||
color={getColorForTheme(color, theme)}
|
||||
color={getColorForTheme(color, theme.type)}
|
||||
onClick={() => onColorSelect(color)}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
@@ -2,8 +2,9 @@ import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { NamedColorsPalette } from './NamedColorsPalette';
|
||||
import { getColorName, getColorDefinitionByName } from '../../utils/namedColorsPalette';
|
||||
import { withKnobs, select } from '@storybook/addon-knobs';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
|
||||
const BasicGreen = getColorDefinitionByName('green');
|
||||
@@ -12,7 +13,7 @@ const LightBlue = getColorDefinitionByName('light-blue');
|
||||
|
||||
const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module);
|
||||
|
||||
NamedColorsPaletteStories.addDecorator(withKnobs).addDecorator(withCenteredStory);
|
||||
NamedColorsPaletteStories.addDecorator(withCenteredStory);
|
||||
|
||||
NamedColorsPaletteStories.add('Named colors swatch - support for named colors', () => {
|
||||
const selectedColor = select(
|
||||
@@ -28,7 +29,10 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors',
|
||||
return (
|
||||
<UseState initialState={selectedColor}>
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return <NamedColorsPalette color={selectedColor} onChange={updateSelectedColor} />;
|
||||
return renderComponentWithTheme(NamedColorsPalette, {
|
||||
color: selectedColor,
|
||||
onChange: updateSelectedColor,
|
||||
});
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
@@ -45,7 +49,10 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors',
|
||||
return (
|
||||
<UseState initialState={selectedColor}>
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return <NamedColorsPalette color={getColorName(selectedColor)} onChange={updateSelectedColor} />;
|
||||
return renderComponentWithTheme(NamedColorsPalette, {
|
||||
color: getColorName(selectedColor),
|
||||
onChange: updateSelectedColor,
|
||||
});
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,8 @@ import { mount, ReactWrapper } from 'enzyme';
|
||||
import { NamedColorsPalette } from './NamedColorsPalette';
|
||||
import { ColorSwatch } from './NamedColorsGroup';
|
||||
import { getColorDefinitionByName } from '../../utils';
|
||||
import { GrafanaTheme } from '../../types';
|
||||
import { getTheme } from '../../themes';
|
||||
import { GrafanaThemeType } from '../../types';
|
||||
|
||||
describe('NamedColorsPalette', () => {
|
||||
|
||||
@@ -17,18 +18,18 @@ describe('NamedColorsPalette', () => {
|
||||
});
|
||||
|
||||
it('should render provided color variant specific for theme', () => {
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Dark} onChange={() => {}} />);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme()} onChange={() => {}} />);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
|
||||
wrapper.unmount();
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Light} onChange={() => {}} />);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light);
|
||||
});
|
||||
|
||||
it('should render dar variant of provided color when theme not provided', () => {
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} />);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
import { ColorPickerProps } from './ColorPicker';
|
||||
import { PopperContentProps } from '../Tooltip/PopperController';
|
||||
import { Switch } from '../Switch/Switch';
|
||||
import { withTheme } from '../../themes/ThemeContext';
|
||||
|
||||
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperContentProps {
|
||||
yaxis?: number;
|
||||
@@ -12,7 +13,6 @@ export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperC
|
||||
|
||||
export const SeriesColorPickerPopover: FunctionComponent<SeriesColorPickerPopoverProps> = props => {
|
||||
const { yaxis, onToggleAxis, color, ...colorPickerProps } = props;
|
||||
|
||||
return (
|
||||
<ColorPickerPopover
|
||||
{...colorPickerProps}
|
||||
@@ -69,8 +69,8 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
||||
}
|
||||
|
||||
render() {
|
||||
const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
|
||||
const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse';
|
||||
const leftButtonClass = this.state.yaxis === 1 ? 'btn-primary' : 'btn-inverse';
|
||||
const rightButtonClass = this.state.yaxis === 2 ? 'btn-primary' : 'btn-inverse';
|
||||
|
||||
return (
|
||||
<div className="p-b-1">
|
||||
@@ -85,3 +85,6 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This component is to enable SeriecColorPickerPopover usage via series-color-picker-popover directive
|
||||
export const SeriesColorPickerPopoverWithTheme = withTheme(SeriesColorPickerPopover);
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
|
||||
import SpectrumPalette from './SpectrumPalette';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { getThemeKnob } from '../../utils/storybook/themeKnob';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
|
||||
const SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalette', module);
|
||||
|
||||
SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
|
||||
SpectrumPaletteStories.addDecorator(withCenteredStory);
|
||||
|
||||
SpectrumPaletteStories.add('Named colors swatch - support for named colors', () => {
|
||||
const selectedTheme = getThemeKnob();
|
||||
SpectrumPaletteStories.add('default', () => {
|
||||
return (
|
||||
<UseState initialState="red">
|
||||
{(selectedColor, updateSelectedColor) => {
|
||||
return <SpectrumPalette theme={selectedTheme} color={selectedColor} onChange={updateSelectedColor} />;
|
||||
return renderComponentWithTheme(SpectrumPalette, { color: selectedColor, onChange: updateSelectedColor });
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface SpectrumPaletteProps extends Themeable {
|
||||
onChange: (color: string) => void;
|
||||
}
|
||||
|
||||
const renderPointer = (theme?: GrafanaTheme) => (props: SpectrumPalettePointerProps) => (
|
||||
const renderPointer = (theme: GrafanaTheme) => (props: SpectrumPalettePointerProps) => (
|
||||
<SpectrumPalettePointer {...props} theme={theme} />
|
||||
);
|
||||
|
||||
@@ -92,7 +92,7 @@ const SpectrumPalette: React.FunctionComponent<SpectrumPaletteProps> = ({ color,
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
<ColorInput color={color} onChange={onChange} style={{ marginTop: '16px' }} />
|
||||
<ColorInput theme={theme} color={color} onChange={onChange} style={{ marginTop: '16px' }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React from 'react';
|
||||
import { GrafanaTheme, Themeable } from '../../types';
|
||||
import { Themeable } from '../../types';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
|
||||
export interface SpectrumPalettePointerProps extends Themeable {
|
||||
direction?: string;
|
||||
}
|
||||
|
||||
const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProps> = ({
|
||||
theme,
|
||||
direction,
|
||||
}) => {
|
||||
const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProps> = ({ theme, direction }) => {
|
||||
const styles = {
|
||||
picker: {
|
||||
width: '16px',
|
||||
@@ -17,7 +15,14 @@ const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProp
|
||||
},
|
||||
};
|
||||
|
||||
const pointerColor = theme === GrafanaTheme.Light ? '#3F444D' : '#8E8E8E';
|
||||
|
||||
const pointerColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.dark3,
|
||||
dark: theme.colors.gray2,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
let pointerStyles: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -167,6 +167,7 @@ $arrowSize: 15px;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sp-replacer:hover,
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Gauge, Props } from './Gauge';
|
||||
import { TimeSeriesVMs } from '../../types/series';
|
||||
import { ValueMapping, MappingType } from '../../types';
|
||||
import { getTheme } from '../../themes';
|
||||
|
||||
jest.mock('jquery', () => ({
|
||||
plot: jest.fn(),
|
||||
@@ -23,8 +23,9 @@ const setup = (propOverrides?: object) => {
|
||||
stat: 'avg',
|
||||
height: 300,
|
||||
width: 300,
|
||||
timeSeries: {} as TimeSeriesVMs,
|
||||
value: 25,
|
||||
decimals: 0,
|
||||
theme: getTheme()
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import $ from 'jquery';
|
||||
|
||||
import { ValueMapping, Threshold, BasicGaugeColor, TimeSeriesVMs, GrafanaTheme } from '../../types';
|
||||
import { ValueMapping, Threshold, BasicGaugeColor, GrafanaThemeType } from '../../types';
|
||||
import { getMappedValue } from '../../utils/valueMappings';
|
||||
import { getColorFromHexRgbOrName, getValueFormat } from '../../utils';
|
||||
import { Themeable } from '../../index';
|
||||
|
||||
type TimeSeriesValue = string | number | null;
|
||||
|
||||
export interface Props {
|
||||
export interface Props extends Themeable {
|
||||
decimals: number;
|
||||
height: number;
|
||||
valueMappings: ValueMapping[];
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
timeSeries: TimeSeriesVMs;
|
||||
thresholds: Threshold[];
|
||||
showThresholdMarkers: boolean;
|
||||
showThresholdLabels: boolean;
|
||||
@@ -22,7 +22,7 @@ export interface Props {
|
||||
suffix: string;
|
||||
unit: string;
|
||||
width: number;
|
||||
theme?: GrafanaTheme;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const FONT_SCALE = 1;
|
||||
@@ -41,7 +41,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
thresholds: [],
|
||||
unit: 'none',
|
||||
stat: 'avg',
|
||||
theme: GrafanaTheme.Dark,
|
||||
theme: GrafanaThemeType.Dark,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -77,19 +77,19 @@ export class Gauge extends PureComponent<Props> {
|
||||
const { thresholds, theme } = this.props;
|
||||
|
||||
if (thresholds.length === 1) {
|
||||
return getColorFromHexRgbOrName(thresholds[0].color, theme);
|
||||
return getColorFromHexRgbOrName(thresholds[0].color, theme.type);
|
||||
}
|
||||
|
||||
const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
|
||||
if (atThreshold) {
|
||||
return getColorFromHexRgbOrName(atThreshold.color, theme);
|
||||
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);
|
||||
return getColorFromHexRgbOrName(nearestThreshold.color, theme.type);
|
||||
}
|
||||
|
||||
return BasicGaugeColor.Red;
|
||||
@@ -104,13 +104,13 @@ export class Gauge extends PureComponent<Props> {
|
||||
return [
|
||||
...thresholdsSortedByIndex.map(threshold => {
|
||||
if (threshold.index === 0) {
|
||||
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme) };
|
||||
return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) };
|
||||
}
|
||||
|
||||
const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
|
||||
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme) };
|
||||
return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) };
|
||||
}),
|
||||
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme) },
|
||||
{ value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -122,29 +122,12 @@ export class Gauge extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
draw() {
|
||||
const {
|
||||
maxValue,
|
||||
minValue,
|
||||
timeSeries,
|
||||
showThresholdLabels,
|
||||
showThresholdMarkers,
|
||||
width,
|
||||
height,
|
||||
stat,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let value: TimeSeriesValue = '';
|
||||
|
||||
if (timeSeries[0]) {
|
||||
value = timeSeries[0].stats[stat];
|
||||
} else {
|
||||
value = null;
|
||||
}
|
||||
const { maxValue, minValue, showThresholdLabels, showThresholdMarkers, width, height, theme, value } = this.props;
|
||||
|
||||
const formattedValue = this.formatValue(value) as string;
|
||||
const dimension = Math.min(width, height * 1.3);
|
||||
const backgroundColor = theme === GrafanaTheme.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
|
||||
const backgroundColor = theme.type === GrafanaThemeType.Light ? 'rgb(230,230,230)' : theme.colors.dark3;
|
||||
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio;
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
@@ -194,7 +177,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
try {
|
||||
$.plot(this.canvasElement, [plotSeries], options);
|
||||
} catch (err) {
|
||||
console.log('Gauge rendering error', err, options, timeSeries);
|
||||
console.log('Gauge rendering error', err, options, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
// Libraries
|
||||
import React, { SFC } from 'react';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
onClose?: () => void;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
children: JSX.Element | JSX.Element[] | boolean;
|
||||
onAdd?: () => void;
|
||||
}
|
||||
|
||||
export const PanelOptionsGroup: SFC<Props> = props => {
|
||||
export const PanelOptionsGroup: FunctionComponent<Props> = props => {
|
||||
return (
|
||||
<div className="panel-options-group">
|
||||
{props.title && (
|
||||
{props.onAdd ? (
|
||||
<div className="panel-options-group__header">
|
||||
{props.title}
|
||||
{props.onClose && (
|
||||
<button className="btn btn-link" onClick={props.onClose}>
|
||||
<i className="fa fa-remove" />
|
||||
</button>
|
||||
)}
|
||||
<button className="panel-options-group__add-btn" onClick={props.onAdd}>
|
||||
<div className="panel-options-group__add-circle">
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<span className="panel-options-group__title">{props.title}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
props.title && (
|
||||
<div className="panel-options-group__header">
|
||||
<span className="panel-options-group__title">{props.title}</span>
|
||||
{props.onClose && (
|
||||
<button className="btn btn-link" onClick={props.onClose}>
|
||||
<i className="fa fa-remove" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div className="panel-options-group__body">{props.children}</div>
|
||||
{props.children && <div className="panel-options-group__body">{props.children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,18 +7,57 @@
|
||||
|
||||
.panel-options-group__header {
|
||||
padding: 4px 8px;
|
||||
font-size: 1.1rem;
|
||||
background: $panel-options-group-header-bg;
|
||||
position: relative;
|
||||
border-radius: $border-radius $border-radius 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0px;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-options-group__add-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
.panel-options-group__add-circle {
|
||||
background-color: $btn-primary-bg;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-options-group__add-circle {
|
||||
@include gradientBar($btn-success-bg, $btn-success-bg-hl, #fff);
|
||||
|
||||
border-radius: 50px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 6px;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-options-group__title {
|
||||
font-size: 1.1rem;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.panel-options-group__body {
|
||||
padding: 20px;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps,
|
||||
return (
|
||||
<div className="gf-form-select-box__option-group">
|
||||
<div className="gf-form-select-box__option-group__header" onClick={this.onToggleChildren}>
|
||||
<span className="flex-grow">{label}</span>
|
||||
<span className="flex-grow-1">{label}</span>
|
||||
<i className={`fa ${expanded ? 'fa-caret-left' : 'fa-caret-down'}`} />{' '}
|
||||
</div>
|
||||
{expanded && children}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Threshold, Themeable } from '../../types';
|
||||
import { Threshold } from '../../types';
|
||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
import { colors } from '../../utils';
|
||||
import { getColorFromHexRgbOrName } from '@grafana/ui';
|
||||
import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui';
|
||||
|
||||
export interface Props extends Themeable {
|
||||
export interface Props {
|
||||
thresholds: Threshold[];
|
||||
onChange: (thresholds: Threshold[]) => void;
|
||||
}
|
||||
@@ -164,7 +164,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
<div className="thresholds-row-input-inner-color">
|
||||
{threshold.color && (
|
||||
<div className="thresholds-row-input-inner-color-colorpicker">
|
||||
<ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
|
||||
<ColorPicker
|
||||
color={threshold.color}
|
||||
onChange={color => this.onChangeThresholdColor(threshold, color)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -188,27 +191,35 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { thresholds } = this.state;
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<div className="thresholds">
|
||||
{thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||
<div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme) }}
|
||||
/>
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
<ThemeContext.Consumer>
|
||||
{theme => {
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<div className="thresholds">
|
||||
{thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="thresholds-row" key={`${threshold.index}-${index}`}>
|
||||
<div
|
||||
className="thresholds-row-add-button"
|
||||
onClick={() => this.onAddThreshold(threshold.index + 1)}
|
||||
>
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme.type) }}
|
||||
/>
|
||||
<div className="thresholds-row-input">{this.renderInput(threshold)}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}}
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.thresholds {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.thresholds-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 70px;
|
||||
height: 62px;
|
||||
}
|
||||
|
||||
.thresholds-row:first-child > .thresholds-row-color-indicator {
|
||||
@@ -21,21 +21,21 @@
|
||||
}
|
||||
|
||||
.thresholds-row-add-button {
|
||||
@include buttonBackground($btn-success-bg, $btn-success-bg-hl, #fff);
|
||||
|
||||
align-self: center;
|
||||
margin-right: 5px;
|
||||
color: $green;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: $green;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.thresholds-row-add-button > i {
|
||||
color: $white;
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.thresholds-row-color-indicator {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ValueMappingsEditor } from './ValueMappingsEditor';
|
||||
|
||||
const ValueMappingsEditorStories = storiesOf('UI/ValueMappingsEditor', module);
|
||||
|
||||
ValueMappingsEditorStories.add('default', () => {
|
||||
return <ValueMappingsEditor valueMappings={[]} onChange={action('Mapping changed')} />;
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
|
||||
import { MappingType } from '../../types/panel';
|
||||
import { MappingType } from '../../types';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import MappingRow from './MappingRow';
|
||||
import { MappingType, ValueMapping } from '../../types/panel';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
import { MappingType, ValueMapping } from '../../types';
|
||||
import { PanelOptionsGroup } from '..';
|
||||
|
||||
export interface Props {
|
||||
valueMappings: ValueMapping[];
|
||||
@@ -81,8 +81,7 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
||||
const { valueMappings } = this.state;
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Value Mappings">
|
||||
<div>
|
||||
<PanelOptionsGroup title="Add value mapping" onAdd={this.addMapping}>
|
||||
{valueMappings.length > 0 &&
|
||||
valueMappings.map((valueMapping, index) => (
|
||||
<MappingRow
|
||||
@@ -92,13 +91,6 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="add-mapping-row" onClick={this.addMapping}>
|
||||
<div className="add-mapping-row-icon">
|
||||
<i className="fa fa-plus" />
|
||||
</div>
|
||||
<div className="add-mapping-row-label">Add mapping</div>
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,55 +2,37 @@
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<Component
|
||||
title="Value Mappings"
|
||||
onAdd={[Function]}
|
||||
title="Add value mapping"
|
||||
>
|
||||
<div>
|
||||
<MappingRow
|
||||
key="Ok-0"
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"id": 1,
|
||||
"operator": "",
|
||||
"text": "Ok",
|
||||
"type": 1,
|
||||
"value": "20",
|
||||
}
|
||||
<MappingRow
|
||||
key="Ok-0"
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"id": 1,
|
||||
"operator": "",
|
||||
"text": "Ok",
|
||||
"type": 1,
|
||||
"value": "20",
|
||||
}
|
||||
/>
|
||||
<MappingRow
|
||||
key="Meh-1"
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"from": "21",
|
||||
"id": 2,
|
||||
"operator": "",
|
||||
"text": "Meh",
|
||||
"to": "30",
|
||||
"type": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<MappingRow
|
||||
key="Meh-1"
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"from": "21",
|
||||
"id": 2,
|
||||
"operator": "",
|
||||
"text": "Meh",
|
||||
"to": "30",
|
||||
"type": 2,
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="add-mapping-row"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<div
|
||||
className="add-mapping-row-icon"
|
||||
>
|
||||
<i
|
||||
className="fa fa-plus"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="add-mapping-row-label"
|
||||
>
|
||||
Add mapping
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
@@ -14,8 +14,8 @@ export { FormLabel } from './FormLabel/FormLabel';
|
||||
export { FormField } from './FormField/FormField';
|
||||
|
||||
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
|
||||
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
|
||||
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
|
||||
export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
|
||||
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './components';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './themes';
|
||||
export * from './themes/ThemeContext';
|
||||
|
||||
20
packages/grafana-ui/src/themes/ThemeContext.tsx
Normal file
20
packages/grafana-ui/src/themes/ThemeContext.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { GrafanaThemeType, Themeable } from '../types';
|
||||
import { getTheme } from './index';
|
||||
|
||||
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
type Subtract<T, K> = Omit<T, keyof K>;
|
||||
|
||||
// Use Grafana Dark theme by default
|
||||
export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark));
|
||||
|
||||
export const withTheme = <P extends Themeable>(Component: React.ComponentType<P>) => {
|
||||
const WithTheme: React.FunctionComponent<Subtract<P, Themeable>> = props => {
|
||||
// @ts-ignore
|
||||
return <ThemeContext.Consumer>{theme => <Component {...props} theme={theme} />}</ThemeContext.Consumer>;
|
||||
};
|
||||
|
||||
WithTheme.displayName = `WithTheme(${Component.displayName})`;
|
||||
|
||||
return WithTheme;
|
||||
};
|
||||
69
packages/grafana-ui/src/themes/dark.ts
Normal file
69
packages/grafana-ui/src/themes/dark.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
import defaultTheme from './default';
|
||||
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
|
||||
|
||||
const basicColors = {
|
||||
black: '#000000',
|
||||
white: '#ffffff',
|
||||
dark1: '#141414',
|
||||
dark2: '#1f1f20',
|
||||
dark3: '#262628',
|
||||
dark4: '#333333',
|
||||
dark5: '#444444',
|
||||
gray1: '#555555',
|
||||
gray2: '#8e8e8e',
|
||||
gray3: '#b3b3b3',
|
||||
gray4: '#d8d9da',
|
||||
gray5: '#ececec',
|
||||
gray6: '#f4f5f8',
|
||||
gray7: '#fbfbfb',
|
||||
grayBlue: '#212327',
|
||||
blue: '#33b5e5',
|
||||
blueDark: '#005f81',
|
||||
blueLight: '#00a8e6', // not used in dark theme
|
||||
green: '#299c46',
|
||||
red: '#d44a3a',
|
||||
yellow: '#ecbb13',
|
||||
pink: '#ff4444',
|
||||
purple: '#9933cc',
|
||||
variable: '#32d1df',
|
||||
orange: '#eb7b18',
|
||||
};
|
||||
|
||||
const darkTheme: GrafanaTheme = {
|
||||
...defaultTheme,
|
||||
type: GrafanaThemeType.Dark,
|
||||
name: 'Grafana Dark',
|
||||
colors: {
|
||||
...basicColors,
|
||||
inputBlack: '#09090b',
|
||||
queryRed: '#e24d42',
|
||||
queryGreen: '#74e680',
|
||||
queryPurple: '#fe85fc',
|
||||
queryKeyword: '#66d9ef',
|
||||
queryOrange: 'eb7b18',
|
||||
online: '#10a345',
|
||||
warn: '#f79520',
|
||||
critical: '#ed2e18',
|
||||
bodyBg: '#171819',
|
||||
pageBg: '#161719',
|
||||
bodyColor: basicColors.gray4,
|
||||
textColor: basicColors.gray4,
|
||||
textColorStrong: basicColors.white,
|
||||
textColorWeak: basicColors.gray2,
|
||||
textColorEmphasis: basicColors.gray5,
|
||||
textColorFaint: basicColors.dark5,
|
||||
linkColor: new tinycolor(basicColors.white).darken(11).toString(),
|
||||
linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(),
|
||||
linkColorHover: basicColors.white,
|
||||
linkColorExternal: basicColors.blue,
|
||||
headingColor: new tinycolor(basicColors.white).darken(11).toString(),
|
||||
},
|
||||
background: {
|
||||
dropdown: basicColors.dark3,
|
||||
scrollbar: '#aeb5df',
|
||||
scrollbar2: '#3a3a3a',
|
||||
},
|
||||
};
|
||||
|
||||
export default darkTheme;
|
||||
62
packages/grafana-ui/src/themes/default.ts
Normal file
62
packages/grafana-ui/src/themes/default.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
|
||||
const theme = {
|
||||
name: 'Grafana Default',
|
||||
typography: {
|
||||
fontFamily: {
|
||||
sansSerif: "'Roboto', Helvetica, Arial, sans-serif;",
|
||||
serif: "Georgia, 'Times New Roman', Times, serif;",
|
||||
monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;"
|
||||
},
|
||||
size: {
|
||||
base: '13px',
|
||||
xs: '10px',
|
||||
s: '12px',
|
||||
m: '14px',
|
||||
l: '18px',
|
||||
},
|
||||
heading: {
|
||||
h1: '2rem',
|
||||
h2: '1.75rem',
|
||||
h3: '1.5rem',
|
||||
h4: '1.3rem',
|
||||
h5: '1.2rem',
|
||||
h6: '1rem',
|
||||
},
|
||||
weight: {
|
||||
light: 300,
|
||||
normal: 400,
|
||||
semibold: 500,
|
||||
},
|
||||
lineHeight: {
|
||||
xs: 1,
|
||||
s: 1.1,
|
||||
m: 4/3,
|
||||
l: 1.5
|
||||
}
|
||||
},
|
||||
brakpoints: {
|
||||
xs: '0',
|
||||
s: '544px',
|
||||
m: '768px',
|
||||
l: '992px',
|
||||
xl: '1200px'
|
||||
},
|
||||
spacing: {
|
||||
xs: '0',
|
||||
s: '0.2rem',
|
||||
m: '1rem',
|
||||
l: '1.5rem',
|
||||
xl: '3rem',
|
||||
gutter: '30px',
|
||||
},
|
||||
border: {
|
||||
radius: {
|
||||
xs: '2px',
|
||||
s: '3px',
|
||||
m: '5px',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
||||
14
packages/grafana-ui/src/themes/index.ts
Normal file
14
packages/grafana-ui/src/themes/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import darkTheme from './dark';
|
||||
import lightTheme from './light';
|
||||
import { GrafanaTheme } from '../types/theme';
|
||||
|
||||
let themeMock: ((name?: string) => GrafanaTheme) | null;
|
||||
|
||||
export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme);
|
||||
|
||||
export const mockTheme = (mock: (name: string) => GrafanaTheme) => {
|
||||
themeMock = mock;
|
||||
return () => {
|
||||
themeMock = null;
|
||||
};
|
||||
};
|
||||
70
packages/grafana-ui/src/themes/light.ts
Normal file
70
packages/grafana-ui/src/themes/light.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
import defaultTheme from './default';
|
||||
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
|
||||
|
||||
const basicColors = {
|
||||
black: '#000000',
|
||||
white: '#ffffff',
|
||||
dark1: '#13161d',
|
||||
dark2: '#1e2028',
|
||||
dark3: '#303133',
|
||||
dark4: '#35373f',
|
||||
dark5: '#41444b',
|
||||
gray1: '#52545c',
|
||||
gray2: '#767980',
|
||||
gray3: '#acb6bf',
|
||||
gray4: '#c7d0d9',
|
||||
gray5: '#dde4ed',
|
||||
gray6: '#e9edf2',
|
||||
gray7: '#f7f8fa',
|
||||
grayBlue: '#212327', // not used in light theme
|
||||
blue: '#0083b3',
|
||||
blueDark: '#005f81',
|
||||
blueLight: '#00a8e6',
|
||||
green: '#3aa655',
|
||||
red: '#d44939',
|
||||
yellow: '#ff851b',
|
||||
pink: '#e671b8',
|
||||
purple: '#9954bb',
|
||||
variable: '#0083b3',
|
||||
orange: '#ff7941',
|
||||
};
|
||||
|
||||
const lightTheme: GrafanaTheme = {
|
||||
...defaultTheme,
|
||||
type: GrafanaThemeType.Light,
|
||||
name: 'Grafana Light',
|
||||
colors: {
|
||||
...basicColors,
|
||||
variable: basicColors.blue,
|
||||
inputBlack: '#09090b',
|
||||
queryRed: basicColors.red,
|
||||
queryGreen: basicColors.green,
|
||||
queryPurple: basicColors.purple,
|
||||
queryKeyword: basicColors.blue,
|
||||
queryOrange: basicColors.orange,
|
||||
online: '#01a64f',
|
||||
warn: '#f79520',
|
||||
critical: '#ec2128',
|
||||
bodyBg: basicColors.gray7,
|
||||
pageBg: basicColors.gray7,
|
||||
bodyColor: basicColors.gray1,
|
||||
textColor: basicColors.gray1,
|
||||
textColorStrong: basicColors.dark2,
|
||||
textColorWeak: basicColors.gray2,
|
||||
textColorEmphasis: basicColors.gray5,
|
||||
textColorFaint: basicColors.dark4,
|
||||
linkColor: basicColors.gray1,
|
||||
linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
|
||||
linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(),
|
||||
linkColorExternal: basicColors.blueLight,
|
||||
headingColor: basicColors.gray1,
|
||||
},
|
||||
background: {
|
||||
dropdown: basicColors.white,
|
||||
scrollbar: basicColors.gray5,
|
||||
scrollbar2: basicColors.gray5,
|
||||
},
|
||||
};
|
||||
|
||||
export default lightTheme;
|
||||
52
packages/grafana-ui/src/themes/selectThemeVariant.test.ts
Normal file
52
packages/grafana-ui/src/themes/selectThemeVariant.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { GrafanaThemeType } from '../types/theme';
|
||||
import { selectThemeVariant } from './selectThemeVariant';
|
||||
import { mockTheme } from './index';
|
||||
|
||||
const lightThemeMock = {
|
||||
color: {
|
||||
red: '#ff0000',
|
||||
green: '#00ff00',
|
||||
},
|
||||
};
|
||||
|
||||
const darkThemeMock = {
|
||||
color: {
|
||||
red: '#ff0000',
|
||||
green: '#00ff00',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Theme variable variant selector', () => {
|
||||
// @ts-ignore
|
||||
const restoreTheme = mockTheme(name => (name === GrafanaThemeType.Light ? lightThemeMock : darkThemeMock));
|
||||
|
||||
afterAll(() => {
|
||||
restoreTheme();
|
||||
});
|
||||
it('return correct variable value for given theme', () => {
|
||||
const theme = lightThemeMock;
|
||||
|
||||
const selectedValue = selectThemeVariant(
|
||||
{
|
||||
dark: theme.color.red,
|
||||
light: theme.color.green,
|
||||
},
|
||||
GrafanaThemeType.Light
|
||||
);
|
||||
|
||||
expect(selectedValue).toBe(lightThemeMock.color.green);
|
||||
});
|
||||
|
||||
it('return dark theme variant if no theme given', () => {
|
||||
const theme = lightThemeMock;
|
||||
|
||||
const selectedValue = selectThemeVariant(
|
||||
{
|
||||
dark: theme.color.red,
|
||||
light: theme.color.green,
|
||||
}
|
||||
);
|
||||
|
||||
expect(selectedValue).toBe(lightThemeMock.color.red);
|
||||
});
|
||||
});
|
||||
9
packages/grafana-ui/src/themes/selectThemeVariant.ts
Normal file
9
packages/grafana-ui/src/themes/selectThemeVariant.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { GrafanaThemeType } from '../types/theme';
|
||||
|
||||
type VariantDescriptor = {
|
||||
[key in GrafanaThemeType]: string | number;
|
||||
};
|
||||
|
||||
export const selectThemeVariant = (variants: VariantDescriptor, currentTheme?: GrafanaThemeType) => {
|
||||
return variants[currentTheme || GrafanaThemeType.Dark];
|
||||
};
|
||||
@@ -52,3 +52,20 @@ export interface TimeSeriesVMs {
|
||||
[index: number]: TimeSeriesVM;
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface Column {
|
||||
text: string;
|
||||
title?: string;
|
||||
type?: string;
|
||||
sort?: boolean;
|
||||
desc?: boolean;
|
||||
filterable?: boolean;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface TableData {
|
||||
columns: Column[];
|
||||
rows: any[];
|
||||
type: string;
|
||||
columnMap: any;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TimeRange, RawTimeRange } from './time';
|
||||
import { TimeSeries } from './series';
|
||||
import { PluginMeta } from './plugin';
|
||||
import { TableData, TimeSeries } from './data';
|
||||
|
||||
export interface DataQueryResponse {
|
||||
data: TimeSeries[];
|
||||
data: TimeSeries[] | [TableData] | any;
|
||||
}
|
||||
|
||||
export interface DataQuery {
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
export * from './series';
|
||||
|
||||
export * from './data';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
export * from './plugin';
|
||||
export * from './datasource';
|
||||
|
||||
export enum GrafanaTheme {
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
}
|
||||
|
||||
export interface Themeable {
|
||||
theme?: GrafanaTheme;
|
||||
}
|
||||
export * from './theme';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TimeSeries, LoadingState } from './series';
|
||||
import { TimeSeries, LoadingState, TableData } from './data';
|
||||
import { TimeRange } from './time';
|
||||
|
||||
export type InterpolateFunction = (value: string, format?: string | Function) => string;
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
timeSeries: TimeSeries[];
|
||||
panelData: PanelData;
|
||||
timeRange: TimeRange;
|
||||
loading: LoadingState;
|
||||
options: T;
|
||||
@@ -14,6 +14,11 @@ export interface PanelProps<T = any> {
|
||||
onInterpolate: InterpolateFunction;
|
||||
}
|
||||
|
||||
export interface PanelData {
|
||||
timeSeries?: TimeSeries[];
|
||||
tableData?: TableData;
|
||||
}
|
||||
|
||||
export interface PanelOptionsProps<T = any> {
|
||||
options: T;
|
||||
onChange: (options: T) => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentClass } from 'react';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource';
|
||||
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
|
||||
|
||||
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||
/**
|
||||
@@ -41,22 +41,43 @@ export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
|
||||
pluginExports?: PluginExports;
|
||||
}
|
||||
|
||||
export interface ExploreDataSourceApi<TQuery extends DataQuery = DataQuery> extends DataSourceApi {
|
||||
modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;
|
||||
getHighlighterExpression?(query: TQuery): string;
|
||||
languageProvider?: any;
|
||||
}
|
||||
|
||||
export interface QueryEditorProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||
datasource: DSType;
|
||||
query: TQuery;
|
||||
onRunQuery: () => void;
|
||||
onChange: (value: TQuery) => void;
|
||||
}
|
||||
|
||||
export interface ExploreQueryFieldProps<DSType extends DataSourceApi, TQuery extends DataQuery> {
|
||||
datasource: DSType;
|
||||
query: TQuery;
|
||||
error?: string | JSX.Element;
|
||||
hint?: QueryHint;
|
||||
history: any[];
|
||||
onExecuteQuery?: () => void;
|
||||
onQueryChange?: (value: TQuery) => void;
|
||||
onExecuteHint?: (action: QueryFixAction) => void;
|
||||
}
|
||||
|
||||
export interface ExploreStartPageProps {
|
||||
onClickExample: (query: DataQuery) => void;
|
||||
}
|
||||
|
||||
export interface PluginExports {
|
||||
Datasource?: DataSourceApi;
|
||||
QueryCtrl?: any;
|
||||
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi,DataQuery>>;
|
||||
QueryEditor?: ComponentClass<QueryEditorProps<DataSourceApi, DataQuery>>;
|
||||
ConfigCtrl?: any;
|
||||
AnnotationsQueryCtrl?: any;
|
||||
VariableQueryEditor?: any;
|
||||
ExploreQueryField?: any;
|
||||
ExploreStartPage?: any;
|
||||
ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DataSourceApi, DataQuery>>;
|
||||
ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
|
||||
|
||||
// Panel plugin
|
||||
PanelCtrl?: any;
|
||||
@@ -114,5 +135,3 @@ export interface PluginMetaInfo {
|
||||
updated: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
129
packages/grafana-ui/src/types/theme.ts
Normal file
129
packages/grafana-ui/src/types/theme.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
export enum GrafanaThemeType {
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
}
|
||||
|
||||
export interface GrafanaTheme {
|
||||
type: GrafanaThemeType;
|
||||
name: string;
|
||||
// TODO: not sure if should be a part of theme
|
||||
brakpoints: {
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
l: string;
|
||||
xl: string;
|
||||
};
|
||||
typography: {
|
||||
fontFamily: {
|
||||
sansSerif: string;
|
||||
serif: string;
|
||||
monospace: string;
|
||||
};
|
||||
size: {
|
||||
base: string;
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
l: string;
|
||||
};
|
||||
weight: {
|
||||
light: number;
|
||||
normal: number;
|
||||
semibold: number;
|
||||
};
|
||||
lineHeight: {
|
||||
xs: number; //1
|
||||
s: number; //1.1
|
||||
m: number; // 4/3
|
||||
l: number; // 1.5
|
||||
};
|
||||
// TODO: Refactor to use size instead of custom defs
|
||||
heading: {
|
||||
h1: string;
|
||||
h2: string;
|
||||
h3: string;
|
||||
h4: string;
|
||||
h5: string;
|
||||
h6: string;
|
||||
};
|
||||
};
|
||||
spacing: {
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
l: string;
|
||||
gutter: string;
|
||||
};
|
||||
border: {
|
||||
radius: {
|
||||
xs: string;
|
||||
s: string;
|
||||
m: string;
|
||||
};
|
||||
};
|
||||
background: {
|
||||
dropdown: string;
|
||||
scrollbar: string;
|
||||
scrollbar2: string;
|
||||
};
|
||||
colors: {
|
||||
black: string;
|
||||
white: string;
|
||||
dark1: string;
|
||||
dark2: string;
|
||||
dark3: string;
|
||||
dark4: string;
|
||||
dark5: string;
|
||||
gray1: string;
|
||||
gray2: string;
|
||||
gray3: string;
|
||||
gray4: string;
|
||||
gray5: string;
|
||||
gray6: string;
|
||||
gray7: string;
|
||||
grayBlue: string;
|
||||
inputBlack: string;
|
||||
|
||||
// Accent colors
|
||||
blue: string;
|
||||
blueLight: string;
|
||||
blueDark: string;
|
||||
green: string;
|
||||
red: string;
|
||||
yellow: string;
|
||||
pink: string;
|
||||
purple: string;
|
||||
variable: string;
|
||||
orange: string;
|
||||
queryRed: string;
|
||||
queryGreen: string;
|
||||
queryPurple: string;
|
||||
queryKeyword: string;
|
||||
queryOrange: string;
|
||||
|
||||
// Status colors
|
||||
online: string;
|
||||
warn: string;
|
||||
critical: string;
|
||||
|
||||
// TODO: move to background section
|
||||
bodyBg: string;
|
||||
pageBg: string;
|
||||
bodyColor: string;
|
||||
textColor: string;
|
||||
textColorStrong: string;
|
||||
textColorWeak: string;
|
||||
textColorFaint: string;
|
||||
textColorEmphasis: string;
|
||||
linkColor: string;
|
||||
linkColorDisabled: string;
|
||||
linkColorHover: string;
|
||||
linkColorExternal: string;
|
||||
headingColor: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Themeable {
|
||||
theme: GrafanaTheme;
|
||||
}
|
||||
@@ -5,20 +5,20 @@ import {
|
||||
getColorFromHexRgbOrName,
|
||||
getColorDefinitionByName,
|
||||
} from './namedColorsPalette';
|
||||
import { GrafanaTheme } from '../types/index';
|
||||
import { GrafanaThemeType } from '../types/index';
|
||||
|
||||
describe('colors', () => {
|
||||
const SemiDarkBlue = getColorDefinitionByName('semi-dark-blue');
|
||||
|
||||
describe('getColorDefinition', () => {
|
||||
it('returns undefined for unknown hex', () => {
|
||||
expect(getColorDefinition('#ff0000', GrafanaTheme.Light)).toBeUndefined();
|
||||
expect(getColorDefinition('#ff0000', GrafanaTheme.Dark)).toBeUndefined();
|
||||
expect(getColorDefinition('#ff0000', GrafanaThemeType.Light)).toBeUndefined();
|
||||
expect(getColorDefinition('#ff0000', GrafanaThemeType.Dark)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns definition for known hex', () => {
|
||||
expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue);
|
||||
expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue);
|
||||
expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue);
|
||||
expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,8 +28,8 @@ describe('colors', () => {
|
||||
});
|
||||
|
||||
it('returns name for known hex', () => {
|
||||
expect(getColorName(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue.name);
|
||||
expect(getColorName(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue.name);
|
||||
expect(getColorName(SemiDarkBlue.variants.light, GrafanaThemeType.Light)).toEqual(SemiDarkBlue.name);
|
||||
expect(getColorName(SemiDarkBlue.variants.dark, GrafanaThemeType.Dark)).toEqual(SemiDarkBlue.name);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,12 +53,14 @@ describe('colors', () => {
|
||||
});
|
||||
|
||||
it("returns correct variant's hex for known color if theme specified", () => {
|
||||
expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaTheme.Light)).toBe(SemiDarkBlue.variants.light);
|
||||
expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaThemeType.Light)).toBe(SemiDarkBlue.variants.light);
|
||||
});
|
||||
|
||||
it('returns color if specified as hex or rgb/a', () => {
|
||||
expect(getColorFromHexRgbOrName('ff0000')).toBe('ff0000');
|
||||
expect(getColorFromHexRgbOrName('#ff0000')).toBe('#ff0000');
|
||||
expect(getColorFromHexRgbOrName('#FF0000')).toBe('#FF0000');
|
||||
expect(getColorFromHexRgbOrName('#CCC')).toBe('#CCC');
|
||||
expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)');
|
||||
expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { flatten } from 'lodash';
|
||||
import { GrafanaTheme } from '../types';
|
||||
import { GrafanaThemeType } from '../types';
|
||||
|
||||
type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple';
|
||||
|
||||
@@ -68,16 +68,16 @@ export const getColorDefinitionByName = (name: Color): ColorDefinition => {
|
||||
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === name)[0];
|
||||
};
|
||||
|
||||
export const getColorDefinition = (hex: string, theme: GrafanaTheme): ColorDefinition | undefined => {
|
||||
export const getColorDefinition = (hex: string, theme: GrafanaThemeType): ColorDefinition | undefined => {
|
||||
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.variants[theme] === hex)[0];
|
||||
};
|
||||
|
||||
const isHex = (color: string) => {
|
||||
const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6})$/gi;
|
||||
const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{3})$/gi;
|
||||
return hexRegex.test(color);
|
||||
};
|
||||
|
||||
export const getColorName = (color?: string, theme?: GrafanaTheme): Color | undefined => {
|
||||
export const getColorName = (color?: string, theme?: GrafanaThemeType): Color | undefined => {
|
||||
if (!color) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export const getColorName = (color?: string, theme?: GrafanaTheme): Color | unde
|
||||
return undefined;
|
||||
}
|
||||
if (isHex(color)) {
|
||||
const definition = getColorDefinition(color, theme || GrafanaTheme.Dark);
|
||||
const definition = getColorDefinition(color, theme || GrafanaThemeType.Dark);
|
||||
return definition ? definition.name : undefined;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export const getColorByName = (colorName: string) => {
|
||||
return definition.length > 0 ? definition[0] : undefined;
|
||||
};
|
||||
|
||||
export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): string => {
|
||||
export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaThemeType): string => {
|
||||
if (color.indexOf('rgb') > -1 || isHex(color)) {
|
||||
return color;
|
||||
}
|
||||
@@ -112,14 +112,14 @@ export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): s
|
||||
return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark;
|
||||
};
|
||||
|
||||
export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaTheme) => {
|
||||
export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaThemeType) => {
|
||||
return theme ? color.variants[theme] : color.variants.dark;
|
||||
};
|
||||
|
||||
const buildNamedColorsPalette = () => {
|
||||
const palette = new Map<Hue, ColorDefinition[]>();
|
||||
|
||||
const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true);
|
||||
const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true);
|
||||
const DarkGreen = buildColorDefinition('green', 'dark-green', ['#19730E', '#37872D']);
|
||||
const SemiDarkGreen = buildColorDefinition('green', 'semi-dark-green', ['#37872D', '#56A64B']);
|
||||
const LightGreen = buildColorDefinition('green', 'light-green', ['#73BF69', '#96D98D']);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { GrafanaTheme } from '../../types';
|
||||
|
||||
export const getThemeKnob = (defaultTheme: GrafanaTheme = GrafanaTheme.Dark) => {
|
||||
return select(
|
||||
'Theme',
|
||||
{
|
||||
Default: defaultTheme,
|
||||
Light: GrafanaTheme.Light,
|
||||
Dark: GrafanaTheme.Dark,
|
||||
},
|
||||
defaultTheme
|
||||
);
|
||||
};
|
||||
41
packages/grafana-ui/src/utils/storybook/withTheme.tsx
Normal file
41
packages/grafana-ui/src/utils/storybook/withTheme.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { RenderFunction } from '@storybook/react';
|
||||
import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { getTheme } from '../../themes';
|
||||
import { GrafanaThemeType } from '../../types';
|
||||
|
||||
const ThemableStory: React.FunctionComponent<{}> = ({ children }) => {
|
||||
const themeKnob = select(
|
||||
'Theme',
|
||||
{
|
||||
Light: GrafanaThemeType.Light,
|
||||
Dark: GrafanaThemeType.Dark,
|
||||
},
|
||||
GrafanaThemeType.Dark
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={getTheme(themeKnob)}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
// Temporary solution. When we update to Storybook V5 we will be able to pass data from decorator to story
|
||||
// https://github.com/storybooks/storybook/issues/340#issuecomment-456013702
|
||||
export const renderComponentWithTheme = (component: React.ComponentType<any>, props: any) => {
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => {
|
||||
return React.createElement(component, {
|
||||
...props,
|
||||
theme,
|
||||
});
|
||||
}}
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
export const withTheme = (story: RenderFunction) => <ThemableStory>{story()}</ThemableStory>;
|
||||
@@ -31,11 +31,16 @@ case "$1" in
|
||||
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
||||
fi
|
||||
|
||||
if [ ! -f $PROVISIONING_CFG_DIR ]; then
|
||||
if [ ! -d $PROVISIONING_CFG_DIR ]; then
|
||||
mkdir -p $PROVISIONING_CFG_DIR/dashboards $PROVISIONING_CFG_DIR/datasources
|
||||
cp /usr/share/grafana/conf/provisioning/dashboards/sample.yaml $PROVISIONING_CFG_DIR/dashboards/sample.yaml
|
||||
cp /usr/share/grafana/conf/provisioning/datasources/sample.yaml $PROVISIONING_CFG_DIR/datasources/sample.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d $PROVISIONING_CFG_DIR/notifiers ]; then
|
||||
mkdir -p $PROVISIONING_CFG_DIR/notifiers
|
||||
cp /usr/share/grafana/conf/provisioning/notifiers/sample.yaml $PROVISIONING_CFG_DIR/notifiers/sample.yaml
|
||||
fi
|
||||
|
||||
# configuration files should not be modifiable by grafana user, as this can be a security issue
|
||||
chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
|
||||
|
||||
@@ -39,6 +39,7 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
useradd -r -u $GF_UID -g grafana grafana && \
|
||||
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
|
||||
"$GF_PATHS_PROVISIONING/dashboards" \
|
||||
"$GF_PATHS_PROVISIONING/notifiers" \
|
||||
"$GF_PATHS_LOGS" \
|
||||
"$GF_PATHS_PLUGINS" \
|
||||
"$GF_PATHS_DATA" && \
|
||||
|
||||
@@ -45,11 +45,16 @@ if [ $1 -eq 1 ] ; then
|
||||
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
||||
fi
|
||||
|
||||
if [ ! -f $PROVISIONING_CFG_DIR ]; then
|
||||
if [ ! -d $PROVISIONING_CFG_DIR ]; then
|
||||
mkdir -p $PROVISIONING_CFG_DIR/dashboards $PROVISIONING_CFG_DIR/datasources
|
||||
cp /usr/share/grafana/conf/provisioning/dashboards/sample.yaml $PROVISIONING_CFG_DIR/dashboards/sample.yaml
|
||||
cp /usr/share/grafana/conf/provisioning/datasources/sample.yaml $PROVISIONING_CFG_DIR/datasources/sample.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d $PROVISIONING_CFG_DIR/notifiers ]; then
|
||||
mkdir -p $PROVISIONING_CFG_DIR/notifiers
|
||||
cp /usr/share/grafana/conf/provisioning/notifiers/sample.yaml $PROVISIONING_CFG_DIR/notifiers/sample.yaml
|
||||
fi
|
||||
|
||||
# Set user permissions on /var/log/grafana, /var/lib/grafana
|
||||
mkdir -p /var/log/grafana /var/lib/grafana
|
||||
|
||||
@@ -210,6 +210,65 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
|
||||
return Success("Annotation updated")
|
||||
}
|
||||
|
||||
func PatchAnnotation(c *m.ReqContext, cmd dtos.PatchAnnotationsCmd) Response {
|
||||
annotationID := c.ParamsInt64(":annotationId")
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
|
||||
if resp := canSave(c, repo, annotationID); resp != nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId})
|
||||
|
||||
if err != nil || len(items) == 0 {
|
||||
return Error(404, "Could not find annotation to update", err)
|
||||
}
|
||||
|
||||
existing := annotations.Item{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Id: annotationID,
|
||||
Epoch: items[0].Time,
|
||||
Text: items[0].Text,
|
||||
Tags: items[0].Tags,
|
||||
RegionId: items[0].RegionId,
|
||||
}
|
||||
|
||||
if cmd.Tags != nil {
|
||||
existing.Tags = cmd.Tags
|
||||
}
|
||||
|
||||
if cmd.Text != "" && cmd.Text != existing.Text {
|
||||
existing.Text = cmd.Text
|
||||
}
|
||||
|
||||
if cmd.Time > 0 && cmd.Time != existing.Epoch {
|
||||
existing.Epoch = cmd.Time
|
||||
}
|
||||
|
||||
if err := repo.Update(&existing); err != nil {
|
||||
return Error(500, "Failed to update annotation", err)
|
||||
}
|
||||
|
||||
// Update region end time if provided
|
||||
if existing.RegionId != 0 && cmd.TimeEnd > 0 {
|
||||
itemRight := existing
|
||||
itemRight.RegionId = existing.Id
|
||||
itemRight.Epoch = cmd.TimeEnd
|
||||
|
||||
// We don't know id of region right event, so set it to 0 and find then using query like
|
||||
// ... WHERE region_id = <item.RegionId> AND id != <item.RegionId> ...
|
||||
itemRight.Id = 0
|
||||
|
||||
if err := repo.Update(&itemRight); err != nil {
|
||||
return Error(500, "Failed to update annotation for region end time", err)
|
||||
}
|
||||
}
|
||||
|
||||
return Success("Annotation patched")
|
||||
}
|
||||
|
||||
func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
|
||||
repo := annotations.GetRepository()
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
IsRegion: false,
|
||||
}
|
||||
|
||||
patchCmd := dtos.PatchAnnotationsCmd{
|
||||
Time: 1000,
|
||||
Text: "annotation text",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
}
|
||||
|
||||
Convey("When user is an Org Viewer", func() {
|
||||
role := m.ROLE_VIEWER
|
||||
Convey("Should not be allowed to save an annotation", func() {
|
||||
@@ -40,6 +46,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteAnnotationByID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
@@ -67,6 +78,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteAnnotationByID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
@@ -100,6 +116,13 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
Id: 1,
|
||||
}
|
||||
|
||||
patchCmd := dtos.PatchAnnotationsCmd{
|
||||
Time: 8000,
|
||||
Text: "annotation text 50",
|
||||
Tags: []string{"foo", "bar"},
|
||||
Id: 1,
|
||||
}
|
||||
|
||||
deleteCmd := dtos.DeleteAnnotationsCmd{
|
||||
DashboardId: 1,
|
||||
PanelId: 1,
|
||||
@@ -136,6 +159,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteAnnotationByID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
@@ -163,6 +191,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteAnnotationByID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
@@ -189,6 +222,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
deleteAnnotationsScenario("When calling POST on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
@@ -264,6 +303,29 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m.
|
||||
})
|
||||
}
|
||||
|
||||
func patchAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := setupScenarioContext(url)
|
||||
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
sc.context.OrgId = TestOrgID
|
||||
sc.context.OrgRole = role
|
||||
|
||||
return PatchAnnotation(c, cmd)
|
||||
})
|
||||
|
||||
fakeAnnoRepo = &fakeAnnotationsRepo{}
|
||||
annotations.SetRepository(fakeAnnoRepo)
|
||||
|
||||
sc.m.Patch(routePattern, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func deleteAnnotationsScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
@@ -108,8 +108,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
|
||||
r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
|
||||
|
||||
// api renew session based on remember cookie
|
||||
r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing)
|
||||
// api renew session based on cookie
|
||||
r.Get("/api/login/ping", quota("session"), Wrap(hs.LoginAPIPing))
|
||||
|
||||
// authed api
|
||||
r.Group("/api", func(apiRoute routing.RouteRegister) {
|
||||
@@ -354,6 +354,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), Wrap(PostAnnotation))
|
||||
annotationsRoute.Delete("/:annotationId", Wrap(DeleteAnnotationByID))
|
||||
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), Wrap(UpdateAnnotation))
|
||||
annotationsRoute.Patch("/:annotationId", bind(dtos.PatchAnnotationsCmd{}), Wrap(PatchAnnotation))
|
||||
annotationsRoute.Delete("/region/:regionId", Wrap(DeleteAnnotationRegion))
|
||||
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), Wrap(PostGraphiteAnnotation))
|
||||
})
|
||||
|
||||
@@ -94,14 +94,13 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
m *macaron.Macaron
|
||||
context *m.ReqContext
|
||||
resp *httptest.ResponseRecorder
|
||||
handlerFunc handlerFunc
|
||||
defaultHandler macaron.Handler
|
||||
req *http.Request
|
||||
url string
|
||||
userAuthTokenService *fakeUserAuthTokenService
|
||||
m *macaron.Macaron
|
||||
context *m.ReqContext
|
||||
resp *httptest.ResponseRecorder
|
||||
handlerFunc handlerFunc
|
||||
defaultHandler macaron.Handler
|
||||
req *http.Request
|
||||
url string
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) exec() {
|
||||
@@ -123,30 +122,7 @@ func setupScenarioContext(url string) *scenarioContext {
|
||||
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||
}))
|
||||
|
||||
sc.userAuthTokenService = newFakeUserAuthTokenService()
|
||||
sc.m.Use(middleware.GetContextHandler(sc.userAuthTokenService))
|
||||
sc.m.Use(middleware.GetContextHandler(nil))
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
type fakeUserAuthTokenService struct {
|
||||
initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
|
||||
}
|
||||
|
||||
func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
|
||||
return &fakeUserAuthTokenService{
|
||||
initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
|
||||
return s.initContextWithTokenProvider(ctx, orgID)
|
||||
}
|
||||
|
||||
func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}
|
||||
|
||||
@@ -22,6 +22,14 @@ type UpdateAnnotationsCmd struct {
|
||||
TimeEnd int64 `json:"timeEnd"`
|
||||
}
|
||||
|
||||
type PatchAnnotationsCmd struct {
|
||||
Id int64 `json:"id"`
|
||||
Time int64 `json:"time"`
|
||||
Text string `json:"text"`
|
||||
Tags []string `json:"tags"`
|
||||
TimeEnd int64 `json:"timeEnd"`
|
||||
}
|
||||
|
||||
type DeleteAnnotationsCmd struct {
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
|
||||
@@ -5,6 +5,7 @@ type PlaylistDashboard struct {
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Uri string `json:"uri"`
|
||||
Url string `json:"url"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/cache"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
@@ -48,14 +47,14 @@ type HTTPServer struct {
|
||||
streamManager *live.StreamManager
|
||||
httpSrv *http.Server
|
||||
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
CacheService *cache.CacheService `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
AuthTokenService auth.UserAuthTokenService `inject:""`
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
CacheService *cache.CacheService `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
AuthTokenService models.UserTokenService `inject:""`
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Init() error {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -126,17 +127,23 @@ func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response
|
||||
|
||||
func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
|
||||
if user == nil {
|
||||
hs.log.Error("User login with nil user")
|
||||
hs.log.Error("user login with nil user")
|
||||
}
|
||||
|
||||
err := hs.AuthTokenService.UserAuthenticatedHook(user, c)
|
||||
userToken, err := hs.AuthTokenService.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent())
|
||||
if err != nil {
|
||||
hs.log.Error("User auth hook failed", "error", err)
|
||||
hs.log.Error("failed to create auth token", "error", err)
|
||||
}
|
||||
|
||||
middleware.WriteSessionCookie(c, userToken.UnhashedToken, hs.Cfg.LoginMaxLifetimeDays)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Logout(c *m.ReqContext) {
|
||||
hs.AuthTokenService.UserSignedOutHook(c)
|
||||
if err := hs.AuthTokenService.RevokeToken(c.UserToken); err != nil && err != m.ErrUserTokenNotFound {
|
||||
hs.log.Error("failed to revoke auth token", "error", err)
|
||||
}
|
||||
|
||||
middleware.WriteSessionCookie(c, "", -1)
|
||||
|
||||
if setting.SignoutRedirectUrl != "" {
|
||||
c.Redirect(setting.SignoutRedirectUrl)
|
||||
@@ -176,7 +183,8 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *m.ReqContext, cookieName string
|
||||
Value: hex.EncodeToString(encryptedError),
|
||||
HttpOnly: true,
|
||||
Path: setting.AppSubUrl + "/",
|
||||
Secure: hs.Cfg.SecurityHTTPSCookies,
|
||||
Secure: hs.Cfg.CookieSecure,
|
||||
SameSite: hs.Cfg.CookieSameSite,
|
||||
})
|
||||
|
||||
return nil
|
||||
|
||||
@@ -214,7 +214,8 @@ func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value stri
|
||||
Value: value,
|
||||
HttpOnly: true,
|
||||
Path: setting.AppSubUrl + "/",
|
||||
Secure: hs.Cfg.SecurityHTTPSCookies,
|
||||
Secure: hs.Cfg.CookieSecure,
|
||||
SameSite: hs.Cfg.CookieSameSite,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ func populateDashboardsByID(dashboardByIDs []int64, dashboardIDOrder map[int64]i
|
||||
Slug: item.Slug,
|
||||
Title: item.Title,
|
||||
Uri: "db/" + item.Slug,
|
||||
Url: m.GetDashboardUrl(item.Uid, item.Slug),
|
||||
Order: dashboardIDOrder[item.Id],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx
|
||||
|
||||
func newHTTPClient() httpClient {
|
||||
return &http.Client{
|
||||
Timeout: time.Duration(setting.DataProxyTimeout) * time.Second,
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/elasticsearch"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
_ "github.com/grafana/grafana/pkg/metrics"
|
||||
_ "github.com/grafana/grafana/pkg/plugins"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting"
|
||||
_ "github.com/grafana/grafana/pkg/services/auth"
|
||||
_ "github.com/grafana/grafana/pkg/services/cleanup"
|
||||
_ "github.com/grafana/grafana/pkg/services/notifications"
|
||||
_ "github.com/grafana/grafana/pkg/services/provisioning"
|
||||
|
||||
54
pkg/infra/usagestats/service.go
Normal file
54
pkg/infra/usagestats/service.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package usagestats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var metricsLogger log.Logger = log.New("metrics")
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&UsageStatsService{})
|
||||
}
|
||||
|
||||
type UsageStatsService struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
SQLStore *sqlstore.SqlStore `inject:""`
|
||||
|
||||
oauthProviders map[string]bool
|
||||
}
|
||||
|
||||
func (uss *UsageStatsService) Init() error {
|
||||
|
||||
uss.oauthProviders = social.GetOAuthProviders(uss.Cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uss *UsageStatsService) Run(ctx context.Context) error {
|
||||
uss.updateTotalStats()
|
||||
|
||||
onceEveryDayTick := time.NewTicker(time.Hour * 24)
|
||||
everyMinuteTicker := time.NewTicker(time.Minute)
|
||||
defer onceEveryDayTick.Stop()
|
||||
defer everyMinuteTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-onceEveryDayTick.C:
|
||||
uss.sendUsageStats(uss.oauthProviders)
|
||||
case <-everyMinuteTicker.C:
|
||||
uss.updateTotalStats()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
177
pkg/infra/usagestats/usage_stats.go
Normal file
177
pkg/infra/usagestats/usage_stats.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package usagestats
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
|
||||
|
||||
func (uss *UsageStatsService) sendUsageStats(oauthProviders map[string]bool) {
|
||||
if !setting.ReportingEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
metricsLogger.Debug(fmt.Sprintf("Sending anonymous usage stats to %s", usageStatsURL))
|
||||
|
||||
version := strings.Replace(setting.BuildVersion, ".", "_", -1)
|
||||
|
||||
metrics := map[string]interface{}{}
|
||||
report := map[string]interface{}{
|
||||
"version": version,
|
||||
"metrics": metrics,
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"edition": getEdition(),
|
||||
"packaging": setting.Packaging,
|
||||
}
|
||||
|
||||
statsQuery := models.GetSystemStatsQuery{}
|
||||
if err := uss.Bus.Dispatch(&statsQuery); err != nil {
|
||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
|
||||
metrics["stats.users.count"] = statsQuery.Result.Users
|
||||
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
|
||||
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
|
||||
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
|
||||
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
|
||||
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
|
||||
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
|
||||
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
|
||||
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
|
||||
metrics["stats.stars.count"] = statsQuery.Result.Stars
|
||||
metrics["stats.folders.count"] = statsQuery.Result.Folders
|
||||
metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
|
||||
metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
|
||||
metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
|
||||
metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
|
||||
metrics["stats.teams.count"] = statsQuery.Result.Teams
|
||||
metrics["stats.total_auth_token.count"] = statsQuery.Result.AuthTokens
|
||||
|
||||
userCount := statsQuery.Result.Users
|
||||
avgAuthTokensPerUser := statsQuery.Result.AuthTokens
|
||||
if userCount != 0 {
|
||||
avgAuthTokensPerUser = avgAuthTokensPerUser / userCount
|
||||
}
|
||||
|
||||
metrics["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser
|
||||
|
||||
dsStats := models.GetDataSourceStatsQuery{}
|
||||
if err := uss.Bus.Dispatch(&dsStats); err != nil {
|
||||
metricsLogger.Error("Failed to get datasource stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// send counters for each data source
|
||||
// but ignore any custom data sources
|
||||
// as sending that name could be sensitive information
|
||||
dsOtherCount := 0
|
||||
for _, dsStat := range dsStats.Result {
|
||||
if models.IsKnownDataSourcePlugin(dsStat.Type) {
|
||||
metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
|
||||
} else {
|
||||
dsOtherCount += dsStat.Count
|
||||
}
|
||||
}
|
||||
metrics["stats.ds.other.count"] = dsOtherCount
|
||||
|
||||
metrics["stats.packaging."+setting.Packaging+".count"] = 1
|
||||
|
||||
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
|
||||
if err := uss.Bus.Dispatch(&dsAccessStats); err != nil {
|
||||
metricsLogger.Error("Failed to get datasource access stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// send access counters for each data source
|
||||
// but ignore any custom data sources
|
||||
// as sending that name could be sensitive information
|
||||
dsAccessOtherCount := make(map[string]int64)
|
||||
for _, dsAccessStat := range dsAccessStats.Result {
|
||||
if dsAccessStat.Access == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
access := strings.ToLower(dsAccessStat.Access)
|
||||
|
||||
if models.IsKnownDataSourcePlugin(dsAccessStat.Type) {
|
||||
metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
|
||||
} else {
|
||||
old := dsAccessOtherCount[access]
|
||||
dsAccessOtherCount[access] = old + dsAccessStat.Count
|
||||
}
|
||||
}
|
||||
|
||||
for access, count := range dsAccessOtherCount {
|
||||
metrics["stats.ds_access.other."+access+".count"] = count
|
||||
}
|
||||
|
||||
anStats := models.GetAlertNotifierUsageStatsQuery{}
|
||||
if err := uss.Bus.Dispatch(&anStats); err != nil {
|
||||
metricsLogger.Error("Failed to get alert notification stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stats := range anStats.Result {
|
||||
metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
|
||||
}
|
||||
|
||||
authTypes := map[string]bool{}
|
||||
authTypes["anonymous"] = setting.AnonymousEnabled
|
||||
authTypes["basic_auth"] = setting.BasicAuthEnabled
|
||||
authTypes["ldap"] = setting.LdapEnabled
|
||||
authTypes["auth_proxy"] = setting.AuthProxyEnabled
|
||||
|
||||
for provider, enabled := range oauthProviders {
|
||||
authTypes["oauth_"+provider] = enabled
|
||||
}
|
||||
|
||||
for authType, enabled := range authTypes {
|
||||
enabledValue := 0
|
||||
if enabled {
|
||||
enabledValue = 1
|
||||
}
|
||||
metrics["stats.auth_enabled."+authType+".count"] = enabledValue
|
||||
}
|
||||
|
||||
out, _ := json.MarshalIndent(report, "", " ")
|
||||
data := bytes.NewBuffer(out)
|
||||
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
go client.Post(usageStatsURL, "application/json", data)
|
||||
}
|
||||
|
||||
func (uss *UsageStatsService) updateTotalStats() {
|
||||
statsQuery := models.GetSystemStatsQuery{}
|
||||
if err := uss.Bus.Dispatch(&statsQuery); err != nil {
|
||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
metrics.M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
|
||||
metrics.M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
|
||||
metrics.M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
|
||||
metrics.M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
|
||||
metrics.M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
|
||||
}
|
||||
|
||||
func getEdition() string {
|
||||
if setting.IsEnterprise {
|
||||
return "enterprise"
|
||||
} else {
|
||||
return "oss"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package metrics
|
||||
package usagestats
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -15,14 +15,21 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestMetrics(t *testing.T) {
|
||||
Convey("Test send usage stats", t, func() {
|
||||
uss := &UsageStatsService{
|
||||
Bus: bus.New(),
|
||||
SQLStore: sqlstore.InitTestDB(t),
|
||||
}
|
||||
|
||||
var getSystemStatsQuery *models.GetSystemStatsQuery
|
||||
bus.AddHandler("test", func(query *models.GetSystemStatsQuery) error {
|
||||
uss.Bus.AddHandler(func(query *models.GetSystemStatsQuery) error {
|
||||
|
||||
query.Result = &models.SystemStats{
|
||||
Dashboards: 1,
|
||||
Datasources: 2,
|
||||
@@ -38,13 +45,14 @@ func TestMetrics(t *testing.T) {
|
||||
ProvisionedDashboards: 12,
|
||||
Snapshots: 13,
|
||||
Teams: 14,
|
||||
AuthTokens: 15,
|
||||
}
|
||||
getSystemStatsQuery = query
|
||||
return nil
|
||||
})
|
||||
|
||||
var getDataSourceStatsQuery *models.GetDataSourceStatsQuery
|
||||
bus.AddHandler("test", func(query *models.GetDataSourceStatsQuery) error {
|
||||
uss.Bus.AddHandler(func(query *models.GetDataSourceStatsQuery) error {
|
||||
query.Result = []*models.DataSourceStats{
|
||||
{
|
||||
Type: models.DS_ES,
|
||||
@@ -68,7 +76,7 @@ func TestMetrics(t *testing.T) {
|
||||
})
|
||||
|
||||
var getDataSourceAccessStatsQuery *models.GetDataSourceAccessStatsQuery
|
||||
bus.AddHandler("test", func(query *models.GetDataSourceAccessStatsQuery) error {
|
||||
uss.Bus.AddHandler(func(query *models.GetDataSourceAccessStatsQuery) error {
|
||||
query.Result = []*models.DataSourceAccessStats{
|
||||
{
|
||||
Type: models.DS_ES,
|
||||
@@ -116,7 +124,7 @@ func TestMetrics(t *testing.T) {
|
||||
})
|
||||
|
||||
var getAlertNotifierUsageStatsQuery *models.GetAlertNotifierUsageStatsQuery
|
||||
bus.AddHandler("test", func(query *models.GetAlertNotifierUsageStatsQuery) error {
|
||||
uss.Bus.AddHandler(func(query *models.GetAlertNotifierUsageStatsQuery) error {
|
||||
query.Result = []*models.NotifierUsageStats{
|
||||
{
|
||||
Type: "slack",
|
||||
@@ -155,11 +163,11 @@ func TestMetrics(t *testing.T) {
|
||||
"grafana_com": true,
|
||||
}
|
||||
|
||||
sendUsageStats(oauthProviders)
|
||||
uss.sendUsageStats(oauthProviders)
|
||||
|
||||
Convey("Given reporting not enabled and sending usage stats", func() {
|
||||
setting.ReportingEnabled = false
|
||||
sendUsageStats(oauthProviders)
|
||||
uss.sendUsageStats(oauthProviders)
|
||||
|
||||
Convey("Should not gather stats or call http endpoint", func() {
|
||||
So(getSystemStatsQuery, ShouldBeNil)
|
||||
@@ -179,7 +187,7 @@ func TestMetrics(t *testing.T) {
|
||||
setting.Packaging = "deb"
|
||||
|
||||
wg.Add(1)
|
||||
sendUsageStats(oauthProviders)
|
||||
uss.sendUsageStats(oauthProviders)
|
||||
|
||||
Convey("Should gather stats and call http endpoint", func() {
|
||||
if waitTimeout(&wg, 2*time.Second) {
|
||||
@@ -221,6 +229,8 @@ func TestMetrics(t *testing.T) {
|
||||
So(metrics.Get("stats.provisioned_dashboards.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.ProvisionedDashboards)
|
||||
So(metrics.Get("stats.snapshots.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Snapshots)
|
||||
So(metrics.Get("stats.teams.count").MustInt(), ShouldEqual, getSystemStatsQuery.Result.Teams)
|
||||
So(metrics.Get("stats.total_auth_token.count").MustInt64(), ShouldEqual, 15)
|
||||
So(metrics.Get("stats.avg_auth_token_per_user.count").MustInt64(), ShouldEqual, 5)
|
||||
|
||||
So(metrics.Get("stats.ds."+models.DS_ES+".count").MustInt(), ShouldEqual, 9)
|
||||
So(metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt(), ShouldEqual, 10)
|
||||
@@ -246,6 +256,7 @@ func TestMetrics(t *testing.T) {
|
||||
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
|
||||
|
||||
So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
@@ -273,23 +273,35 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendIfNotEmpty(slice []string, values ...string) []string {
|
||||
for _, v := range values {
|
||||
if v != "" {
|
||||
slice = append(slice, v)
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
var searchResult *ldap.SearchResult
|
||||
var err error
|
||||
|
||||
for _, searchBase := range a.server.SearchBaseDNs {
|
||||
attributes := make([]string, 0)
|
||||
inputs := a.server.Attr
|
||||
attributes = appendIfNotEmpty(attributes,
|
||||
inputs.Username,
|
||||
inputs.Surname,
|
||||
inputs.Email,
|
||||
inputs.Name,
|
||||
inputs.MemberOf)
|
||||
|
||||
searchReq := ldap.SearchRequest{
|
||||
BaseDN: searchBase,
|
||||
Scope: ldap.ScopeWholeSubtree,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
Attributes: []string{
|
||||
a.server.Attr.Username,
|
||||
a.server.Attr.Surname,
|
||||
a.server.Attr.Email,
|
||||
a.server.Attr.Name,
|
||||
a.server.Attr.MemberOf,
|
||||
},
|
||||
Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1),
|
||||
Attributes: attributes,
|
||||
Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1),
|
||||
}
|
||||
|
||||
a.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq))
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/ldap.v3"
|
||||
@@ -322,11 +323,51 @@ func TestLdapAuther(t *testing.T) {
|
||||
So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When searching for a user and not all five attributes are mapped", t, func() {
|
||||
mockLdapConnection := &mockLdapConn{}
|
||||
entry := ldap.Entry{
|
||||
DN: "dn", Attributes: []*ldap.EntryAttribute{
|
||||
{Name: "username", Values: []string{"roelgerrits"}},
|
||||
{Name: "surname", Values: []string{"Gerrits"}},
|
||||
{Name: "email", Values: []string{"roel@test.com"}},
|
||||
{Name: "name", Values: []string{"Roel"}},
|
||||
{Name: "memberof", Values: []string{"admins"}},
|
||||
}}
|
||||
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
|
||||
mockLdapConnection.setSearchResult(&result)
|
||||
|
||||
// Set up attribute map without surname and email
|
||||
ldapAuther := &ldapAuther{
|
||||
server: &LdapServerConf{
|
||||
Attr: LdapAttributeMap{
|
||||
Username: "username",
|
||||
Name: "name",
|
||||
MemberOf: "memberof",
|
||||
},
|
||||
SearchBaseDNs: []string{"BaseDNHere"},
|
||||
},
|
||||
conn: mockLdapConnection,
|
||||
log: log.New("test-logger"),
|
||||
}
|
||||
|
||||
searchResult, err := ldapAuther.searchForUser("roelgerrits")
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(searchResult, ShouldNotBeNil)
|
||||
|
||||
// User should be searched in ldap
|
||||
So(mockLdapConnection.searchCalled, ShouldBeTrue)
|
||||
|
||||
// No empty attributes should be added to the search request
|
||||
So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3)
|
||||
})
|
||||
}
|
||||
|
||||
type mockLdapConn struct {
|
||||
result *ldap.SearchResult
|
||||
searchCalled bool
|
||||
result *ldap.SearchResult
|
||||
searchCalled bool
|
||||
searchAttributes []string
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Bind(username, password string) error {
|
||||
@@ -339,8 +380,9 @@ func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) {
|
||||
c.result = result
|
||||
}
|
||||
|
||||
func (c *mockLdapConn) Search(*ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
c.searchCalled = true
|
||||
c.searchAttributes = sr.Attributes
|
||||
return c.result, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -68,23 +61,6 @@ var (
|
||||
grafanaBuildVersion *prometheus.GaugeVec
|
||||
)
|
||||
|
||||
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
|
||||
counter := prometheus.NewCounterVec(opts, labels)
|
||||
|
||||
for _, label := range labelValues {
|
||||
counter.WithLabelValues(label).Add(0)
|
||||
}
|
||||
|
||||
return counter
|
||||
}
|
||||
|
||||
func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
|
||||
counter := prometheus.NewCounter(opts)
|
||||
counter.Add(0)
|
||||
|
||||
return counter
|
||||
}
|
||||
|
||||
func init() {
|
||||
M_Instance_Start = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Name: "instance_start_total",
|
||||
@@ -308,7 +284,7 @@ func init() {
|
||||
Name: "build_info",
|
||||
Help: "A metric with a constant '1' value labeled by version, revision, branch, and goversion from which Grafana was built.",
|
||||
Namespace: exporterName,
|
||||
}, []string{"version", "revision", "branch", "goversion"})
|
||||
}, []string{"version", "revision", "branch", "goversion", "edition"})
|
||||
}
|
||||
|
||||
// SetBuildInformation sets the build information for this binary
|
||||
@@ -317,8 +293,13 @@ func SetBuildInformation(version, revision, branch string) {
|
||||
// Once this have been released for some time we should be able to remote `M_Grafana_Version`
|
||||
// The reason we added a new one is that its common practice in the prometheus community
|
||||
// to name this metric `*_build_info` so its easy to do aggregation on all programs.
|
||||
edition := "oss"
|
||||
if setting.IsEnterprise {
|
||||
edition = "enterprise"
|
||||
}
|
||||
|
||||
M_Grafana_Version.WithLabelValues(version).Set(1)
|
||||
grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version()).Set(1)
|
||||
grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version(), edition).Set(1)
|
||||
}
|
||||
|
||||
func initMetricVars() {
|
||||
@@ -362,154 +343,19 @@ func initMetricVars() {
|
||||
|
||||
}
|
||||
|
||||
func updateTotalStats() {
|
||||
statsQuery := models.GetSystemStatsQuery{}
|
||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||
return
|
||||
func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec {
|
||||
counter := prometheus.NewCounterVec(opts, labels)
|
||||
|
||||
for _, label := range labelValues {
|
||||
counter.WithLabelValues(label).Add(0)
|
||||
}
|
||||
|
||||
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
|
||||
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
|
||||
M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
|
||||
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
|
||||
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
|
||||
return counter
|
||||
}
|
||||
|
||||
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
|
||||
func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter {
|
||||
counter := prometheus.NewCounter(opts)
|
||||
counter.Add(0)
|
||||
|
||||
func getEdition() string {
|
||||
if setting.IsEnterprise {
|
||||
return "enterprise"
|
||||
} else {
|
||||
return "oss"
|
||||
}
|
||||
}
|
||||
|
||||
func sendUsageStats(oauthProviders map[string]bool) {
|
||||
if !setting.ReportingEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
metricsLogger.Debug("Sending anonymous usage stats to stats.grafana.org")
|
||||
|
||||
version := strings.Replace(setting.BuildVersion, ".", "_", -1)
|
||||
|
||||
metrics := map[string]interface{}{}
|
||||
report := map[string]interface{}{
|
||||
"version": version,
|
||||
"metrics": metrics,
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"edition": getEdition(),
|
||||
"packaging": setting.Packaging,
|
||||
}
|
||||
|
||||
statsQuery := models.GetSystemStatsQuery{}
|
||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
|
||||
metrics["stats.users.count"] = statsQuery.Result.Users
|
||||
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
|
||||
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
|
||||
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
|
||||
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
|
||||
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
|
||||
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
|
||||
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
|
||||
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
|
||||
metrics["stats.stars.count"] = statsQuery.Result.Stars
|
||||
metrics["stats.folders.count"] = statsQuery.Result.Folders
|
||||
metrics["stats.dashboard_permissions.count"] = statsQuery.Result.DashboardPermissions
|
||||
metrics["stats.folder_permissions.count"] = statsQuery.Result.FolderPermissions
|
||||
metrics["stats.provisioned_dashboards.count"] = statsQuery.Result.ProvisionedDashboards
|
||||
metrics["stats.snapshots.count"] = statsQuery.Result.Snapshots
|
||||
metrics["stats.teams.count"] = statsQuery.Result.Teams
|
||||
|
||||
dsStats := models.GetDataSourceStatsQuery{}
|
||||
if err := bus.Dispatch(&dsStats); err != nil {
|
||||
metricsLogger.Error("Failed to get datasource stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// send counters for each data source
|
||||
// but ignore any custom data sources
|
||||
// as sending that name could be sensitive information
|
||||
dsOtherCount := 0
|
||||
for _, dsStat := range dsStats.Result {
|
||||
if models.IsKnownDataSourcePlugin(dsStat.Type) {
|
||||
metrics["stats.ds."+dsStat.Type+".count"] = dsStat.Count
|
||||
} else {
|
||||
dsOtherCount += dsStat.Count
|
||||
}
|
||||
}
|
||||
metrics["stats.ds.other.count"] = dsOtherCount
|
||||
|
||||
metrics["stats.packaging."+setting.Packaging+".count"] = 1
|
||||
|
||||
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
|
||||
if err := bus.Dispatch(&dsAccessStats); err != nil {
|
||||
metricsLogger.Error("Failed to get datasource access stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// send access counters for each data source
|
||||
// but ignore any custom data sources
|
||||
// as sending that name could be sensitive information
|
||||
dsAccessOtherCount := make(map[string]int64)
|
||||
for _, dsAccessStat := range dsAccessStats.Result {
|
||||
if dsAccessStat.Access == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
access := strings.ToLower(dsAccessStat.Access)
|
||||
|
||||
if models.IsKnownDataSourcePlugin(dsAccessStat.Type) {
|
||||
metrics["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
|
||||
} else {
|
||||
old := dsAccessOtherCount[access]
|
||||
dsAccessOtherCount[access] = old + dsAccessStat.Count
|
||||
}
|
||||
}
|
||||
|
||||
for access, count := range dsAccessOtherCount {
|
||||
metrics["stats.ds_access.other."+access+".count"] = count
|
||||
}
|
||||
|
||||
anStats := models.GetAlertNotifierUsageStatsQuery{}
|
||||
if err := bus.Dispatch(&anStats); err != nil {
|
||||
metricsLogger.Error("Failed to get alert notification stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stats := range anStats.Result {
|
||||
metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
|
||||
}
|
||||
|
||||
authTypes := map[string]bool{}
|
||||
authTypes["anonymous"] = setting.AnonymousEnabled
|
||||
authTypes["basic_auth"] = setting.BasicAuthEnabled
|
||||
authTypes["ldap"] = setting.LdapEnabled
|
||||
authTypes["auth_proxy"] = setting.AuthProxyEnabled
|
||||
|
||||
for provider, enabled := range oauthProviders {
|
||||
authTypes["oauth_"+provider] = enabled
|
||||
}
|
||||
|
||||
for authType, enabled := range authTypes {
|
||||
enabledValue := 0
|
||||
if enabled {
|
||||
enabledValue = 1
|
||||
}
|
||||
metrics["stats.auth_enabled."+authType+".count"] = enabledValue
|
||||
}
|
||||
|
||||
out, _ := json.MarshalIndent(report, "", " ")
|
||||
data := bytes.NewBuffer(out)
|
||||
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
go client.Post(usageStatsURL, "application/json", data)
|
||||
return counter
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
||||
@@ -30,7 +29,6 @@ type InternalMetricsService struct {
|
||||
|
||||
intervalSeconds int64
|
||||
graphiteCfg *graphitebridge.Config
|
||||
oauthProviders map[string]bool
|
||||
}
|
||||
|
||||
func (im *InternalMetricsService) Init() error {
|
||||
@@ -50,22 +48,6 @@ func (im *InternalMetricsService) Run(ctx context.Context) error {
|
||||
|
||||
M_Instance_Start.Inc()
|
||||
|
||||
// set the total stats gauges before we publishing metrics
|
||||
updateTotalStats()
|
||||
|
||||
onceEveryDayTick := time.NewTicker(time.Hour * 24)
|
||||
everyMinuteTicker := time.NewTicker(time.Minute)
|
||||
defer onceEveryDayTick.Stop()
|
||||
defer everyMinuteTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-onceEveryDayTick.C:
|
||||
sendUsageStats(im.oauthProviders)
|
||||
case <-everyMinuteTicker.C:
|
||||
updateTotalStats()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
|
||||
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -24,8 +22,6 @@ func (im *InternalMetricsService) readSettings() error {
|
||||
return fmt.Errorf("Unable to parse metrics graphite section, %v", err)
|
||||
}
|
||||
|
||||
im.oauthProviders = social.GetOAuthProviders(im.Cfg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -21,7 +23,7 @@ var (
|
||||
ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN)
|
||||
)
|
||||
|
||||
func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler {
|
||||
func GetContextHandler(ats m.UserTokenService) macaron.Handler {
|
||||
return func(c *macaron.Context) {
|
||||
ctx := &m.ReqContext{
|
||||
Context: c,
|
||||
@@ -49,7 +51,7 @@ func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler {
|
||||
case initContextWithApiKey(ctx):
|
||||
case initContextWithBasicAuth(ctx, orgId):
|
||||
case initContextWithAuthProxy(ctx, orgId):
|
||||
case ats.InitContextWithToken(ctx, orgId):
|
||||
case initContextWithToken(ats, ctx, orgId):
|
||||
case initContextWithAnonymousUser(ctx):
|
||||
}
|
||||
|
||||
@@ -166,6 +168,69 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext, orgID int64) bool {
|
||||
rawToken := ctx.GetCookie(setting.LoginCookieName)
|
||||
if rawToken == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
token, err := authTokenService.LookupToken(rawToken)
|
||||
if err != nil {
|
||||
ctx.Logger.Error("failed to look up user based on cookie", "error", err)
|
||||
WriteSessionCookie(ctx, "", -1)
|
||||
return false
|
||||
}
|
||||
|
||||
query := m.GetSignedInUserQuery{UserId: token.UserId, OrgId: orgID}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
ctx.Logger.Error("failed to get user with id", "userId", token.UserId, "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.SignedInUser = query.Result
|
||||
ctx.IsSignedIn = true
|
||||
ctx.UserToken = token
|
||||
|
||||
rotated, err := authTokenService.TryRotateToken(token, ctx.RemoteAddr(), ctx.Req.UserAgent())
|
||||
if err != nil {
|
||||
ctx.Logger.Error("failed to rotate token", "error", err)
|
||||
return true
|
||||
}
|
||||
|
||||
if rotated {
|
||||
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetimeDays)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func WriteSessionCookie(ctx *m.ReqContext, value string, maxLifetimeDays int) {
|
||||
if setting.Env == setting.DEV {
|
||||
ctx.Logger.Info("new token", "unhashed token", value)
|
||||
}
|
||||
|
||||
var maxAge int
|
||||
if maxLifetimeDays <= 0 {
|
||||
maxAge = -1
|
||||
} else {
|
||||
maxAgeHours := (time.Duration(setting.LoginMaxLifetimeDays) * 24 * time.Hour) + time.Hour
|
||||
maxAge = int(maxAgeHours.Seconds())
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Del("Set-Cookie")
|
||||
cookie := http.Cookie{
|
||||
Name: setting.LoginCookieName,
|
||||
Value: url.QueryEscape(value),
|
||||
HttpOnly: true,
|
||||
Path: setting.AppSubUrl + "/",
|
||||
Secure: setting.CookieSecure,
|
||||
MaxAge: maxAge,
|
||||
SameSite: setting.CookieSameSite,
|
||||
}
|
||||
|
||||
http.SetCookie(ctx.Resp, &cookie)
|
||||
}
|
||||
|
||||
func AddDefaultResponseHeaders() macaron.Handler {
|
||||
return func(ctx *m.ReqContext) {
|
||||
if ctx.IsApiRequest() && ctx.Req.Method == "GET" {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user