Compare commits

...

73 Commits

Author SHA1 Message Date
Kyle Brandt
6082d19825 release 6.2.5 2019-06-25 13:56:19 -04:00
Tobias Skarhed
8a68c29e54 Panel: Fully escape html in drilldown links (was only sanitized before) (#17731)
* Sanitize HTML

* Replace sanitization lib and check for config

* Add htmlToText

* Refactor: Renaming htmlToText to escapeHtml

(cherry picked from commit 75c29566a6 -
changed TS types LinkSrv templateSrv to any)
2019-06-25 13:56:19 -04:00
gotjosh
ca5acbe6f2 Grafana-CLI: Wrapper for grafana-cli within RPM/DEB packages and config/homepath are now global flags (#17695)
* Feature: Introduce a grafana-cli wrapper

When our users install the *nix packed version of grafana, tendency is to use the services and scripts installed as part of the package for grafana-server. These leverage the default configuration options by specifying the several default paths.

This introduces a similar approach for the grafana-cli binary. We exposed it through a wrapper to ensure a proper configuration is in place. To enable that, we add the .real suffix to the original binary (grafana-cli.real) and then use a bash script named grafana-cli as the wrapper.

* Make the config and homepath flags global

* Introduce `configOverrides` as a global flag

This flag allows us to pass configuration overrides as a string.

The string follows the convention of configuration arguments separated by a space e.g. "cfg:default.paths.data=/dev/nullX cfg:default.paths.logs=/dev/nullX"

Also, it is backwards compatible with similar the previous configuration method through tailing arguments. Tailing arguments take presedence over the configuration options string.

* Only log configuration information in debug mode

* Move the grafana-cli binary to $GRAFANA_HOME/bin

As part of the package install process, we copy all the release files and
directories into the grafana home directory. This includes the /bin folder
from where we copied the binaries into their respective destinations.
After that, the /bin folder gets deleted as we don't want to keep
duplicates of the binaries around.

As part of this commit, we moved the re-creation of /bin within
grafana-home and the copy of the original binary (again) after the
folder gets deleted.

(cherry picked from commit 2fb45eeec8)
2019-06-25 13:56:19 -04:00
Kyle Brandt
cb6e6de6a7 config: fix connstr for remote_cache (#17675)
fixes #17643 and adds test to check for commented out lines (but will only catch `;`, not `#`).

(cherry picked from commit 49f0f0e89e)
2019-06-25 13:56:19 -04:00
Ryan McKinley
095c4cab6a TablePanel: fix annotations display (#17646)
(cherry picked from commit 35e1524b02)
2019-06-25 13:56:19 -04:00
Kyle Brandt
b80ed6e371 middleware: fix Strict-Transport-Security header (#17644)
fixes #17641

(cherry picked from commit bd08d8ce8e)
2019-06-25 13:56:19 -04:00
Dave
b56b7d459f Elasticsearch: Fix empty query request to send properly (#17488)
* ensure that empty Elasticsearch queries are properly set to *.  Fixes issue that appears when passing raw data from variables.

* combine null check and empty check into one

(cherry picked from commit c78b6e2a67)
2019-06-25 13:56:19 -04:00
Josue Abreu
ee01897049 release 6.2.4 2019-06-18 11:39:19 +01:00
gotjosh
bf260f71be grafana-cli: Fix receiving flags via command line (#17617)
`grafana-cli` uses the third-party library to define the flags and not
the standard library. Using `flag.Parse` conflicts with the defined
flags from our third-party library.

In the case where `flag.Parse` is used, the CLI assumes that definitions
provided are not needed and will not define them; producing errors of
the kind `flag provided but not defined --example-flag`.

Using the context to read any arguments (including flags) is the
recommended approach by the third-party library.

(cherry picked from commit 7d68d6ede2)
2019-06-18 11:39:19 +01:00
Yann Verry
3232ea4552 HTTPServer: Fix X-XSS-Protection header formatting (#17620)
Fixes #17619

(cherry picked from commit ed613194ac)
2019-06-18 11:39:19 +01:00
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
147 changed files with 5745 additions and 2125 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

@@ -43,7 +43,9 @@ var (
workingDir string
includeBuildId bool = true
buildId string = "0"
binaries []string = []string{"grafana-server", "grafana-cli"}
serverBinary string = "grafana-server"
cliBinary string = "grafana-cli"
binaries []string = []string{serverBinary, cliBinary}
isDev bool = false
enterprise bool = false
skipRpmGen bool = false
@@ -230,6 +232,7 @@ type linuxPackageOptions struct {
packageType string
packageArch string
homeDir string
homeBinDir string
binPath string
serverBinPath string
cliBinPath string
@@ -240,10 +243,11 @@ type linuxPackageOptions struct {
initdScriptFilePath string
systemdServiceFilePath string
postinstSrc string
initdScriptSrc string
defaultFileSrc string
systemdFileSrc string
postinstSrc string
initdScriptSrc string
defaultFileSrc string
systemdFileSrc string
cliBinaryWrapperSrc string
depends []string
}
@@ -258,6 +262,7 @@ func createDebPackages() {
packageType: "deb",
packageArch: debPkgArch,
homeDir: "/usr/share/grafana",
homeBinDir: "/usr/share/grafana/bin",
binPath: "/usr/sbin",
configDir: "/etc/grafana",
etcDefaultPath: "/etc/default",
@@ -265,10 +270,11 @@ func createDebPackages() {
initdScriptFilePath: "/etc/init.d/grafana-server",
systemdServiceFilePath: "/usr/lib/systemd/system/grafana-server.service",
postinstSrc: "packaging/deb/control/postinst",
initdScriptSrc: "packaging/deb/init.d/grafana-server",
defaultFileSrc: "packaging/deb/default/grafana-server",
systemdFileSrc: "packaging/deb/systemd/grafana-server.service",
postinstSrc: "packaging/deb/control/postinst",
initdScriptSrc: "packaging/deb/init.d/grafana-server",
defaultFileSrc: "packaging/deb/default/grafana-server",
systemdFileSrc: "packaging/deb/systemd/grafana-server.service",
cliBinaryWrapperSrc: "packaging/wrappers/grafana-cli",
depends: []string{"adduser", "libfontconfig1"},
})
@@ -286,6 +292,7 @@ func createRpmPackages() {
packageType: "rpm",
packageArch: rpmPkgArch,
homeDir: "/usr/share/grafana",
homeBinDir: "/usr/share/grafana/bin",
binPath: "/usr/sbin",
configDir: "/etc/grafana",
etcDefaultPath: "/etc/sysconfig",
@@ -293,10 +300,11 @@ func createRpmPackages() {
initdScriptFilePath: "/etc/init.d/grafana-server",
systemdServiceFilePath: "/usr/lib/systemd/system/grafana-server.service",
postinstSrc: "packaging/rpm/control/postinst",
initdScriptSrc: "packaging/rpm/init.d/grafana-server",
defaultFileSrc: "packaging/rpm/sysconfig/grafana-server",
systemdFileSrc: "packaging/rpm/systemd/grafana-server.service",
postinstSrc: "packaging/rpm/control/postinst",
initdScriptSrc: "packaging/rpm/init.d/grafana-server",
defaultFileSrc: "packaging/rpm/sysconfig/grafana-server",
systemdFileSrc: "packaging/rpm/systemd/grafana-server.service",
cliBinaryWrapperSrc: "packaging/wrappers/grafana-cli",
depends: []string{"/sbin/service", "fontconfig", "freetype", "urw-fonts"},
})
@@ -323,10 +331,12 @@ func createPackage(options linuxPackageOptions) {
runPrint("mkdir", "-p", filepath.Join(packageRoot, "/usr/lib/systemd/system"))
runPrint("mkdir", "-p", filepath.Join(packageRoot, "/usr/sbin"))
// copy binary
for _, binary := range binaries {
runPrint("cp", "-p", filepath.Join(workingDir, "tmp/bin/"+binary), filepath.Join(packageRoot, "/usr/sbin/"+binary))
}
// copy grafana-cli wrapper
runPrint("cp", "-p", options.cliBinaryWrapperSrc, filepath.Join(packageRoot, "/usr/sbin/"+cliBinary))
// copy grafana-server binary
runPrint("cp", "-p", filepath.Join(workingDir, "tmp/bin/"+serverBinary), filepath.Join(packageRoot, "/usr/sbin/"+serverBinary))
// copy init.d script
runPrint("cp", "-p", options.initdScriptSrc, filepath.Join(packageRoot, options.initdScriptFilePath))
// copy environment var file
@@ -338,6 +348,13 @@ func createPackage(options linuxPackageOptions) {
// remove bin path
runPrint("rm", "-rf", filepath.Join(packageRoot, options.homeDir, "bin"))
// create /bin within home
runPrint("mkdir", "-p", filepath.Join(packageRoot, options.homeBinDir))
// The grafana-cli binary is exposed through a wrapper to ensure a proper
// configuration is in place. To enable that, we need to store the original
// binary in a separate location to avoid conflicts.
runPrint("cp", "-p", filepath.Join(workingDir, "tmp/bin/"+cliBinary), filepath.Join(packageRoot, options.homeBinDir, cliBinary))
args := []string{
"-s", "dir",
"--description", "Grafana",
@@ -391,7 +408,7 @@ func createPackage(options linuxPackageOptions) {
args = append(args, "--iteration", linuxPackageIteration)
}
// add dependenciesj
// add dependencies
for _, dep := range options.depends {
args = append(args, "--depends", dep)
}

View File

@@ -113,7 +113,7 @@ 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 =
@@ -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

@@ -37,7 +37,7 @@ If running the command returns this error:
then there are two flags that can be used to set homepath and the config file path.
`grafana-cli admin reset-admin-password --homepath "/usr/share/grafana" newpass`
`grafana-cli --homepath "/usr/share/grafana" admin reset-admin-password newpass`
If you have not lost the admin password then it is better to set in the Grafana UI. If you need to set the password in a script then the [Grafana API](http://docs.grafana.org/http_api/user/#change-password) can be used. Here is an example using curl with basic auth:

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.5",
"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

39
packaging/wrappers/grafana-cli Executable file
View File

@@ -0,0 +1,39 @@
#! /usr/bin/env bash
# Wrapper for the grafana-cli binary
# This file serves as a wrapper for the grafana-cli binary. It ensures we set
# the system-wide Grafana configuration that was bundled with the package as we
# use the binary.
DEFAULT=/etc/default/grafana
GRAFANA_HOME=/usr/share/grafana
CONF_DIR=/etc/grafana
DATA_DIR=/var/lib/grafana
PLUGINS_DIR=/var/lib/grafana/plugins
LOG_DIR=/var/log/grafana
CONF_FILE=$CONF_DIR/grafana.ini
PROVISIONING_CFG_DIR=$CONF_DIR/provisioning
EXECUTABLE=$GRAFANA_HOME/bin/grafana-cli
if [ ! -x $EXECUTABLE ]; then
echo "Program not installed or not executable"
exit 5
fi
# overwrite settings from default file
if [ -f "$DEFAULT" ]; then
. "$DEFAULT"
fi
OPTS="--homepath=${GRAFANA_HOME} \
--config=${CONF_FILE} \
--pluginsDir=${PLUGINS_DIR} \
--configOverrides='cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR \
cfg:default.paths.data=${DATA_DIR} \
cfg:default.paths.logs=${LOG_DIR} \
cfg:default.paths.plugins=${PLUGINS_DIR}'"
eval $EXECUTABLE "$OPTS" "$@"

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

@@ -1,34 +1,43 @@
package commands
import (
"flag"
"os"
"strings"
"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}
debug := cmd.GlobalBool("debug")
cfg := setting.NewCfg()
configOptions := strings.Split(cmd.GlobalString("configOverrides"), " ")
cfg.Load(&setting.CommandLineArgs{
Config: cmd.String("config"),
HomePath: cmd.String("homepath"),
Args: flag.Args(),
Config: cmd.ConfigFile(),
HomePath: cmd.HomePath(),
Args: append(configOptions, cmd.Args()...), // tailing arguments have precedence over the options string
})
if debug {
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 +49,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)
@@ -96,14 +105,15 @@ 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",
},
{
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),
},
},
},

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

@@ -23,6 +23,7 @@ func main() {
app.Author = "Grafana Project"
app.Email = "https://github.com/grafana/grafana"
app.Version = version
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "pluginsDir",
@@ -50,6 +51,18 @@ func main() {
Name: "debug, d",
Usage: "enable debug logging",
},
cli.StringFlag{
Name: "configOverrides",
Usage: "configuration options to override defaults as a string. e.g. cfg:default.paths.log=/dev/null",
},
cli.StringFlag{
Name: "homepath",
Usage: "path to grafana install/home path, defaults to working directory",
},
cli.StringFlag{
Name: "config",
Usage: "path to config file",
},
}
app.Before = func(c *cli.Context) error {

View File

@@ -1,4 +1,4 @@
package commands
package utils
import (
"github.com/codegangsta/cli"
@@ -22,30 +22,38 @@ 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) HomePath() string { return c.GlobalString("homepath") }
func (c *ContextCommandLine) ConfigFile() string { return c.GlobalString("config") }
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")
}
func (c *ContextCommandLine) OptionsString() string {
return c.GlobalString("configOverrides")
}

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,34 @@ 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 {
strictHeaderValues := []string{fmt.Sprintf("max-age=%v", setting.StrictTransportSecurityMaxAge)}
if setting.StrictTransportSecurityPreload {
strictHeaderValues = append(strictHeaderValues, "preload")
}
if setting.StrictTransportSecuritySubDomains {
strictHeaderValues = append(strictHeaderValues, "includeSubDomains")
}
w.Header().Add("Strict-Transport-Security", strings.Join(strictHeaderValues, "; "))
}
if setting.ContentTypeProtectionHeader {
w.Header().Add("X-Content-Type-Options", "nosniff")
}
if setting.XSSProtectionHeader {
w.Header().Add("X-XSS-Protection", "1; mode=block")
}
}
func AddNoCacheHeaders(w macaron.ResponseWriter) {
w.Header().Add("Cache-Control", "no-cache")
w.Header().Add("Pragma", "no-cache")

View File

@@ -21,6 +21,39 @@ import (
"gopkg.in/macaron.v1"
)
func TestMiddleWareSecurityHeaders(t *testing.T) {
setting.ERR_TEMPLATE_NAME = "error-template"
Convey("Given the grafana middleware", t, func() {
middlewareScenario(t, "middleware should get correct x-xss-protection header", func(sc *scenarioContext) {
setting.XSSProtectionHeader = true
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("X-XSS-Protection"), ShouldEqual, "1; mode=block")
})
middlewareScenario(t, "middleware should not get x-xss-protection when disabled", func(sc *scenarioContext) {
setting.XSSProtectionHeader = false
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("X-XSS-Protection"), ShouldBeEmpty)
})
middlewareScenario(t, "middleware should add correct Strict-Transport-Security header", func(sc *scenarioContext) {
setting.StrictTransportSecurity = true
setting.Protocol = setting.HTTPS
setting.StrictTransportSecurityMaxAge = 64000
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000")
setting.StrictTransportSecurityPreload = true
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000; preload")
setting.StrictTransportSecuritySubDomains = true
sc.fakeReq("GET", "/api/").exec()
So(sc.resp.Header().Get("Strict-Transport-Security"), ShouldEqual, "max-age=64000; preload; includeSubDomains")
})
})
}
func TestMiddlewareContext(t *testing.T) {
setting.ERR_TEMPLATE_NAME = "error-template"

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

@@ -1,13 +1,16 @@
package setting
import (
"gopkg.in/ini.v1"
"bufio"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"testing"
"gopkg.in/ini.v1"
. "github.com/smartystreets/goconvey/convey"
)
@@ -25,6 +28,22 @@ func TestLoadingSettings(t *testing.T) {
So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/")
})
Convey("default.ini should have no semi-colon commented entries", func() {
file, err := os.Open("../../conf/defaults.ini")
if err != nil {
t.Errorf("failed to load defaults.ini file: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
// This only catches values commented out with ";" and will not catch those that are commented out with "#".
if strings.HasPrefix(scanner.Text(), ";") {
t.Errorf("entries in defaults.ini must not be commented or environment variables will not work: %v", scanner.Text())
}
}
})
Convey("Should be able to override via environment variables", func() {
os.Setenv("GF_SECURITY_ADMIN_USER", "superduper")

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

@@ -101,3 +101,11 @@ export function sanitize(unsanitizedString: string): string {
export function hasAnsiCodes(input: string): boolean {
return /\u001b\[\d{1,2}m/.test(input);
}
export function escapeHtml(str: string): string {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}

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() {

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