Compare commits

...

63 Commits

Author SHA1 Message Date
gotjosh
8a30e47aa4 release 6.2.3 2019-06-17 12:07:51 +01:00
gotjosh
7c176cfa22 cli: grafana-cli should receive flags from the command line (#17606)
grafana-cli should allow configuration overrides to be received from the command line. e.g.

```
grafana-cli admin reset-password cfg:default.paths.logs=custom/log/directory/
```

Seems like we missed the inclusion of `flag.Parse` as we run the command,  to be able to consume them.

Additionally, it'll be useful for the user to know whenever these are being overriden or not - hence the addition of logging the configuration to be used as we run the command.

(cherry picked from commit 6fbca90269)
2019-06-17 12:07:51 +01:00
Sofia Papagiannaki
053f5f5dcc AuthProxy: Optimistic lock pattern for remote cache Set (#17485)
* Implementation of optimistic lock pattern

Try to insert the remote cache key and handle integrity error

* Remove transaction

Integrity error inside a transaction results in deadlock

* Remove check for existing remote cache key

Is no longer needed since integrity constrain violations are handled

* Add check for integrity constrain violation

Do not update the row if the insert statement fails
for other than an integrity constrain violation

* Handle failing inserts because of deadlocks

If the insert statement fails because of a deadlock
try to update the row

* Add utility function for returning SQL error code

Useful for debugging

* Add logging for failing expired cache key deletion

Do not shallow it completely

* Revert "Add utility function for returning SQL error code"

This reverts commit 8e0b82c79633e7d8bc350823cbbab2ac7a58c0a5.

* Better log for failing deletion of expired cache key

* Add some comments

* Remove check for existing cache key

Attempt to insert the key without checking if it's already there
and handle the error situations

* Do not propagate deadlocks created during update

Most probably somebody else is trying to insert/update
the key at the same time so it is safe enough to ignore it

(cherry picked from commit 7b70e7db2d)
2019-06-17 12:07:51 +01:00
Maxim Ivanov
c533ec7dea OAuth: Fix for wrong user token updated on OAuth refresh in DS proxy (#17541)
(cherry picked from commit 151fe240fc)
2019-06-17 12:07:51 +01:00
Kyle Brandt
22a991ff66 middleware: add security related HTTP(S) response headers (#17522)
* x_xss_protection
  * strict_transport_security (HSTS)
  * x_content_type_options

these are currently defaulted to false (off) until the next minor release.

fixes #17509

(cherry picked from commit 599514ad68)
2019-06-17 12:07:51 +01:00
Kyle Brandt
91feb80187 remote_cache: Fix redis (#17483)
* wip: fix remote cache for redis
connstr parsing and non-negative expires for #17377
TODO: finish parse, check zero case, find out why negative duration in the first place

* finish parse.
Still TODO, find out negative value, and decide if would be better to make database specific entries in the .ini file

* update ini files

* remove accidental uncomment in defaults.ini

* auth_proxy: expiration non-negative so expiration is not in the past

* fix test, revert neg in redis

* review: use errutil

(cherry picked from commit c09fe3c3b4)
2019-06-17 12:07:51 +01:00
Kyle Brandt
9a93382348 auth_proxy: non-negative cache TTL (#17495)
fixes setex error with redis on #17377

(cherry picked from commit 826d33ea37)
2019-06-17 12:07:51 +01:00
Andrej Ocenas
07540dfdce release 6.2.2 2019-06-05 15:04:21 +02:00
Torkel Ödegaard
8b86d5d1ed PluginConfig: Fixed plugin config page navigation when using subpath (#17364)
(cherry picked from commit de7856cf93)
2019-06-05 15:04:21 +02:00
Marcus Efraimsson
f26831c8a9 Explore: Update time range before running queries (#17349)
This makes sure that refresh/update/run query are parsing a
relative time range to get proper epoch time range before
running queries.

Fixes #17322

(cherry picked from commit e951e71843)
2019-06-05 15:04:21 +02:00
Andrej Ocenas
6a32c29b92 Perf: Fix slow dashboards ACL query (#17427)
Fix slow ACL query for dashboards that was used as subquery on multiple places slowing down search and login in instances with many dashboards.

(cherry picked from commit 1c3ad78672)
2019-06-05 15:04:21 +02:00
Marcus Efraimsson
80475fa13a Database: Initialize xorm with an empty schema for postgres (#17357)
xorm introduced some changes in
https://github.com/go-xorm/xorm/pull/824 and
https://github.com/go-xorm/xorm/pull/876 which by default will use
public as the postgres schema and this was a breaking change compared
to before. Grafana has implemented a custom postgres dialect so above
changes wasn't a problem here. However, Grafana's custom database
migration was using xorm dialect to check if the migration table exists
or not.
For those using a custom search_path (schema) in postgres configured on
server, database or user level the migration table check would not find
the migration table since it was looking in public schema due to xorm
changes above. This had the consequence that Grafana's database
migration failed the second time since migration had already run
migrations in another schema.
This change will make xorm use an empty default schema for postgres and
by that mimic the functionality of how it was functioning before
xorm's changes above.
Fixes #16720

Co-Authored-By: Carl Bergquist <carl@grafana.com>
(cherry picked from commit b7a9533476)
2019-06-05 15:04:21 +02:00
Dan Cech
2b400ac38e Security: Prevent csv formula injection attack (#17363)
* mitigate https://www.owasp.org/index.php/CSV_Injection

- prepend csv cell values that begin with -, +, = or @ with '
- trim trailing whitespace from all csv values

* test for csv formula injection mitigation

(cherry picked from commit 5e7537878e)
2019-06-05 15:04:21 +02:00
Joshua Piccari
c3e5e69e73 CloudWatch: Avoid exception while accessing results (#17283)
When accessing the `series` property of query results, if a query is
hidden, an exception is thrown. This is caused by lack of checks to
verify that the query result exists before accessing the `series`
property.

Closes #17112

(cherry picked from commit 5fa5d4bdd5)
2019-06-05 15:04:21 +02:00
Marcus Efraimsson
9e40b07fd7 backport parts of #17065 2019-05-27 14:30:34 +02:00
Torkel Ödegaard
99472f9465 Gauge/BarGauge: font size improvements (#17292)
(cherry picked from commit 5358c5fe6b)
2019-05-27 14:30:34 +02:00
Torkel Ödegaard
5169765db3 Chore: Update jquery to 3.4.1 in grafana ui (#17295)
(cherry picked from commit 3dda812f12)
2019-05-27 14:30:34 +02:00
Andrej Ocenas
7b0efb787b CLI: Add command to migrate all datasources to use encrypted password fields (#17118)
closes: #17107
(cherry picked from commit 151b24b95f)
2019-05-27 14:30:34 +02:00
Marcus Efraimsson
efd14ac7db database: retry transaction if sqlite returns busy error (#17297)
Adds an additional sqlite error code 5 (SQLITE_BUSY) to the
transaction retry handler to add retries when sqlite
returns database is locked error.
More info: https://www.sqlite.org/rescode.html#busy
Backports #17276 for v6.2.x
2019-05-27 14:30:34 +02:00
Marcus Efraimsson
b0647a4537 auth proxy: log any error in middleware (#17296)
Backports #17275 for v6.2.x
2019-05-27 14:30:34 +02:00
Marcus Efraimsson
1f0e94b633 Auth Proxy: Resolve database is locked errors (#17274)
The change was originally made in #16950, but that change
was too big to cherry pick. This change adds a major database
performance improvement since the number of selects to fetch
a cached item is drastically decreased and by that removes
sqlite database is locked errors.

Fixes #17247
2019-05-27 14:30:34 +02:00
Marcus Efraimsson
eda28131ef release v6.2.1 2019-05-27 14:30:34 +02:00
Tim Butler
d385a668a9 Tech: Update jQuery to 3.4.1 (#17290)
Fixes #17289

Special notes for your reviewer:
Updates jQuery to 3.4.1 (from 3.4.0) to fix the jQuery bug: https://blog.jquery.com/2019/05/01/jquery-3-4-1-triggering-focus-events-in-ie-and-finding-root-elements-in-ios-10/
(cherry picked from commit df6a4914c4)
2019-05-27 14:30:34 +02:00
Torkel Ödegaard
ef359424a5 Singlestat: fixes issue with value placement and line wraps (#17249)
Fixes #17237
(cherry picked from commit 6acc7d37da)
2019-05-27 14:30:34 +02:00
Brian Gann
dce4514f7f Build: Fix filter for building msi during release (#17236)
(cherry picked from commit 972df40af8)
2019-05-27 14:30:34 +02:00
Hugo Häggmark
ad7bd8850f Explore: Fixes filtering in Prometheus queries when clicking in Table (#17083)
Fixes: #17071
(cherry picked from commit fdd421e24c)
2019-05-27 14:30:34 +02:00
Torkel Ödegaard
5d16da732f Search: removed old not working search shortcuts (#17226)
(cherry picked from commit e421723992)
2019-05-22 12:28:56 +02:00
Daniel Lee
ee6703fedd azuremonitor: revert to clearing chained dropdowns (#17212)
* azuremonitor: revert to clearing chained dropdowns

After feedback from users, changing back to clearing
dropdowns to the right in the chain. E.g. if the user
changes the subscription dropdown which is first in
the chain then all the dependent dropdowns to the right
should be cleared (reset to default values).

Also, now triggers getting subscriptions every time the
dropdown menu is shown rather than just the first time.
It is apparently common to add subscriptions while
building queries.

(cherry picked from commit 577beebcca)
2019-05-22 12:24:43 +02:00
Torkel Ödegaard
ec8be5bf61 Search: changed how search filter on current folder works (#17219)
(cherry picked from commit a96b522a42)
2019-05-22 12:24:43 +02:00
Torkel Ödegaard
f3c2d03637 codespell: update codespell ignore list 2019-05-21 17:28:57 +02:00
Torkel Ödegaard
eb2b31007d Release: Updated version 2019-05-21 16:58:15 +02:00
Ryan McKinley
98f9dbde95 DataSourceMeta: add an option to get hidden queries (#17124)
* add an option to get hidden queries

* make sure you have meta

* supportsHiddenQueries

* remove spaces

* DataSources: hidden queries flag

(cherry picked from commit 1033f0f905)
2019-05-21 16:46:52 +02:00
Dominik Prokop
7e090ea2a6 Panel: Apply option defaults on panel init and on save model retrieval (#17174)
* Apply panel options defaults on panel init and on save model retrieval

* Remove unnecessary argument, added tests

* Make FieldPropertiesEditor statefull to enable onBlur changes

* Remove unnecessary import

* Post-review updates

Fixes #17154

(cherry picked from commit 73e4178aef)
2019-05-21 16:38:36 +02:00
Torkel Ödegaard
6a02faeeab BarGauge: Fix for negative min values (#17192)
(cherry picked from commit 874039992f)
2019-05-21 15:54:46 +02:00
Daniel Lee
a6a939cfd3 Azuremonitor: multiple subscription support for alerting (#17195)
* fix: azuremonitor adds multi-sub support to alerting

* fix: AzureMonitor missing parameter in metadata func

getMetricMetadata function when called in the query ctrl
was missing a parameter for Subscription Id.

Also, made some tweaks to what happens when a chained
dropdown is changed to not reset all the fields that
are dependent on it.

(cherry picked from commit fa9ffe38d2)
2019-05-21 15:54:46 +02:00
Brian Gann
5a10298bd3 MSI: Generate sha256sum during MSI build process in circleci (#17120)
* build: generate sha256 during msi build

(cherry picked from commit f98095d629)
2019-05-21 15:54:46 +02:00
Torkel Ödegaard
f52f7c101c Release: Improved cherry pick task (#17087)
* Release: Improved cherry pick task

* Minor tweak to formatting

(cherry picked from commit 058f5a1682)
2019-05-21 15:54:10 +02:00
Torkel Ödegaard
7824f66cd3 Explore: fixed cherrypick / merge issue 2019-05-15 13:10:59 +02:00
Torkel Ödegaard
71c1e8a731 Release: Bumped version to v6.2.0-beta2 2019-05-15 12:21:21 +02:00
Carl Bergquist
74bc94bcec Remotecache: Avoid race condition in Set causing error on insert. (#17082)
* remotecache: avoid race condition in set

since set called the database twice without transactions another
operation could insert a value before the first operation completed.
which would raise an error on insert since the data have been inserted
by the other request.

closes #17079

(cherry picked from commit aed3d0d3ad)
2019-05-15 12:14:16 +02:00
Brian Gann
f566da0ff6 Build: Support publishing MSI to grafana.com (#17073)
* add test for msi, and support for publishing msi
* update arch and os in test
* Build: Fixed issues with os naming

(cherry picked from commit d0ea98f6bd)
2019-05-15 12:14:16 +02:00
Torkel Ödegaard
216aff96fd Panels: Fixed alert icon position in panel header (#17070)
(cherry picked from commit 238a929262)
2019-05-15 12:14:15 +02:00
Torkel Ödegaard
9de11c25a6 Gauge: Fix switching orientation issue when switching from BarGauge to Gauge (#17064)
(cherry picked from commit 68ad93f410)
2019-05-15 12:14:15 +02:00
Torkel Ödegaard
599e1030d8 Dashboard: Fixes lazy loading & expanding collapsed rows on mobile (#17055)
* Dashboard: Fixes lazy loading & expanding collapsing rows on mobile

Fixes #16978

* Updated dashboard tags

(cherry picked from commit 74a31bd9b4)
2019-05-15 12:14:15 +02:00
Daniel Lee
d62da61d8a fix: Azure Monitor adds missing closing div tag to query editor (#17057)
(cherry picked from commit 4bc1a66fe4)
2019-05-15 12:14:15 +02:00
Johannes Schill
a2ca973925 Search: Set element height to 100% to avoid Chrome 74's overflow (#17054)
Fixes #16981

(cherry picked from commit ceb21bd653)
2019-05-15 12:14:15 +02:00
Johannes Schill
98da29fd7b Dashboard: Fixes scrolling issues for Edge browser (#17033)
* Fix: Only set scrollTop on CustomScroll element when it's needed and move arrow functions out of the props

* Fix: Update snapshots

* Minor refactoring to reuse same functions when rendering custom scrollbar

Fixes #16796

(cherry picked from commit 1001cd7ac3)
2019-05-15 12:14:15 +02:00
Torkel Ödegaard
888ff61d30 Dashboard: show refresh button in kiosk mode (#17032)
Fixes #16945

(cherry picked from commit 3ce2f3b58d)
2019-05-15 12:14:15 +02:00
Torkel Ödegaard
c1be3adf3b Gauge: tweaks to background color and height usage (#17019)
(cherry picked from commit 597c380ead)
2019-05-15 12:14:15 +02:00
Marcus Efraimsson
ede9d9964d Explore: Fix empty result from datasource should render logs container (#16999)
Make sure to return an empty logs model instead of undefined so that 
the logs container renders an empty graph and log result in Explore.

Fixes #16997

(cherry picked from commit 8eb78ea931)
2019-05-15 12:14:15 +02:00
Hugo Häggmark
5cd69e8d39 Explore: Fixes zoom exception in Loki/Graph (#16991)
Fixes: #16986
(cherry picked from commit d5a35f3631)
2019-05-15 12:14:14 +02:00
Torkel Ödegaard
ceb3672482 Panels: Fixed error panel tooltip (#16993)
Fixes #16989

(cherry picked from commit 5573d28582)
2019-05-15 12:13:29 +02:00
Will Medlar
e519a9d2c4 Docker: Prevent a permission denied error when writing files to the default provisioning directory (#16831)
* build: Grant ownership of provisioning directory to runtime user

(cherry picked from commit 5e44f001fb)
2019-05-15 12:07:59 +02:00
Ryan McKinley
2a3d6604c0 GettingStarted: convert to react panel plugin (#16985)
* getting started

* getting started

(cherry picked from commit f617cd8975)
2019-05-15 11:53:46 +02:00
Marcus Efraimsson
9cf0ea5395 plugins: fix how datemath utils are exposed to plugins (#16976)
Fixes a regression accidentally introduced by #16890 so that datemath 
utils are exposed to plugins in a backward-compatible way.

Fixes #16962

(cherry picked from commit 0c1530c7a8)
2019-05-15 11:53:35 +02:00
Torkel Ödegaard
db37e138bf GettingStarted: Fixes layout issues, fixes #16926 (#16941)
(cherry picked from commit a9e01d8b04)
2019-05-15 11:53:27 +02:00
Torkel Ödegaard
a5a6d43f47 PanelModel: Fix crash after window resize, fixes #16933 (#16942)
(cherry picked from commit f58ab7945b)
2019-05-15 11:52:31 +02:00
Torkel Ödegaard
d9950aa4f1 Singlestat: fixed centering issue for very small panels (#16944)
(cherry picked from commit e97853abc9)
2019-05-15 11:52:23 +02:00
Stephen SORRIAUX
6e9a395063 InfluxDB: Fix HTTP method should default to GET (#16949)
Fixes #16929

(cherry picked from commit e7a9afe983)
2019-05-15 11:52:14 +02:00
Ryan McKinley
7374aafb90 AppPlugin: Menu Edit Url Fix (#16934)
(cherry picked from commit 17ce1273ca)
2019-05-15 11:52:02 +02:00
Ryan McKinley
b54e9880b4 Plugins: update beta notice style (#16928)
(cherry picked from commit b08cf1e7ac)
2019-05-15 11:51:50 +02:00
Brian Gann
09672a287f Plugins: Support templated urls in routes (#16599)
This adds support for using templated/dynamic urls in routes.
* refactor interpolateString into utils and add interpolation support for app plugin routes.
* cleanup and add error check for url parse failure
* add docs for interpolated route urls

Closes #16835

(cherry picked from commit b07d0b1026)
2019-05-15 11:50:34 +02:00
Andrej Ocenas
9d877d670e release 6.2.0-beta1 2019-05-07 16:08:46 +02:00
137 changed files with 5573 additions and 2084 deletions

View File

@@ -81,7 +81,7 @@ jobs:
- run:
# Important: all words have to be in lowercase, and separated by "\n".
name: exclude known exceptions
command: 'echo -e "unknwon" > words_to_ignore.txt'
command: 'echo -e "unknwon\nreferer\nerrorstring" > words_to_ignore.txt'
- run:
name: check documentation spelling errors
command: 'codespell -I ./words_to_ignore.txt docs/'
@@ -566,6 +566,7 @@ jobs:
root: .
paths:
- dist/grafana-*.msi
- dist/grafana-*.msi.sha256
store-build-artifacts:
docker:
@@ -700,7 +701,7 @@ workflows:
- backend-lint
- mysql-integration-test
- postgres-integration-test
filters: *filter-only-master
filters: *filter-only-release
build-branches-and-prs:
jobs:

View File

@@ -66,8 +66,8 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
"$GF_PATHS_DATA" && \
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
chmod 777 -R "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
COPY --from=0 /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-server /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-cli ./bin/
COPY --from=1 /usr/src/app/public ./public

View File

@@ -113,9 +113,9 @@ type = database
# cache connectionstring options
# database: will use Grafana primary database.
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0`. Only addr is required.
# memcache: 127.0.0.1:11211
connstr =
;connstr =
#################################### Data proxy ###########################
[dataproxy]
@@ -179,6 +179,31 @@ cookie_samesite = lax
# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
allow_embedding = false
# Set to true if you want to enable http strict transport security (HSTS) response header.
# This is only sent when HTTPS is enabled in this configuration.
# HSTS tells browsers that the site should only be accessed using HTTPS.
# The default will change to true in the next minor release, 6.3.
strict_transport_security = false
# Sets how long a browser should cache HSTS. Only applied if strict_transport_security is enabled.
strict_transport_security_max_age_seconds = 86400
# Set to true if to enable HSTS preloading option. Only applied if strict_transport_security is enabled.
strict_transport_security_preload = false
# Set to true if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled.
strict_transport_security_subdomains = false
# Set to true to enable the X-Content-Type-Options response header.
# The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised
# in the Content-Type headers should not be changed and be followed. The default will change to true in the next minor release, 6.3.
x_content_type_options = false
# Set to true to enable the X-XSS-Protection header, which tells browsers to stop pages from loading
# when they detect reflected cross-site scripting (XSS) attacks. The default will change to true in the next minor release, 6.3.
x_xss_protection = false
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options

View File

@@ -109,7 +109,7 @@ log_queries =
# cache connectionstring options
# database: will use Grafana primary database.
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=grafana`
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0`. Only addr is required.
# memcache: 127.0.0.1:11211
;connstr =
@@ -175,6 +175,30 @@ log_queries =
# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
;allow_embedding = false
# Set to true if you want to enable http strict transport security (HSTS) response header.
# This is only sent when HTTPS is enabled in this configuration.
# HSTS tells browsers that the site should only be accessed using HTTPS.
# The default version will change to true in the next minor release, 6.3.
;strict_transport_security = false
# Sets how long a browser should cache HSTS. Only applied if strict_transport_security is enabled.
;strict_transport_security_max_age_seconds = 86400
# Set to true if to enable HSTS preloading option. Only applied if strict_transport_security is enabled.
;strict_transport_security_preload = false
# Set to true if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled.
;strict_transport_security_subdomains = false
# Set to true to enable the X-Content-Type-Options response header.
# The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised
# in the Content-Type headers should not be changed and be followed. The default will change to true in the next minor release, 6.3.
;x_content_type_options = false
# Set to true to enable the X-XSS-Protection header, which tells browsers to stop pages from loading
# when they detect reflected cross-site scripting (XSS) attacks. The default will change to true in the next minor release, 6.3.
;x_xss_protection = false
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options

File diff suppressed because it is too large Load Diff

View File

@@ -48,28 +48,6 @@
"y": 0
},
"headings": false,
"id": 8,
"limit": 1000,
"links": [],
"query": "",
"recent": false,
"search": true,
"starred": false,
"tags": ["panel-demo"],
"timeFrom": null,
"timeShift": null,
"title": "tag: panel-demo",
"type": "dashlist"
},
{
"folderId": null,
"gridPos": {
"h": 13,
"w": 6,
"x": 12,
"y": 0
},
"headings": false,
"id": 2,
"limit": 1000,
"links": [],
@@ -83,6 +61,28 @@
"title": "tag: panel-tests",
"type": "dashlist"
},
{
"folderId": null,
"gridPos": {
"h": 26,
"w": 6,
"x": 12,
"y": 0
},
"headings": false,
"id": 3,
"limit": 1000,
"links": [],
"query": "",
"recent": false,
"search": true,
"starred": false,
"tags": ["gdev", "demo"],
"timeFrom": null,
"timeShift": null,
"title": "tag: dashboard-demo",
"type": "dashlist"
},
{
"folderId": null,
"gridPos": {
@@ -114,28 +114,6 @@
"y": 13
},
"headings": false,
"id": 3,
"limit": 1000,
"links": [],
"query": "",
"recent": false,
"search": true,
"starred": false,
"tags": ["gdev", "demo"],
"timeFrom": null,
"timeShift": null,
"title": "tag: dashboard-demo",
"type": "dashlist"
},
{
"folderId": null,
"gridPos": {
"h": 13,
"w": 6,
"x": 12,
"y": 13
},
"headings": false,
"id": 4,
"limit": 1000,
"links": [],
@@ -146,7 +124,7 @@
"tags": ["templating", "gdev"],
"timeFrom": null,
"timeShift": null,
"title": "tag: templating",
"title": "tag: templating ",
"type": "dashlist"
}
],
@@ -167,5 +145,5 @@
"timezone": "",
"title": "Grafana Dev Overview & Home",
"uid": "j6T00KRZz",
"version": 1
"version": 2
}

View File

@@ -15,26 +15,27 @@
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 7501,
"links": [],
"panels": [
{
"datasource": "gdev-testdata",
"gridPos": {
"h": 7,
"w": 18,
"w": 24,
"x": 0,
"y": 0
},
"id": 7,
"id": 2,
"links": [],
"options": {
"displayMode": "gradient",
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
"unit": "decgbytes"
},
"mappings": [],
"override": {},
@@ -47,7 +48,7 @@
{
"color": "orange",
"index": 1,
"value": 40
"value": 60
},
{
"color": "red",
@@ -59,95 +60,188 @@
},
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "sda1",
"refId": "A",
"scenarioId": "random_walk"
},
{
"alias": "sda2",
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "sda3",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "sda4",
"refId": "D",
"scenarioId": "random_walk"
},
{
"alias": "sda5",
"refId": "E",
"scenarioId": "csv_metric_values",
"stringInput": "10003,33333"
"scenarioId": "random_walk"
},
{
"alias": "sda6",
"refId": "F",
"scenarioId": "random_walk"
},
{
"alias": "sda7",
"refId": "G",
"scenarioId": "random_walk"
},
{
"alias": "sda8",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
"scenarioId": "random_walk"
},
{
"alias": "sda9",
"refId": "I",
"scenarioId": "random_walk"
},
{
"alias": "sda10",
"refId": "J",
"scenarioId": "random_walk"
},
{
"alias": "sda11",
"refId": "K",
"scenarioId": "random_walk"
},
{
"alias": "sda12",
"refId": "L",
"scenarioId": "random_walk"
},
{
"alias": "sda13",
"refId": "M",
"scenarioId": "random_walk"
},
{
"alias": "sda14",
"refId": "N",
"scenarioId": "random_walk"
},
{
"alias": "sda15",
"refId": "O",
"scenarioId": "random_walk"
},
{
"alias": "sda16",
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Usage",
"title": "",
"transparent": true,
"type": "bargauge"
},
{
"datasource": "gdev-testdata",
"gridPos": {
"h": 22,
"w": 6,
"x": 18,
"y": 0
"h": 10,
"w": 16,
"x": 0,
"y": 7
},
"id": 8,
"id": 4,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 20
},
{
"color": "orange",
"index": 2,
"value": 40
},
{
"color": "red",
"index": 3,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"refId": "A",
"scenarioId": "random_walk"
},
{
"alias": "Area B",
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "Basement",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "Garage",
"refId": "D",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Gradient mode",
"type": "bargauge"
},
{
"datasource": "gdev-testdata",
"gridPos": {
"h": 10,
"w": 6,
"x": 16,
"y": 7
},
"id": 6,
"links": [],
"options": {
"displayMode": "basic",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
@@ -160,19 +254,24 @@
"override": {},
"thresholds": [
{
"color": "green",
"color": "blue",
"index": 0,
"value": null
},
{
"color": "orange",
"color": "green",
"index": 1,
"value": 55
"value": 42.5
},
{
"color": "orange",
"index": 2,
"value": 80
},
{
"color": "red",
"index": 2,
"value": 95
"index": 3,
"value": 90
}
],
"values": false
@@ -181,10 +280,6 @@
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "E",
"scenarioId": "random_walk"
},
{
"refId": "H",
"scenarioId": "csv_metric_values",
@@ -194,22 +289,6 @@
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
@@ -241,47 +320,78 @@
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Basic",
"type": "bargauge"
},
{
"datasource": "gdev-testdata",
"gridPos": {
"h": 22,
"w": 2,
"x": 22,
"y": 7
},
"id": 8,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"max": 100,
"min": 0
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "red",
"index": 0,
"value": null
},
{
"color": "red",
"index": 1,
"value": 90
}
],
"values": false
},
"orientation": "vertical"
},
"targets": [
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "G",
"scenarioId": "random_walk"
},
{
"refId": "R",
"scenarioId": "random_walk"
},
{
"refId": "S",
"refId": "A",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Usage",
"title": "Completion",
"type": "bargauge"
},
{
"datasource": "gdev-testdata",
"gridPos": {
"h": 15,
"w": 11,
"h": 12,
"w": 22,
"x": 0,
"y": 7
"y": 17
},
"id": 6,
"id": 10,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
"unit": "decgbytes"
},
"mappings": [],
"override": {},
@@ -294,12 +404,12 @@
{
"color": "green",
"index": 1,
"value": 20
"value": 30
},
{
"color": "orange",
"index": 2,
"value": 40
"value": 60
},
{
"color": "red",
@@ -309,69 +419,113 @@
],
"values": false
},
"orientation": "horizontal"
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"alias": "sda1",
"refId": "A",
"scenarioId": "random_walk"
},
{
"alias": "Area B",
"alias": "sda2",
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "Basement",
"alias": "sda3",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "Garage",
"alias": "sda4",
"refId": "D",
"scenarioId": "random_walk"
},
{
"alias": "Attic",
"alias": "sda5",
"refId": "E",
"scenarioId": "random_walk"
},
{
"alias": "sda6",
"refId": "F",
"scenarioId": "random_walk"
},
{
"alias": "sda7",
"refId": "G",
"scenarioId": "random_walk"
},
{
"alias": "sda8",
"refId": "H",
"scenarioId": "random_walk"
},
{
"alias": "sda9",
"refId": "I",
"scenarioId": "random_walk"
},
{
"alias": "sda10",
"refId": "J",
"scenarioId": "random_walk"
},
{
"alias": "sda11",
"refId": "K",
"scenarioId": "random_walk"
},
{
"alias": "sda12",
"refId": "L",
"scenarioId": "random_walk"
},
{
"alias": "sda13",
"refId": "M",
"scenarioId": "random_walk"
},
{
"alias": "sda14",
"refId": "N",
"scenarioId": "random_walk"
},
{
"alias": "sda15",
"refId": "O",
"scenarioId": "random_walk"
},
{
"alias": "sda16",
"refId": "P",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Temperature",
"title": "",
"type": "bargauge"
},
{
"datasource": "gdev-testdata",
"gridPos": {
"h": 15,
"w": 7,
"x": 11,
"y": 7
"h": 8,
"w": 24,
"x": 0,
"y": 29
},
"id": 9,
"id": 11,
"links": [],
"options": {
"displayMode": "basic",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
"unit": "decgbytes"
},
"mappings": [],
"override": {},
@@ -384,12 +538,12 @@
{
"color": "green",
"index": 1,
"value": 20
"value": 30
},
{
"color": "orange",
"index": 2,
"value": 40
"value": 60
},
{
"color": "red",
@@ -399,89 +553,113 @@
],
"values": false
},
"orientation": "horizontal"
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"alias": "sda1",
"refId": "A",
"scenarioId": "random_walk"
},
{
"alias": "Area B",
"alias": "sda2",
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "Basement",
"alias": "sda3",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "Garage",
"alias": "sda4",
"refId": "D",
"scenarioId": "random_walk"
},
{
"alias": "Attic",
"alias": "sda5",
"refId": "E",
"scenarioId": "random_walk"
},
{
"alias": "sda6",
"refId": "F",
"scenarioId": "random_walk"
},
{
"alias": "sda7",
"refId": "G",
"scenarioId": "random_walk"
},
{
"alias": "sda8",
"refId": "H",
"scenarioId": "random_walk"
},
{
"alias": "sda9",
"refId": "I",
"scenarioId": "random_walk"
},
{
"alias": "sda10",
"refId": "J",
"scenarioId": "random_walk"
},
{
"alias": "sda11",
"refId": "K",
"scenarioId": "random_walk"
},
{
"alias": "sda12",
"refId": "L",
"scenarioId": "random_walk"
},
{
"alias": "sda13",
"refId": "M",
"scenarioId": "random_walk"
},
{
"alias": "sda14",
"refId": "N",
"scenarioId": "random_walk"
},
{
"alias": "sda15",
"refId": "O",
"scenarioId": "random_walk"
},
{
"alias": "sda16",
"refId": "P",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Temperature",
"title": "",
"type": "bargauge"
}
],
"refresh": false,
"refresh": "10s",
"schemaVersion": 18,
"style": "dark",
"tags": ["gdev", "bargauge", "panel-demo"],
"tags": ["gdev", "demo"],
"templating": {
"list": []
},
"time": {
"from": "now-30m",
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["1s", "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"refresh_intervals": ["2s", "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "Bar Gauge Animated Demo",
"uid": "k5IUwQeikaa",
"version": 1
"title": "Bar Gauge Demo",
"uid": "vmie2cmWz",
"version": 3
}

View File

@@ -1,376 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"gridPos": {
"h": 7,
"w": 18,
"x": 0,
"y": 0
},
"id": 7,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "orange",
"index": 1,
"value": 40
},
{
"color": "red",
"index": 2,
"value": 80
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "E",
"scenarioId": "csv_metric_values",
"stringInput": "10003,33333"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "G",
"scenarioId": "random_walk"
},
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
},
{
"refId": "K",
"scenarioId": "random_walk"
},
{
"refId": "L",
"scenarioId": "random_walk"
},
{
"refId": "M",
"scenarioId": "random_walk"
},
{
"refId": "N",
"scenarioId": "random_walk"
},
{
"refId": "O",
"scenarioId": "random_walk"
},
{
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Usage",
"type": "bargauge"
},
{
"gridPos": {
"h": 20,
"w": 6,
"x": 18,
"y": 0
},
"id": 8,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "orange",
"index": 1,
"value": 65
},
{
"color": "red",
"index": 2,
"value": 95
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "E",
"scenarioId": "random_walk"
},
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
},
{
"refId": "K",
"scenarioId": "random_walk"
},
{
"refId": "L",
"scenarioId": "random_walk"
},
{
"refId": "M",
"scenarioId": "random_walk"
},
{
"refId": "N",
"scenarioId": "random_walk"
},
{
"refId": "O",
"scenarioId": "random_walk"
},
{
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "G",
"scenarioId": "random_walk"
},
{
"refId": "R",
"scenarioId": "random_walk"
},
{
"refId": "S",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Usage",
"type": "bargauge"
},
{
"gridPos": {
"h": 13,
"w": 18,
"x": 0,
"y": 7
},
"id": 6,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 20
},
{
"color": "orange",
"index": 2,
"value": 40
},
{
"color": "red",
"index": 3,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"refId": "A",
"scenarioId": "random_walk"
},
{
"alias": "Area B",
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "Basement",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "Garage",
"refId": "D",
"scenarioId": "random_walk"
},
{
"alias": "Attic",
"refId": "E",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Temperature",
"type": "bargauge"
}
],
"schemaVersion": 18,
"style": "dark",
"tags": ["gdev", "bargauge", "panel-demo"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "Bar Gauge Gradient Demo",
"uid": "RndRQw6mz",
"version": 1
}

View File

@@ -1,405 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"gridPos": {
"h": 7,
"w": 22,
"x": 0,
"y": 0
},
"id": 7,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "orange",
"index": 1,
"value": 40
},
{
"color": "red",
"index": 2,
"value": 80
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "E",
"scenarioId": "csv_metric_values",
"stringInput": "10003,33333"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "G",
"scenarioId": "random_walk"
},
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
},
{
"refId": "K",
"scenarioId": "random_walk"
},
{
"refId": "L",
"scenarioId": "random_walk"
},
{
"refId": "M",
"scenarioId": "random_walk"
},
{
"refId": "N",
"scenarioId": "random_walk"
},
{
"refId": "O",
"scenarioId": "random_walk"
},
{
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Usage",
"type": "bargauge"
},
{
"gridPos": {
"h": 20,
"w": 2,
"x": 22,
"y": 0
},
"id": 11,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "percent"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "red",
"index": 1,
"value": 80
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Progress",
"type": "bargauge"
},
{
"gridPos": {
"h": 13,
"w": 10,
"x": 0,
"y": 7
},
"id": 6,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 20
},
{
"color": "orange",
"index": 2,
"value": 40
},
{
"color": "red",
"index": 3,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"refId": "A",
"scenarioId": "random_walk"
},
{
"alias": "Area B",
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "Basement",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "Garage",
"refId": "D",
"scenarioId": "random_walk"
},
{
"alias": "Attic",
"refId": "E",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Temperature",
"type": "bargauge"
},
{
"gridPos": {
"h": 13,
"w": 12,
"x": 10,
"y": 7
},
"id": 8,
"links": [],
"options": {
"displayMode": "basic",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "purple",
"index": 1,
"value": 50
},
{
"color": "blue",
"index": 2,
"value": 70
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
},
{
"refId": "K",
"scenarioId": "random_walk"
},
{
"refId": "L",
"scenarioId": "random_walk"
},
{
"refId": "M",
"scenarioId": "random_walk"
},
{
"refId": "N",
"scenarioId": "random_walk"
},
{
"refId": "O",
"scenarioId": "random_walk"
},
{
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Usage",
"type": "bargauge"
}
],
"schemaVersion": 18,
"style": "dark",
"tags": ["gdev", "bargauge", "panel-demo"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "Bar Gauge All Modes Demo",
"uid": "zt2f6NgZzaa",
"version": 1
}

View File

@@ -0,0 +1,829 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"gridPos": {
"h": 7,
"w": 6,
"x": 0,
"y": 0
},
"id": 6,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 20
},
{
"color": "orange",
"index": 2,
"value": 40
},
{
"color": "red",
"index": 3,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Title above bar",
"type": "bargauge"
},
{
"gridPos": {
"h": 7,
"w": 5,
"x": 6,
"y": 0
},
"id": 12,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 20
},
{
"color": "orange",
"index": 2,
"value": 40
},
{
"color": "red",
"index": 3,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Title to left of bar",
"type": "bargauge"
},
{
"gridPos": {
"h": 7,
"w": 7,
"x": 11,
"y": 0
},
"id": 13,
"links": [],
"options": {
"displayMode": "basic",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 20
},
{
"color": "orange",
"index": 2,
"value": 40
},
{
"color": "red",
"index": 3,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Basic mode",
"type": "bargauge"
},
{
"gridPos": {
"h": 7,
"w": 6,
"x": 18,
"y": 0
},
"id": 14,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 20
},
{
"color": "orange",
"index": 2,
"value": 40
},
{
"color": "red",
"index": 3,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "LED",
"type": "bargauge"
},
{
"gridPos": {
"h": 9,
"w": 11,
"x": 0,
"y": 7
},
"id": 7,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "orange",
"index": 1,
"value": 40
},
{
"color": "red",
"index": 2,
"value": 80
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "E",
"scenarioId": "csv_metric_values",
"stringInput": "10003,33333"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "G",
"scenarioId": "random_walk"
},
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
},
{
"refId": "K",
"scenarioId": "random_walk"
},
{
"refId": "L",
"scenarioId": "random_walk"
},
{
"refId": "M",
"scenarioId": "random_walk"
},
{
"refId": "N",
"scenarioId": "random_walk"
},
{
"refId": "O",
"scenarioId": "random_walk"
},
{
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "LED Vertical",
"type": "bargauge"
},
{
"gridPos": {
"h": 9,
"w": 13,
"x": 11,
"y": 7
},
"id": 8,
"links": [],
"options": {
"displayMode": "basic",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "purple",
"index": 1,
"value": 50
},
{
"color": "blue",
"index": 2,
"value": 70
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
},
{
"refId": "K",
"scenarioId": "random_walk"
},
{
"refId": "L",
"scenarioId": "random_walk"
},
{
"refId": "M",
"scenarioId": "random_walk"
},
{
"refId": "N",
"scenarioId": "random_walk"
},
{
"refId": "O",
"scenarioId": "random_walk"
},
{
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Basic vertical ",
"type": "bargauge"
},
{
"gridPos": {
"h": 7,
"w": 11,
"x": 0,
"y": 16
},
"id": 16,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["last"],
"defaults": {
"max": 100,
"min": 0
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "blue",
"index": 1,
"value": 40
},
{
"color": "red",
"index": 2,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.3.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0,-100"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Negative value below min",
"type": "bargauge"
},
{
"gridPos": {
"h": 7,
"w": 3,
"x": 11,
"y": 16
},
"id": 17,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["last"],
"defaults": {
"max": 100,
"min": 0
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "blue",
"index": 1,
"value": 40
},
{
"color": "red",
"index": 2,
"value": 80
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.3.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,0,-100"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Negative value below min",
"type": "bargauge"
},
{
"gridPos": {
"h": 7,
"w": 3,
"x": 14,
"y": 16
},
"id": 18,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["last"],
"defaults": {
"max": 100,
"min": -10
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "blue",
"index": 1,
"value": 40
},
{
"color": "red",
"index": 2,
"value": 80
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.3.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,6"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Positive value above min",
"type": "bargauge"
},
{
"gridPos": {
"h": 7,
"w": 3,
"x": 17,
"y": 16
},
"id": 19,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["last"],
"defaults": {
"max": 35,
"min": -20
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 5
},
{
"color": "#EAB839",
"index": 2,
"value": 25
},
{
"color": "red",
"index": 3,
"value": 30
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.3.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,90,30,5,6"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Negative min ",
"type": "bargauge"
},
{
"gridPos": {
"h": 7,
"w": 4,
"x": 20,
"y": 16
},
"id": 20,
"links": [],
"options": {
"displayMode": "gradient",
"fieldOptions": {
"calcs": ["last"],
"defaults": {
"max": 35,
"min": -20
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "blue",
"index": 0,
"value": null
},
{
"color": "green",
"index": 1,
"value": 5
},
{
"color": "#EAB839",
"index": 2,
"value": 25
},
{
"color": "red",
"index": 3,
"value": 30
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.3.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "30,30"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Negative min",
"type": "bargauge"
}
],
"schemaVersion": 18,
"style": "dark",
"tags": ["gdev", "panel-tests"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "Panel Tests - Bar Gauge",
"uid": "O6f11TZWk",
"version": 12
}

View File

@@ -1,400 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"gridPos": {
"h": 8,
"w": 22,
"x": 0,
"y": 0
},
"id": 7,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "orange",
"index": 1,
"value": 40
},
{
"color": "red",
"index": 2,
"value": 80
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "E",
"scenarioId": "csv_metric_values",
"stringInput": "10003,33333"
},
{
"refId": "F",
"scenarioId": "random_walk"
},
{
"refId": "G",
"scenarioId": "random_walk"
},
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
},
{
"refId": "K",
"scenarioId": "random_walk"
},
{
"refId": "L",
"scenarioId": "random_walk"
},
{
"refId": "M",
"scenarioId": "random_walk"
},
{
"refId": "N",
"scenarioId": "random_walk"
},
{
"refId": "O",
"scenarioId": "random_walk"
},
{
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Usage",
"type": "bargauge"
},
{
"gridPos": {
"h": 21,
"w": 2,
"x": 22,
"y": 0
},
"id": 11,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "percent"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "red",
"index": 1,
"value": 80
}
],
"values": false
},
"orientation": "vertical"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "A",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Progress",
"type": "bargauge"
},
{
"gridPos": {
"h": 13,
"w": 10,
"x": 0,
"y": 8
},
"id": 6,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "celsius"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "orange",
"index": 1,
"value": 40
},
{
"color": "red",
"index": 2,
"value": 80
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"alias": "Inside",
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"alias": "Outhouse",
"refId": "A",
"scenarioId": "random_walk"
},
{
"alias": "Area B",
"refId": "B",
"scenarioId": "random_walk"
},
{
"alias": "Basement",
"refId": "C",
"scenarioId": "random_walk"
},
{
"alias": "Garage",
"refId": "D",
"scenarioId": "random_walk"
},
{
"alias": "Attic",
"refId": "E",
"scenarioId": "random_walk"
},
{
"refId": "F",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Temperature",
"type": "bargauge"
},
{
"gridPos": {
"h": 13,
"w": 12,
"x": 10,
"y": 8
},
"id": 8,
"links": [],
"options": {
"displayMode": "lcd",
"fieldOptions": {
"calcs": ["mean"],
"defaults": {
"decimals": null,
"max": 100,
"min": 0,
"unit": "watt"
},
"mappings": [],
"override": {},
"thresholds": [
{
"color": "green",
"index": 0,
"value": null
},
{
"color": "orange",
"index": 1,
"value": 85
},
{
"color": "red",
"index": 2,
"value": 95
}
],
"values": false
},
"orientation": "horizontal"
},
"pluginVersion": "6.2.0-pre",
"targets": [
{
"refId": "H",
"scenarioId": "csv_metric_values",
"stringInput": "100,100,100"
},
{
"refId": "A",
"scenarioId": "random_walk"
},
{
"refId": "B",
"scenarioId": "random_walk"
},
{
"refId": "C",
"scenarioId": "random_walk"
},
{
"refId": "D",
"scenarioId": "random_walk"
},
{
"refId": "I",
"scenarioId": "random_walk"
},
{
"refId": "J",
"scenarioId": "random_walk"
},
{
"refId": "K",
"scenarioId": "random_walk"
},
{
"refId": "L",
"scenarioId": "random_walk"
},
{
"refId": "M",
"scenarioId": "random_walk"
},
{
"refId": "N",
"scenarioId": "random_walk"
},
{
"refId": "O",
"scenarioId": "random_walk"
},
{
"refId": "P",
"scenarioId": "random_walk"
},
{
"refId": "Q",
"scenarioId": "random_walk"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Usage",
"type": "bargauge"
}
],
"schemaVersion": 18,
"style": "dark",
"tags": ["gdev", "bargauge", "panel-demo"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
},
"timezone": "",
"title": "Bar Gauge LED Demo",
"uid": "0G3rbkqmkaa",
"version": 1
}

View File

@@ -320,6 +320,30 @@ When `false`, the HTTP header `X-Frame-Options: deny` will be set in Grafana HTT
browsers to not allow rendering Grafana in a `<frame>`, `<iframe>`, `<embed>` or `<object>`. The main goal is to
mitigate the risk of [Clickjacking](https://www.owasp.org/index.php/Clickjacking). Default is `false`.
### strict_transport_security
Set to `true` if you want to enable http `Strict-Transport-Security` (HSTS) response header. This is only sent when HTTPS is enabled in this configuration. HSTS tells browsers that the site should only be accessed using HTTPS. The default value is `false` until the next minor release, `6.3`.
### strict_transport_security_max_age_seconds
Sets how long a browser should cache HSTS in seconds. Only applied if strict_transport_security is enabled. The default value is `86400`.
### strict_transport_security_preload
Set to `true` if to enable HSTS `preloading` option. Only applied if strict_transport_security is enabled. The default value is `false`.
### strict_transport_security_subdomains
Set to `true` if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled. The default value is `false`.
### x_content_type_options
Set to `true` to enable the X-Content-Type-Options response header. The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised in the Content-Type headers should not be changed and be followed. The default value is `false` until the next minor release, `6.3`.
### x_xss_protection
Set to `false` to disable the X-XSS-Protection header, which tells browsers to stop pages from loading when they detect reflected cross-site scripting (XSS) attacks. The default value is `false` until the next minor release, `6.3`.
<hr />
## [users]

View File

@@ -51,6 +51,36 @@ then the Grafana proxy will transform it into "https://management.azure.com/foo/
The `method` parameter is optional. It can be set to any HTTP verb to provide more fine-grained control.
### Dynamic Routes
When using routes, you can also reference a variable stored in JsonData or SecureJsonData which will be interpolated when connecting to the datasource.
With JsonData:
```json
"routes": [
{
"path": "custom/api/v5/*",
"method": "*",
"url": "{{.JsonData.dynamicUrl}}",
...
},
]
```
With SecureJsonData:
```json
"routes": [{
"path": "custom/api/v5/*",
"method": "*",
"url": "{{.SecureJsonData.dynamicUrl}}",
...
}]
```
In the above example, the app is able to set the value for `dynamicUrl` in JsonData or SecureJsonData and it will be replaced on-demand.
An app using this feature can be found [here](https://github.com/grafana/kentik-app).
## Encrypting Sensitive Data
When a user saves a password or secret with your datasource plugin's Config page, then you can save data to a column in the datasource table called `secureJsonData` that is an encrypted blob. Any data saved in the blob is encrypted by Grafana and can only be decrypted by the Grafana server on the backend. This means once a password is saved, no sensitive data is sent to the browser. If the password is saved in the `jsonData` blob or the `password` field then it is unencrypted and anyone with Admin access (with the help of Chrome Developer Tools) can read it.

View File

@@ -5,7 +5,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "6.2.0-pre",
"version": "6.2.3",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"
@@ -191,7 +191,7 @@
"eventemitter3": "2.0.3",
"file-saver": "1.3.8",
"immutable": "3.8.2",
"jquery": "3.4.0",
"jquery": "3.4.1",
"lodash": "4.17.11",
"moment": "2.24.0",
"mousetrap": "1.6.3",

View File

@@ -23,7 +23,7 @@
"@types/react-color": "2.17.0",
"classnames": "2.2.6",
"d3": "5.9.1",
"jquery": "3.4.0",
"jquery": "3.4.1",
"lodash": "4.17.11",
"moment": "2.24.0",
"papaparse": "4.6.3",

View File

@@ -1,6 +1,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import { BarGauge, Props, getValueColor, getBasicAndGradientStyles, getBarGradient, getTitleStyles } from './BarGauge';
import {
BarGauge,
Props,
getValueColor,
getBasicAndGradientStyles,
getBarGradient,
getTitleStyles,
getValuePercent,
} from './BarGauge';
import { VizOrientation, DisplayValue } from '../../types';
import { getTheme } from '../../themes';
@@ -63,6 +71,24 @@ describe('BarGauge', () => {
});
});
describe('Get value percent', () => {
it('0 to 100 and value 40', () => {
expect(getValuePercent(40, 0, 100)).toEqual(0.4);
});
it('50 to 100 and value 75', () => {
expect(getValuePercent(75, 50, 100)).toEqual(0.5);
});
it('-30 to 30 and value 0', () => {
expect(getValuePercent(0, -30, 30)).toEqual(0.5);
});
it('-30 to 30 and value 30', () => {
expect(getValuePercent(30, -30, 30)).toEqual(1);
});
});
describe('Vertical bar without title', () => {
it('should not include title height in height', () => {
const props = getProps({

View File

@@ -11,8 +11,9 @@ import { DisplayValue, Themeable, TimeSeriesValue, Threshold, VizOrientation } f
const MIN_VALUE_HEIGHT = 18;
const MAX_VALUE_HEIGHT = 50;
const MIN_VALUE_WIDTH = 50;
const MAX_VALUE_WIDTH = 100;
const LINE_HEIGHT = 1.5;
const MAX_VALUE_WIDTH = 150;
const TITLE_LINE_HEIGHT = 1.5;
const VALUE_LINE_HEIGHT = 1;
export interface Props extends Themeable {
height: number;
@@ -161,7 +162,7 @@ export class BarGauge extends PureComponent<Props> {
const cells: JSX.Element[] = [];
for (let i = 0; i < cellCount; i++) {
const currentValue = (valueRange / cellCount) * i;
const currentValue = minValue + (valueRange / cellCount) * i;
const cellColor = this.getCellColor(currentValue);
const cellStyles: CSSProperties = {
borderRadius: '2px',
@@ -227,7 +228,7 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
return {
fontSize: 14,
width: width,
height: 14 * LINE_HEIGHT,
height: 14 * TITLE_LINE_HEIGHT,
placement: 'below',
};
}
@@ -238,7 +239,7 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
const titleHeight = Math.max(Math.min(height * maxTitleHeightRatio, MAX_VALUE_HEIGHT), 17);
return {
fontSize: titleHeight / LINE_HEIGHT,
fontSize: titleHeight / TITLE_LINE_HEIGHT,
width: 0,
height: titleHeight,
placement: 'above',
@@ -251,7 +252,7 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
const titleHeight = Math.max(height * maxTitleHeightRatio, MIN_VALUE_HEIGHT);
return {
fontSize: titleHeight / LINE_HEIGHT,
fontSize: titleHeight / TITLE_LINE_HEIGHT,
height: 0,
width: Math.min(Math.max(width * maxTitleWidthRatio, 50), 200),
placement: 'left',
@@ -345,11 +346,6 @@ function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
}
}
// console.log('titleDim', titleDim);
// console.log('valueWidth', valueWidth);
// console.log('width', width);
// console.log('total', titleDim.width + maxBarWidth + valueWidth);
return {
valueWidth,
valueHeight,
@@ -360,6 +356,10 @@ function calculateBarAndValueDimensions(props: Props): BarAndValueDimensions {
};
}
export function getValuePercent(value: number, minValue: number, maxValue: number): number {
return Math.min((value - minValue) / (maxValue - minValue), 1);
}
/**
* Only exported to for unit test
*/
@@ -367,7 +367,7 @@ export function getBasicAndGradientStyles(props: Props): BasicAndGradientStyles
const { displayMode, maxValue, minValue, value } = props;
const { valueWidth, valueHeight, maxBarHeight, maxBarWidth } = calculateBarAndValueDimensions(props);
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
const valuePercent = getValuePercent(value.numeric, minValue, maxValue);
const valueColor = getValueColor(props);
const valueStyles = getValueStyles(value.text, valueColor, valueWidth, valueHeight);
const isBasic = displayMode === 'basic';
@@ -450,7 +450,7 @@ export function getBarGradient(props: Props, maxSize: number): string {
for (let i = 0; i < thresholds.length; i++) {
const threshold = thresholds[i];
const color = getColorFromHexRgbOrName(threshold.color);
const valuePercent = Math.min(threshold.value / (maxValue - minValue), 1);
const valuePercent = getValuePercent(threshold.value, minValue, maxValue);
const pos = valuePercent * maxSize;
const offset = Math.round(pos - (pos - lastpos) / 2);
@@ -486,7 +486,7 @@ export function getValueColor(props: Props): string {
* Only exported to for unit test
*/
function getValueStyles(value: string, color: string, width: number, height: number): CSSProperties {
const heightFont = height / LINE_HEIGHT;
const heightFont = height / VALUE_LINE_HEIGHT;
const guess = width / (value.length * 1.1);
const fontSize = Math.min(Math.max(guess, 14), heightFont);
@@ -496,33 +496,15 @@ function getValueStyles(value: string, color: string, width: number, height: num
width: `${width}px`,
display: 'flex',
alignItems: 'center',
fontSize: fontSize.toFixed(2) + 'px',
lineHeight: VALUE_LINE_HEIGHT,
fontSize: fontSize.toFixed(4) + 'px',
};
}
// let canvasElement: HTMLCanvasElement | null = null;
//
// interface TextDimensions {
// width: number;
// height: number;
// }
//
// /**
// * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
// *
// * @param {String} text The text to be rendered.
// * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
// *
// * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
// */
// function getTextWidth(text: string): number {
// // re-use canvas object for better performance
// canvasElement = canvasElement || document.createElement('canvas');
// const context = canvasElement.getContext('2d');
// if (context) {
// context.font = 'normal 16px Roboto';
// const metrics = context.measureText(text);
// return metrics.width;
// }
// return 16;
// const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
// var context = canvas.getContext("2d");
// context.font = "'Roboto', 'Helvetica Neue', Arial, sans-serif";
// var metrics = context.measureText(text);
// return metrics.width;
// }

View File

@@ -18,8 +18,9 @@ exports[`BarGauge Render with basic options should render 1`] = `
"alignItems": "center",
"color": "#73BF69",
"display": "flex",
"fontSize": "27.27px",
"fontSize": "27.2727px",
"height": "300px",
"lineHeight": 1,
"paddingLeft": "10px",
"width": "60px",
}

View File

@@ -42,9 +42,10 @@ export class CustomScrollbar extends Component<Props> {
updateScroll() {
const ref = this.ref.current;
const { scrollTop } = this.props;
if (ref && !isNil(this.props.scrollTop)) {
ref.scrollTop(this.props.scrollTop);
if (ref && !isNil(scrollTop)) {
ref.scrollTop(scrollTop);
}
}
@@ -70,6 +71,44 @@ export class CustomScrollbar extends Component<Props> {
this.updateScroll();
}
renderTrack = (track: 'track-vertical' | 'track-horizontal', hideTrack: boolean | undefined, passedProps: any) => {
return (
<div
{...passedProps}
className={cx(
css`
visibility: ${hideTrack ? 'none' : 'visible'};
`,
track
)}
/>
);
};
renderThumb = (thumb: 'thumb-horizontal' | 'thumb-vertical', passedProps: any) => {
return <div {...passedProps} className={thumb} />;
};
renderTrackHorizontal = (passedProps: any) => {
return this.renderTrack('track-horizontal', this.props.hideHorizontalTrack, passedProps);
};
renderTrackVertical = (passedProps: any) => {
return this.renderTrack('track-vertical', this.props.hideVerticalTrack, passedProps);
};
renderThumbHorizontal = (passedProps: any) => {
return this.renderThumb('thumb-horizontal', passedProps);
};
renderThumbVertical = (passedProps: any) => {
return this.renderThumb('thumb-vertical', passedProps);
};
renderView = (passedProps: any) => {
return <div {...passedProps} className="view" />;
};
render() {
const {
className,
@@ -80,8 +119,6 @@ export class CustomScrollbar extends Component<Props> {
autoHide,
autoHideTimeout,
hideTracksWhenNotNeeded,
hideHorizontalTrack,
hideVerticalTrack,
} = this.props;
return (
@@ -97,31 +134,11 @@ export class CustomScrollbar extends Component<Props> {
// Before these where set to inhert but that caused problems with cut of legends in firefox
autoHeightMax={autoHeightMax}
autoHeightMin={autoHeightMin}
renderTrackHorizontal={props => (
<div
{...props}
className={cx(
css`
visibility: ${hideHorizontalTrack ? 'none' : 'visible'};
`,
'track-horizontal'
)}
/>
)}
renderTrackVertical={props => (
<div
{...props}
className={cx(
css`
visibility: ${hideVerticalTrack ? 'none' : 'visible'};
`,
'track-vertical'
)}
/>
)}
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
renderView={props => <div {...props} className="view" />}
renderTrackHorizontal={this.renderTrackHorizontal}
renderTrackVertical={this.renderTrackVertical}
renderThumbHorizontal={this.renderThumbHorizontal}
renderThumbVertical={this.renderThumbVertical}
renderView={this.renderView}
>
{children}
</Scrollbars>

View File

@@ -37,7 +37,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
</p>
</div>
<div
className="css-17l4171 track-horizontal"
className="css-52gpmd track-horizontal"
style={
Object {
"display": "none",
@@ -58,7 +58,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
/>
</div>
<div
className="css-17l4171 track-vertical"
className="css-52gpmd track-vertical"
style={
Object {
"display": "none",

View File

@@ -58,7 +58,7 @@ export class Gauge extends PureComponent<Props> {
if (length > 12) {
return FONT_SCALE - (length * 5) / 110;
}
return FONT_SCALE - (length * 5) / 100;
return FONT_SCALE - (length * 5) / 101;
}
draw() {
@@ -69,16 +69,17 @@ export class Gauge extends PureComponent<Props> {
const backgroundColor = selectThemeVariant(
{
dark: theme.colors.dark3,
light: '#e6e6e6',
dark: theme.colors.dark8,
light: theme.colors.gray6,
},
theme.type
);
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
const gaugeWidth = Math.min(dimension / 6, 40) / gaugeWidthReduceRatio;
const gaugeWidth = Math.min(dimension / 5.5, 40) / gaugeWidthReduceRatio;
const thresholdMarkersWidth = gaugeWidth / 5;
const fontSize = Math.min(dimension / 5.5, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
const fontSize = Math.min(dimension / 4, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
const thresholdLabelFontSize = fontSize / 2.5;
const options: any = {
@@ -181,7 +182,7 @@ function calculateGaugeAutoProps(width: number, height: number, title: string |
const titleFontSize = Math.min((width * 0.15) / 1.5, 20); // 20% of height * line-height, max 40px
const titleHeight = titleFontSize * 1.5;
const availableHeight = showLabel ? height - titleHeight : height;
const gaugeHeight = Math.min(availableHeight * 0.7, width);
const gaugeHeight = Math.min(availableHeight, width);
return {
showLabel,

View File

@@ -4,7 +4,7 @@ import React, { FunctionComponent } from 'react';
interface Props {
title?: string;
onClose?: () => void;
children: JSX.Element | JSX.Element[] | boolean;
children: React.ReactNode;
onAdd?: () => void;
}

View File

@@ -1,116 +1,129 @@
// Libraries
import React, { PureComponent, ChangeEvent } from 'react';
import React, { ChangeEvent, useState, useCallback } from 'react';
// Components
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
import { FormField } from '../FormField/FormField';
import { FormLabel } from '../FormLabel/FormLabel';
import { UnitPicker } from '../UnitPicker/UnitPicker';
// Types
import { Field } from '../../types/data';
import { toNumberString, toIntegerOrUndefined } from '../../utils';
import { toIntegerOrUndefined } from '../../utils';
import { SelectOptionItem } from '../Select/Select';
import { VAR_SERIES_NAME, VAR_FIELD_NAME, VAR_CALC, VAR_CELL_PREFIX } from '../../utils/fieldDisplay';
import { PanelOptionsGroup } from '../index';
const labelWidth = 6;
export interface Props {
title: string;
options: Partial<Field>;
value: Partial<Field>;
onChange: (fieldProperties: Partial<Field>) => void;
showMinMax: boolean;
}
export class FieldPropertiesEditor extends PureComponent<Props> {
onTitleChange = (event: ChangeEvent<HTMLInputElement>) =>
this.props.onChange({ ...this.props.options, title: event.target.value });
export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMinMax }) => {
const { unit, title } = value;
// @ts-ignore
onUnitChange = (unit: SelectOptionItem<string>) => this.props.onChange({ ...this.props.value, unit: unit.value });
const [decimals, setDecimals] = useState(
value.decimals !== undefined && value.decimals !== null ? value.decimals.toString() : ''
);
const [min, setMin] = useState(value.min !== undefined && value.min !== null ? value.min.toString() : '');
const [max, setMax] = useState(value.max !== undefined && value.max !== null ? value.max.toString() : '');
onDecimalChange = (event: ChangeEvent<HTMLInputElement>) => {
this.props.onChange({
...this.props.options,
decimals: toIntegerOrUndefined(event.target.value),
});
const onTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange({ ...value, title: event.target.value });
};
onMinChange = (event: ChangeEvent<HTMLInputElement>) => {
this.props.onChange({
...this.props.options,
min: toIntegerOrUndefined(event.target.value),
});
const onDecimalChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setDecimals(event.target.value);
},
[value.decimals, onChange]
);
const onMinChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setMin(event.target.value);
},
[value.min, onChange]
);
const onMaxChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setMax(event.target.value);
},
[value.max, onChange]
);
const onUnitChange = (unit: SelectOptionItem<string>) => {
onChange({ ...value, unit: unit.value });
};
onMaxChange = (event: ChangeEvent<HTMLInputElement>) => {
this.props.onChange({
...this.props.options,
max: toIntegerOrUndefined(event.target.value),
const commitChanges = useCallback(() => {
onChange({
...value,
decimals: toIntegerOrUndefined(decimals),
min: toIntegerOrUndefined(min),
max: toIntegerOrUndefined(max),
});
};
}, [min, max, decimals]);
render() {
const { showMinMax, title } = this.props;
const { unit, decimals, min, max } = this.props.options;
const titleTooltip = (
<div>
Template Variables:
<br />
{'$' + VAR_SERIES_NAME}
<br />
{'$' + VAR_FIELD_NAME}
<br />
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
const titleTooltip = (
<div>
Template Variables:
<br />
{'$' + VAR_SERIES_NAME}
<br />
{'$' + VAR_FIELD_NAME}
<br />
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
</div>
);
return (
<PanelOptionsGroup title="Field">
<FormField
label="Title"
labelWidth={labelWidth}
onChange={onTitleChange}
value={title}
tooltip={titleTooltip}
placeholder="Auto"
/>
<div className="gf-form">
<FormLabel width={labelWidth}>Unit</FormLabel>
<UnitPicker defaultValue={unit} onChange={onUnitChange} />
</div>
);
return (
<PanelOptionsGroup title={title}>
{showMinMax && (
<>
<FormField
label="Title"
label="Min"
labelWidth={labelWidth}
onChange={this.onTitleChange}
value={this.props.options.title}
tooltip={titleTooltip}
placeholder="Auto"
onChange={onMinChange}
onBlur={commitChanges}
value={min}
type="number"
/>
<div className="gf-form">
<FormLabel width={labelWidth}>Unit</FormLabel>
<UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
</div>
{showMinMax && (
<>
<FormField
label="Min"
labelWidth={labelWidth}
onChange={this.onMinChange}
value={toNumberString(min)}
type="number"
/>
<FormField
label="Max"
labelWidth={labelWidth}
onChange={this.onMaxChange}
value={toNumberString(max)}
type="number"
/>
</>
)}
<FormField
label="Decimals"
label="Max"
labelWidth={labelWidth}
placeholder="auto"
onChange={this.onDecimalChange}
value={toNumberString(decimals)}
onChange={onMaxChange}
onBlur={commitChanges}
value={max}
type="number"
/>
</>
</PanelOptionsGroup>
);
}
}
)}
<FormField
label="Decimals"
labelWidth={labelWidth}
placeholder="auto"
onChange={onDecimalChange}
onBlur={commitChanges}
value={decimals}
type="number"
/>
</PanelOptionsGroup>
);
};

View File

@@ -80,6 +80,13 @@ export interface DataSourcePluginMeta extends PluginMeta {
mixed?: boolean;
hasQueryHelp?: boolean;
queryOptions?: PluginMetaQueryOptions;
sort?: number;
/**
* By default, hidden queries are not passed to the datasource
* Set this to true in plugin.json to have hidden queries passed to the
* DataSource query method
*/
hiddenQueries?: boolean;
}
interface PluginMetaQueryOptions {

View File

@@ -32,6 +32,7 @@ export interface PanelData {
}
export interface PanelProps<T = any> {
id: number; // ID within the current dashboard
data: PanelData;
// TODO: annotation?: PanelData;

View File

@@ -47,8 +47,8 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
"$GF_PATHS_DATA" && \
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
EXPOSE 3000

View File

@@ -233,7 +233,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/"})
}
if len(appLink.Children) > 0 {

View File

@@ -67,14 +67,14 @@ func (provider *accessTokenProvider) getAccessToken(data templateData) (string,
}
}
urlInterpolated, err := interpolateString(provider.route.TokenAuth.Url, data)
urlInterpolated, err := InterpolateString(provider.route.TokenAuth.Url, data)
if err != nil {
return "", err
}
params := make(url.Values)
for key, value := range provider.route.TokenAuth.Params {
interpolatedParam, err := interpolateString(value, data)
interpolatedParam, err := InterpolateString(value, data)
if err != nil {
return "", err
}
@@ -119,7 +119,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
conf := &jwt.Config{}
if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok {
interpolatedVal, err := interpolateString(val, data)
interpolatedVal, err := InterpolateString(val, data)
if err != nil {
return "", err
}
@@ -127,7 +127,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
}
if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok {
interpolatedVal, err := interpolateString(val, data)
interpolatedVal, err := InterpolateString(val, data)
if err != nil {
return "", err
}
@@ -135,7 +135,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
}
if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok {
interpolatedVal, err := interpolateString(val, data)
interpolatedVal, err := InterpolateString(val, data)
if err != nil {
return "", err
}

View File

@@ -1,13 +1,11 @@
package pluginproxy
import (
"bytes"
"context"
"fmt"
"net/http"
"net/url"
"strings"
"text/template"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@@ -24,7 +22,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
SecureJsonData: ds.SecureJsonData.Decrypt(),
}
interpolatedURL, err := interpolateString(route.Url, data)
interpolatedURL, err := InterpolateString(route.Url, data)
if err != nil {
logger.Error("Error interpolating proxy url", "error", err)
return
@@ -81,24 +79,9 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
logger.Info("Requesting", "url", req.URL.String())
}
func interpolateString(text string, data templateData) (string, error) {
t, err := template.New("content").Parse(text)
if err != nil {
return "", fmt.Errorf("could not parse template %s", text)
}
var contentBuf bytes.Buffer
err = t.Execute(&contentBuf, data)
if err != nil {
return "", fmt.Errorf("failed to execute template %s", text)
}
return contentBuf.String(), nil
}
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
for _, header := range route.Headers {
interpolated, err := interpolateString(header.Content, data)
interpolated, err := InterpolateString(header.Content, data)
if err != nil {
return err
}

View File

@@ -14,7 +14,7 @@ func TestDsAuthProvider(t *testing.T) {
},
}
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
interpolated, err := InterpolateString("{{.SecureJsonData.Test}}", data)
So(err, ShouldBeNil)
So(interpolated, ShouldEqual, "0asd+asd")
})

View File

@@ -354,7 +354,7 @@ func addOAuthPassThruAuth(c *m.ReqContext, req *http.Request) {
// If the tokens are not the same, update the entry in the DB
if token.AccessToken != authInfoQuery.Result.OAuthAccessToken {
updateAuthCommand := &m.UpdateAuthInfoCommand{
UserId: authInfoQuery.Result.Id,
UserId: authInfoQuery.Result.UserId,
AuthModule: authInfoQuery.Result.AuthModule,
AuthId: authInfoQuery.Result.AuthId,
OAuthToken: token,

View File

@@ -2,12 +2,13 @@ package pluginproxy
import (
"encoding/json"
"github.com/grafana/grafana/pkg/setting"
"net"
"net/http"
"net/http/httputil"
"net/url"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
@@ -38,6 +39,24 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http.
return result, err
}
func updateURL(route *plugins.AppPluginRoute, orgId int64, appID string) (string, error) {
query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appID}
if err := bus.Dispatch(&query); err != nil {
return "", err
}
data := templateData{
JsonData: query.Result.JsonData,
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
}
interpolated, err := InterpolateString(route.Url, data)
if err != nil {
return "", err
}
return interpolated, err
}
// NewApiPluginProxy create a plugin proxy
func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string, cfg *setting.Cfg) *httputil.ReverseProxy {
targetURL, _ := url.Parse(route.Url)
@@ -48,7 +67,6 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
req.Host = targetURL.Host
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
// clear cookie headers
req.Header.Del("Cookie")
req.Header.Del("Set-Cookie")
@@ -72,13 +90,13 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
}
// Create a HTTP header with the context in it.
ctxJson, err := json.Marshal(ctx.SignedInUser)
ctxJSON, err := json.Marshal(ctx.SignedInUser)
if err != nil {
ctx.JsonApiErr(500, "failed to marshal context to json.", err)
return
}
req.Header.Add("X-Grafana-Context", string(ctxJson))
req.Header.Add("X-Grafana-Context", string(ctxJSON))
if cfg.SendUserHeader && !ctx.SignedInUser.IsAnonymous {
req.Header.Add("X-Grafana-User", ctx.SignedInUser.Login)
@@ -97,6 +115,27 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
}
}
if len(route.Url) > 0 {
interpolatedURL, err := updateURL(route, ctx.OrgId, appID)
if err != nil {
ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
}
targetURL, err := url.Parse(interpolatedURL)
if err != nil {
ctx.JsonApiErr(500, "Could not parse custom url: %v", err)
return
}
req.URL.Scheme = targetURL.Scheme
req.URL.Host = targetURL.Host
req.Host = targetURL.Host
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
if err != nil {
ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
return
}
}
// reqBytes, _ := httputil.DumpRequestOut(req, true);
// log.Trace("Proxying plugin request: %s", string(reqBytes))
}

View File

@@ -53,6 +53,7 @@ func TestPluginProxy(t *testing.T) {
},
},
&setting.Cfg{SendUserHeader: true},
nil,
)
Convey("Should add header with username", func() {
@@ -69,6 +70,7 @@ func TestPluginProxy(t *testing.T) {
},
},
&setting.Cfg{SendUserHeader: false},
nil,
)
Convey("Should not add header with username", func() {
// Get will return empty string even if header is not set
@@ -82,6 +84,7 @@ func TestPluginProxy(t *testing.T) {
SignedInUser: &m.SignedInUser{IsAnonymous: true},
},
&setting.Cfg{SendUserHeader: true},
nil,
)
Convey("Should not add header with username", func() {
@@ -89,14 +92,59 @@ func TestPluginProxy(t *testing.T) {
So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
})
})
Convey("When getting templated url", t, func() {
route := &plugins.AppPluginRoute{
Url: "{{.JsonData.dynamicUrl}}",
Method: "GET",
}
bus.AddHandler("test", func(query *m.GetPluginSettingByIdQuery) error {
query.Result = &m.PluginSetting{
JsonData: map[string]interface{}{
"dynamicUrl": "https://dynamic.grafana.com",
},
}
return nil
})
req := getPluginProxiedRequest(
&m.ReqContext{
SignedInUser: &m.SignedInUser{
Login: "test_user",
},
},
&setting.Cfg{SendUserHeader: true},
route,
)
Convey("Headers should be updated", func() {
header, err := getHeaders(route, 1, "my-app")
So(err, ShouldBeNil)
So(header.Get("X-Grafana-User"), ShouldEqual, "")
})
Convey("Should set req.URL to be interpolated value from jsonData", func() {
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com")
})
Convey("Route url should not be modified", func() {
So(route.Url, ShouldEqual, "{{.JsonData.dynamicUrl}}")
})
})
}
// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
func getPluginProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request {
route := &plugins.AppPluginRoute{}
func getPluginProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg, route *plugins.AppPluginRoute) *http.Request {
// insert dummy route if none is specified
if route == nil {
route = &plugins.AppPluginRoute{
Path: "api/v4/",
Url: "https://www.google.com",
ReqRole: m.ROLE_EDITOR,
}
}
proxy := NewApiPluginProxy(ctx, "", route, "", cfg)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
req, err := http.NewRequest(http.MethodGet, route.Url, nil)
So(err, ShouldBeNil)
proxy.Director(req)
return req

View File

@@ -0,0 +1,49 @@
package pluginproxy
import (
"bytes"
"fmt"
"net/url"
"text/template"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
)
// InterpolateString accepts template data and return a string with substitutions
func InterpolateString(text string, data templateData) (string, error) {
t, err := template.New("content").Parse(text)
if err != nil {
return "", fmt.Errorf("could not parse template %s", text)
}
var contentBuf bytes.Buffer
err = t.Execute(&contentBuf, data)
if err != nil {
return "", fmt.Errorf("failed to execute template %s", text)
}
return contentBuf.String(), nil
}
// InterpolateURL accepts template data and return a string with substitutions
func InterpolateURL(anURL *url.URL, route *plugins.AppPluginRoute, orgID int64, appID string) (*url.URL, error) {
query := m.GetPluginSettingByIdQuery{OrgId: orgID, PluginId: appID}
result, err := url.Parse(anURL.String())
if query.Result != nil {
if len(query.Result.JsonData) > 0 {
data := templateData{
JsonData: query.Result.JsonData,
}
interpolatedResult, err := InterpolateString(anURL.String(), data)
if err == nil {
result, err = url.Parse(interpolatedResult)
if err != nil {
return nil, fmt.Errorf("Error parsing plugin route url %v", err)
}
}
}
}
return result, err
}

View File

@@ -0,0 +1,21 @@
package pluginproxy
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestInterpolateString(t *testing.T) {
Convey("When interpolating string", t, func() {
data := templateData{
SecureJsonData: map[string]string{
"Test": "0asd+asd",
},
}
interpolated, err := InterpolateString("{{.SecureJsonData.Test}}", data)
So(err, ShouldBeNil)
So(interpolated, ShouldEqual, "0asd+asd")
})
}

View File

@@ -7,28 +7,33 @@ import (
"github.com/codegangsta/cli"
"github.com/fatih/color"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/datamigrations"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
func runDbCommand(command func(commandLine CommandLine) error) func(context *cli.Context) {
func runDbCommand(command func(commandLine utils.CommandLine, sqlStore *sqlstore.SqlStore) error) func(context *cli.Context) {
return func(context *cli.Context) {
cmd := &contextCommandLine{context}
cmd := &utils.ContextCommandLine{Context: context}
cfg := setting.NewCfg()
cfg.Load(&setting.CommandLineArgs{
Config: cmd.String("config"),
HomePath: cmd.String("homepath"),
Args: flag.Args(),
})
cfg.LogConfigSources()
engine := &sqlstore.SqlStore{}
engine.Cfg = cfg
engine.Bus = bus.GetBus()
engine.Init()
if err := command(cmd); err != nil {
if err := command(cmd, engine); err != nil {
logger.Errorf("\n%s: ", color.RedString("Error"))
logger.Errorf("%s\n\n", err)
@@ -40,10 +45,10 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
}
}
func runPluginCommand(command func(commandLine CommandLine) error) func(context *cli.Context) {
func runPluginCommand(command func(commandLine utils.CommandLine) error) func(context *cli.Context) {
return func(context *cli.Context) {
cmd := &contextCommandLine{context}
cmd := &utils.ContextCommandLine{Context: context}
if err := command(cmd); err != nil {
logger.Errorf("\n%s: ", color.RedString("Error"))
logger.Errorf("%s %s\n\n", color.RedString("✗"), err)
@@ -91,19 +96,33 @@ var pluginCommands = []cli.Command{
},
}
var dbCommandFlags = []cli.Flag{
cli.StringFlag{
Name: "homepath",
Usage: "path to grafana install/home path, defaults to working directory",
},
cli.StringFlag{
Name: "config",
Usage: "path to config file",
},
}
var adminCommands = []cli.Command{
{
Name: "reset-admin-password",
Usage: "reset-admin-password <new password>",
Action: runDbCommand(resetPasswordCommand),
Flags: []cli.Flag{
cli.StringFlag{
Name: "homepath",
Usage: "path to grafana install/home path, defaults to working directory",
},
cli.StringFlag{
Name: "config",
Usage: "path to config file",
Flags: dbCommandFlags,
},
{
Name: "data-migration",
Usage: "Runs a script that migrates or cleanups data in your db",
Subcommands: []cli.Command{
{
Name: "encrypt-datasource-passwords",
Usage: "Migrates passwords from unsecured fields to secure_json_data field. Return ok unless there is an error. Safe to execute multiple times.",
Action: runDbCommand(datamigrations.EncryptDatasourcePaswords),
Flags: dbCommandFlags,
},
},
},

View File

@@ -0,0 +1,126 @@
package datamigrations
import (
"context"
"encoding/json"
"github.com/fatih/color"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
)
var (
datasourceTypes = []string{
"mysql",
"influxdb",
"elasticsearch",
"graphite",
"prometheus",
"opentsdb",
}
)
// EncryptDatasourcePaswords migrates un-encrypted secrets on datasources
// to the secureJson Column.
func EncryptDatasourcePaswords(c utils.CommandLine, sqlStore *sqlstore.SqlStore) error {
return sqlStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
passwordsUpdated, err := migrateColumn(session, "password")
if err != nil {
return err
}
basicAuthUpdated, err := migrateColumn(session, "basic_auth_password")
if err != nil {
return err
}
logger.Info("\n")
if passwordsUpdated > 0 {
logger.Infof("%s Encrypted password field for %d datasources \n", color.GreenString("✔"), passwordsUpdated)
}
if basicAuthUpdated > 0 {
logger.Infof("%s Encrypted basic_auth_password field for %d datasources \n", color.GreenString("✔"), basicAuthUpdated)
}
if passwordsUpdated == 0 && basicAuthUpdated == 0 {
logger.Infof("%s All datasources secrets are allready encrypted\n", color.GreenString("✔"))
}
logger.Info("\n")
logger.Warn("Warning: Datasource provisioning files need to be manually changed to prevent overwriting of " +
"the data during provisioning. See https://grafana.com/docs/installation/upgrading/#upgrading-to-v6-2 for " +
"details")
return nil
})
}
func migrateColumn(session *sqlstore.DBSession, column string) (int, error) {
var rows []map[string]string
session.Cols("id", column, "secure_json_data")
session.Table("data_source")
session.In("type", datasourceTypes)
session.Where(column + " IS NOT NULL AND " + column + " != ''")
err := session.Find(&rows)
if err != nil {
return 0, errutil.Wrapf(err, "failed to select column: %s", column)
}
rowsUpdated, err := updateRows(session, rows, column)
return rowsUpdated, errutil.Wrapf(err, "failed to update column: %s", column)
}
func updateRows(session *sqlstore.DBSession, rows []map[string]string, passwordFieldName string) (int, error) {
var rowsUpdated int
for _, row := range rows {
newSecureJSONData, err := getUpdatedSecureJSONData(row, passwordFieldName)
if err != nil {
return 0, err
}
data, err := json.Marshal(newSecureJSONData)
if err != nil {
return 0, errutil.Wrap("marshaling newSecureJsonData failed", err)
}
newRow := map[string]interface{}{"secure_json_data": data, passwordFieldName: ""}
session.Table("data_source")
session.Where("id = ?", row["id"])
// Setting both columns while having value only for secure_json_data should clear the [passwordFieldName] column
session.Cols("secure_json_data", passwordFieldName)
_, err = session.Update(newRow)
if err != nil {
return 0, err
}
rowsUpdated++
}
return rowsUpdated, nil
}
func getUpdatedSecureJSONData(row map[string]string, passwordFieldName string) (map[string]interface{}, error) {
encryptedPassword, err := util.Encrypt([]byte(row[passwordFieldName]), setting.SecretKey)
if err != nil {
return nil, err
}
var secureJSONData map[string]interface{}
if err := json.Unmarshal([]byte(row["secure_json_data"]), &secureJSONData); err != nil {
return nil, err
}
jsonFieldName := util.ToCamelCase(passwordFieldName)
secureJSONData[jsonFieldName] = encryptedPassword
return secureJSONData, nil
}

View File

@@ -0,0 +1,67 @@
package datamigrations
import (
"testing"
"time"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands/commandstest"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/assert"
)
func TestPasswordMigrationCommand(t *testing.T) {
//setup datasources with password, basic_auth and none
sqlstore := sqlstore.InitTestDB(t)
session := sqlstore.NewSession()
defer session.Close()
datasources := []*models.DataSource{
{Type: "influxdb", Name: "influxdb", Password: "foobar"},
{Type: "graphite", Name: "graphite", BasicAuthPassword: "foobar"},
{Type: "prometheus", Name: "prometheus", SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{})},
}
// set required default values
for _, ds := range datasources {
ds.Created = time.Now()
ds.Updated = time.Now()
ds.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{})
}
_, err := session.Insert(&datasources)
assert.Nil(t, err)
//run migration
err = EncryptDatasourcePaswords(&commandstest.FakeCommandLine{}, sqlstore)
assert.Nil(t, err)
//verify that no datasources still have password or basic_auth
var dss []*models.DataSource
err = session.SQL("select * from data_source").Find(&dss)
assert.Nil(t, err)
assert.Equal(t, len(dss), 3)
for _, ds := range dss {
sj := ds.SecureJsonData.Decrypt()
if ds.Name == "influxdb" {
assert.Equal(t, ds.Password, "")
v, exist := sj["password"]
assert.True(t, exist)
assert.Equal(t, v, "foobar", "expected password to be moved to securejson")
}
if ds.Name == "graphite" {
assert.Equal(t, ds.BasicAuthPassword, "")
v, exist := sj["basicAuthPassword"]
assert.True(t, exist)
assert.Equal(t, v, "foobar", "expected basic_auth_password to be moved to securejson")
}
if ds.Name == "prometheus" {
assert.Equal(t, len(sj), 0)
}
}
}

View File

@@ -14,13 +14,14 @@ import (
"strings"
"github.com/fatih/color"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
)
func validateInput(c CommandLine, pluginFolder string) error {
func validateInput(c utils.CommandLine, pluginFolder string) error {
arg := c.Args().First()
if arg == "" {
return errors.New("please specify plugin to install")
@@ -46,7 +47,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
return nil
}
func installCommand(c CommandLine) error {
func installCommand(c utils.CommandLine) error {
pluginFolder := c.PluginDirectory()
if err := validateInput(c, pluginFolder); err != nil {
return err
@@ -60,7 +61,7 @@ func installCommand(c CommandLine) error {
// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API
// and then extracts the zip into the plugins directory.
func InstallPlugin(pluginName, version string, c CommandLine) error {
func InstallPlugin(pluginName, version string, c utils.CommandLine) error {
pluginFolder := c.PluginDirectory()
downloadURL := c.PluginURL()
if downloadURL == "" {

View File

@@ -3,9 +3,10 @@ package commands
import (
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
)
func listremoteCommand(c CommandLine) error {
func listremoteCommand(c utils.CommandLine) error {
plugin, err := s.ListAllPlugins(c.RepoDirectory())
if err != nil {

View File

@@ -5,9 +5,10 @@ import (
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
)
func validateVersionInput(c CommandLine) error {
func validateVersionInput(c utils.CommandLine) error {
arg := c.Args().First()
if arg == "" {
return errors.New("please specify plugin to list versions for")
@@ -16,7 +17,7 @@ func validateVersionInput(c CommandLine) error {
return nil
}
func listversionsCommand(c CommandLine) error {
func listversionsCommand(c utils.CommandLine) error {
if err := validateVersionInput(c); err != nil {
return err
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
)
var ls_getPlugins func(path string) []m.InstalledPlugin = s.GetLocalPlugins
@@ -31,7 +32,7 @@ var validateLsCommand = func(pluginDir string) error {
return nil
}
func lsCommand(c CommandLine) error {
func lsCommand(c utils.CommandLine) error {
pluginDir := c.PluginDirectory()
if err := validateLsCommand(pluginDir); err != nil {
return err

View File

@@ -5,12 +5,13 @@ import (
"fmt"
"strings"
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
)
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
func removeCommand(c CommandLine) error {
func removeCommand(c utils.CommandLine) error {
pluginPath := c.PluginDirectory()
plugin := c.Args().First()

View File

@@ -6,13 +6,15 @@ import (
"github.com/fatih/color"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
const AdminUserId = 1
func resetPasswordCommand(c CommandLine) error {
func resetPasswordCommand(c utils.CommandLine, sqlStore *sqlstore.SqlStore) error {
newPassword := c.Args().First()
password := models.Password(newPassword)

View File

@@ -4,6 +4,7 @@ import (
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/hashicorp/go-version"
)
@@ -27,7 +28,7 @@ func ShouldUpgrade(installed string, remote m.Plugin) bool {
return false
}
func upgradeAllCommand(c CommandLine) error {
func upgradeAllCommand(c utils.CommandLine) error {
pluginsDir := c.PluginDirectory()
localPlugins := s.GetLocalPlugins(pluginsDir)

View File

@@ -4,9 +4,10 @@ import (
"github.com/fatih/color"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
)
func upgradeCommand(c CommandLine) error {
func upgradeCommand(c utils.CommandLine) error {
pluginsDir := c.PluginDirectory()
pluginName := c.Args().First()

View File

@@ -1,6 +1,7 @@
package main
import (
"flag"
"fmt"
"os"
"runtime"
@@ -17,6 +18,7 @@ var version = "master"
func main() {
setupLogging()
flag.Parse()
app := cli.NewApp()
app.Name = "Grafana cli"
app.Usage = ""

View File

@@ -1,4 +1,4 @@
package commands
package utils
import (
"github.com/codegangsta/cli"
@@ -22,30 +22,30 @@ type CommandLine interface {
PluginURL() string
}
type contextCommandLine struct {
type ContextCommandLine struct {
*cli.Context
}
func (c *contextCommandLine) ShowHelp() {
func (c *ContextCommandLine) ShowHelp() {
cli.ShowCommandHelp(c.Context, c.Command.Name)
}
func (c *contextCommandLine) ShowVersion() {
func (c *ContextCommandLine) ShowVersion() {
cli.ShowVersion(c.Context)
}
func (c *contextCommandLine) Application() *cli.App {
func (c *ContextCommandLine) Application() *cli.App {
return c.App
}
func (c *contextCommandLine) PluginDirectory() string {
func (c *ContextCommandLine) PluginDirectory() string {
return c.GlobalString("pluginsDir")
}
func (c *contextCommandLine) RepoDirectory() string {
func (c *ContextCommandLine) RepoDirectory() string {
return c.GlobalString("repo")
}
func (c *contextCommandLine) PluginURL() string {
func (c *ContextCommandLine) PluginURL() string {
return c.GlobalString("pluginUrl")
}

View File

@@ -39,10 +39,14 @@ func (dc *databaseCache) Run(ctx context.Context) error {
}
func (dc *databaseCache) internalRunGC() {
now := getTime().Unix()
sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0`
err := dc.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
now := getTime().Unix()
sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0`
_, err := session.Exec(sql, now)
return err
})
_, err := dc.SQLStore.NewSession().Exec(sql, now)
if err != nil {
dc.log.Error("failed to run garbage collect", "error", err)
}
@@ -66,7 +70,10 @@ func (dc *databaseCache) Get(key string) (interface{}, error) {
if cacheHit.Expires > 0 {
existedButExpired := getTime().Unix()-cacheHit.CreatedAt >= cacheHit.Expires
if existedButExpired {
_ = dc.Delete(key) //ignore this error since we will return `ErrCacheItemNotFound` anyway
err = dc.Delete(key) //ignore this error since we will return `ErrCacheItemNotFound` anyway
if err != nil {
dc.log.Debug("Deletion of expired key failed: %v", err)
}
return nil, ErrCacheItemNotFound
}
}
@@ -87,37 +94,46 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration
}
session := dc.SQLStore.NewSession()
var cacheHit CacheData
has, err := session.Where("cache_key = ?", key).Get(&cacheHit)
if err != nil {
return err
}
defer session.Close()
var expiresInSeconds int64
if expire != 0 {
expiresInSeconds = int64(expire) / int64(time.Second)
}
// insert or update depending on if item already exist
if has {
sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
} else {
sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`
_, err = session.Exec(sql, key, data, getTime().Unix(), expiresInSeconds)
// attempt to insert the key
sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`
_, err = session.Exec(sql, key, data, getTime().Unix(), expiresInSeconds)
if err != nil {
// attempt to update if a unique constrain violation or a deadlock (for MySQL) occurs
// if the update fails propagate the error
// which eventually will result in a key that is not finally set
// but since it's a cache does not harm a lot
if dc.SQLStore.Dialect.IsUniqueConstraintViolation(err) || dc.SQLStore.Dialect.IsDeadlock(err) {
sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
if err != nil && dc.SQLStore.Dialect.IsDeadlock(err) {
// most probably somebody else is upserting the key
// so it is safe enough not to propagate this error
return nil
}
}
}
return err
}
func (dc *databaseCache) Delete(key string) error {
sql := "DELETE FROM cache_data WHERE cache_key=?"
_, err := dc.SQLStore.NewSession().Exec(sql, key)
return dc.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
sql := "DELETE FROM cache_data WHERE cache_key=?"
_, err := session.Exec(sql, key)
return err
})
return err
}
// CacheData is the struct representing the table in the database
type CacheData struct {
CacheKey string
Data []byte

View File

@@ -1,9 +1,13 @@
package remotecache
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
redis "gopkg.in/redis.v2"
)
@@ -13,12 +17,47 @@ type redisStorage struct {
c *redis.Client
}
func newRedisStorage(opts *setting.RemoteCacheOptions) *redisStorage {
opt := &redis.Options{
Network: "tcp",
Addr: opts.ConnStr,
// parseRedisConnStr parses k=v pairs in csv and builds a redis Options object
func parseRedisConnStr(connStr string) (*redis.Options, error) {
keyValueCSV := strings.Split(connStr, ",")
options := &redis.Options{Network: "tcp"}
for _, rawKeyValue := range keyValueCSV {
keyValueTuple := strings.Split(rawKeyValue, "=")
if len(keyValueTuple) != 2 {
return nil, fmt.Errorf("incorrect redis connection string format detected for '%v', format is key=value,key=value", rawKeyValue)
}
connKey := keyValueTuple[0]
connVal := keyValueTuple[1]
switch connKey {
case "addr":
options.Addr = connVal
case "password":
options.Password = connVal
case "db":
i, err := strconv.ParseInt(connVal, 10, 64)
if err != nil {
return nil, errutil.Wrap("value for db in redis connection string must be a number", err)
}
options.DB = i
case "pool_size":
i, err := strconv.Atoi(connVal)
if err != nil {
return nil, errutil.Wrap("value for pool_size in redis connection string must be a number", err)
}
options.PoolSize = i
default:
return nil, fmt.Errorf("unrecorgnized option '%v' in redis connection string", connVal)
}
}
return &redisStorage{c: redis.NewClient(opt)}
return options, nil
}
func newRedisStorage(opts *setting.RemoteCacheOptions) (*redisStorage, error) {
opt, err := parseRedisConnStr(opts.ConnStr)
if err != nil {
return nil, err
}
return &redisStorage{c: redis.NewClient(opt)}, nil
}
// Set sets value to given key in session.
@@ -28,7 +67,6 @@ func (s *redisStorage) Set(key string, val interface{}, expires time.Duration) e
if err != nil {
return err
}
status := s.c.SetEx(key, expires, string(value))
return status.Err()
}

View File

@@ -10,7 +10,7 @@ import (
func TestRedisCacheStorage(t *testing.T) {
opts := &setting.RemoteCacheOptions{Name: redisCacheType, ConnStr: "localhost:6379"}
opts := &setting.RemoteCacheOptions{Name: redisCacheType, ConnStr: "addr=localhost:6379"}
client := createTestClient(t, opts, nil)
runTestsForClient(t, client)
}

View File

@@ -91,7 +91,7 @@ func (ds *RemoteCache) Run(ctx context.Context) error {
func createClient(opts *setting.RemoteCacheOptions, sqlstore *sqlstore.SqlStore) (CacheStorage, error) {
if opts.Name == redisCacheType {
return newRedisStorage(opts), nil
return newRedisStorage(opts)
}
if opts.Name == memcachedCacheType {

View File

@@ -31,6 +31,7 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
// Check if allowed to continue with this IP
if result, err := auth.IsAllowedIP(); result == false {
ctx.Logger.Error("auth proxy: failed to check whitelisted ip addresses", "message", err.Error(), "error", err.DetailsError)
ctx.Handle(407, err.Error(), err.DetailsError)
return true
}
@@ -38,6 +39,7 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
// Try to get user id from various sources
id, err := auth.GetUserID()
if err != nil {
ctx.Logger.Error("auth proxy: failed to login", "message", err.Error(), "error", err.DetailsError)
ctx.Handle(500, err.Error(), err.DetailsError)
return true
}
@@ -45,6 +47,7 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
// Get full user info
user, err := auth.GetSignedUser(id)
if err != nil {
ctx.Logger.Error("auth proxy: failed to get signed in user", "message", err.Error(), "error", err.DetailsError)
ctx.Handle(500, err.Error(), err.DetailsError)
return true
}
@@ -54,7 +57,8 @@ func initContextWithAuthProxy(store *remotecache.RemoteCache, ctx *m.ReqContext,
ctx.IsSignedIn = true
// Remember user data it in cache
if err := auth.Remember(); err != nil {
if err := auth.Remember(id); err != nil {
ctx.Logger.Error("auth proxy: failed to store user in cache", "message", err.Error(), "error", err.DetailsError)
ctx.Handle(500, err.Error(), err.DetailsError)
return true
}

View File

@@ -298,23 +298,24 @@ func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, *Error
}
// Remember user in cache
func (auth *AuthProxy) Remember() *Error {
func (auth *AuthProxy) Remember(id int64) *Error {
key := auth.getKey()
// Make sure we do not rewrite the expiration time
if auth.InCache() {
// Check if user already in cache
userID, err := auth.store.Get(key)
if err != nil && err != remotecache.ErrCacheItemNotFound {
return newError("failed to lookup user in cache", err)
}
if userID != nil {
return nil
}
var (
key = auth.getKey()
value, _ = auth.GetUserIDViaCache()
expiration = time.Duration(-auth.cacheTTL) * time.Minute
err = auth.store.Set(key, value, expiration)
)
expiration := time.Duration(auth.cacheTTL) * time.Minute
err = auth.store.Set(key, id, expiration)
if err != nil {
return newError(err.Error(), nil)
return newError("failed to store user in cache", err)
}
return nil

View File

@@ -1,6 +1,7 @@
package middleware
import (
"fmt"
"net/http"
"net/url"
"strconv"
@@ -241,10 +242,35 @@ func AddDefaultResponseHeaders() macaron.Handler {
if !setting.AllowEmbedding {
AddXFrameOptionsDenyHeader(w)
}
AddSecurityHeaders(w)
})
}
}
// AddSecurityHeaders adds various HTTP(S) response headers that enable various security protections behaviors in the client's browser.
func AddSecurityHeaders(w macaron.ResponseWriter) {
if setting.Protocol == setting.HTTPS && setting.StrictTransportSecurity {
strictHeader := "Strict-Transport-Security"
w.Header().Add(strictHeader, fmt.Sprintf("max-age=%v", setting.StrictTransportSecurityMaxAge))
if setting.StrictTransportSecurityPreload {
w.Header().Add(strictHeader, "preload")
}
if setting.StrictTransportSecuritySubDomains {
w.Header().Add(strictHeader, "includeSubDomains")
}
}
if setting.ContentTypeProtectionHeader {
w.Header().Add("X-Content-Type-Options", "nosniff")
}
if setting.XSSProtectionHeader {
w.Header().Add("X-XSS-Protection", "1")
w.Header().Add("X-XSS-Protection", "mode=block")
}
}
func AddNoCacheHeaders(w macaron.ResponseWriter) {
w.Header().Add("Cache-Control", "no-cache")
w.Header().Add("Pragma", "no-cache")

View File

@@ -18,16 +18,17 @@ import (
// DataSourcePlugin contains all metadata about a datasource plugin
type DataSourcePlugin struct {
FrontendPluginBase
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"`
Explore bool `json:"explore"`
Table bool `json:"tables"`
Logs bool `json:"logs"`
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"`
Routes []*AppPluginRoute `json:"routes"`
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"`
Explore bool `json:"explore"`
Table bool `json:"tables"`
HiddenQueries bool `json:"hiddenQueries"`
Logs bool `json:"logs"`
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"`
Routes []*AppPluginRoute `json:"routes"`
Backend bool `json:"backend,omitempty"`
Executable string `json:"executable,omitempty"`

View File

@@ -48,6 +48,7 @@ type Dialect interface {
NoOpSql() string
IsUniqueConstraintViolation(err error) bool
IsDeadlock(err error) bool
}
func NewDialect(engine *xorm.Engine) Dialect {

View File

@@ -134,12 +134,20 @@ func (db *Mysql) CleanDB() error {
return nil
}
func (db *Mysql) IsUniqueConstraintViolation(err error) bool {
func (db *Mysql) isThisError(err error, errcode uint16) bool {
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == mysqlerr.ER_DUP_ENTRY {
if driverErr.Number == errcode {
return true
}
}
return false
}
func (db *Mysql) IsUniqueConstraintViolation(err error) bool {
return db.isThisError(err, mysqlerr.ER_DUP_ENTRY)
}
func (db *Mysql) IsDeadlock(err error) bool {
return db.isThisError(err, mysqlerr.ER_LOCK_DEADLOCK)
}

View File

@@ -138,12 +138,20 @@ func (db *Postgres) CleanDB() error {
return nil
}
func (db *Postgres) IsUniqueConstraintViolation(err error) bool {
func (db *Postgres) isThisError(err error, errcode string) bool {
if driverErr, ok := err.(*pq.Error); ok {
if driverErr.Code == "23505" {
if string(driverErr.Code) == errcode {
return true
}
}
return false
}
func (db *Postgres) IsUniqueConstraintViolation(err error) bool {
return db.isThisError(err, "23505")
}
func (db *Postgres) IsDeadlock(err error) bool {
return db.isThisError(err, "40P01")
}

View File

@@ -85,12 +85,20 @@ func (db *Sqlite3) CleanDB() error {
return nil
}
func (db *Sqlite3) IsUniqueConstraintViolation(err error) bool {
func (db *Sqlite3) isThisError(err error, errcode int) bool {
if driverErr, ok := err.(sqlite3.Error); ok {
if driverErr.ExtendedCode == sqlite3.ErrConstraintUnique {
if int(driverErr.ExtendedCode) == errcode {
return true
}
}
return false
}
func (db *Sqlite3) IsUniqueConstraintViolation(err error) bool {
return db.isThisError(err, int(sqlite3.ErrConstraintUnique))
}
func (db *Sqlite3) IsDeadlock(err error) bool {
return false // No deadlock
}

View File

@@ -45,31 +45,48 @@ func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permi
sb.sql.WriteString(` AND
(
dashboard.id IN (
SELECT distinct d.id AS DashboardId
FROM dashboard AS d
LEFT JOIN dashboard folder on folder.id = d.folder_id
LEFT JOIN dashboard_acl AS da ON
da.dashboard_id = d.id OR
da.dashboard_id = d.folder_id OR
(
-- include default permissions -->
da.org_id = -1 AND (
(folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
(folder.id IS NULL AND d.has_acl = ` + falseStr + `)
)
)
LEFT JOIN team_member as ugm on ugm.team_id = da.team_id
WHERE
d.org_id = ? AND
da.permission >= ? AND
(
da.user_id = ? OR
ugm.user_id = ? OR
da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)
)
SELECT distinct DashboardId from (
SELECT d.id AS DashboardId
FROM dashboard AS d
LEFT JOIN dashboard AS folder on folder.id = d.folder_id
LEFT JOIN dashboard_acl AS da ON
da.dashboard_id = d.id OR
da.dashboard_id = d.folder_id
LEFT JOIN team_member as ugm on ugm.team_id = da.team_id
WHERE
d.org_id = ? AND
da.permission >= ? AND
(
da.user_id = ? OR
ugm.user_id = ? OR
da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)
)
UNION
SELECT d.id AS DashboardId
FROM dashboard AS d
LEFT JOIN dashboard AS folder on folder.id = d.folder_id
LEFT JOIN dashboard_acl AS da ON
(
-- include default permissions -->
da.org_id = -1 AND (
(folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
(folder.id IS NULL AND d.has_acl = ` + falseStr + `)
)
)
WHERE
d.org_id = ? AND
da.permission >= ? AND
(
da.user_id = ? OR
da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)
)
) AS a
)
)`)
sb.params = append(sb.params, user.OrgId, permission, user.UserId, user.UserId)
sb.params = append(sb.params, okRoles...)
sb.params = append(sb.params, user.OrgId, permission, user.UserId)
sb.params = append(sb.params, okRoles...)
}

View File

@@ -0,0 +1,343 @@
package sqlstore
import (
"context"
"math/rand"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/assert"
)
func TestSqlBuilder(t *testing.T) {
t.Run("writeDashboardPermissionFilter", func(t *testing.T) {
t.Run("user ACL", func(t *testing.T) {
test(t,
DashboardProps{},
&DashboardPermission{User: true, Permission: models.PERMISSION_VIEW},
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_VIEW},
shouldFind,
)
test(t,
DashboardProps{},
&DashboardPermission{User: true, Permission: models.PERMISSION_VIEW},
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT},
shouldNotFind,
)
test(t,
DashboardProps{},
&DashboardPermission{User: true, Permission: models.PERMISSION_EDIT},
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT},
shouldFind,
)
test(t,
DashboardProps{},
&DashboardPermission{User: true, Permission: models.PERMISSION_VIEW},
Search{RequiredPermission: models.PERMISSION_VIEW},
shouldNotFind,
)
})
t.Run("role ACL", func(t *testing.T) {
test(t,
DashboardProps{},
&DashboardPermission{Role: models.ROLE_VIEWER, Permission: models.PERMISSION_VIEW},
Search{UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
shouldFind,
)
test(t,
DashboardProps{},
&DashboardPermission{Role: models.ROLE_VIEWER, Permission: models.PERMISSION_VIEW},
Search{UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_EDIT},
shouldNotFind,
)
test(t,
DashboardProps{},
&DashboardPermission{Role: models.ROLE_EDITOR, Permission: models.PERMISSION_VIEW},
Search{UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
shouldNotFind,
)
test(t,
DashboardProps{},
&DashboardPermission{Role: models.ROLE_EDITOR, Permission: models.PERMISSION_VIEW},
Search{UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
shouldNotFind,
)
})
t.Run("team ACL", func(t *testing.T) {
test(t,
DashboardProps{},
&DashboardPermission{Team: true, Permission: models.PERMISSION_VIEW},
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_VIEW},
shouldFind,
)
test(t,
DashboardProps{},
&DashboardPermission{Team: true, Permission: models.PERMISSION_VIEW},
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT},
shouldNotFind,
)
test(t,
DashboardProps{},
&DashboardPermission{Team: true, Permission: models.PERMISSION_EDIT},
Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT},
shouldFind,
)
test(t,
DashboardProps{},
&DashboardPermission{Team: true, Permission: models.PERMISSION_EDIT},
Search{UserFromACL: false, RequiredPermission: models.PERMISSION_EDIT},
shouldNotFind,
)
})
t.Run("defaults for user ACL", func(t *testing.T) {
test(t,
DashboardProps{},
nil,
Search{OrgId: -1, UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
shouldNotFind,
)
test(t,
DashboardProps{OrgId: -1},
nil,
Search{OrgId: -1, UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_VIEW},
shouldFind,
)
test(t,
DashboardProps{OrgId: -1},
nil,
Search{OrgId: -1, UsersOrgRole: models.ROLE_EDITOR, RequiredPermission: models.PERMISSION_EDIT},
shouldFind,
)
test(t,
DashboardProps{OrgId: -1},
nil,
Search{OrgId: -1, UsersOrgRole: models.ROLE_VIEWER, RequiredPermission: models.PERMISSION_EDIT},
shouldNotFind,
)
})
})
}
var shouldFind = true
var shouldNotFind = false
type DashboardProps struct {
OrgId int64
}
type DashboardPermission struct {
User bool
Team bool
Role models.RoleType
Permission models.PermissionType
}
type Search struct {
UsersOrgRole models.RoleType
UserFromACL bool
RequiredPermission models.PermissionType
OrgId int64
}
type dashboardResponse struct {
Id int64
}
func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *DashboardPermission, search Search, shouldFind bool) {
// Will also cleanup the db
sqlStore := InitTestDB(t)
dashboard, err := createDummyDashboard(dashboardProps)
if !assert.Equal(t, nil, err) {
return
}
var aclUserId int64
if dashboardPermission != nil {
aclUserId, err = createDummyAcl(dashboardPermission, search, dashboard.Id)
if !assert.Equal(t, nil, err) {
return
}
}
dashboards, err := getDashboards(sqlStore, search, aclUserId)
if !assert.Equal(t, nil, err) {
return
}
if shouldFind {
if assert.Equal(t, 1, len(dashboards), "Should return one dashboard") {
assert.Equal(t, dashboards[0].Id, dashboard.Id, "Should return created dashboard")
}
} else {
assert.Equal(t, 0, len(dashboards), "Should node return any dashboard")
}
}
func createDummyUser() (*models.User, error) {
uid := rand.Intn(9999999)
createUserCmd := &models.CreateUserCommand{
Email: string(uid) + "@example.com",
Login: string(uid),
Name: string(uid),
Company: "",
OrgName: "",
Password: string(uid),
EmailVerified: true,
IsAdmin: false,
SkipOrgSetup: false,
DefaultOrgRole: string(models.ROLE_VIEWER),
}
err := CreateUser(context.Background(), createUserCmd)
if err != nil {
return nil, err
}
return &createUserCmd.Result, nil
}
func createDummyTeam() (*models.Team, error) {
cmd := &models.CreateTeamCommand{
// Does not matter in this tests actually
OrgId: 1,
Name: "test",
Email: "test@example.com",
}
err := CreateTeam(cmd)
if err != nil {
return nil, err
}
return &cmd.Result, nil
}
func createDummyDashboard(dashboardProps DashboardProps) (*models.Dashboard, error) {
json, _ := simplejson.NewJson([]byte(`{"schemaVersion":17,"title":"gdev dashboards","uid":"","version":1}`))
saveDashboardCmd := &models.SaveDashboardCommand{
Dashboard: json,
UserId: 0,
Overwrite: false,
Message: "",
RestoredFrom: 0,
PluginId: "",
FolderId: 0,
IsFolder: false,
UpdatedAt: time.Time{},
}
if dashboardProps.OrgId != 0 {
saveDashboardCmd.OrgId = dashboardProps.OrgId
} else {
saveDashboardCmd.OrgId = 1
}
err := SaveDashboard(saveDashboardCmd)
if err != nil {
return nil, err
}
return saveDashboardCmd.Result, nil
}
func createDummyAcl(dashboardPermission *DashboardPermission, search Search, dashboardId int64) (int64, error) {
acl := &models.DashboardAcl{
OrgId: 1,
Created: time.Now(),
Updated: time.Now(),
Permission: dashboardPermission.Permission,
DashboardId: dashboardId,
}
var user *models.User
var err error
if dashboardPermission.User {
user, err = createDummyUser()
if err != nil {
return 0, err
}
acl.UserId = user.Id
}
if dashboardPermission.Team {
team, err := createDummyTeam()
if err != nil {
return 0, err
}
if search.UserFromACL {
user, err = createDummyUser()
if err != nil {
return 0, err
}
addTeamMemberCmd := &models.AddTeamMemberCommand{
UserId: user.Id,
OrgId: 1,
TeamId: team.Id,
}
err = AddTeamMember(addTeamMemberCmd)
if err != nil {
return 0, err
}
}
acl.TeamId = team.Id
}
if len(string(dashboardPermission.Role)) > 0 {
acl.Role = &dashboardPermission.Role
}
updateAclCmd := &models.UpdateDashboardAclCommand{
DashboardId: dashboardId,
Items: []*models.DashboardAcl{acl},
}
err = UpdateDashboardAcl(updateAclCmd)
if user != nil {
return user.Id, err
}
return 0, err
}
func getDashboards(sqlStore *SqlStore, search Search, aclUserId int64) ([]*dashboardResponse, error) {
builder := &SqlBuilder{}
signedInUser := &models.SignedInUser{
UserId: 9999999999,
}
if search.OrgId == 0 {
signedInUser.OrgId = 1
} else {
signedInUser.OrgId = search.OrgId
}
if len(string(search.UsersOrgRole)) > 0 {
signedInUser.OrgRole = search.UsersOrgRole
} else {
signedInUser.OrgRole = models.ROLE_VIEWER
}
if search.UserFromACL {
signedInUser.UserId = aclUserId
}
var res []*dashboardResponse
builder.Write("SELECT * FROM dashboard WHERE true")
builder.writeDashboardPermissionFilter(signedInUser, search.RequiredPermission)
err := sqlStore.engine.SQL(builder.GetSqlString(), builder.params...).Find(&res)
return res, err
}

View File

@@ -39,6 +39,11 @@ var (
const ContextSessionName = "db-session"
func init() {
// This change will make xorm use an empty default schema for postgres and
// by that mimic the functionality of how it was functioning before
// xorm's changes above.
xorm.DefaultPostgresSchema = ""
registry.Register(&registry.Descriptor{
Name: "SqlStore",
Instance: &SqlStore{},
@@ -88,12 +93,12 @@ func (ss *SqlStore) inTransactionWithRetryCtx(ctx context.Context, callback dbTr
err = callback(sess)
// special handling of database locked errors for sqlite, then we can retry 3 times
// special handling of database locked errors for sqlite, then we can retry 5 times
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
if sqlError.Code == sqlite3.ErrLocked {
if sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy {
sess.Rollback()
time.Sleep(time.Millisecond * time.Duration(10))
sqlog.Info("Database table locked, sleeping then retrying", "retry", retry)
sqlog.Info("Database locked, sleeping then retrying", "error", err, "retry", retry)
return ss.inTransactionWithRetryCtx(ctx, callback, retry+1)
}
}

View File

@@ -25,12 +25,12 @@ func (ss *SqlStore) inTransactionWithRetry(ctx context.Context, fn func(ctx cont
err = fn(withValue)
// special handling of database locked errors for sqlite, then we can retry 3 times
// special handling of database locked errors for sqlite, then we can retry 5 times
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
if sqlError.Code == sqlite3.ErrLocked {
if sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy {
sess.Rollback()
time.Sleep(time.Millisecond * time.Duration(10))
ss.log.Info("Database table locked, sleeping then retrying", "retry", retry)
ss.log.Info("Database locked, sleeping then retrying", "error", err, "retry", retry)
return ss.inTransactionWithRetry(ctx, fn, retry+1)
}
}
@@ -69,12 +69,12 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc,
err = callback(sess)
// special handling of database locked errors for sqlite, then we can retry 3 times
// special handling of database locked errors for sqlite, then we can retry 5 times
if sqlError, ok := err.(sqlite3.Error); ok && retry < 5 {
if sqlError.Code == sqlite3.ErrLocked {
if sqlError.Code == sqlite3.ErrLocked || sqlError.Code == sqlite3.ErrBusy {
sess.Rollback()
time.Sleep(time.Millisecond * time.Duration(10))
sqlog.Info("Database table locked, sleeping then retrying", "retry", retry)
sqlog.Info("Database locked, sleeping then retrying", "error", err, "retry", retry)
return inTransactionWithRetry(callback, retry+1)
}
}

View File

@@ -216,8 +216,8 @@ func UpdateAuthInfo(cmd *m.UpdateAuthInfoCommand) error {
UserId: cmd.UserId,
AuthModule: cmd.AuthModule,
}
_, err := sess.Update(authUser, cond)
upd, err := sess.Update(authUser, cond)
sqlog.Debug("Updated user_auth", "user_id", cmd.UserId, "auth_module", cmd.AuthModule, "rows", upd)
return err
})
}

View File

@@ -86,14 +86,20 @@ var (
EnforceDomain bool
// Security settings.
SecretKey string
DisableGravatar bool
EmailCodeValidMinutes int
DataProxyWhiteList map[string]bool
DisableBruteForceLoginProtection bool
CookieSecure bool
CookieSameSite http.SameSite
AllowEmbedding bool
SecretKey string
DisableGravatar bool
EmailCodeValidMinutes int
DataProxyWhiteList map[string]bool
DisableBruteForceLoginProtection bool
CookieSecure bool
CookieSameSite http.SameSite
AllowEmbedding bool
XSSProtectionHeader bool
ContentTypeProtectionHeader bool
StrictTransportSecurity bool
StrictTransportSecurityMaxAge int
StrictTransportSecurityPreload bool
StrictTransportSecuritySubDomains bool
// Snapshots
ExternalSnapshotUrl string
@@ -693,6 +699,13 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
AllowEmbedding = security.Key("allow_embedding").MustBool(false)
ContentTypeProtectionHeader = security.Key("x_content_type_options").MustBool(false)
XSSProtectionHeader = security.Key("x_xss_protection").MustBool(false)
StrictTransportSecurity = security.Key("strict_transport_security").MustBool(false)
StrictTransportSecurityMaxAge = security.Key("strict_transport_security_max_age_seconds").MustInt(86400)
StrictTransportSecurityPreload = security.Key("strict_transport_security_preload").MustBool(false)
StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false)
// read snapshots settings
snapshots := iniFile.Section("snapshots")
ExternalSnapshotUrl, err = valueAsString(snapshots, "external_snapshot_url", "")

View File

@@ -85,14 +85,17 @@ func (e *AzureMonitorDatasource) buildQueries(queries []*tsdb.Query, timeRange *
azlog.Debug("AzureMonitor", "target", azureMonitorTarget)
urlComponents := map[string]string{}
urlComponents["subscription"] = fmt.Sprintf("%v", query.Model.Get("subscription").MustString())
urlComponents["resourceGroup"] = fmt.Sprintf("%v", azureMonitorTarget["resourceGroup"])
urlComponents["metricDefinition"] = fmt.Sprintf("%v", azureMonitorTarget["metricDefinition"])
urlComponents["resourceName"] = fmt.Sprintf("%v", azureMonitorTarget["resourceName"])
ub := urlBuilder{
ResourceGroup: urlComponents["resourceGroup"],
MetricDefinition: urlComponents["metricDefinition"],
ResourceName: urlComponents["resourceName"],
DefaultSubscription: query.DataSource.JsonData.Get("subscriptionId").MustString(),
Subscription: urlComponents["subscription"],
ResourceGroup: urlComponents["resourceGroup"],
MetricDefinition: urlComponents["metricDefinition"],
ResourceName: urlComponents["resourceName"],
}
azureURL := ub.Build()
@@ -199,8 +202,7 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo *mode
}
cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor")
subscriptionID := dsInfo.JsonData.Get("subscriptionId").MustString()
proxyPass := fmt.Sprintf("%s/subscriptions/%s", cloudName, subscriptionID)
proxyPass := fmt.Sprintf("%s/subscriptions", cloudName)
u, _ := url.Parse(dsInfo.Url)
u.Path = path.Join(u.Path, "render")

View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
@@ -27,7 +28,13 @@ func TestAzureMonitorDatasource(t *testing.T) {
},
Queries: []*tsdb.Query{
{
DataSource: &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"subscriptionId": "default-subscription",
}),
},
Model: simplejson.NewFromAny(map[string]interface{}{
"subscription": "12345678-aaaa-bbbb-cccc-123456789abc",
"azureMonitor": map[string]interface{}{
"timeGrain": "PT1M",
"aggregation": "Average",
@@ -49,7 +56,7 @@ func TestAzureMonitorDatasource(t *testing.T) {
So(len(queries), ShouldEqual, 1)
So(queries[0].RefID, ShouldEqual, "A")
So(queries[0].URL, ShouldEqual, "resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics")
So(queries[0].URL, ShouldEqual, "12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics")
So(queries[0].Target, ShouldEqual, "aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&timespan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z")
So(len(queries[0].Params), ShouldEqual, 5)
So(queries[0].Params["timespan"][0], ShouldEqual, "2018-03-15T13:00:00Z/2018-03-15T13:34:00Z")

View File

@@ -7,22 +7,30 @@ import (
// urlBuilder builds the URL for calling the Azure Monitor API
type urlBuilder struct {
ResourceGroup string
MetricDefinition string
ResourceName string
DefaultSubscription string
Subscription string
ResourceGroup string
MetricDefinition string
ResourceName string
}
// Build checks the metric definition property to see which form of the url
// should be returned
func (ub *urlBuilder) Build() string {
subscription := ub.Subscription
if ub.Subscription == "" {
subscription = ub.DefaultSubscription
}
if strings.Count(ub.MetricDefinition, "/") > 1 {
rn := strings.Split(ub.ResourceName, "/")
lastIndex := strings.LastIndex(ub.MetricDefinition, "/")
service := ub.MetricDefinition[lastIndex+1:]
md := ub.MetricDefinition[0:lastIndex]
return fmt.Sprintf("resourceGroups/%s/providers/%s/%s/%s/%s/providers/microsoft.insights/metrics", ub.ResourceGroup, md, rn[0], service, rn[1])
return fmt.Sprintf("%s/resourceGroups/%s/providers/%s/%s/%s/%s/providers/microsoft.insights/metrics", subscription, ub.ResourceGroup, md, rn[0], service, rn[1])
}
return fmt.Sprintf("resourceGroups/%s/providers/%s/%s/providers/microsoft.insights/metrics", ub.ResourceGroup, ub.MetricDefinition, ub.ResourceName)
return fmt.Sprintf("%s/resourceGroups/%s/providers/%s/%s/providers/microsoft.insights/metrics", subscription, ub.ResourceGroup, ub.MetricDefinition, ub.ResourceName)
}

View File

@@ -11,35 +11,51 @@ func TestURLBuilder(t *testing.T) {
Convey("when metric definition is in the short form", func() {
ub := &urlBuilder{
ResourceGroup: "rg",
MetricDefinition: "Microsoft.Compute/virtualMachines",
ResourceName: "rn",
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricDefinition: "Microsoft.Compute/virtualMachines",
ResourceName: "rn",
}
url := ub.Build()
So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
})
Convey("when metric definition is in the short form and a subscription is defined", func() {
ub := &urlBuilder{
DefaultSubscription: "default-sub",
Subscription: "specified-sub",
ResourceGroup: "rg",
MetricDefinition: "Microsoft.Compute/virtualMachines",
ResourceName: "rn",
}
url := ub.Build()
So(url, ShouldEqual, "specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics")
})
Convey("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func() {
ub := &urlBuilder{
ResourceGroup: "rg",
MetricDefinition: "Microsoft.Storage/storageAccounts/blobServices",
ResourceName: "rn1/default",
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricDefinition: "Microsoft.Storage/storageAccounts/blobServices",
ResourceName: "rn1/default",
}
url := ub.Build()
So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics")
})
Convey("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func() {
ub := &urlBuilder{
ResourceGroup: "rg",
MetricDefinition: "Microsoft.Storage/storageAccounts/fileServices",
ResourceName: "rn1/default",
DefaultSubscription: "default-sub",
ResourceGroup: "rg",
MetricDefinition: "Microsoft.Storage/storageAccounts/fileServices",
ResourceName: "rn1/default",
}
url := ub.Build()
So(url, ShouldEqual, "resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
So(url, ShouldEqual, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics")
})
})
}

View File

@@ -1,11 +1,28 @@
package errutil
import "golang.org/x/xerrors"
import (
"fmt"
"golang.org/x/xerrors"
)
// Wrap is a simple wrapper around Errorf that is doing error wrapping. You can read how that works in
// https://godoc.org/golang.org/x/xerrors#Errorf but its API is very implicit which is a reason for this wrapper.
// There is also a discussion (https://github.com/golang/go/issues/29934) where many comments make arguments for such
// wrapper so hopefully it will be added in the standard lib later.
func Wrap(message string, err error) error {
if err == nil {
return nil
}
return xerrors.Errorf("%v: %w", message, err)
}
// Wrapf is a simple wrapper around Errorf that is doing error wrapping
// Wrapf allows you to send a format and args instead of just a message.
func Wrapf(err error, message string, a ...interface{}) error {
if err == nil {
return nil
}
return Wrap(fmt.Sprintf(message, a...), err)
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"math"
"regexp"
"strings"
"time"
)
@@ -66,3 +67,19 @@ func GetAgeString(t time.Time) string {
return "< 1m"
}
// ToCamelCase changes kebab case, snake case or mixed strings to camel case. See unit test for examples.
func ToCamelCase(str string) string {
var finalParts []string
parts := strings.Split(str, "_")
for _, part := range parts {
finalParts = append(finalParts, strings.Split(part, "-")...)
}
for index, part := range finalParts[1:] {
finalParts[index+1] = strings.Title(part)
}
return strings.Join(finalParts, "")
}

View File

@@ -37,3 +37,12 @@ func TestDateAge(t *testing.T) {
So(GetAgeString(time.Now().Add(-time.Hour*24*409)), ShouldEqual, "1y")
})
}
func TestToCamelCase(t *testing.T) {
Convey("ToCamelCase", t, func() {
So(ToCamelCase("kebab-case-string"), ShouldEqual, "kebabCaseString")
So(ToCamelCase("snake_case_string"), ShouldEqual, "snakeCaseString")
So(ToCamelCase("mixed-case_string"), ShouldEqual, "mixedCaseString")
So(ToCamelCase("alreadyCamelCase"), ShouldEqual, "alreadyCamelCase")
})
}

View File

@@ -13,8 +13,6 @@ export class HelpCtrl {
{ keys: ['g', 'h'], description: 'Go to Home Dashboard' },
{ keys: ['g', 'p'], description: 'Go to Profile' },
{ keys: ['s', 'o'], description: 'Open search' },
{ keys: ['s', 's'], description: 'Open search with starred filter' },
{ keys: ['s', 't'], description: 'Open search in tags view' },
{ keys: ['esc'], description: 'Exit edit/setting views' },
],
Dashboard: [

View File

@@ -32,6 +32,10 @@ class SearchQueryParser {
}
}
interface OpenSearchParams {
query?: string;
}
export class SearchCtrl {
isOpen: boolean;
query: SearchQuery;
@@ -88,7 +92,7 @@ export class SearchCtrl {
appEvents.emit('search-query');
}
openSearch(evt, payload) {
openSearch(payload: OpenSearchParams = {}) {
if (this.isOpen) {
this.closeSearch();
return;
@@ -99,19 +103,16 @@ export class SearchCtrl {
this.selectedIndex = -1;
this.results = [];
this.query = {
query: evt ? `${evt.query} ` : '',
parsedQuery: this.queryParser.parse(evt && evt.query),
query: payload.query ? `${payload.query} ` : '',
parsedQuery: this.queryParser.parse(payload.query),
tags: [],
starred: false,
};
this.currentSearchId = 0;
this.ignoreClose = true;
this.isLoading = true;
if (payload && payload.starred) {
this.query.starred = true;
}
this.$timeout(() => {
this.ignoreClose = false;
this.giveSearchFocus = true;

View File

@@ -356,7 +356,12 @@ export function seriesDataToLogsModel(seriesData: SeriesData[], intervalMs: numb
return logsModel;
}
return undefined;
return {
hasUniqueLabels: false,
rows: [],
meta: [],
series: [],
};
}
export function logSeriesToLogsModel(logSeries: SeriesData[]): LogsModel {

View File

@@ -43,21 +43,11 @@ export class KeybindingSrv {
this.bind('g h', this.goToHome);
this.bind('g a', this.openAlerting);
this.bind('g p', this.goToProfile);
this.bind('s s', this.openSearchStarred);
this.bind('s o', this.openSearch);
this.bind('s t', this.openSearchTags);
this.bind('f', this.openSearch);
this.bindGlobal('esc', this.exit);
}
openSearchStarred() {
appEvents.emit('show-dash-search', { starred: true });
}
openSearchTags() {
appEvents.emit('show-dash-search', { tagsMode: true });
}
openSearch() {
appEvents.emit('show-dash-search');
}

View File

@@ -92,6 +92,7 @@ describe('file_export', () => {
[0x123, 'some string with \n in the middle', 10.01, false],
[0b1011, 'some string with ; in the middle', -12.34, true],
[123, 'some string with ;; in the middle', -12.34, true],
[1234, '=a bogus formula ', '-and another', '+another', '@ref'],
],
};
@@ -108,7 +109,8 @@ describe('file_export', () => {
'501;"some string with "" at the end""";0.01;false\r\n' +
'291;"some string with \n in the middle";10.01;false\r\n' +
'11;"some string with ; in the middle";-12.34;true\r\n' +
'123;"some string with ;; in the middle";-12.34;true';
'123;"some string with ;; in the middle";-12.34;true\r\n' +
'1234;"\'=a bogus formula";"\'-and another";"\'+another";"\'@ref"';
expect(returnedText).toBe(expectedText);
});

View File

@@ -333,22 +333,29 @@ describe('LogsParsers', () => {
});
});
const emptyLogsModel = {
hasUniqueLabels: false,
rows: [],
meta: [],
series: [],
};
describe('seriesDataToLogsModel', () => {
it('given empty series should return undefined', () => {
expect(seriesDataToLogsModel([] as SeriesData[], 0)).toBeUndefined();
it('given empty series should return empty logs model', () => {
expect(seriesDataToLogsModel([] as SeriesData[], 0)).toMatchObject(emptyLogsModel);
});
it('given series without correct series name should not be processed', () => {
it('given series without correct series name should return empty logs model', () => {
const series: SeriesData[] = [
{
fields: [],
rows: [],
},
];
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
});
it('given series without a time field should not be processed', () => {
it('given series without a time field should return empty logs model', () => {
const series: SeriesData[] = [
{
fields: [
@@ -360,10 +367,10 @@ describe('seriesDataToLogsModel', () => {
rows: [],
},
];
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
});
it('given series without a string field should not be processed', () => {
it('given series without a string field should return empty logs model', () => {
const series: SeriesData[] = [
{
fields: [
@@ -375,7 +382,7 @@ describe('seriesDataToLogsModel', () => {
rows: [],
},
];
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
});
it('given one series should return expected logs model', () => {

View File

@@ -17,7 +17,11 @@ function csvEscaped(text) {
return text;
}
return text.split(QUOTE).join(QUOTE + QUOTE);
return text
.split(QUOTE)
.join(QUOTE + QUOTE)
.replace(/^([-+=@])/, "'$1")
.replace(/\s+$/, '');
}
const domParser = new DOMParser();

View File

@@ -212,7 +212,7 @@ exports[`ServerStats Should render table with stats 1`] = `
</div>
</div>
<div
className="css-17l4171 track-horizontal"
className="css-52gpmd track-horizontal"
style={
Object {
"display": "none",
@@ -233,7 +233,7 @@ exports[`ServerStats Should render table with stats 1`] = `
/>
</div>
<div
className="css-17l4171 track-vertical"
className="css-52gpmd track-vertical"
style={
Object {
"display": "none",

View File

@@ -60,17 +60,14 @@ export class DashNav extends PureComponent<Props> {
}
}
onOpenSearch = () => {
const { dashboard } = this.props;
const haveFolder = dashboard.meta.folderId > 0;
appEvents.emit(
'show-dash-search',
haveFolder
? {
query: 'folder:current',
}
: null
);
onDahboardNameClick = () => {
appEvents.emit('show-dash-search');
};
onFolderNameClick = () => {
appEvents.emit('show-dash-search', {
query: 'folder:current',
});
};
onClose = () => {
@@ -148,11 +145,20 @@ export class DashNav extends PureComponent<Props> {
return (
<>
<div>
<a className="navbar-page-btn" onClick={this.onOpenSearch}>
<div className="navbar-page-btn">
{!this.isInFullscreenOrSettings && <i className="gicon gicon-dashboard" />}
{haveFolder && <span className="navbar-page-btn--folder">{folderTitle} / </span>}
{dashboard.title} <i className="fa fa-caret-down" />
</a>
{haveFolder && (
<>
<a className="navbar-page-btn__folder" onClick={this.onFolderNameClick}>
{folderTitle}
</a>
<i className="fa fa-chevron-right navbar-page-btn__folder-icon" />
</>
)}
<a onClick={this.onDahboardNameClick}>
{dashboard.title} <i className="fa fa-caret-down navbar-page-btn__search" />
</a>
</div>
</div>
{this.isSettings && <span className="navbar-settings-title">&nbsp;/ Settings</span>}
<div className="navbar__spacer" />

View File

@@ -61,6 +61,7 @@ export interface State {
isFullscreen: boolean;
fullscreenPanel: PanelModel | null;
scrollTop: number;
updateScrollTop: number;
rememberScrollTop: number;
showLoadingState: boolean;
}
@@ -73,6 +74,7 @@ export class DashboardPage extends PureComponent<Props, State> {
showLoadingState: false,
fullscreenPanel: null,
scrollTop: 0,
updateScrollTop: null,
rememberScrollTop: 0,
};
@@ -168,7 +170,7 @@ export class DashboardPage extends PureComponent<Props, State> {
isEditing: false,
isFullscreen: false,
fullscreenPanel: null,
scrollTop: this.state.rememberScrollTop,
updateScrollTop: this.state.rememberScrollTop,
},
this.triggerPanelsRendering.bind(this)
);
@@ -204,7 +206,7 @@ export class DashboardPage extends PureComponent<Props, State> {
setScrollTop = (e: MouseEvent<HTMLElement>): void => {
const target = e.target as HTMLElement;
this.setState({ scrollTop: target.scrollTop });
this.setState({ scrollTop: target.scrollTop, updateScrollTop: null });
};
onAddPanel = () => {
@@ -251,7 +253,7 @@ export class DashboardPage extends PureComponent<Props, State> {
render() {
const { dashboard, editview, $injector, isInitSlow, initError } = this.props;
const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state;
const { isSettingsOpening, isEditing, isFullscreen, scrollTop, updateScrollTop } = this.state;
if (!dashboard) {
if (isInitSlow) {
@@ -285,9 +287,9 @@ export class DashboardPage extends PureComponent<Props, State> {
/>
<div className="scroll-canvas scroll-canvas--dashboard">
<CustomScrollbar
autoHeightMin={'100%'}
autoHeightMin="100%"
setScrollTop={this.setScrollTop}
scrollTop={scrollTop}
scrollTop={updateScrollTop}
updateAfterMountMs={500}
className="custom-scrollbar--page"
>

View File

@@ -111,7 +111,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
autoHideTimeout={200}
className="custom-scrollbar--page"
hideTracksWhenNotNeeded={false}
scrollTop={0}
scrollTop={null}
setScrollTop={[Function]}
updateAfterMountMs={500}
>
@@ -349,7 +349,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
autoHideTimeout={200}
className="custom-scrollbar--page"
hideTracksWhenNotNeeded={false}
scrollTop={0}
scrollTop={null}
setScrollTop={[Function]}
updateAfterMountMs={500}
>

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { DashboardGrid, Props } from './DashboardGrid';
import { DashboardModel } from '../state';
interface ScenarioContext {
props: Props;
wrapper?: ShallowWrapper<Props, any, DashboardGrid>;
setup?: (fn: () => void) => void;
setProps: (props: Partial<Props>) => void;
}
function getTestDashboard(overrides?: any, metaOverrides?: any): DashboardModel {
const data = Object.assign(
{
title: 'My dashboard',
panels: [
{
id: 1,
type: 'graph',
title: 'My graph',
gridPos: { x: 0, y: 0, w: 24, h: 10 },
},
{
id: 2,
type: 'graph2',
title: 'My graph2',
gridPos: { x: 0, y: 10, w: 25, h: 10 },
},
{
id: 3,
type: 'graph3',
title: 'My graph3',
gridPos: { x: 0, y: 20, w: 25, h: 100 },
},
{
id: 4,
type: 'graph4',
title: 'My graph4',
gridPos: { x: 0, y: 120, w: 25, h: 10 },
},
],
},
overrides
);
const meta = Object.assign({ canSave: true, canEdit: true }, metaOverrides);
return new DashboardModel(data, meta);
}
function dashboardGridScenario(description, scenarioFn: (ctx: ScenarioContext) => void) {
describe(description, () => {
let setupFn: () => void;
const ctx: ScenarioContext = {
setup: fn => {
setupFn = fn;
},
props: {
isEditing: false,
isFullscreen: false,
scrollTop: null,
dashboard: getTestDashboard(),
},
setProps: (props: Partial<Props>) => {
Object.assign(ctx.props, props);
if (ctx.wrapper) {
ctx.wrapper.setProps(ctx.props);
}
},
};
beforeEach(() => {
setupFn();
ctx.wrapper = shallow(<DashboardGrid {...ctx.props} />);
});
scenarioFn(ctx);
});
}
describe('DashboardGrid', () => {
dashboardGridScenario('Can render dashboard grid', ctx => {
ctx.setup(() => {});
it('Should render', () => {
expect(ctx.wrapper).toMatchSnapshot();
});
});
});

View File

@@ -205,7 +205,7 @@ export class DashboardGrid extends PureComponent<Props> {
return false;
}
const top = parseInt(elem.style.top.replace('px', ''), 10);
const top = elem.offsetTop;
const height = panel.gridPos.h * GRID_CELL_HEIGHT + 40;
const bottom = top + height;

View File

@@ -1,4 +0,0 @@
import { react2AngularDirective } from 'app/core/utils/react2angular';
import DashboardGrid from './DashboardGrid';
react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);

View File

@@ -250,9 +250,10 @@ export class PanelChrome extends PureComponent<Props, State> {
{loading === LoadingState.Loading && this.renderLoadingState()}
<div className="panel-content">
<PanelComponent
id={panel.id}
data={data}
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
options={panel.getOptions(plugin.defaults)}
options={panel.getOptions()}
width={width - theme.panelPadding * 2}
height={innerPanelHeight}
renderCounter={renderCounter}

View File

@@ -81,16 +81,21 @@ export class PanelHeader extends Component<Props, State> {
return (
<>
<PanelHeaderCorner
panel={panel}
title={panel.title}
description={panel.description}
scopedVars={panel.scopedVars}
links={panel.links}
error={error}
/>
<div className={panelHeaderClass}>
<div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
<PanelHeaderCorner
panel={panel}
title={panel.title}
description={panel.description}
scopedVars={panel.scopedVars}
links={panel.links}
error={error}
/>
<div
className="panel-title-container"
onClick={this.onMenuToggle}
onMouseDown={this.onMouseDown}
aria-label="Panel Title"
>
<div className="panel-title">
<span className="icon-gf panel-alert-icon" />
<span className="panel-title-text">

View File

@@ -0,0 +1,996 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
<SizeMe(GridWrapper)
className="layout"
isDraggable={true}
isFullscreen={false}
isResizable={true}
layout={
Array [
Object {
"h": 10,
"i": "1",
"w": 24,
"x": 0,
"y": 0,
},
Object {
"h": 10,
"i": "2",
"w": 25,
"x": 0,
"y": 10,
},
Object {
"h": 100,
"i": "3",
"w": 25,
"x": 0,
"y": 20,
},
Object {
"h": 10,
"i": "4",
"w": 25,
"x": 0,
"y": 120,
},
]
}
onDragStop={[Function]}
onLayoutChange={[Function]}
onResize={[Function]}
onResizeStop={[Function]}
onWidthChange={[Function]}
>
<div
className=""
id="panel-1"
key="1"
>
<DashboardPanel
dashboard={
DashboardModel {
"annotations": Object {
"list": Array [
Object {
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
],
},
"autoUpdate": undefined,
"description": undefined,
"editable": true,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {
"panel-added": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"panel-removed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"repeats-processed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"row-collapsed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"row-expanded": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"view-mode-changed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
},
"_eventsCount": 6,
},
},
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": Array [],
"meta": Object {
"canEdit": true,
"canMakeEditable": false,
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
"originalTime": Object {
"from": "now-6h",
"to": "now",
},
"panels": Array [
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 24,
"x": 0,
"y": 0,
},
"id": 1,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph",
"transparent": false,
"type": "graph",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 10,
},
"id": 2,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph2",
"transparent": false,
"type": "graph2",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 100,
"w": 25,
"x": 0,
"y": 20,
},
"id": 3,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph3",
"transparent": false,
"type": "graph3",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 120,
},
"id": 4,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph4",
"transparent": false,
"type": "graph4",
},
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
"templating": Object {
"list": Array [],
},
"time": Object {
"from": "now-6h",
"to": "now",
},
"timepicker": Object {},
"timezone": "",
"title": "My dashboard",
"uid": null,
"version": 0,
}
}
isInView={false}
panel={
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 24,
"x": 0,
"y": 0,
},
"id": 1,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph",
"transparent": false,
"type": "graph",
}
}
/>
</div>
<div
className=""
id="panel-2"
key="2"
>
<DashboardPanel
dashboard={
DashboardModel {
"annotations": Object {
"list": Array [
Object {
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
],
},
"autoUpdate": undefined,
"description": undefined,
"editable": true,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {
"panel-added": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"panel-removed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"repeats-processed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"row-collapsed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"row-expanded": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"view-mode-changed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
},
"_eventsCount": 6,
},
},
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": Array [],
"meta": Object {
"canEdit": true,
"canMakeEditable": false,
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
"originalTime": Object {
"from": "now-6h",
"to": "now",
},
"panels": Array [
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 24,
"x": 0,
"y": 0,
},
"id": 1,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph",
"transparent": false,
"type": "graph",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 10,
},
"id": 2,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph2",
"transparent": false,
"type": "graph2",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 100,
"w": 25,
"x": 0,
"y": 20,
},
"id": 3,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph3",
"transparent": false,
"type": "graph3",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 120,
},
"id": 4,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph4",
"transparent": false,
"type": "graph4",
},
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
"templating": Object {
"list": Array [],
},
"time": Object {
"from": "now-6h",
"to": "now",
},
"timepicker": Object {},
"timezone": "",
"title": "My dashboard",
"uid": null,
"version": 0,
}
}
isInView={false}
panel={
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 10,
},
"id": 2,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph2",
"transparent": false,
"type": "graph2",
}
}
/>
</div>
<div
className=""
id="panel-3"
key="3"
>
<DashboardPanel
dashboard={
DashboardModel {
"annotations": Object {
"list": Array [
Object {
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
],
},
"autoUpdate": undefined,
"description": undefined,
"editable": true,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {
"panel-added": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"panel-removed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"repeats-processed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"row-collapsed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"row-expanded": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"view-mode-changed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
},
"_eventsCount": 6,
},
},
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": Array [],
"meta": Object {
"canEdit": true,
"canMakeEditable": false,
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
"originalTime": Object {
"from": "now-6h",
"to": "now",
},
"panels": Array [
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 24,
"x": 0,
"y": 0,
},
"id": 1,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph",
"transparent": false,
"type": "graph",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 10,
},
"id": 2,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph2",
"transparent": false,
"type": "graph2",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 100,
"w": 25,
"x": 0,
"y": 20,
},
"id": 3,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph3",
"transparent": false,
"type": "graph3",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 120,
},
"id": 4,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph4",
"transparent": false,
"type": "graph4",
},
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
"templating": Object {
"list": Array [],
},
"time": Object {
"from": "now-6h",
"to": "now",
},
"timepicker": Object {},
"timezone": "",
"title": "My dashboard",
"uid": null,
"version": 0,
}
}
isInView={false}
panel={
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 100,
"w": 25,
"x": 0,
"y": 20,
},
"id": 3,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph3",
"transparent": false,
"type": "graph3",
}
}
/>
</div>
<div
className=""
id="panel-4"
key="4"
>
<DashboardPanel
dashboard={
DashboardModel {
"annotations": Object {
"list": Array [
Object {
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard",
},
],
},
"autoUpdate": undefined,
"description": undefined,
"editable": true,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {
"panel-added": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"panel-removed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"repeats-processed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"row-collapsed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"row-expanded": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
"view-mode-changed": EE {
"context": [Circular],
"fn": [Function],
"once": false,
},
},
"_eventsCount": 6,
},
},
"gnetId": null,
"graphTooltip": 0,
"id": null,
"links": Array [],
"meta": Object {
"canEdit": true,
"canMakeEditable": false,
"canSave": true,
"canShare": true,
"canStar": true,
"fullscreen": false,
"isEditing": false,
"showSettings": true,
},
"originalTemplating": Array [],
"originalTime": Object {
"from": "now-6h",
"to": "now",
},
"panels": Array [
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 24,
"x": 0,
"y": 0,
},
"id": 1,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph",
"transparent": false,
"type": "graph",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 10,
},
"id": 2,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph2",
"transparent": false,
"type": "graph2",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 100,
"w": 25,
"x": 0,
"y": 20,
},
"id": 3,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph3",
"transparent": false,
"type": "graph3",
},
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 120,
},
"id": 4,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph4",
"transparent": false,
"type": "graph4",
},
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
"templating": Object {
"list": Array [],
},
"time": Object {
"from": "now-6h",
"to": "now",
},
"timepicker": Object {},
"timezone": "",
"title": "My dashboard",
"uid": null,
"version": 0,
}
}
isInView={false}
panel={
PanelModel {
"cachedPluginOptions": Object {},
"datasource": null,
"events": Emitter {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
},
"gridPos": Object {
"h": 10,
"w": 25,
"x": 0,
"y": 120,
},
"id": 4,
"isInView": false,
"targets": Array [
Object {
"refId": "A",
},
],
"title": "My graph4",
"transparent": false,
"type": "graph4",
}
}
/>
</div>
</SizeMe(GridWrapper)>
`;

View File

@@ -1,5 +1,3 @@
import './dashgrid/DashboardGridDirective';
// Services
import './services/UnsavedChangesSrv';
import './services/DashboardLoaderSrv';

View File

@@ -53,8 +53,8 @@ export class VisualizationTab extends PureComponent<Props, State> {
}
getReactPanelOptions = () => {
const { panel, plugin } = this.props;
return panel.getOptions(plugin.defaults);
const { panel } = this.props;
return panel.getOptions();
};
renderPanelOptions() {

View File

@@ -7,45 +7,70 @@ describe('PanelModel', () => {
describe('when creating new panel model', () => {
let model;
let modelJson;
let persistedOptionsMock;
const defaultOptionsMock = {
fieldOptions: {
thresholds: [
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: null,
},
],
},
showThresholds: true,
};
beforeEach(() => {
persistedOptionsMock = {
fieldOptions: {
thresholds: [
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: null,
},
],
},
};
modelJson = {
type: 'table',
showColumns: true,
targets: [{ refId: 'A' }, { noRefId: true }],
options: {
fieldOptions: {
thresholds: [
{
color: '#F2495C',
index: 1,
value: 50,
},
{
color: '#73BF69',
index: 0,
value: null,
},
],
},
},
options: persistedOptionsMock,
};
model = new PanelModel(modelJson);
model.pluginLoaded(
getPanelPlugin(
{
id: 'table',
},
null, // react
TablePanelCtrl // angular
)
const panelPlugin = getPanelPlugin(
{
id: 'table',
},
null, // react
TablePanelCtrl // angular
);
panelPlugin.setDefaults(defaultOptionsMock);
model.pluginLoaded(panelPlugin);
});
it('should apply defaults', () => {
expect(model.gridPos.h).toBe(3);
});
it('should apply option defaults', () => {
expect(model.getOptions().showThresholds).toBeTruthy();
});
it('should set model props on instance', () => {
expect(model.showColumns).toBe(true);
});
@@ -89,11 +114,22 @@ describe('PanelModel', () => {
});
describe('when changing panel type', () => {
const newPanelPluginDefaults = {
showThresholdLabels: false,
};
beforeEach(() => {
model.changePlugin(getPanelPlugin({ id: 'graph' }));
const newPlugin = getPanelPlugin({ id: 'graph' });
newPlugin.setDefaults(newPanelPluginDefaults);
model.changePlugin(newPlugin);
model.alert = { id: 2 };
});
it('should apply next panel option defaults', () => {
expect(model.getOptions().showThresholdLabels).toBeFalsy();
expect(model.getOptions().showThresholds).toBeUndefined();
});
it('should remove table properties but keep core props', () => {
expect(model.showColumns).toBe(undefined);
});
@@ -153,19 +189,5 @@ describe('PanelModel', () => {
expect(panelQueryRunner).toBe(sameQueryRunner);
});
});
describe('get panel options', () => {
it('should apply defaults', () => {
model.options = { existingProp: 10 };
const options = model.getOptions({
defaultProp: true,
existingProp: 0,
});
expect(options.defaultProp).toBe(true);
expect(options.existingProp).toBe(10);
expect(model.options).toBe(options);
});
});
});
});

View File

@@ -157,8 +157,8 @@ export class PanelModel {
}
}
getOptions(panelDefaults: any) {
return _.defaultsDeep(this.options || {}, panelDefaults);
getOptions() {
return this.options;
}
updateOptions(options: object) {
@@ -179,7 +179,6 @@ export class PanelModel {
model[property] = _.cloneDeep(this[property]);
}
return model;
}
@@ -247,9 +246,18 @@ export class PanelModel {
});
}
private applyPluginOptionDefaults(plugin: PanelPlugin) {
if (plugin.angularConfigCtrl) {
return;
}
this.options = _.defaultsDeep({}, this.options || {}, plugin.defaults);
}
pluginLoaded(plugin: PanelPlugin) {
this.plugin = plugin;
this.applyPluginOptionDefaults(plugin);
if (plugin.panel && plugin.onPanelMigration) {
const version = getPluginVersion(plugin);
if (version !== this.pluginVersion) {
@@ -284,7 +292,7 @@ export class PanelModel {
// switch
this.type = pluginId;
this.plugin = newPlugin;
this.applyPluginOptionDefaults(newPlugin);
// Let panel plugins inspect options from previous panel and keep any that it can use
if (newPlugin.onPanelTypeChanged) {
this.options = this.options || {};
@@ -324,7 +332,7 @@ export class PanelModel {
}
hasTitle() {
return !!this.title.length;
return this.title && this.title.length > 0;
}
destroy() {

View File

@@ -97,9 +97,6 @@ export class PanelQueryRunner {
delayStateNotification,
} = options;
// filter out hidden queries & deep clone them
const clonedAndFilteredQueries = cloneDeep(queries.filter(q => !q.hide));
const request: DataQueryRequest = {
requestId: getNextRequestId(),
timezone,
@@ -109,7 +106,7 @@ export class PanelQueryRunner {
timeInfo,
interval: '',
intervalMs: 0,
targets: clonedAndFilteredQueries,
targets: cloneDeep(queries),
maxDataPoints: maxDataPoints || widthPixels,
scopedVars: scopedVars || {},
cacheTimeout,
@@ -124,6 +121,10 @@ export class PanelQueryRunner {
try {
const ds = await getDataSource(datasource, request.scopedVars);
if (ds.meta && !ds.meta.hiddenQueries) {
request.targets = request.targets.filter(q => !q.hide);
}
// Attach the datasource name to each query
request.targets = request.targets.map(query => {
if (!query.datasource) {

View File

@@ -275,7 +275,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
<LogsContainer
width={width}
exploreId={exploreId}
onChangeTime={this.onChangeTime}
onClickLabel={this.onClickLabel}
onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning}

View File

@@ -1,13 +1,14 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import moment from 'moment';
import { RawTimeRange, TimeRange, LogLevel, TimeZone, AbsoluteTimeRange } from '@grafana/ui';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
import { StoreState } from 'app/types';
import { toggleLogs, changeDedupStrategy } from './state/actions';
import { toggleLogs, changeDedupStrategy, changeTime } from './state/actions';
import Logs from './Logs';
import Panel from './Panel';
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
@@ -20,7 +21,6 @@ interface LogsContainerProps {
logsHighlighterExpressions?: string[];
logsResult?: LogsModel;
dedupedResult?: LogsModel;
onChangeTime: (range: AbsoluteTimeRange) => void;
onClickLabel: (key: string, value: string) => void;
onStartScanning: () => void;
onStopScanning: () => void;
@@ -35,9 +35,19 @@ interface LogsContainerProps {
dedupStrategy: LogsDedupStrategy;
hiddenLogLevels: Set<LogLevel>;
width: number;
changeTime: typeof changeTime;
}
export class LogsContainer extends PureComponent<LogsContainerProps> {
onChangeTime = (absRange: AbsoluteTimeRange) => {
const { exploreId, timeZone, changeTime } = this.props;
const range = {
from: timeZone.isUtc ? moment.utc(absRange.from) : moment(absRange.from),
to: timeZone.isUtc ? moment.utc(absRange.to) : moment(absRange.to),
};
changeTime(exploreId, range);
};
onClickLogsButton = () => {
this.props.toggleLogs(this.props.exploreId, this.props.showingLogs);
};
@@ -61,7 +71,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
logsHighlighterExpressions,
logsResult,
dedupedResult,
onChangeTime,
onClickLabel,
onStartScanning,
onStopScanning,
@@ -83,7 +92,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
exploreId={exploreId}
highlighterExpressions={logsHighlighterExpressions}
loading={loading}
onChangeTime={onChangeTime}
onChangeTime={this.onChangeTime}
onClickLabel={onClickLabel}
onStartScanning={onStartScanning}
onStopScanning={onStopScanning}
@@ -130,6 +139,7 @@ const mapDispatchToProps = {
toggleLogs,
changeDedupStrategy,
toggleLogLevelAction,
changeTime,
};
export default hot(module)(

View File

@@ -26,7 +26,7 @@ export default class Table extends PureComponent<TableProps> {
if (e.target) {
const link = e.target as HTMLElement;
if (link.className === 'link') {
const columnKey = column.Header;
const columnKey = column.Header().props.title;
const rowValue = rowInfo.row[columnKey];
this.props.onClickCell(columnKey, rowValue);
}

View File

@@ -220,6 +220,7 @@ export interface LoadExploreDataSourcesPayload {
export interface RunQueriesPayload {
exploreId: ExploreId;
range: TimeRange;
}
/**

View File

@@ -559,6 +559,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
supportsTable,
datasourceError,
containerWidth,
range,
} = getState().explore[exploreId];
if (datasourceError) {
@@ -576,7 +577,10 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
// but we're using the datasource interval limit for now
const interval = datasourceInstance.interval;
dispatch(runQueriesAction({ exploreId }));
const timeZone = getTimeZone(getState().user);
const updatedRange = getTimeRange(timeZone, range.raw);
dispatch(runQueriesAction({ exploreId, range: updatedRange }));
// Keep table queries first since they need to return quickly
const tableQueriesPromise =
(ignoreUIState || showingTable) && supportsTable

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