diff --git a/.bra.toml b/.bra.toml
index 36421a3c5a6..dcf316466d6 100644
--- a/.bra.toml
+++ b/.bra.toml
@@ -1,6 +1,6 @@
[run]
init_cmds = [
- ["go", "build", "-o", "./bin/grafana-server", "./pkg/cmd/grafana-server"],
+ ["go", "run", "build.go", "-dev", "build-server"],
["./bin/grafana-server", "cfg:app_mode=development"]
]
watch_all = true
@@ -12,6 +12,6 @@ watch_dirs = [
watch_exts = [".go", ".ini", ".toml"]
build_delay = 1500
cmds = [
- ["go", "build", "-o", "./bin/grafana-server", "./pkg/cmd/grafana-server"],
+ ["go", "run", "build.go", "-dev", "build-server"],
["./bin/grafana-server", "cfg:app_mode=development"]
]
diff --git a/.circleci/config.yml b/.circleci/config.yml
index cfa8b762e49..b6514c4cb47 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,6 +1,38 @@
version: 2
jobs:
+ codespell:
+ docker:
+ - image: circleci/python
+ steps:
+ - checkout
+ - run:
+ name: install codespell
+ command: 'sudo pip install codespell'
+ - 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'
+ - run:
+ name: check documentation spelling errors
+ command: 'codespell -I ./words_to_ignore.txt docs/'
+
+ gometalinter:
+ docker:
+ - image: circleci/golang:1.10
+ working_directory: /go/src/github.com/grafana/grafana
+ steps:
+ - checkout
+ - run:
+ name: install gometalinter tool
+ command: 'go get -u github.com/alecthomas/gometalinter'
+ - run:
+ name: install linters
+ command: 'gometalinter --install'
+ - run:
+ name: run some linters
+ command: 'gometalinter --vendor --deadline 6m --disable-all --enable=structcheck --enable=unconvert --enable=varcheck ./pkg/...'
+
test-frontend:
docker:
- image: circleci/node:6.11.4
@@ -85,7 +117,7 @@ jobs:
- image: circleci/python:2.7-stretch
steps:
- attach_workspace:
- at: dist
+ at: .
- run:
name: install awscli
command: 'sudo pip install awscli'
@@ -103,6 +135,10 @@ workflows:
version: 2
test-and-build:
jobs:
+ - codespell:
+ filters:
+ tags:
+ only: /.*/
- build:
filters:
tags:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f763a15c82..63ec1965098 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-# 5.1.0 (unreleased)
+# 5.1.0-beta1 (2018-04-20)
* **MSSQL**: New Microsoft SQL Server data source [#10093](https://github.com/grafana/grafana/pull/10093), [#11298](https://github.com/grafana/grafana/pull/11298), thx [@linuxchips](https://github.com/linuxchips)
* **Prometheus**: The heatmap panel now support Prometheus histograms [#10009](https://github.com/grafana/grafana/issues/10009)
@@ -15,8 +15,13 @@
* **Alerting**: Add support for retries on alert queries [#5855](https://github.com/grafana/grafana/issues/5855), thx [@Thib17](https://github.com/Thib17)
* **Table**: Table plugin value mappings [#7119](https://github.com/grafana/grafana/issues/7119), thx [infernix](https://github.com/infernix)
* **IE11**: IE 11 compatibility [#11165](https://github.com/grafana/grafana/issues/11165)
+* **Scrolling**: Better scrolling experience [#11053](https://github.com/grafana/grafana/issues/11053), [#11252](https://github.com/grafana/grafana/issues/11252), [#10836](https://github.com/grafana/grafana/issues/10836), [#11185](https://github.com/grafana/grafana/issues/11185), [#11168](https://github.com/grafana/grafana/issues/11168)
+* **Docker**: Improved docker image (breaking changes regarding file ownership) [grafana-docker #141](https://github.com/grafana/grafana-docker/issues/141), thx [@Spindel](https://github.com/Spindel), [@ChristianKniep](https://github.com/ChristianKniep), [@brancz](https://github.com/brancz) and [@jangaraj](https://github.com/jangaraj)
+* **Folders**: A folder admin cannot add user/team permissions for folder/its dashboards [#11173](https://github.com/grafana/grafana/issues/11173)
+* **Provisioning**: Improved workflow for provisioned dashboards [#10883](https://github.com/grafana/grafana/issues/10883)
### Minor
+
* **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
* **Cloudwatch**: Support high resolution metrics [#10925](https://github.com/grafana/grafana/pull/10925), thx [@mtanda](https://github.com/mtanda)
* **Cloudwatch**: Add dimension filtering to CloudWatch `dimension_values()` [#10029](https://github.com/grafana/grafana/issues/10029), thx [@willyhutw](https://github.com/willyhutw)
@@ -28,7 +33,35 @@
* **AuthProxy**: Support IPv6 in Auth proxy white list [#11330](https://github.com/grafana/grafana/pull/11330), thx [@corny](https://github.com/corny)
* **SMTP**: Don't connect to STMP server using TLS unless configured. [#7189](https://github.com/grafana/grafana/issues/7189)
* **Prometheus**: Escape backslash in labels correctly. [#10555](https://github.com/grafana/grafana/issues/10555), thx [@roidelapluie](https://github.com/roidelapluie)
-* **Variables** Case-insensitive sorting for template values [#11128](https://github.com/grafana/grafana/issues/11128) thx [@cross](https://github.com/cross)
+* **Variables**: Case-insensitive sorting for template values [#11128](https://github.com/grafana/grafana/issues/11128) thx [@cross](https://github.com/cross)
+* **Annotations (native)**: Change default limit from 10 to 100 when querying api [#11569](https://github.com/grafana/grafana/issues/11569), thx [@flopp999](https://github.com/flopp999)
+* **MySQL/Postgres/MSSQL**: PostgreSQL datasource generates invalid query with dates before 1970 [#11530](https://github.com/grafana/grafana/issues/11530) thx [@ryantxu](https://github.com/ryantxu)
+* **Kiosk**: Adds url parameter for starting a dashboard in inactive mode [#11228](https://github.com/grafana/grafana/issues/11228), thx [@towolf](https://github.com/towolf)
+* **Dashboard**: Enable closing timepicker using escape key [#11332](https://github.com/grafana/grafana/issues/11332)
+* **Datasources**: Rename direct access mode in the data source settings [#11391](https://github.com/grafana/grafana/issues/11391)
+* **Search**: Display dashboards in folder indented [#11073](https://github.com/grafana/grafana/issues/11073)
+* **Units**: Use B/s instead Bps for Bytes per second [#9342](https://github.com/grafana/grafana/pull/9342), thx [@mayli](https://github.com/mayli)
+* **Units**: Radiation units [#11001](https://github.com/grafana/grafana/issues/11001), thx [@victorclaessen](https://github.com/victorclaessen)
+* **Units**: Timeticks unit [#11183](https://github.com/grafana/grafana/pull/11183), thx [@jtyr](https://github.com/jtyr)
+* **Units**: Concentration units and "Normal cubic metre" [#11211](https://github.com/grafana/grafana/issues/11211), thx [@flopp999](https://github.com/flopp999)
+* **Units**: New currency - Czech koruna [#11384](https://github.com/grafana/grafana/pull/11384), thx [@Rohlik](https://github.com/Rohlik)
+* **Avatar**: Fix DISABLE_GRAVATAR option [#11095](https://github.com/grafana/grafana/issues/11095)
+* **Heatmap**: Disable log scale when using time time series buckets [#10792](https://github.com/grafana/grafana/issues/10792)
+* **Provisioning**: Remove `id` from json when provisioning dashboards, [#11138](https://github.com/grafana/grafana/issues/11138)
+* **Prometheus**: tooltip for legend format not showing properly [#11516](https://github.com/grafana/grafana/issues/11516), thx [@svenklemm](https://github.com/svenklemm)
+* **Playlist**: Empty playlists cannot be deleted [#11133](https://github.com/grafana/grafana/issues/11133), thx [@kichristensen](https://github.com/kichristensen)
+* **Switch Orgs**: Alphabetic order in Switch Organization modal [#11556](https://github.com/grafana/grafana/issues/11556)
+* **Postgres**: improve `$__timeFilter` macro [#11578](https://github.com/grafana/grafana/issues/11578), thx [@svenklemm](https://github.com/svenklemm)
+* **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747)
+* **Dashboard**: Sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572)
+* **Dashboard**: Add search filter/tabs to new panel control [#10427](https://github.com/grafana/grafana/issues/10427)
+* **Folders**: User with org viewer role should not be able to save/move dashboards in/to general folder [#11553](https://github.com/grafana/grafana/issues/11553)
+* **Influxdb**: Dont assume the first column in table response is time. [#11476](https://github.com/grafana/grafana/issues/11476), thx [@hahnjo](https://github.com/hahnjo)
+
+### Tech
+* Backend code simplification [#11613](https://github.com/grafana/grafana/pull/11613), thx [@knweiss](https://github.com/knweiss)
+* Add codespell to CI [#11602](https://github.com/grafana/grafana/pull/11602), thx [@mjtrangoni](https://github.com/mjtrangoni)
+* Migrated JavaScript files to TypeScript
# 5.0.4 (2018-03-28)
@@ -56,7 +89,7 @@
* **Dashboards**: Changing templated value from dropdown is causing unsaved changes [#11063](https://github.com/grafana/grafana/issues/11063)
* **Prometheus**: Fixes bundled Prometheus 2.0 dashboard [#11016](https://github.com/grafana/grafana/issues/11016), thx [@roidelapluie](https://github.com/roidelapluie)
* **Sidemenu**: Profile menu "invisible" when gravatar is disabled [#11097](https://github.com/grafana/grafana/issues/11097)
-* **Dashboard**: Fixes a bug with resizeable handles for panels [#11103](https://github.com/grafana/grafana/issues/11103)
+* **Dashboard**: Fixes a bug with resizable handles for panels [#11103](https://github.com/grafana/grafana/issues/11103)
* **Alerting**: Telegram inline image mode fails when caption too long [#10975](https://github.com/grafana/grafana/issues/10975)
* **Alerting**: Fixes silent failing validation [#11145](https://github.com/grafana/grafana/pull/11145)
* **OAuth**: Only use jwt token if it contains an email address [#11127](https://github.com/grafana/grafana/pull/11127)
@@ -120,7 +153,7 @@ Grafana v5.0 is going to be the biggest and most foundational release Grafana ha
### New Major Features
- **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611)
- **Teams** User groups (teams) implemented. Can be used in folder & dashboard permission list.
-- **Dashboard grid**: Panels are now layed out in a two dimensional grid (with x, y, w, h). [#9093](https://github.com/grafana/grafana/issues/9093).
+- **Dashboard grid**: Panels are now laid out in a two dimensional grid (with x, y, w, h). [#9093](https://github.com/grafana/grafana/issues/9093).
- **Templating**: Vertical repeat direction for panel repeats.
- **UX**: Major update to page header and navigation
- **Dashboard settings**: Combine dashboard settings views into one with side menu, [#9750](https://github.com/grafana/grafana/issues/9750)
@@ -154,7 +187,7 @@ Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w:
* **Dashboard history**: New config file option versions_to_keep sets how many versions per dashboard to store, [#9671](https://github.com/grafana/grafana/issues/9671)
* **Dashboard as cfg**: Load dashboards from file into Grafana on startup/change [#9654](https://github.com/grafana/grafana/issues/9654) [#5269](https://github.com/grafana/grafana/issues/5269)
* **Prometheus**: Grafana can now send alerts to Prometheus Alertmanager while firing [#7481](https://github.com/grafana/grafana/issues/7481), thx [@Thib17](https://github.com/Thib17) and [@mtanda](https://github.com/mtanda)
-* **Table**: Support multiple table formated queries in table panel [#9170](https://github.com/grafana/grafana/issues/9170), thx [@davkal](https://github.com/davkal)
+* **Table**: Support multiple table formatted queries in table panel [#9170](https://github.com/grafana/grafana/issues/9170), thx [@davkal](https://github.com/davkal)
* **Security**: Protect against brute force (frequent) login attempts [#7616](https://github.com/grafana/grafana/issues/7616)
## Minor
@@ -176,7 +209,7 @@ Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w:
* **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand)
* **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
* **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
-* **Pagerduty**: Pagerduty dont auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222)
+* **Pagerduty**: Pagerduty don't auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222)
* **Cloudwatch**: Fix for multi-valued templated queries. [#9903](https://github.com/grafana/grafana/issues/9903)
## Tech
@@ -254,7 +287,7 @@ The following properties have been deprecated and will be removed in a future re
* **Annotations**: Add support for creating annotations from graph panel [#8197](https://github.com/grafana/grafana/pull/8197)
* **GCS**: Adds support for Google Cloud Storage [#8370](https://github.com/grafana/grafana/issues/8370) thx [@chuhlomin](https://github.com/chuhlomin)
* **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187)
-* **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk)
+* **Graph**: Add support for local formatting in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk)
* **Jaeger**: Add support for open tracing using jaeger in Grafana. [#9213](https://github.com/grafana/grafana/pull/9213)
* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/2764)
* **CLI**: Make it possible to install plugins from any url [#5873](https://github.com/grafana/grafana/issues/5873)
@@ -291,7 +324,7 @@ The following properties have been deprecated and will be removed in a future re
* **Graphite**: Fix for Grafana internal metrics to Graphite sending NaN values [#9279](https://github.com/grafana/grafana/issues/9279)
* **HTTP API**: Fix for HEAD method requests [#9307](https://github.com/grafana/grafana/issues/9307)
* **Templating**: Fix for duplicate template variable queries when refresh is set to time range change [#9185](https://github.com/grafana/grafana/issues/9185)
-* **Metrics**: dont write NaN values to graphite [#9279](https://github.com/grafana/grafana/issues/9279)
+* **Metrics**: don't write NaN values to graphite [#9279](https://github.com/grafana/grafana/issues/9279)
# 4.5.1 (2017-09-15)
@@ -328,12 +361,12 @@ The following properties have been deprecated and will be removed in a future re
### Breaking change
* **InfluxDB/Elasticsearch**: The panel & data source option named "Group by time interval" is now named "Min time interval" and does now always define a lower limit for the auto group by time. Without having to use `>` prefix (that prefix still works). This should in theory have close to zero actual impact on existing dashboards. It does mean that if you used this setting to define a hard group by time interval of, say "1d", if you zoomed to a time range wide enough the time range could increase above the "1d" range as the setting is now always considered a lower limit.
-* **Elasticsearch**: Elasticsearch metric queries without date histogram now return table formated data making table panel much easier to use for this use case. Should not break/change existing dashboards with stock panels but external panel plugins can be affected.
+* **Elasticsearch**: Elasticsearch metric queries without date histogram now return table formatted data making table panel much easier to use for this use case. Should not break/change existing dashboards with stock panels but external panel plugins can be affected.
## Changes
* **InfluxDB**: Change time range filter for absolute time ranges to be inclusive instead of exclusive [#8319](https://github.com/grafana/grafana/issues/8319), thx [@Oxydros](https://github.com/Oxydros)
-* **InfluxDB**: Added paranthesis around tag filters in queries [#9131](https://github.com/grafana/grafana/pull/9131)
+* **InfluxDB**: Added parenthesis around tag filters in queries [#9131](https://github.com/grafana/grafana/pull/9131)
## Bug Fixes
@@ -345,7 +378,7 @@ The following properties have been deprecated and will be removed in a future re
## Bug Fixes
-* **Search**: Fix for issue that casued search view to hide when you clicked starred or tags filters, fixes [#8981](https://github.com/grafana/grafana/issues/8981)
+* **Search**: Fix for issue that caused search view to hide when you clicked starred or tags filters, fixes [#8981](https://github.com/grafana/grafana/issues/8981)
* **Modals**: ESC key now closes modal again, fixes [#8981](https://github.com/grafana/grafana/issues/8988), thx [@j-white](https://github.com/j-white)
# 4.4.2 (2017-08-01)
@@ -684,12 +717,12 @@ due to too many connections/file handles on the data source backend. This proble
### Enhancements
* **Login**: Adds option to disable username/password logins, closes [#4674](https://github.com/grafana/grafana/issues/4674)
* **SingleStat**: Add seriename as option in singlestat panel, closes [#4740](https://github.com/grafana/grafana/issues/4740)
-* **Localization**: Week start day now dependant on browser locale setting, closes [#3003](https://github.com/grafana/grafana/issues/3003)
+* **Localization**: Week start day now dependent on browser locale setting, closes [#3003](https://github.com/grafana/grafana/issues/3003)
* **Templating**: Update panel repeats for variables that change on time refresh, closes [#5021](https://github.com/grafana/grafana/issues/5021)
* **Templating**: Add support for numeric and alphabetical sorting of variable values, closes [#2839](https://github.com/grafana/grafana/issues/2839)
* **Elasticsearch**: Support to set Precision Threshold for Unique Count metric, closes [#4689](https://github.com/grafana/grafana/issues/4689)
* **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609)
-* **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456)
+* **Database**: Allow database config using one property, closes [#5456](https://github.com/grafana/grafana/pull/5456)
* **Graphite**: Add support for groupByNodes, closes [#5613](https://github.com/grafana/grafana/pull/5613)
* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
* **OpenTSDB**: Add support for explicitTags for OpenTSDB>=2.3, closes [#6360](https://github.com/grafana/grafana/pull/6361)
@@ -756,7 +789,7 @@ due to too many connections/file handles on the data source backend. This proble
* **Datasource**: Pending data source requests are cancelled before new ones are issues (Graphite & Prometheus), closes [#5321](https://github.com/grafana/grafana/issues/5321)
### Breaking changes
-* **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log ouput.
+* **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log output.
* **Graphite** : The Graph panel no longer have a Graphite PNG option. closes [#5367](https://github.com/grafana/grafana/issues/5367)
### Bug fixes
@@ -774,7 +807,7 @@ due to too many connections/file handles on the data source backend. This proble
* **Annotations**: Annotations can now use a template variable as data source, closes [#5054](https://github.com/grafana/grafana/issues/5054)
* **Time picker**: Fixed issue timepicker and UTC when reading time from URL, fixes [#5078](https://github.com/grafana/grafana/issues/5078)
* **CloudWatch**: Support for Multiple Account by AssumeRole, closes [#3522](https://github.com/grafana/grafana/issues/3522)
-* **Singlestat**: Fixed alignment and minium height issue, fixes [#5113](https://github.com/grafana/grafana/issues/5113), fixes [#4679](https://github.com/grafana/grafana/issues/4679)
+* **Singlestat**: Fixed alignment and minimum height issue, fixes [#5113](https://github.com/grafana/grafana/issues/5113), fixes [#4679](https://github.com/grafana/grafana/issues/4679)
* **Share modal**: Fixed link when using grafana under dashboard sub url, fixes [#5109](https://github.com/grafana/grafana/issues/5109)
* **Prometheus**: Fixed bug in query editor that caused it not to load when reloading page, fixes [#5107](https://github.com/grafana/grafana/issues/5107)
* **Elasticsearch**: Fixed bug when template variable query returns numeric values, fixes [#5097](https://github.com/grafana/grafana/issues/5097), fixes [#5088](https://github.com/grafana/grafana/issues/5088)
@@ -791,7 +824,7 @@ due to too many connections/file handles on the data source backend. This proble
* **Graph**: Fixed broken PNG rendering in graph panel, fixes [#5025](https://github.com/grafana/grafana/issues/5025)
* **Graph**: Fixed broken xaxis on graph panel, fixes [#5024](https://github.com/grafana/grafana/issues/5024)
-* **Influxdb**: Fixes crash when hiding middle serie, fixes [#5005](https://github.com/grafana/grafana/issues/5005)
+* **Influxdb**: Fixes crash when hiding middle series, fixes [#5005](https://github.com/grafana/grafana/issues/5005)
# 3.0.1 Stable (2016-05-11)
@@ -803,7 +836,7 @@ due to too many connections/file handles on the data source backend. This proble
### Bug fixes
* **Dashboard title**: Fixed max dashboard title width (media query) for large screens, fixes [#4859](https://github.com/grafana/grafana/issues/4859)
* **Annotations**: Fixed issue with entering annotation edit view, fixes [#4857](https://github.com/grafana/grafana/issues/4857)
-* **Remove query**: Fixed issue with removing query for data sources without collapsable query editors, fixes [#4856](https://github.com/grafana/grafana/issues/4856)
+* **Remove query**: Fixed issue with removing query for data sources without collapsible query editors, fixes [#4856](https://github.com/grafana/grafana/issues/4856)
* **Graphite PNG**: Fixed issue graphite png rendering option, fixes [#4864](https://github.com/grafana/grafana/issues/4864)
* **InfluxDB**: Fixed issue missing plus group by iconn, fixes [#4862](https://github.com/grafana/grafana/issues/4862)
* **Graph**: Fixes missing line mode for thresholds, fixes [#4902](https://github.com/grafana/grafana/pull/4902)
@@ -819,11 +852,11 @@ due to too many connections/file handles on the data source backend. This proble
### Bug fixes
* **InfluxDB 0.12**: Fixed issue templating and `show tag values` query only returning tags for first measurement, fixes [#4726](https://github.com/grafana/grafana/issues/4726)
-* **Templating**: Fixed issue with regex formating when matching multiple values, fixes [#4755](https://github.com/grafana/grafana/issues/4755)
+* **Templating**: Fixed issue with regex formatting when matching multiple values, fixes [#4755](https://github.com/grafana/grafana/issues/4755)
* **Templating**: Fixed issue with custom all value and escaping, fixes [#4736](https://github.com/grafana/grafana/issues/4736)
* **Dashlist**: Fixed issue dashboard list panel and caching tags, fixes [#4768](https://github.com/grafana/grafana/issues/4768)
* **Graph**: Fixed issue with unneeded scrollbar in legend for Firefox, fixes [#4760](https://github.com/grafana/grafana/issues/4760)
-* **Table panel**: Fixed issue table panel formating string array properties, fixes [#4791](https://github.com/grafana/grafana/issues/4791)
+* **Table panel**: Fixed issue table panel formatting string array properties, fixes [#4791](https://github.com/grafana/grafana/issues/4791)
* **grafana-cli**: Improve error message when failing to install plugins due to corrupt response, fixes [#4651](https://github.com/grafana/grafana/issues/4651)
* **Singlestat**: Fixes prefix an postfix for gauges, fixes [#4812](https://github.com/grafana/grafana/issues/4812)
* **Singlestat**: Fixes auto-refresh on change for some options, fixes [#4809](https://github.com/grafana/grafana/issues/4809)
@@ -915,7 +948,7 @@ slack channel (link to slack channel in readme).
### Bug fixes
* **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)
* **InfluxDB**: Fix for InfluxDB and table panel when using Format As Table and having group by time, fixes [#3928](https://github.com/grafana/grafana/issues/3928)
-* **Panel Time shift**: Fix for panel time range and using dashboard times liek `Today` and `This Week`, fixes [#3941](https://github.com/grafana/grafana/issues/3941)
+* **Panel Time shift**: Fix for panel time range and using dashboard times like `Today` and `This Week`, fixes [#3941](https://github.com/grafana/grafana/issues/3941)
* **Row repeat**: Repeated rows will now appear next to each other and not by the bottom of the dashboard, fixes [#3942](https://github.com/grafana/grafana/issues/3942)
* **Png renderer**: Fix for phantomjs path on windows, fixes [#3657](https://github.com/grafana/grafana/issues/3657)
@@ -939,7 +972,7 @@ slack channel (link to slack channel in readme).
### Bug Fixes
* **metric editors**: Fix for clicking typeahead auto dropdown option, fixes [#3428](https://github.com/grafana/grafana/issues/3428)
* **influxdb**: Fixed issue showing Group By label only on first query, fixes [#3453](https://github.com/grafana/grafana/issues/3453)
-* **logging**: Add more verbose info logging for http reqeusts, closes [#3405](https://github.com/grafana/grafana/pull/3405)
+* **logging**: Add more verbose info logging for http requests, closes [#3405](https://github.com/grafana/grafana/pull/3405)
# 2.6.0-Beta1 (2015-12-04)
@@ -966,7 +999,7 @@ slack channel (link to slack channel in readme).
**New Feature: Mix data sources**
- A built in data source is now available named `-- Mixed --`, When picked in the metrics tab,
-it allows you to add queries of differnet data source types & instances to the same graph/panel!
+it allows you to add queries of different data source types & instances to the same graph/panel!
[Issue #436](https://github.com/grafana/grafana/issues/436)
**New Feature: Elasticsearch Metrics Query Editor and Viz Support**
@@ -1005,7 +1038,7 @@ it allows you to add queries of differnet data source types & instances to the s
- [Issue #2564](https://github.com/grafana/grafana/issues/2564). Templating: Another atempt at fixing #2534 (Init multi value template var used in repeat panel from url)
- [Issue #2620](https://github.com/grafana/grafana/issues/2620). Graph: multi series tooltip did no highlight correct point when stacking was enabled and series were of different resolution
- [Issue #2636](https://github.com/grafana/grafana/issues/2636). InfluxDB: Do no show template vars in dropdown for tag keys and group by keys
-- [Issue #2604](https://github.com/grafana/grafana/issues/2604). InfluxDB: More alias options, can now use `$[0-9]` syntax to reference part of a measurement name (seperated by dots)
+- [Issue #2604](https://github.com/grafana/grafana/issues/2604). InfluxDB: More alias options, can now use `$[0-9]` syntax to reference part of a measurement name (separated by dots)
**Breaking Changes**
- Notice to makers/users of custom data sources, there is a minor breaking change in 2.2 that
@@ -1087,7 +1120,7 @@ Grunt & Watch tasks:
- [Issue #1826](https://github.com/grafana/grafana/issues/1826). User role 'Viewer' are now prohibited from entering edit mode (and doing other transient dashboard edits). A new role `Read Only Editor` will replace the old Viewer behavior
- [Issue #1928](https://github.com/grafana/grafana/issues/1928). HTTP API: GET /api/dashboards/db/:slug response changed property `model` to `dashboard` to match the POST request nameing
- Backend render URL changed from `/render/dashboard/solo` `render/dashboard-solo/` (in order to have consistent dashboard url `/dashboard/:type/:slug`)
-- Search HTTP API response has changed (simplified), tags list moved to seperate HTTP resource URI
+- Search HTTP API response has changed (simplified), tags list moved to separate HTTP resource URI
- Datasource HTTP api breaking change, ADD datasource is now POST /api/datasources/, update is now PUT /api/datasources/:id
**Fixes**
@@ -1104,7 +1137,7 @@ Grunt & Watch tasks:
# 2.0.2 (2015-04-22)
**Fixes**
-- [Issue #1832](https://github.com/grafana/grafana/issues/1832). Graph Panel + Legend Table mode: Many series casued zero height graph, now legend will never reduce the height of the graph below 50% of row height.
+- [Issue #1832](https://github.com/grafana/grafana/issues/1832). Graph Panel + Legend Table mode: Many series caused zero height graph, now legend will never reduce the height of the graph below 50% of row height.
- [Issue #1846](https://github.com/grafana/grafana/issues/1846). Snapshots: Fixed issue with snapshoting dashboards with an interval template variable
- [Issue #1848](https://github.com/grafana/grafana/issues/1848). Panel timeshift: You can now use panel timeshift without a relative time override
@@ -1146,7 +1179,7 @@ Grunt & Watch tasks:
**Fixes**
- [Issue #1649](https://github.com/grafana/grafana/issues/1649). HTTP API: grafana /render calls nows with api keys
-- [Issue #1667](https://github.com/grafana/grafana/issues/1667). Datasource proxy & session timeout fix (casued 401 Unauthorized error after a while)
+- [Issue #1667](https://github.com/grafana/grafana/issues/1667). Datasource proxy & session timeout fix (caused 401 Unauthorized error after a while)
- [Issue #1707](https://github.com/grafana/grafana/issues/1707). Unsaved changes: Do not show for snapshots, scripted and file based dashboards
- [Issue #1703](https://github.com/grafana/grafana/issues/1703). Unsaved changes: Do not show for users with role `Viewer`
- [Issue #1675](https://github.com/grafana/grafana/issues/1675). Data source proxy: Fixed issue with Gzip enabled and data source proxy
@@ -1159,14 +1192,14 @@ Grunt & Watch tasks:
**Important Note**
-Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated backend server. Please read the [Documentation](http://docs.grafana.org) for more detailed about this SIGNIFCANT change to Grafana
+Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated backend server. Please read the [Documentation](http://docs.grafana.org) for more detailed about this SIGNIFICANT change to Grafana
**New features**
- [Issue #1623](https://github.com/grafana/grafana/issues/1623). Share Dashboard: Dashboard snapshot sharing (dash and data snapshot), save to local or save to public snapshot dashboard snapshots.raintank.io site
- [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site
-- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes inbetween the user is promted with a warning if he really wants to overwrite the other's changes
+- [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is promted with a warning if he really wants to overwrite the other's changes
- [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views
-- [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, usefull when you want to ignore last minute because it contains incomplete data
+- [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, useful when you want to ignore last minute because it contains incomplete data
- [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift
- [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as
- [Issue #1458](https://github.com/grafana/grafana/issues/1458). User: persisted user option for dark or light theme (no longer an option on a dashboard)
@@ -1197,7 +1230,7 @@ Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated
**OpenTSDB breaking change**
- [Issue #1438](https://github.com/grafana/grafana/issues/1438). OpenTSDB: Automatic downsample interval passed to OpenTSDB (depends on timespan and graph width)
-- NOTICE, Downsampling is now enabled by default, so if you have not picked a downsample aggregator in your metric query do so or your graphs will be missleading
+- NOTICE, Downsampling is now enabled by default, so if you have not picked a downsample aggregator in your metric query do so or your graphs will be misleading
- This will make Grafana a lot quicker for OpenTSDB users when viewing large time spans without having to change the downsample interval manually.
**Tech**
@@ -1228,7 +1261,7 @@ Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated
- [Issue #1114](https://github.com/grafana/grafana/issues/1114). Graphite: Lexer fix, allow equal sign (=) in metric paths
- [Issue #1136](https://github.com/grafana/grafana/issues/1136). Graph: Fix to legend value Max and negative values
- [Issue #1150](https://github.com/grafana/grafana/issues/1150). SinglestatPanel: Fixed absolute drilldown link issue
-- [Issue #1123](https://github.com/grafana/grafana/issues/1123). Firefox: Workaround for Firefox bug, casued input text fields to not be selectable and not have placeable cursor
+- [Issue #1123](https://github.com/grafana/grafana/issues/1123). Firefox: Workaround for Firefox bug, caused input text fields to not be selectable and not have placeable cursor
- [Issue #1108](https://github.com/grafana/grafana/issues/1108). Graph: Fix for tooltip series order when series draw order was changed with zindex property
# 1.9.0-rc1 (2014-11-17)
@@ -1305,7 +1338,7 @@ Read this [blog post](https://grafana.com/blog/2014/09/11/grafana-1.8.0-rc1-rele
- [Issue #234](https://github.com/grafana/grafana/issues/234). Templating: Interval variable type for time intervals summarize/group by parameter, included "auto" option, and auto step counts option.
- [Issue #262](https://github.com/grafana/grafana/issues/262). Templating: Ability to use template variables for function parameters via custom variable type, can be used as parameter for movingAverage or scaleToSeconds for example
- [Issue #312](https://github.com/grafana/grafana/issues/312). Templating: Can now use template variables in panel titles
-- [Issue #613](https://github.com/grafana/grafana/issues/613). Templating: Full support for InfluxDB, filter by part of series names, extract series substrings, nested queries, multipe where clauses!
+- [Issue #613](https://github.com/grafana/grafana/issues/613). Templating: Full support for InfluxDB, filter by part of series names, extract series substrings, nested queries, multiple where clauses!
- Template variables can be initialized from url, with var-my_varname=value, breaking change, before it was just my_varname.
- Templating and url state sync has some issues that are not solved for this release, see [Issue #772](https://github.com/grafana/grafana/issues/772) for more details.
@@ -1394,7 +1427,7 @@ Read this [blog post](https://grafana.com/blog/2014/09/11/grafana-1.8.0-rc1-rele
- [Issue #136](https://github.com/grafana/grafana/issues/136). Graph: New legend display option "Align as table"
- [Issue #556](https://github.com/grafana/grafana/issues/556). Graph: New legend display option "Right side", will show legend to the right of the graph
- [Issue #604](https://github.com/grafana/grafana/issues/604). Graph: New axis format, 'bps' (SI unit in steps of 1000) useful for network gear metics
-- [Issue #626](https://github.com/grafana/grafana/issues/626). Graph: Downscale y axis to more precise unit, value of 0.1 for seconds format will be formated as 100 ms. Thanks @kamaradclimber
+- [Issue #626](https://github.com/grafana/grafana/issues/626). Graph: Downscale y axis to more precise unit, value of 0.1 for seconds format will be formatted as 100 ms. Thanks @kamaradclimber
- [Issue #618](https://github.com/grafana/grafana/issues/618). OpenTSDB: Series alias option to override metric name returned from opentsdb. Thanks @heldr
**Documentation**
@@ -1424,13 +1457,13 @@ Read this [blog post](https://grafana.com/blog/2014/09/11/grafana-1.8.0-rc1-rele
- [Issue #522](https://github.com/grafana/grafana/issues/522). Series names and column name typeahead cache fix
- [Issue #504](https://github.com/grafana/grafana/issues/504). Fixed influxdb issue with raw query that caused wrong value column detection
- [Issue #526](https://github.com/grafana/grafana/issues/526). Default property that marks which datasource is default in config.js is now optional
-- [Issue #342](https://github.com/grafana/grafana/issues/342). Auto-refresh caused 2 refreshes (and hence mulitple queries) each time (at least in firefox)
+- [Issue #342](https://github.com/grafana/grafana/issues/342). Auto-refresh caused 2 refreshes (and hence multiple queries) each time (at least in firefox)
# 1.6.0 (2014-06-16)
#### New features or improvements
- [Issue #427](https://github.com/grafana/grafana/issues/427). New Y-axis formater for metric values that represent seconds, Thanks @jippi
-- [Issue #390](https://github.com/grafana/grafana/issues/390). Allow special characters in serie names (influxdb datasource), Thanks @majst01
+- [Issue #390](https://github.com/grafana/grafana/issues/390). Allow special characters in series names (influxdb datasource), Thanks @majst01
- [Issue #428](https://github.com/grafana/grafana/issues/428). Refactoring of filterSrv, Thanks @Tetha
- [Issue #445](https://github.com/grafana/grafana/issues/445). New config for playlist feature. Set playlist_timespan to set default playlist interval, Thanks @rmca
- [Issue #461](https://github.com/grafana/grafana/issues/461). New graphite function definition added isNonNull, Thanks @tmonk42
@@ -1451,13 +1484,13 @@ Read this [blog post](https://grafana.com/blog/2014/09/11/grafana-1.8.0-rc1-rele
- [Issue #475](https://github.com/grafana/grafana/issues/475). Add panel icon and Row edit button is replaced by the Row edit menu
- New graphs now have a default empty query
- Add Row button now creates a row with default height of 250px (no longer opens dashboard settings modal)
-- Clean up of config.sample.js, graphiteUrl removed (still works, but depricated, removed in future)
+- Clean up of config.sample.js, graphiteUrl removed (still works, but deprecated, removed in future)
Use datasources config instead. panel_names removed from config.js. Use plugins.panels to add custom panels
- Graphite panel is now renamed graph (Existing dashboards will still work)
#### Fixes
- [Issue #126](https://github.com/grafana/grafana/issues/126). Graphite query lexer change, can now handle regex parameters for aliasSub function
-- [Issue #447](https://github.com/grafana/grafana/issues/447). Filter option loading when having muliple nested filters now works better. Options are now reloaded correctly and there are no multiple renders/refresh inbetween.
+- [Issue #447](https://github.com/grafana/grafana/issues/447). Filter option loading when having muliple nested filters now works better. Options are now reloaded correctly and there are no multiple renders/refresh in between.
- [Issue #412](https://github.com/grafana/grafana/issues/412). After a filter option is changed and a nested template param is reloaded, if the current value exists after the options are reloaded the current selected value is kept.
- [Issue #460](https://github.com/grafana/grafana/issues/460). Legend Current value did not display when value was zero
- [Issue #328](https://github.com/grafana/grafana/issues/328). Fix to series toggling bug that caused annotations to be hidden when toggling/hiding series.
diff --git a/appveyor.yml b/appveyor.yml
index 5d67edca9d9..2b0bddde162 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
environment:
nodejs_version: "6"
GOPATH: c:\gopath
- GOVERSION: 1.9.2
+ GOVERSION: 1.10
install:
- rmdir c:\go /s /q
diff --git a/build.go b/build.go
index c38c452f61f..b86fc838e6b 100644
--- a/build.go
+++ b/build.go
@@ -41,6 +41,7 @@ var (
includeBuildNumber bool = true
buildNumber int = 0
binaries []string = []string{"grafana-server", "grafana-cli"}
+ isDev bool = false
)
const minGoVersion = 1.8
@@ -61,6 +62,7 @@ func main() {
flag.BoolVar(&race, "race", race, "Use race detector")
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
+ flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
flag.Parse()
readVersionFromPackageJson()
@@ -394,7 +396,9 @@ func build(binaryName, pkg string, tags []string) {
binary += ".exe"
}
- rmr(binary, binary+".md5")
+ if !isDev {
+ rmr(binary, binary+".md5")
+ }
args := []string{"build", "-ldflags", ldflags()}
if len(tags) > 0 {
args = append(args, "-tags", strings.Join(tags, ","))
@@ -405,16 +409,21 @@ func build(binaryName, pkg string, tags []string) {
args = append(args, "-o", binary)
args = append(args, pkg)
- setBuildEnv()
- runPrint("go", "version")
+ if !isDev {
+ setBuildEnv()
+ runPrint("go", "version")
+ }
+
runPrint("go", args...)
- // Create an md5 checksum of the binary, to be included in the archive for
- // automatic upgrades.
- err := md5File(binary)
- if err != nil {
- log.Fatal(err)
+ if !isDev {
+ // Create an md5 checksum of the binary, to be included in the archive for
+ // automatic upgrades.
+ err := md5File(binary)
+ if err != nil {
+ log.Fatal(err)
+ }
}
}
@@ -435,6 +444,10 @@ func rmr(paths ...string) {
}
func clean() {
+ if isDev {
+ return
+ }
+
rmr("dist")
rmr("tmp")
rmr(filepath.Join(os.Getenv("GOPATH"), fmt.Sprintf("pkg/%s_%s/github.com/grafana", goos, goarch)))
@@ -550,7 +563,7 @@ func shaFilesInDist() {
return nil
}
- if strings.Contains(path, ".sha256") == false {
+ if !strings.Contains(path, ".sha256") {
err := shaFile(path)
if err != nil {
log.Printf("Failed to create sha file. error: %v\n", err)
diff --git a/codecov.yml b/codecov.yml
index 82a86e0232b..b2a839365ac 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -8,6 +8,4 @@ coverage:
patch: yes
changes: no
-comment:
- layout: "diff"
- behavior: "once"
+comment: off
diff --git a/conf/sample.ini b/conf/sample.ini
index 1af5bbdb62b..9f0c2a73c25 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -64,7 +64,7 @@
#################################### Database ####################################
[database]
# You can configure the database connection by specifying type, host, name, user and password
-# as seperate properties or as on string using the url propertie.
+# as separate properties or as on string using the url properties.
# Either "mysql", "postgres" or "sqlite3", it's your choice
;type = sqlite3
diff --git a/docker/blocks/graphite/files/carbon.conf b/docker/blocks/graphite/files/carbon.conf
index 50762b3fff5..fc03aba6398 100644
--- a/docker/blocks/graphite/files/carbon.conf
+++ b/docker/blocks/graphite/files/carbon.conf
@@ -38,7 +38,7 @@ CACHE_QUERY_PORT = 7002
LOG_UPDATES = False
-# Enable AMQP if you want to receve metrics using an amqp broker
+# Enable AMQP if you want to receive metrics using an amqp broker
# ENABLE_AMQP = False
# Verbose means a line will be logged for every metric received
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf b/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf
index fc36328b25f..f8a53a61115 100644
--- a/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf
+++ b/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf
@@ -41,7 +41,7 @@ PICKLE_RECEIVER_PORT = 2004
CACHE_QUERY_INTERFACE = 0.0.0.0
CACHE_QUERY_PORT = 7002
-# Enable AMQP if you want to receve metrics using you amqp broker
+# Enable AMQP if you want to receive metrics using you amqp broker
ENABLE_AMQP = True
# Verbose means a line will be logged for every metric received
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf b/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf
index 3e10dcec9cf..6741932da37 100644
--- a/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf
+++ b/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf
@@ -265,7 +265,7 @@ WHISPER_FALLOCATE_CREATE = True
# CARBON_METRIC_PREFIX = carbon
# CARBON_METRIC_INTERVAL = 60
-# Enable AMQP if you want to receve metrics using an amqp broker
+# Enable AMQP if you want to receive metrics using an amqp broker
# ENABLE_AMQP = False
# Verbose means a line will be logged for every metric received
diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf b/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf
index 2e1b0bc4db3..f558b273f57 100644
--- a/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf
+++ b/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf
@@ -30,7 +30,7 @@ give_completer_focus = shift-space
# pertain only to specific metric types.
#
# The dashboard presents only metrics that fall into specified naming schemes
-# defined in this file. This creates a simpler, more targetted view of the
+# defined in this file. This creates a simpler, more targeted view of the
# data. The general form for defining a naming scheme is as follows:
#
#[Metric Type]
diff --git a/docker/blocks/mssql_tests/dashboard.json b/docker/blocks/mssql_tests/dashboard.json
index 20e3907b48b..80994254093 100644
--- a/docker/blocks/mssql_tests/dashboard.json
+++ b/docker/blocks/mssql_tests/dashboard.json
@@ -100,7 +100,7 @@
"gnetId": null,
"graphTooltip": 0,
"id": null,
- "iteration": 1521715844826,
+ "iteration": 1523320861623,
"links": [],
"panels": [
{
@@ -443,7 +443,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -522,7 +526,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -601,7 +609,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -680,7 +692,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -759,7 +775,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -838,7 +858,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -927,7 +951,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1026,7 +1054,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1115,7 +1147,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1196,7 +1232,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1285,7 +1325,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1366,7 +1410,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1455,7 +1503,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1536,7 +1588,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1619,7 +1675,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1702,7 +1762,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1792,7 +1856,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1875,7 +1943,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1965,7 +2037,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2048,7 +2124,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2138,7 +2218,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2221,7 +2305,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2311,7 +2399,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2394,7 +2486,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
}
],
"refresh": false,
@@ -2504,5 +2600,5 @@
"timezone": "",
"title": "Microsoft SQL Server Data Source Test",
"uid": "GlAqcPgmz",
- "version": 57
+ "version": 58
}
\ No newline at end of file
diff --git a/docker/blocks/mysql/dashboard.json b/docker/blocks/mysql/dashboard.json
index e2b791f82e6..dba7847cc72 100644
--- a/docker/blocks/mysql/dashboard.json
+++ b/docker/blocks/mysql/dashboard.json
@@ -2,7 +2,7 @@
"__inputs": [
{
"name": "DS_MYSQL",
- "label": "Mysql",
+ "label": "MySQL",
"description": "",
"type": "datasource",
"pluginId": "mysql",
@@ -20,19 +20,19 @@
"type": "panel",
"id": "graph",
"name": "Graph",
- "version": ""
+ "version": "5.0.0"
},
{
"type": "datasource",
"id": "mysql",
"name": "MySQL",
- "version": "1.0.0"
+ "version": "5.0.0"
},
{
"type": "panel",
"id": "table",
"name": "Table",
- "version": ""
+ "version": "5.0.0"
}
],
"annotations": {
@@ -53,7 +53,7 @@
"gnetId": null,
"graphTooltip": 0,
"id": null,
- "iteration": 1518602729468,
+ "iteration": 1523372133566,
"links": [],
"panels": [
{
@@ -118,7 +118,7 @@
],
"thresholds": [],
"timeFrom": null,
- "timeShift": "1h",
+ "timeShift": null,
"title": "Average logins / $summarize",
"tooltip": {
"shared": true,
@@ -150,7 +150,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -204,7 +208,7 @@
],
"thresholds": [],
"timeFrom": null,
- "timeShift": "1h",
+ "timeShift": null,
"title": "Average payments started/ended / $summarize",
"tooltip": {
"shared": true,
@@ -236,7 +240,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -284,7 +292,7 @@
],
"thresholds": [],
"timeFrom": null,
- "timeShift": "1h",
+ "timeShift": null,
"title": "Max CPU / $summarize",
"tooltip": {
"shared": true,
@@ -316,7 +324,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"columns": [],
@@ -369,7 +381,7 @@
"target": ""
}
],
- "timeShift": "1h",
+ "timeShift": null,
"title": "Values",
"transform": "table",
"type": "table"
@@ -428,7 +440,6 @@
"auto_count": 5,
"auto_min": "10s",
"current": {
- "selected": true,
"text": "1m",
"value": "1m"
},
@@ -545,5 +556,5 @@
"timezone": "",
"title": "Grafana Fake Data Gen - MySQL",
"uid": "DGsCac3kz",
- "version": 6
+ "version": 8
}
\ No newline at end of file
diff --git a/docker/blocks/mysql/docker-compose.yaml b/docker/blocks/mysql/docker-compose.yaml
index f7881e66539..53ff9da62a7 100644
--- a/docker/blocks/mysql/docker-compose.yaml
+++ b/docker/blocks/mysql/docker-compose.yaml
@@ -7,9 +7,6 @@
MYSQL_PASSWORD: password
ports:
- "3306:3306"
- volumes:
- - /etc/localtime:/etc/localtime:ro
- - /etc/timezone:/etc/timezone:ro
command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all]
fake-mysql-data:
diff --git a/docker/blocks/mysql_tests/Dockerfile b/docker/blocks/mysql_tests/Dockerfile
new file mode 100644
index 00000000000..fa91fa3c023
--- /dev/null
+++ b/docker/blocks/mysql_tests/Dockerfile
@@ -0,0 +1,3 @@
+FROM mysql:latest
+ADD setup.sql /docker-entrypoint-initdb.d
+CMD ["mysqld"]
\ No newline at end of file
diff --git a/docker/blocks/mysql_tests/dashboard.json b/docker/blocks/mysql_tests/dashboard.json
index 3ab08a7da35..53f313315bd 100644
--- a/docker/blocks/mysql_tests/dashboard.json
+++ b/docker/blocks/mysql_tests/dashboard.json
@@ -7,14 +7,6 @@
"type": "datasource",
"pluginId": "mysql",
"pluginName": "MySQL"
- },
- {
- "name": "DS_MSSQL_TEST",
- "label": "MSSQL Test",
- "description": "",
- "type": "datasource",
- "pluginId": "mssql",
- "pluginName": "Microsoft SQL Server"
}
],
"__requires": [
@@ -30,12 +22,6 @@
"name": "Graph",
"version": "5.0.0"
},
- {
- "type": "datasource",
- "id": "mssql",
- "name": "Microsoft SQL Server",
- "version": "1.0.0"
- },
{
"type": "datasource",
"id": "mysql",
@@ -114,7 +100,7 @@
"gnetId": null,
"graphTooltip": 0,
"id": null,
- "iteration": 1521715720483,
+ "iteration": 1523320712115,
"links": [],
"panels": [
{
@@ -349,7 +335,7 @@
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
- "pattern": "time_sec",
+ "pattern": "time",
"type": "date"
},
{
@@ -457,7 +443,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -536,7 +526,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -615,7 +609,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -694,7 +692,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -773,7 +775,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -852,7 +858,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -941,7 +951,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1034,7 +1048,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1123,7 +1141,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1204,7 +1226,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1293,7 +1319,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1374,7 +1404,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1463,7 +1497,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1544,7 +1582,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1634,14 +1676,18 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
"bars": true,
"dashLength": 10,
"dashes": false,
- "datasource": "${DS_MSSQL_TEST}",
+ "datasource": "${DS_MYSQL_TEST}",
"fill": 1,
"gridPos": {
"h": 8,
@@ -1717,7 +1763,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1807,7 +1857,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1890,7 +1944,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1980,7 +2038,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2063,7 +2125,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2153,7 +2219,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2236,7 +2306,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
}
],
"refresh": false,
@@ -2315,8 +2389,8 @@
]
},
"time": {
- "from": "2018-03-15T11:30:00.000Z",
- "to": "2018-03-15T12:55:01.000Z"
+ "from": "2018-03-15T12:30:00.000Z",
+ "to": "2018-03-15T13:55:01.000Z"
},
"timepicker": {
"refresh_intervals": [
@@ -2346,5 +2420,5 @@
"timezone": "",
"title": "MySQL Data Source Test",
"uid": "Hmf8FDkmz",
- "version": 9
+ "version": 12
}
\ No newline at end of file
diff --git a/docker/blocks/mysql_tests/docker-compose.yaml b/docker/blocks/mysql_tests/docker-compose.yaml
index 3c59b66b5ac..035a6167017 100644
--- a/docker/blocks/mysql_tests/docker-compose.yaml
+++ b/docker/blocks/mysql_tests/docker-compose.yaml
@@ -1,5 +1,6 @@
mysqltests:
- image: mysql:latest
+ build:
+ context: blocks/mysql_tests
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: grafana_tests
@@ -7,7 +8,4 @@
MYSQL_PASSWORD: password
ports:
- "3306:3306"
- volumes:
- - /etc/localtime:/etc/localtime:ro
- - /etc/timezone:/etc/timezone:ro
tmpfs: /var/lib/mysql:rw
diff --git a/docker/blocks/mysql_tests/setup.sql b/docker/blocks/mysql_tests/setup.sql
new file mode 100644
index 00000000000..be917a1c542
--- /dev/null
+++ b/docker/blocks/mysql_tests/setup.sql
@@ -0,0 +1,2 @@
+CREATE DATABASE grafana_ds_tests;
+GRANT ALL PRIVILEGES ON grafana_ds_tests.* TO 'grafana';
diff --git a/docker/blocks/postgres_tests/Dockerfile b/docker/blocks/postgres_tests/Dockerfile
new file mode 100644
index 00000000000..afe4d199651
--- /dev/null
+++ b/docker/blocks/postgres_tests/Dockerfile
@@ -0,0 +1,3 @@
+FROM postgres:latest
+ADD setup.sql /docker-entrypoint-initdb.d
+CMD ["postgres"]
\ No newline at end of file
diff --git a/docker/blocks/postgres_tests/dashboard.json b/docker/blocks/postgres_tests/dashboard.json
index eea95863716..9efbe90bdfe 100644
--- a/docker/blocks/postgres_tests/dashboard.json
+++ b/docker/blocks/postgres_tests/dashboard.json
@@ -100,7 +100,7 @@
"gnetId": null,
"graphTooltip": 0,
"id": null,
- "iteration": 1521725946837,
+ "iteration": 1523320929325,
"links": [],
"panels": [
{
@@ -443,7 +443,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -522,7 +526,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -601,7 +609,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -680,7 +692,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -759,7 +775,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -838,7 +858,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -927,7 +951,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1008,7 +1036,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1097,7 +1129,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1178,7 +1214,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1267,7 +1307,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1348,7 +1392,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1437,7 +1485,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1518,7 +1570,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1608,7 +1664,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1691,7 +1751,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1781,7 +1845,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1864,7 +1932,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -1954,7 +2026,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2037,7 +2113,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2127,7 +2207,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
},
{
"aliasColors": {},
@@ -2210,7 +2294,11 @@
"min": null,
"show": true
}
- ]
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
}
],
"refresh": false,
diff --git a/docker/blocks/postgres_tests/docker-compose.yaml b/docker/blocks/postgres_tests/docker-compose.yaml
index 44b66e8e558..f5ce0a5a3d3 100644
--- a/docker/blocks/postgres_tests/docker-compose.yaml
+++ b/docker/blocks/postgres_tests/docker-compose.yaml
@@ -1,5 +1,6 @@
postgrestest:
- image: postgres:latest
+ build:
+ context: blocks/postgres_tests
environment:
POSTGRES_USER: grafanatest
POSTGRES_PASSWORD: grafanatest
diff --git a/docker/blocks/postgres_tests/setup.sql b/docker/blocks/postgres_tests/setup.sql
new file mode 100644
index 00000000000..b182b7c292d
--- /dev/null
+++ b/docker/blocks/postgres_tests/setup.sql
@@ -0,0 +1,3 @@
+CREATE DATABASE grafanadstest;
+REVOKE CONNECT ON DATABASE grafanadstest FROM PUBLIC;
+GRANT CONNECT ON DATABASE grafanadstest TO grafanatest;
\ No newline at end of file
diff --git a/docker/blocks/smtp/bootstrap.sh b/docker/blocks/smtp/bootstrap.sh
index a78f9d6dc16..27f6a2c3ef8 100755
--- a/docker/blocks/smtp/bootstrap.sh
+++ b/docker/blocks/smtp/bootstrap.sh
@@ -22,6 +22,6 @@ log() {
log $RUN_CMD
$RUN_CMD
-# Exit immidiately in case of any errors or when we have interactive terminal
+# Exit immediately in case of any errors or when we have interactive terminal
if [[ $? != 0 ]] || test -t 0; then exit $?; fi
log
diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md
index b43ea68bcd8..23fbe0c89fd 100644
--- a/docs/sources/administration/provisioning.md
+++ b/docs/sources/administration/provisioning.md
@@ -138,6 +138,7 @@ datasources:
```
#### Custom Settings per Datasource
+Please refer to each datasource documentation for specific provisioning examples.
| Datasource | Misc |
| ---- | ---- |
@@ -205,7 +206,7 @@ When Grafana starts, it will update/insert all dashboards available in the confi
### Reuseable Dashboard Urls
-If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifer.
+If the dashboard in the json file contains an [uid](/reference/dashboard/#json-fields), Grafana will force insert/update on that uid. This allows you to migrate dashboards betweens Grafana instances and provisioning Grafana from configuration without breaking the urls given since the new dashboard url uses the uid as identifier.
When Grafana starts, it will update/insert all dashboards available in the configured folders. If you modify the file, the dashboard will also be updated.
By default Grafana will delete dashboards in the database if the file is removed. You can disable this behavior using the `disableDeletion` setting.
diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md
index bb119687750..b3b4305a748 100644
--- a/docs/sources/alerting/notifications.md
+++ b/docs/sources/alerting/notifications.md
@@ -153,10 +153,10 @@ Prometheus Alertmanager | `prometheus-alertmanager` | no
# Enable images in notifications {#external-image-store}
-Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessable (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports
+Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessible (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports
Amazon S3, Webdav, Google Cloud Storage and Azure Blob Storage. So to set that up you need to configure the [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini config file.
-Be aware that some notifiers requires public access to the image to be able to include it in the notification. So make sure to enable public access to the images. If your using local image uploader, your Grafana instance need to be accessible by the internet.
+Be aware that some notifiers requires public access to the image to be able to include it in the notification. So make sure to enable public access to the images. If you're using local image uploader, your Grafana instance need to be accessible by the internet.
Currently only the Email Channels attaches images if no external image store is specified. To include images in alert notifications for other channels then you need to set up an external image store.
diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md
index 9bbbd70641d..bcca3c6b2fb 100644
--- a/docs/sources/alerting/rules.md
+++ b/docs/sources/alerting/rules.md
@@ -110,7 +110,7 @@ to `Keep Last State` in order to basically ignore them.
## Notifications
-In alert tab you can also specify alert rule notifications along with a detailed messsage about the alert rule.
+In alert tab you can also specify alert rule notifications along with a detailed message about the alert rule.
The message can contain anything, information about how you might solve the issue, link to runbook, etc.
The actual notifications are configured and shared between multiple alerts. Read the
diff --git a/docs/sources/contribute/cla.md b/docs/sources/contribute/cla.md
index b990187d809..ffb2aaef1b9 100644
--- a/docs/sources/contribute/cla.md
+++ b/docs/sources/contribute/cla.md
@@ -1,6 +1,6 @@
+++
title = "Contributor Licence Agreement (CLA)"
-description = "Contributer Licence Agreement (CLA)"
+description = "Contributor Licence Agreement (CLA)"
type = "docs"
aliases = ["/project/cla", "docs/contributing/cla.html"]
[menu.docs]
@@ -101,4 +101,4 @@ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU [OR US]
-This CLA aggreement is based on the [Harmony Contributor Aggrement Template (combined)](http://www.harmonyagreements.org/agreements.html), [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)
+This CLA agreement is based on the [Harmony Contributor Aggrement Template (combined)](http://www.harmonyagreements.org/agreements.html), [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/)
diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md
index f7f8138b5e9..f5586bef2d8 100644
--- a/docs/sources/features/datasources/cloudwatch.md
+++ b/docs/sources/features/datasources/cloudwatch.md
@@ -43,6 +43,40 @@ server is running on AWS you can use IAM Roles and authentication will be handle
Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
+## IAM Policies
+
+Grafana needs permissions granted via IAM to be able to read CloudWatch metrics
+and EC2 tags/instances. You can attach these permissions to IAM roles and
+utilize Grafana's built-in support for assuming roles.
+
+Here is a minimal policy example:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "AllowReadingMetricsFromCloudWatch",
+ "Effect": "Allow",
+ "Action": [
+ "cloudwatch:ListMetrics",
+ "cloudwatch:GetMetricStatistics"
+ ],
+ "Resource": "*"
+ },
+ {
+ "Sid": "AllowReadingTagsFromEC2",
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DescribeTags",
+ "ec2:DescribeInstances"
+ ],
+ "Resource": "*"
+ }
+ ]
+}
+```
+
### AWS credentials file
Create a file at `~/.aws/credentials`. That is the `HOME` path for user running grafana-server.
@@ -173,3 +207,37 @@ Amazon provides 1 million CloudWatch API requests each month at no additional ch
it costs $0.01 per 1,000 GetMetricStatistics or ListMetrics requests. For each query Grafana will
issue a GetMetricStatistics request and every time you pick a dimension in the query editor
Grafana will issue a ListMetrics request.
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+Using a credentials file
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: Cloudwatch
+ type: cloudwatch
+ jsonData:
+ authType: credentials
+ defaultRegion: eu-west-2
+```
+
+Using `accessKey` and `secretKey`
+
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: Cloudwatch
+ type: cloudwatch
+ jsonData:
+ authType: keys
+ defaultRegion: eu-west-2
+ secureJsonData:
+ accessKey: ""
+ secretKey: ""
+```
diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md
index 6ce17113a9b..1a00c2b18ff 100644
--- a/docs/sources/features/datasources/elasticsearch.md
+++ b/docs/sources/features/datasources/elasticsearch.md
@@ -55,6 +55,22 @@ a time pattern for the index name or a wildcard.
Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. Currently only 2.x and 5.x
are supported.
+### Min time interval
+A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
+number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+
+Identifier | Description
+------------ | -------------
+`y` | year
+`M` | month
+`w` | week
+`d` | day
+`h` | hour
+`m` | minute
+`s` | second
+`ms` | millisecond
+
## Metric Query editor

@@ -137,3 +153,23 @@ Query | You can leave the search query blank or specify a lucene query
Time | The name of the time field, needs to be date field.
Text | Event description field.
Tags | Optional field name to use for event tags (can be an array or a CSV string).
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: Elastic
+ type: elasticsearch
+ access: proxy
+ database: "[metrics-]YYYY.MM.DD"
+ url: http://localhost:9200
+ jsonData:
+ interval: Daily
+ timeField: "@timestamp"
+```
diff --git a/docs/sources/features/datasources/graphite.md b/docs/sources/features/datasources/graphite.md
index 7c4187da9ae..97233572878 100644
--- a/docs/sources/features/datasources/graphite.md
+++ b/docs/sources/features/datasources/graphite.md
@@ -120,3 +120,21 @@ queries via the Dashboard menu / Annotations view.
Graphite supports two ways to query annotations. A regular metric query, for this you use the `Graphite query` textbox. A Graphite events query, use the `Graphite event tags` textbox,
specify a tag or wildcard (leave empty should also work)
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: Graphite
+ type: graphite
+ access: proxy
+ url: http://localhost:8080
+ jsonData:
+ graphiteVersion: "1.1"
+```
diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md
index 6d0918a0d01..d108c97bbfe 100644
--- a/docs/sources/features/datasources/influxdb.md
+++ b/docs/sources/features/datasources/influxdb.md
@@ -39,6 +39,22 @@ Proxy access means that the Grafana backend will proxy all requests from the bro
`grafana-server`. This means that the URL you specify needs to be accessible from the server you are running Grafana on. Proxy access
mode is also more secure as the username & password will never reach the browser.
+### Min time interval
+A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
+This option can also be overridden/configured in a dashboard panel under data source options. It's important to note that this value **needs** to be formatted as a
+number followed by a valid time identifier, e.g. `1m` (1 minute) or `30s` (30 seconds). The following time identifiers are supported:
+
+Identifier | Description
+------------ | -------------
+`y` | year
+`M` | month
+`w` | week
+`d` | day
+`h` | hour
+`m` | minute
+`s` | second
+`ms` | millisecond
+
## Query Editor
{{< docs-imagebox img="/img/docs/v45/influxdb_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v45/influxdb_query.gif" >}}
@@ -174,3 +190,22 @@ SELECT title, description from events WHERE $timeFilter order asc
For InfluxDB you need to enter a query like in the above example. You need to have the ```where $timeFilter```
part. If you only select one column you will not need to enter anything in the column mapping fields. The
Tags field can be a comma separated string.
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: InfluxDB
+ type: influxdb
+ access: proxy
+ database: site
+ user: grafana
+ password: grafana
+ url: http://localhost:8086
+```
diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md
index 7fae7441b6d..8eaab86a906 100644
--- a/docs/sources/features/datasources/mysql.md
+++ b/docs/sources/features/datasources/mysql.md
@@ -225,3 +225,21 @@ tags | Optional field name to use for event tags as a comma separated string.
## Alerting
Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule conditions.
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: MySQL
+ type: mysql
+ url: localhost:3306
+ database: grafana
+ user: grafana
+ password: password
+```
\ No newline at end of file
diff --git a/docs/sources/features/datasources/opentsdb.md b/docs/sources/features/datasources/opentsdb.md
index 03795473ff7..0959817c015 100644
--- a/docs/sources/features/datasources/opentsdb.md
+++ b/docs/sources/features/datasources/opentsdb.md
@@ -78,7 +78,7 @@ the existing time series data in OpenTSDB, you need to run `tsdb uid metasync` o
### Nested Templating
-One template variable can be used to filter tag values for another template varible. First parameter is the metric name,
+One template variable can be used to filter tag values for another template variable. First parameter is the metric name,
second parameter is the tag key for which you need to find tag values, and after that all other dependent template variables.
Some examples are mentioned below to make nested template queries work successfully.
@@ -88,3 +88,22 @@ Query | Description
*tag_values(cpu, hostanme, env=$env, region=$region)* | Return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname
For details on OpenTSDB metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html)
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: OpenTsdb
+ type: opentsdb
+ access: proxy
+ url: http://localhost:4242
+ jsonData:
+ tsdbResolution: 1
+ tsdbVersion: 1
+```
diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md
index 7d52df2fd3e..f3321af46f2 100644
--- a/docs/sources/features/datasources/postgres.md
+++ b/docs/sources/features/datasources/postgres.md
@@ -45,9 +45,9 @@ Macro example | Description
------------ | -------------
*$__time(dateColumn)* | Will be replaced by an expression to rename the column to `time`. For example, *dateColumn as time*
*$__timeSec(dateColumn)* | Will be replaced by an expression to rename the column to `time` and converting the value to unix timestamp. For example, *extract(epoch from dateColumn) as time*
-*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *extract(epoch from dateColumn) BETWEEN 1494410783 AND 1494497183*
-*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)*
-*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)*
+*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
+*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
+*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time*
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
@@ -217,3 +217,25 @@ tags | Optional field name to use for event tags as a comma separated string.
Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule
conditions.
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: Postgres
+ type: postgres
+ url: localhost:5432
+ database: grafana
+ user: grafana
+ secureJsonData:
+ password: "Password!"
+ jsonData:
+ sslmode: "disable" # disable/require/verify-ca/verify-full
+
+```
\ No newline at end of file
diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md
index c9bb16441ca..17a24ed0684 100644
--- a/docs/sources/features/datasources/prometheus.md
+++ b/docs/sources/features/datasources/prometheus.md
@@ -34,7 +34,7 @@ Name | Description
*Basic Auth* | Enable basic authentication to the Prometheus data source.
*User* | Name of your Prometheus user
*Password* | Database user's password
-*Scrape interval* | This will be used as a lower limit for the Prometheus step query parameter. Default value is 15s.
+*Scrape interval* | This will be used as a lower limit for the Prometheus step query parameter. Default value is 15s.
## Query editor
@@ -100,3 +100,19 @@ The step option is useful to limit the number of events returned from your query
## Getting Grafana metrics into Prometheus
Since 4.6.0 Grafana exposes metrics for Prometheus on the `/metrics` endpoint. We also bundle a dashboard within Grafana so you can get started viewing your metrics faster. You can import the bundled dashboard by going to the data source edit page and click the dashboard tab. There you can find a dashboard for Grafana and one for Prometheus. Import and start viewing all the metrics!
+
+## Configure datasource with provisioning
+
+It's now possible to configure datasources using config files with Grafanas provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
+
+Here are some provisioning examples for this datasource.
+
+```yaml
+apiVersion: 1
+
+datasources:
+ - name: Prometheus
+ type: prometheus
+ access: proxy
+ url: http://localhost:9090
+```
\ No newline at end of file
diff --git a/docs/sources/features/panels/alertlist.md b/docs/sources/features/panels/alertlist.md
index 9307bb71391..58aa2c0966a 100644
--- a/docs/sources/features/panels/alertlist.md
+++ b/docs/sources/features/panels/alertlist.md
@@ -14,7 +14,7 @@ weight = 4
{{< docs-imagebox img="/img/docs/v45/alert-list-panel.png" max-width="850px" >}}
-The alert list panel allows you to display your dashbords alerts. The list can be configured to show current state or recent state changes. You can read more about alerts [here](http://docs.grafana.org/alerting/rules).
+The alert list panel allows you to display your dashboards alerts. The list can be configured to show current state or recent state changes. You can read more about alerts [here](http://docs.grafana.org/alerting/rules).
## Alert List Options
diff --git a/docs/sources/features/panels/dashlist.md b/docs/sources/features/panels/dashlist.md
index 8a4ed60875d..2ee578c5b7e 100644
--- a/docs/sources/features/panels/dashlist.md
+++ b/docs/sources/features/panels/dashlist.md
@@ -25,7 +25,7 @@ The dashboard list panel allows you to display dynamic links to other dashboards
1. **Starred**: The starred dashboard selection displays starred dashboards in alphabetical order.
2. **Recently Viewed**: The recently viewed dashboard selection displays recently viewed dashboards in alphabetical order.
3. **Search**: The search dashboard selection displays dashboards by search query or tag(s).
-4. **Show Headings**: When show headings is ticked the choosen list selection(Starred, Recently Viewed, Search) is shown as a heading.
+4. **Show Headings**: When show headings is ticked the chosen list selection(Starred, Recently Viewed, Search) is shown as a heading.
5. **Max Items**: Max items set the maximum of items in a list.
6. **Query**: Here is where you enter your query you want to search by. Queries are case-insensitive, and partial values are accepted.
7. **Tags**: Here is where you enter your tag(s) you want to search by. Note that existing tags will not appear as you type, and *are* case sensitive. To see a list of existing tags, you can always return to the dashboard, open the Dashboard Picker at the top and click `tags` link in the search bar.
diff --git a/docs/sources/features/panels/singlestat.md b/docs/sources/features/panels/singlestat.md
index 510642337ff..0eb442914f5 100644
--- a/docs/sources/features/panels/singlestat.md
+++ b/docs/sources/features/panels/singlestat.md
@@ -30,7 +30,7 @@ The singlestat panel has a normal query editor to allow you define your exact me
* **total** - The sum of all the non-null values in the series
* **first** - The first value in the series
* **delta** - The total incremental increase (of a counter) in the series. An attempt is made to account for counter resets, but this will only be accurate for single instance metrics. Used to show total counter increase in time series.
- * **diff** - The difference betwen 'current' (last value) and 'first'.
+ * **diff** - The difference between 'current' (last value) and 'first'.
* **range** - The difference between 'min' and 'max'. Useful the show the range of change for a gauge.
2. **Prefix/Postfix**: The Prefix/Postfix fields let you define a custom label to appear *before/after* the value. The `$__name` variable can be used here to use the series name or alias from the metric query.
3. **Units**: Units are appended to the the Singlestat within the panel, and will respect the color and threshold settings for the value.
@@ -70,7 +70,7 @@ Gauges gives a clear picture of how high a value is in it's context. It's a grea
{{< docs-imagebox img="/img/docs/v45/singlestat-gauge-options.png" max-width="500px" class="docs-image--right docs-image--no-shadow">}}
-1. **Show**: The show checkbox will toggle wether the gauge is shown in the panel. When unselected, only the Singlestat value will appear.
+1. **Show**: The show checkbox will toggle whether the gauge is shown in the panel. When unselected, only the Singlestat value will appear.
2. **Min/Max**: This sets the start and end point for the gauge.
3. **Threshold Labels**: Check if you want to show the threshold labels. Thresholds are set in the color options.
4. **Threshold Markers**: Check if you want to have a second meter showing the thresholds.
diff --git a/docs/sources/guides/whats-new-in-v2-6.md b/docs/sources/guides/whats-new-in-v2-6.md
index b8996680ce6..1e6f30c597b 100644
--- a/docs/sources/guides/whats-new-in-v2-6.md
+++ b/docs/sources/guides/whats-new-in-v2-6.md
@@ -15,7 +15,7 @@ support for multiple Cloudwatch credentials.
The new table panel is very flexible, supporting both multiple modes for time series as well as for
-table, annotation and raw JSON data. It also provides date formating and value formating and coloring options.
+table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options.
### Time series to rows
diff --git a/docs/sources/guides/whats-new-in-v4-1.md b/docs/sources/guides/whats-new-in-v4-1.md
index bd2b0f1b75f..217b21b545e 100644
--- a/docs/sources/guides/whats-new-in-v4-1.md
+++ b/docs/sources/guides/whats-new-in-v4-1.md
@@ -33,7 +33,7 @@ You can enable/disable the shared tooltip from the dashboard settings menu or cy
{{< imgbox max-width="60%" img="/img/docs/v41/helptext_for_panel_settings.png" caption="Hovering help text" >}}
-You can set a help text in the general tab on any panel. The help text is using Markdown to enable better formating and linking to other sites that can provide more information.
+You can set a help text in the general tab on any panel. The help text is using Markdown to enable better formatting and linking to other sites that can provide more information.
diff --git a/docs/sources/guides/whats-new-in-v4-5.md b/docs/sources/guides/whats-new-in-v4-5.md
index b2de451308a..a5cd3ca982d 100644
--- a/docs/sources/guides/whats-new-in-v4-5.md
+++ b/docs/sources/guides/whats-new-in-v4-5.md
@@ -12,7 +12,7 @@ weight = -4
# What's New in Grafana v4.5
-## Hightlights
+## Highlights
### New prometheus query editor
@@ -62,7 +62,7 @@ Datas source selection & options & help are now above your metric queries.
### Minor Changes
* **InfluxDB**: Change time range filter for absolute time ranges to be inclusive instead of exclusive [#8319](https://github.com/grafana/grafana/issues/8319), thx [@Oxydros](https://github.com/Oxydros)
-* **InfluxDB**: Added paranthesis around tag filters in queries [#9131](https://github.com/grafana/grafana/pull/9131)
+* **InfluxDB**: Added parenthesis around tag filters in queries [#9131](https://github.com/grafana/grafana/pull/9131)
## Bug Fixes
diff --git a/docs/sources/guides/whats-new-in-v4-6.md b/docs/sources/guides/whats-new-in-v4-6.md
index fd75384761f..09955fa58cc 100644
--- a/docs/sources/guides/whats-new-in-v4-6.md
+++ b/docs/sources/guides/whats-new-in-v4-6.md
@@ -45,7 +45,7 @@ This makes exploring and filtering Prometheus data much easier.
* **GCS**: Adds support for Google Cloud Storage [#8370](https://github.com/grafana/grafana/issues/8370) thx [@chuhlomin](https://github.com/chuhlomin)
* **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187)
-* **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk)
+* **Graph**: Add support for local formatting in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk)
* **Jaeger**: Add support for open tracing using jaeger in Grafana. [#9213](https://github.com/grafana/grafana/pull/9213)
* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/2764)
* **CLI**: Make it possible to install plugins from any url [#5873](https://github.com/grafana/grafana/issues/5873)
diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md
index 19c2a5c386c..0dc89912c05 100644
--- a/docs/sources/http_api/annotations.md
+++ b/docs/sources/http_api/annotations.md
@@ -36,6 +36,8 @@ Query Parameters:
- `alertId`: number. Optional. Find annotations for a specified alert.
- `dashboardId`: number. Optional. Find annotations that are scoped to a specific dashboard
- `panelId`: number. Optional. Find annotations that are scoped to a specific panel
+- `userId`: number. Optional. Find annotations created by a specific user
+- `type`: string. Optional. `alert`|`annotation` Return alerts or user created annotations
- `tags`: string. Optional. Use this to filter global annotations. Global annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. `tags=tag1&tags=tag2`.
**Example Response**:
@@ -180,14 +182,14 @@ Content-Type: application/json
## Delete Annotation By Id
-`DELETE /api/annotation/:id`
+`DELETE /api/annotations/:id`
Deletes the annotation that matches the specified id.
**Example Request**:
```http
-DELETE /api/annotation/1 HTTP/1.1
+DELETE /api/annotations/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
@@ -204,14 +206,14 @@ Content-Type: application/json
## Delete Annotation By RegionId
-`DELETE /api/annotation/region/:id`
+`DELETE /api/annotations/region/:id`
Deletes the annotation that matches the specified region id. A region is an annotation that covers a timerange and has a start and end time. In the Grafana database, this is a stored as two annotations connected by a region id.
**Example Request**:
```http
-DELETE /api/annotation/region/1 HTTP/1.1
+DELETE /api/annotations/region/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md
index 364b55b0cfc..9aaf29ec5f4 100644
--- a/docs/sources/http_api/data_source.md
+++ b/docs/sources/http_api/data_source.md
@@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"defaultRegion": "us-west-1"
},
"secureJsonData": {
- "accessKey": "Ol4pIDpeKSA6XikgOl4p",
- "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
+ "accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
+ "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
}
}
```
diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md
index 4c1dff904c8..b9a15450786 100644
--- a/docs/sources/http_api/org.md
+++ b/docs/sources/http_api/org.md
@@ -307,7 +307,7 @@ Content-Type: application/json
`PUT /api/orgs/:orgId`
-Update Organisation, fields *Adress 1*, *Adress 2*, *City* are not implemented yet.
+Update Organisation, fields *Address 1*, *Address 2*, *City* are not implemented yet.
**Example Request**:
@@ -436,4 +436,4 @@ HTTP/1.1 200
Content-Type: application/json
{"message":"User removed from organization"}
-```
\ No newline at end of file
+```
diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md
index 6169280b798..b7fe9040574 100644
--- a/docs/sources/installation/configuration.md
+++ b/docs/sources/installation/configuration.md
@@ -482,7 +482,7 @@ Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.co
First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`.
-Finaly set up the generic oauth module like this:
+Finally set up the generic oauth module like this:
```bash
[auth.generic_oauth]
name = Okta
diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md
index 27c0e41ac15..8b2e15ad124 100644
--- a/docs/sources/installation/debian.md
+++ b/docs/sources/installation/debian.md
@@ -16,6 +16,7 @@ weight = 1
Description | Download
------------ | -------------
Stable for Debian-based Linux | [grafana_5.0.4_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.4_amd64.deb)
+Beta for Debian-based Linux | [grafana_5.1.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_amd64.deb)
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
installation.
@@ -29,12 +30,19 @@ sudo apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_5.0.4_amd64.deb
```
+## Install Latest Beta
+```bash
+wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_amd64.deb
+sudo apt-get install -y adduser libfontconfig
+sudo dpkg -i grafana_5.1.0-beta1_amd64.deb
+```
+
## APT Repository
Add the following line to your `/etc/apt/sources.list` file.
```bash
-deb https://packagecloud.io/grafana/stable/debian/ jessie main
+deb https://packagecloud.io/grafana/stable/debian/ stretch main
```
Use the above line even if you are on Ubuntu or another Debian version.
@@ -42,7 +50,7 @@ There is also a testing repository if you want beta or release
candidates.
```bash
-deb https://packagecloud.io/grafana/testing/debian/ jessie main
+deb https://packagecloud.io/grafana/testing/debian/ stretch main
```
Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md
index 3ca5ba06638..f246bd55d33 100644
--- a/docs/sources/installation/docker.md
+++ b/docs/sources/installation/docker.md
@@ -12,7 +12,7 @@ weight = 4
# Installing using Docker
-Grafana is very easy to install and run using the offical docker container.
+Grafana is very easy to install and run using the official docker container.
```bash
$ docker run -d -p 3000:3000 grafana/grafana
diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md
index 05192512e5a..3650560d5cf 100644
--- a/docs/sources/installation/rpm.md
+++ b/docs/sources/installation/rpm.md
@@ -16,6 +16,7 @@ weight = 2
Description | Download
------------ | -------------
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.4 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm)
+Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.x86_64.rpm)
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
@@ -29,6 +30,12 @@ You can install Grafana using Yum directly.
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4-1.x86_64.rpm
```
+## Install Beta
+
+```bash
+$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.x86_64.rpm
+```
+
Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md
index 49cdd4ca1d3..c72bb4c0921 100644
--- a/docs/sources/installation/upgrading.md
+++ b/docs/sources/installation/upgrading.md
@@ -25,7 +25,7 @@ Before upgrading it can be a good idea to backup your Grafana database. This wil
If you use sqlite you only need to make a backup of your `grafana.db` file. This is usually located at `/var/lib/grafana/grafana.db` on unix system.
If you are unsure what database you use and where it is stored check you grafana configuration file. If you
-installed grafana to custom location using a binary tar/zip it is usally in `/data`.
+installed grafana to custom location using a binary tar/zip it is usually in `/data`.
#### mysql
diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md
index 4f8d7696b57..31fe243c01d 100644
--- a/docs/sources/installation/windows.md
+++ b/docs/sources/installation/windows.md
@@ -13,6 +13,7 @@ weight = 3
Description | Download
------------ | -------------
Latest stable package for Windows | [grafana-5.0.4.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.4.windows-x64.zip)
+Latest beta package for Windows | [grafana.5.1.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.windows-x64.zip)
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
installation.
diff --git a/docs/sources/reference/playlist.md b/docs/sources/reference/playlist.md
index 5a6bf921334..182e69eebd0 100644
--- a/docs/sources/reference/playlist.md
+++ b/docs/sources/reference/playlist.md
@@ -49,7 +49,7 @@ Click the back button to rewind to the previous Dashboard in the Playlist.
In TV mode the top navbar, row & panel controls will all fade to transparent.
This happens automatically after one minute of user inactivity but can also be toggled manually
-with the `d v` sequence shortcut. Any mouse movement or keyboard action will
+with the `d v` sequence shortcut, or by appending the parameter `?inactive` to the dashboard URL. Any mouse movement or keyboard action will
restore navbar & controls.
Another feature is the kiosk mode - in kiosk mode the navbar is completely hidden/removed from view. This can be enabled with the `d k`
diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md
index 016d64d9ee9..6dbc9cc9d11 100644
--- a/docs/sources/reference/templating.md
+++ b/docs/sources/reference/templating.md
@@ -168,7 +168,7 @@ Option | Description
*Include All option* | Add a special `All` option whose value includes all options.
*Custom all value* | By default the `All` value will include all options in combined expression. This can become very long and can have performance problems. Many times it can be better to specify a custom all value, like a wildcard regex. To make it possible to have custom regex, globs or lucene syntax in the **Custom all value** option it is never escaped so you will have to think avbout what is a valid value for your data source.
-### Formating multiple values
+### Formatting multiple values
Interpolating a variable with multiple values selected is tricky as it is not straight forward how to format the multiple values to into a string that
is valid in the given context where the variable is used. Grafana tries to solve this by allowing each data source plugin to
@@ -186,7 +186,7 @@ break the regex expression.
**Elasticsearch** uses lucene query syntax, so the same variable would, in this case, be formatted as `("host1" OR "host2" OR "host3")`. In this case every value
needs to be escaped so that the value can contain lucene control words and quotation marks.
-#### Formating troubles
+#### Formatting troubles
Automatic escaping & formatting can cause problems and it can be tricky to grasp the logic is behind it.
Especially for InfluxDB and Prometheus where the use of regex syntax requires that the variable is used in regex operator context.
diff --git a/docs/sources/tutorials/authproxy.md b/docs/sources/tutorials/authproxy.md
index 8003be20644..6f13de85c18 100644
--- a/docs/sources/tutorials/authproxy.md
+++ b/docs/sources/tutorials/authproxy.md
@@ -108,7 +108,7 @@ In this example we use Apache as a reverseProxy in front of Grafana. Apache hand
* The next part of the configuration is the tricky part. We use Apache’s rewrite engine to create our **X-WEBAUTH-USER header**, populated with the authenticated user.
- * **RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER}, NS]**: This line is a little bit of magic. What it does, is for every request use the rewriteEngines look-ahead (LA-U) feature to determine what the REMOTE_USER variable would be set to after processing the request. Then assign the result to the variable PROXY_USER. This is neccessary as the REMOTE_USER variable is not available to the RequestHeader function.
+ * **RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER}, NS]**: This line is a little bit of magic. What it does, is for every request use the rewriteEngines look-ahead (LA-U) feature to determine what the REMOTE_USER variable would be set to after processing the request. Then assign the result to the variable PROXY_USER. This is necessary as the REMOTE_USER variable is not available to the RequestHeader function.
* **RequestHeader set X-WEBAUTH-USER “%{PROXY_USER}e”**: With the authenticated username now stored in the PROXY_USER variable, we create a new HTTP request header that will be sent to our backend Grafana containing the username.
@@ -149,7 +149,7 @@ auto_sign_up = true
##### Grafana Container
-For this example, we use the offical Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/)
+For this example, we use the official Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/)
* Create a file `grafana.ini` with the following contents
@@ -166,7 +166,7 @@ header_property = username
auto_sign_up = true
```
-* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We dont expose any ports for this container as it will only be connected to by our Apache container.
+* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose any ports for this container as it will only be connected to by our Apache container.
```bash
docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana
@@ -174,7 +174,7 @@ docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana graf
### Apache Container
-For this example we use the offical Apache docker image available at [Docker Hub](https://hub.docker.com/_/httpd/)
+For this example we use the official Apache docker image available at [Docker Hub](https://hub.docker.com/_/httpd/)
* Create a file `httpd.conf` with the following contents
@@ -244,4 +244,4 @@ ProxyPassReverse / http://grafana:3000/
### Use grafana.
-With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file.
\ No newline at end of file
+With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file.
diff --git a/package.json b/package.json
index 030219fe587..ce861a25f7b 100644
--- a/package.json
+++ b/package.json
@@ -136,6 +136,7 @@
"angular-route": "^1.6.6",
"angular-sanitize": "^1.6.6",
"babel-polyfill": "^6.26.0",
+ "baron": "^3.0.3",
"brace": "^0.10.0",
"classnames": "^2.2.5",
"clipboard": "^1.7.1",
@@ -151,7 +152,6 @@
"moment": "^2.18.1",
"mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0",
- "perfect-scrollbar": "^1.2.0",
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
diff --git a/packaging/publish/publish_testing.sh b/packaging/publish/publish_testing.sh
index 08ba2a89dd9..9fd4e1f93b9 100755
--- a/packaging/publish/publish_testing.sh
+++ b/packaging/publish/publish_testing.sh
@@ -1,6 +1,6 @@
#! /usr/bin/env bash
-deb_ver=5.0.0-beta5
-rpm_ver=5.0.0-beta5
+deb_ver=5.1.0-beta1
+rpm_ver=5.1.0-beta1
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb
diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go
index 886913d6324..fdf577a6a6f 100644
--- a/pkg/api/annotations.go
+++ b/pkg/api/annotations.go
@@ -2,7 +2,6 @@ package api
import (
"strings"
- "time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -15,9 +14,10 @@ import (
func GetAnnotations(c *m.ReqContext) Response {
query := &annotations.ItemQuery{
- From: c.QueryInt64("from") / 1000,
- To: c.QueryInt64("to") / 1000,
+ From: c.QueryInt64("from"),
+ To: c.QueryInt64("to"),
OrgId: c.OrgId,
+ UserId: c.QueryInt64("userId"),
AlertId: c.QueryInt64("alertId"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
@@ -37,7 +37,7 @@ func GetAnnotations(c *m.ReqContext) Response {
if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
}
- item.Time = item.Time * 1000
+ item.Time = item.Time
}
return JSON(200, items)
@@ -68,16 +68,12 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
UserId: c.UserId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
- Epoch: cmd.Time / 1000,
+ Epoch: cmd.Time,
Text: cmd.Text,
Data: cmd.Data,
Tags: cmd.Tags,
}
- if item.Epoch == 0 {
- item.Epoch = time.Now().Unix()
- }
-
if err := repo.Save(&item); err != nil {
return Error(500, "Failed to save annotation", err)
}
@@ -97,7 +93,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
}
item.Id = 0
- item.Epoch = cmd.TimeEnd / 1000
+ item.Epoch = cmd.TimeEnd
if err := repo.Save(&item); err != nil {
return Error(500, "Failed save annotation for region end time", err)
@@ -132,9 +128,6 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
return Error(500, "Failed to save Graphite annotation", err)
}
- if cmd.When == 0 {
- cmd.When = time.Now().Unix()
- }
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
@@ -163,7 +156,7 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
- Epoch: cmd.When,
+ Epoch: cmd.When * 1000,
Text: text,
Tags: tagsArray,
}
@@ -191,7 +184,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
OrgId: c.OrgId,
UserId: c.UserId,
Id: annotationID,
- Epoch: cmd.Time / 1000,
+ Epoch: cmd.Time,
Text: cmd.Text,
Tags: cmd.Tags,
}
@@ -203,7 +196,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
if cmd.IsRegion {
itemRight := item
itemRight.RegionId = item.Id
- itemRight.Epoch = cmd.TimeEnd / 1000
+ itemRight.Epoch = cmd.TimeEnd
// We don't know id of region right event, so set it to 0 and find then using query like
// ... WHERE region_id = AND id != ...
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 3c7b81e472d..96b764b95b9 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -149,8 +149,6 @@ func (hs *HTTPServer) registerRoutes() {
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
- teamsRoute.Get("/:teamId", wrap(GetTeamByID))
- teamsRoute.Get("/search", wrap(SearchTeams))
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
@@ -159,6 +157,12 @@ func (hs *HTTPServer) registerRoutes() {
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
}, reqOrgAdmin)
+ // team without requirement of user to be org admin
+ apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
+ teamsRoute.Get("/:teamId", wrap(GetTeamByID))
+ teamsRoute.Get("/search", wrap(SearchTeams))
+ })
+
// org information available to all users.
apiRoute.Group("/org", func(orgRoute RouteRegister) {
orgRoute.Get("/", wrap(GetOrgCurrent))
@@ -170,7 +174,6 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
- orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
@@ -184,6 +187,11 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
}, reqOrgAdmin)
+ // current org without requirement of user to be org admin
+ apiRoute.Group("/org", func(orgRoute RouteRegister) {
+ orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
+ })
+
// create new org
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go
index ce9da1e8790..9f282794076 100644
--- a/pkg/api/avatar/avatar.go
+++ b/pkg/api/avatar/avatar.go
@@ -258,9 +258,6 @@ func (this *thunderTask) fetch() error {
this.Avatar.data = &bytes.Buffer{}
writer := bufio.NewWriter(this.Avatar.data)
- if _, err = io.Copy(writer, resp.Body); err != nil {
- return err
- }
-
- return nil
+ _, err = io.Copy(writer, resp.Body)
+ return err
}
diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go
index 11a028cdd29..c2ab6dd9a1a 100644
--- a/pkg/api/dashboard.go
+++ b/pkg/api/dashboard.go
@@ -102,6 +102,16 @@ func GetDashboard(c *m.ReqContext) Response {
meta.FolderUrl = query.Result.GetUrl()
}
+ isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id}
+ err = bus.Dispatch(isDashboardProvisioned)
+ if err != nil {
+ return Error(500, "Error while checking if dashboard is provisioned", err)
+ }
+
+ if isDashboardProvisioned.Result {
+ meta.Provisioned = true
+ }
+
// make sure db version is in sync with json model version
dash.Data.Set("version", dash.Version)
@@ -228,7 +238,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
err == m.ErrDashboardWithSameUIDExists ||
err == m.ErrFolderNotFound ||
err == m.ErrDashboardFolderCannotHaveParent ||
- err == m.ErrDashboardFolderNameExists {
+ err == m.ErrDashboardFolderNameExists ||
+ err == m.ErrDashboardCannotSaveProvisionedDashboard {
return Error(400, err.Error(), nil)
}
diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go
index 653815aea5c..342eaf556c6 100644
--- a/pkg/api/dashboard_permission.go
+++ b/pkg/api/dashboard_permission.go
@@ -29,6 +29,11 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
}
for _, perm := range acl {
+ perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
+
+ if perm.TeamId > 0 {
+ perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
+ }
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go
index bdf80ef5241..24f0bdca365 100644
--- a/pkg/api/dashboard_permission_test.go
+++ b/pkg/api/dashboard_permission_test.go
@@ -143,7 +143,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) {
})
})
- Convey("When trying to override inherited permissions with lower presedence", func() {
+ Convey("When trying to override inherited permissions with lower precedence", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
CanAdminValue: true,
diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go
index 0d87023ce40..ccde2382787 100644
--- a/pkg/api/dashboard_test.go
+++ b/pkg/api/dashboard_test.go
@@ -42,6 +42,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
+ bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
+ query.Result = false
+ return nil
+ })
+
viewerRole := m.ROLE_VIEWER
editorRole := m.ROLE_EDITOR
@@ -192,6 +197,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
fakeDash.HasAcl = true
setting.ViewersCanEdit = false
+ bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
+ query.Result = false
+ return nil
+ })
+
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{fakeDash}
query.Result = dashboards
@@ -625,6 +635,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
dashTwo.FolderId = 3
dashTwo.HasAcl = false
+ bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
+ query.Result = false
+ return nil
+ })
+
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
dashboards := []*m.Dashboard{dashOne, dashTwo}
query.Result = dashboards
@@ -720,6 +735,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
+ {SaveError: m.ErrDashboardCannotSaveProvisionedDashboard, ExpectedStatusCode: 400},
{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
}
@@ -750,6 +766,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
+ bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
+ query.Result = false
+ return nil
+ })
+
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
query.Result = &m.DashboardVersion{
Data: simplejson.NewFromAny(map[string]interface{}{
diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go
index e4c66aebbda..39a6dca580d 100644
--- a/pkg/api/dtos/dashboard.go
+++ b/pkg/api/dtos/dashboard.go
@@ -28,6 +28,7 @@ type DashboardMeta struct {
FolderId int64 `json:"folderId"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
+ Provisioned bool `json:"provisioned"`
}
type DashboardFullWithMeta struct {
diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go
index 0d0904c99ea..d19ec848ab2 100644
--- a/pkg/api/folder_permission.go
+++ b/pkg/api/folder_permission.go
@@ -33,6 +33,12 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
perm.FolderId = folder.Id
perm.DashboardId = 0
+ perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
+
+ if perm.TeamId > 0 {
+ perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
+ }
+
if perm.Slug != "" {
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
}
diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go
index 387a543ca89..8e01e869329 100644
--- a/pkg/api/http_server.go
+++ b/pkg/api/http_server.go
@@ -139,7 +139,7 @@ func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
}
hs.httpSrv.TLSConfig = tlsCfg
- hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
+ hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
}
diff --git a/pkg/api/index.go b/pkg/api/index.go
index a1d21d1c686..94094706f68 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -118,9 +118,14 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
})
if c.IsSignedIn {
+ // Only set login if it's different from the name
+ var login string
+ if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() {
+ login = c.SignedInUser.Login
+ }
profileNode := &dtos.NavLink{
Text: c.SignedInUser.NameOrFallback(),
- SubTitle: c.SignedInUser.Login,
+ SubTitle: login,
Id: "profile",
Img: data.User.GravatarUrl,
Url: setting.AppSubUrl + "/profile",
@@ -284,6 +289,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
data.NavTree = append(data.NavTree, &dtos.NavLink{
Text: "Help",
+ SubTitle: fmt.Sprintf(`Grafana v%s (%s)`, setting.BuildVersion, setting.BuildCommit),
Id: "help",
Url: "#",
Icon: "gicon gicon-question",
diff --git a/pkg/api/login.go b/pkg/api/login.go
index 671e5fb7ecd..9d0fa31946f 100644
--- a/pkg/api/login.go
+++ b/pkg/api/login.go
@@ -101,13 +101,14 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
return Error(401, "Login is disabled", nil)
}
- authQuery := login.LoginUserQuery{
- Username: cmd.User,
- Password: cmd.Password,
- IpAddress: c.Req.RemoteAddr,
+ authQuery := &m.LoginUserQuery{
+ ReqContext: c,
+ Username: cmd.User,
+ Password: cmd.Password,
+ IpAddress: c.Req.RemoteAddr,
}
- if err := bus.Dispatch(&authQuery); err != nil {
+ if err := bus.Dispatch(authQuery); err != nil {
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
return Error(401, "Invalid username or password", err)
}
diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go
index 1dba38e9cbd..c4a5f8fdacf 100644
--- a/pkg/api/login_oauth.go
+++ b/pkg/api/login_oauth.go
@@ -6,7 +6,6 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
- "errors"
"fmt"
"io/ioutil"
"net/http"
@@ -16,22 +15,15 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
)
-var (
- ErrProviderDeniedRequest = errors.New("Login provider denied login request")
- ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
- ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
- ErrUsersQuotaReached = errors.New("Users quota reached")
- ErrNoEmail = errors.New("Login provider didn't return an email address")
- oauthLogger = log.New("oauth")
-)
+var oauthLogger = log.New("oauth")
func GenStateString() string {
rnd := make([]byte, 32)
@@ -56,7 +48,7 @@ func OAuthLogin(ctx *m.ReqContext) {
if errorParam != "" {
errorDesc := ctx.Query("error_description")
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
- redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
+ redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
return
}
@@ -149,54 +141,43 @@ func OAuthLogin(ctx *m.ReqContext) {
// validate that we got at least an email address
if userInfo.Email == "" {
- redirectWithError(ctx, ErrNoEmail)
+ redirectWithError(ctx, login.ErrNoEmail)
return
}
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
- redirectWithError(ctx, ErrEmailNotAllowed)
+ redirectWithError(ctx, login.ErrEmailNotAllowed)
return
}
- userQuery := m.GetUserByEmailQuery{Email: userInfo.Email}
- err = bus.Dispatch(&userQuery)
+ extUser := &m.ExternalUserInfo{
+ AuthModule: "oauth_" + name,
+ AuthId: userInfo.Id,
+ Name: userInfo.Name,
+ Login: userInfo.Login,
+ Email: userInfo.Email,
+ OrgRoles: map[int64]m.RoleType{},
+ }
- // create account if missing
- if err == m.ErrUserNotFound {
- if !connect.IsSignupAllowed() {
- redirectWithError(ctx, ErrSignUpNotAllowed)
- return
- }
- limitReached, err := quota.QuotaReached(ctx, "user")
- if err != nil {
- ctx.Handle(500, "Failed to get user quota", err)
- return
- }
- if limitReached {
- redirectWithError(ctx, ErrUsersQuotaReached)
- return
- }
- cmd := m.CreateUserCommand{
- Login: userInfo.Login,
- Email: userInfo.Email,
- Name: userInfo.Name,
- Company: userInfo.Company,
- DefaultOrgRole: userInfo.Role,
- }
+ if userInfo.Role != "" {
+ extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
+ }
- if err = bus.Dispatch(&cmd); err != nil {
- ctx.Handle(500, "Failed to create account", err)
- return
- }
-
- userQuery.Result = &cmd.Result
- } else if err != nil {
- ctx.Handle(500, "Unexpected error", err)
+ // add/update user in grafana
+ cmd := &m.UpsertUserCommand{
+ ReqContext: ctx,
+ ExternalUser: extUser,
+ SignupAllowed: connect.IsSignupAllowed(),
+ }
+ err = bus.Dispatch(cmd)
+ if err != nil {
+ redirectWithError(ctx, err)
+ return
}
// login
- loginUserWithUser(userQuery.Result, ctx)
+ loginUserWithUser(cmd.Result, ctx)
metrics.M_Api_Login_OAuth.Inc()
diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go
index 5c06d652b70..c1b8ffe595e 100644
--- a/pkg/api/metrics.go
+++ b/pkg/api/metrics.go
@@ -75,7 +75,7 @@ func GetTestDataScenarios(c *m.ReqContext) Response {
return JSON(200, &result)
}
-// Genereates a index out of range error
+// Generates a index out of range error
func GenerateError(c *m.ReqContext) Response {
var array []string
return JSON(200, array[20])
diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go
index d2413dfbb4c..a90b6425cb6 100644
--- a/pkg/api/playlist.go
+++ b/pkg/api/playlist.go
@@ -33,7 +33,7 @@ func ValidateOrgPlaylist(c *m.ReqContext) {
return
}
- if len(items) == 0 {
+ if len(items) == 0 && c.Context.Req.Method != "DELETE" {
c.JsonApiErr(404, "Playlist is empty", itemsErr)
return
}
diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go
index 3e1a93abb3b..f4eb1140aa0 100644
--- a/pkg/api/pluginproxy/ds_proxy.go
+++ b/pkg/api/pluginproxy/ds_proxy.go
@@ -189,12 +189,6 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
}
func (proxy *DataSourceProxy) validateRequest() error {
- if proxy.ds.Type == m.DS_INFLUXDB {
- if proxy.ctx.Query("db") != proxy.ds.Database {
- return errors.New("Datasource is not configured to allow this database")
- }
- }
-
if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) {
return errors.New("Target url is not a valid target")
}
diff --git a/pkg/api/static/static.go b/pkg/api/static/static.go
index 7a61c85b4f3..2a35dd11fa6 100644
--- a/pkg/api/static/static.go
+++ b/pkg/api/static/static.go
@@ -48,7 +48,7 @@ type StaticOptions struct {
// Expires defines which user-defined function to use for producing a HTTP Expires Header
// https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
AddHeaders func(ctx *macaron.Context)
- // FileSystem is the interface for supporting any implmentation of file system.
+ // FileSystem is the interface for supporting any implementation of file system.
FileSystem http.FileSystem
}
diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go
index f40bc9c081b..9bdb73a5858 100644
--- a/pkg/cmd/grafana-cli/commands/install_command.go
+++ b/pkg/cmd/grafana-cli/commands/install_command.go
@@ -33,7 +33,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
fileInfo, err := os.Stat(pluginsDir)
if err != nil {
if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
- return errors.New(fmt.Sprintf("pluginsDir (%s) is not a directory", pluginsDir))
+ return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir)
}
return nil
}
diff --git a/pkg/cmd/grafana-cli/commands/ls_command.go b/pkg/cmd/grafana-cli/commands/ls_command.go
index 7dcecb9d725..30745ce3172 100644
--- a/pkg/cmd/grafana-cli/commands/ls_command.go
+++ b/pkg/cmd/grafana-cli/commands/ls_command.go
@@ -24,7 +24,7 @@ var validateLsCommand = func(pluginDir string) error {
return fmt.Errorf("error: %s", err)
}
- if pluginDirInfo.IsDir() == false {
+ if !pluginDirInfo.IsDir() {
return errors.New("plugin path is not a directory")
}
diff --git a/pkg/cmd/grafana-cli/commands/remove_command.go b/pkg/cmd/grafana-cli/commands/remove_command.go
index d5ed73def05..e51929dc95c 100644
--- a/pkg/cmd/grafana-cli/commands/remove_command.go
+++ b/pkg/cmd/grafana-cli/commands/remove_command.go
@@ -3,12 +3,11 @@ package commands
import (
"errors"
"fmt"
- m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
- services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"strings"
+
+ services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
)
-var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
func removeCommand(c CommandLine) error {
diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go
index 636292cce11..e01df2dab60 100644
--- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go
+++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go
@@ -53,8 +53,7 @@ func upgradeAllCommand(c CommandLine) error {
for _, p := range pluginsToUpgrade {
logger.Infof("Updating %v \n", p.Id)
- var err error
- err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
+ err := s.RemoveInstalledPlugin(pluginsDir, p.Id)
if err != nil {
return err
}
diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go
index d13e90d6a2f..e743d42022c 100644
--- a/pkg/cmd/grafana-cli/services/services.go
+++ b/pkg/cmd/grafana-cli/services/services.go
@@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"path"
+ "runtime"
"time"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
@@ -42,7 +43,7 @@ func Init(version string, skipTLSVerify bool) {
}
HttpClient = http.Client{
- Timeout: time.Duration(10 * time.Second),
+ Timeout: 10 * time.Second,
Transport: tr,
}
}
@@ -155,6 +156,8 @@ func sendRequest(repoUrl string, subPaths ...string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
req.Header.Set("grafana-version", grafanaVersion)
+ req.Header.Set("grafana-os", runtime.GOOS)
+ req.Header.Set("grafana-arch", runtime.GOARCH)
req.Header.Set("User-Agent", "grafana "+grafanaVersion)
if err != nil {
diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go
index ab0e12f2d9f..777176f27da 100644
--- a/pkg/cmd/grafana-server/main.go
+++ b/pkg/cmd/grafana-server/main.go
@@ -33,7 +33,6 @@ import (
var version = "5.0.0"
var commit = "NA"
var buildstamp string
-var build_date string
var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
diff --git a/pkg/components/apikeygen/apikeygen.go b/pkg/components/apikeygen/apikeygen.go
index 310188a80ef..7824cf7667f 100644
--- a/pkg/components/apikeygen/apikeygen.go
+++ b/pkg/components/apikeygen/apikeygen.go
@@ -33,7 +33,7 @@ func New(orgId int64, name string) KeyGenResult {
jsonString, _ := json.Marshal(jsonKey)
- result.ClientSecret = base64.StdEncoding.EncodeToString([]byte(jsonString))
+ result.ClientSecret = base64.StdEncoding.EncodeToString(jsonString)
return result
}
@@ -44,7 +44,7 @@ func Decode(keyString string) (*ApiKeyJson, error) {
}
var keyObj ApiKeyJson
- err = json.Unmarshal([]byte(jsonString), &keyObj)
+ err = json.Unmarshal(jsonString, &keyObj)
if err != nil {
return nil, ErrInvalidApiKey
}
diff --git a/pkg/components/dashdiffs/compare.go b/pkg/components/dashdiffs/compare.go
index f5f2104cb92..d51011fbead 100644
--- a/pkg/components/dashdiffs/compare.go
+++ b/pkg/components/dashdiffs/compare.go
@@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
diff "github.com/yudai/gojsondiff"
deltaFormatter "github.com/yudai/gojsondiff/formatter"
@@ -15,11 +14,8 @@ import (
var (
// ErrUnsupportedDiffType occurs when an invalid diff type is used.
ErrUnsupportedDiffType = errors.New("dashdiff: unsupported diff type")
-
// ErrNilDiff occurs when two compared interfaces are identical.
ErrNilDiff = errors.New("dashdiff: diff is nil")
-
- diffLogger = log.New("dashdiffs")
)
type DiffType int
diff --git a/pkg/components/dashdiffs/formatter_json.go b/pkg/components/dashdiffs/formatter_json.go
index 3a9ddcc4ee3..488a345d492 100644
--- a/pkg/components/dashdiffs/formatter_json.go
+++ b/pkg/components/dashdiffs/formatter_json.go
@@ -22,7 +22,7 @@ const (
)
var (
- // changeTypeToSymbol is used for populating the terminating characer in
+ // changeTypeToSymbol is used for populating the terminating character in
// the diff
changeTypeToSymbol = map[ChangeType]string{
ChangeNil: "",
diff --git a/pkg/components/dynmap/dynmap.go b/pkg/components/dynmap/dynmap.go
index 797694845cd..2b86ce384eb 100644
--- a/pkg/components/dynmap/dynmap.go
+++ b/pkg/components/dynmap/dynmap.go
@@ -585,7 +585,6 @@ func (v *Value) Null() error {
switch v.data.(type) {
case nil:
valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
- break
}
if valid {
@@ -607,7 +606,6 @@ func (v *Value) Array() ([]*Value, error) {
switch v.data.(type) {
case []interface{}:
valid = true
- break
}
// Unsure if this is a good way to use slices, it's probably not
@@ -638,7 +636,6 @@ func (v *Value) Number() (json.Number, error) {
switch v.data.(type) {
case json.Number:
valid = true
- break
}
if valid {
@@ -687,7 +684,6 @@ func (v *Value) Boolean() (bool, error) {
switch v.data.(type) {
case bool:
valid = true
- break
}
if valid {
@@ -709,7 +705,6 @@ func (v *Value) Object() (*Object, error) {
switch v.data.(type) {
case map[string]interface{}:
valid = true
- break
}
if valid {
@@ -746,7 +741,6 @@ func (v *Value) ObjectArray() ([]*Object, error) {
switch v.data.(type) {
case []interface{}:
valid = true
- break
}
// Unsure if this is a good way to use slices, it's probably not
@@ -782,7 +776,6 @@ func (v *Value) String() (string, error) {
switch v.data.(type) {
case string:
valid = true
- break
}
if valid {
diff --git a/pkg/components/dynmap/dynmap_test.go b/pkg/components/dynmap/dynmap_test.go
index cc002ea06e0..fa5f73c3719 100644
--- a/pkg/components/dynmap/dynmap_test.go
+++ b/pkg/components/dynmap/dynmap_test.go
@@ -21,7 +21,7 @@ func NewAssert(t *testing.T) *Assert {
}
func (assert *Assert) True(value bool, message string) {
- if value == false {
+ if !value {
log.Panicln("Assert: ", message)
}
}
@@ -76,10 +76,10 @@ func TestFirst(t *testing.T) {
assert.True(s == "fallback", "must get string return fallback")
s, err = j.GetString("name")
- assert.True(s == "anton" && err == nil, "name shoud match")
+ assert.True(s == "anton" && err == nil, "name should match")
s, err = j.GetString("address", "street")
- assert.True(s == "Street 42" && err == nil, "street shoud match")
+ assert.True(s == "Street 42" && err == nil, "street should match")
//log.Println("s: ", s.String())
_, err = j.GetNumber("age")
@@ -119,13 +119,13 @@ func TestFirst(t *testing.T) {
assert.True(s == "" && err != nil, "nonexistent string fail")
b, err := j.GetBoolean("true")
- assert.True(b == true && err == nil, "bool true test")
+ assert.True(b && err == nil, "bool true test")
b, err = j.GetBoolean("false")
- assert.True(b == false && err == nil, "bool false test")
+ assert.True(!b && err == nil, "bool false test")
b, err = j.GetBoolean("invalid_field")
- assert.True(b == false && err != nil, "bool invalid test")
+ assert.True(!b && err != nil, "bool invalid test")
list, err := j.GetValueArray("list")
assert.True(list != nil && err == nil, "list should be an array")
diff --git a/pkg/components/imguploader/azureblobuploader.go b/pkg/components/imguploader/azureblobuploader.go
index 40d2de836be..3c0ac5b8884 100644
--- a/pkg/components/imguploader/azureblobuploader.go
+++ b/pkg/components/imguploader/azureblobuploader.go
@@ -225,7 +225,7 @@ func (a *Auth) SignRequest(req *http.Request) {
)
decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
- sha256 := hmac.New(sha256.New, []byte(decodedKey))
+ sha256 := hmac.New(sha256.New, decodedKey)
sha256.Write([]byte(strToSign))
signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
diff --git a/pkg/components/null/float.go b/pkg/components/null/float.go
index 1e78946e878..caf4d8b677c 100644
--- a/pkg/components/null/float.go
+++ b/pkg/components/null/float.go
@@ -50,7 +50,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
}
switch x := v.(type) {
case float64:
- f.Float64 = float64(x)
+ f.Float64 = x
case map[string]interface{}:
err = json.Unmarshal(data, &f.NullFloat64)
case nil:
diff --git a/pkg/log/file.go b/pkg/log/file.go
index 721db1e55b3..d137adbf3de 100644
--- a/pkg/log/file.go
+++ b/pkg/log/file.go
@@ -99,10 +99,7 @@ func (w *FileLogWriter) StartLogger() error {
return err
}
w.mw.SetFd(fd)
- if err = w.initFd(); err != nil {
- return err
- }
- return nil
+ return w.initFd()
}
func (w *FileLogWriter) docheck(size int) {
diff --git a/pkg/login/auth.go b/pkg/login/auth.go
index 5527c7271d6..215a22cde33 100644
--- a/pkg/login/auth.go
+++ b/pkg/login/auth.go
@@ -8,23 +8,22 @@ import (
)
var (
- ErrInvalidCredentials = errors.New("Invalid Username or Password")
- ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
+ ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
+ ErrInvalidCredentials = errors.New("Invalid Username or Password")
+ ErrNoEmail = errors.New("Login provider didn't return an email address")
+ ErrProviderDeniedRequest = errors.New("Login provider denied login request")
+ ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
+ ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
+ ErrUsersQuotaReached = errors.New("Users quota reached")
+ ErrGettingUserQuota = errors.New("Error getting user quota")
)
-type LoginUserQuery struct {
- Username string
- Password string
- User *m.User
- IpAddress string
-}
-
func Init() {
bus.AddHandler("auth", AuthenticateUser)
loadLdapConfig()
}
-func AuthenticateUser(query *LoginUserQuery) error {
+func AuthenticateUser(query *m.LoginUserQuery) error {
if err := validateLoginAttempts(query.Username); err != nil {
return err
}
diff --git a/pkg/login/auth_test.go b/pkg/login/auth_test.go
index 59d3c8f2b33..932125c410e 100644
--- a/pkg/login/auth_test.go
+++ b/pkg/login/auth_test.go
@@ -151,7 +151,7 @@ func TestAuthenticateUser(t *testing.T) {
}
type authScenarioContext struct {
- loginUserQuery *LoginUserQuery
+ loginUserQuery *m.LoginUserQuery
grafanaLoginWasCalled bool
ldapLoginWasCalled bool
loginAttemptValidationWasCalled bool
@@ -161,14 +161,14 @@ type authScenarioContext struct {
type authScenarioFunc func(sc *authScenarioContext)
func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
- loginUsingGrafanaDB = func(query *LoginUserQuery) error {
+ loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
sc.grafanaLoginWasCalled = true
return err
}
}
func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
- loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
+ loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
sc.ldapLoginWasCalled = true
return enabled, err
}
@@ -182,7 +182,7 @@ func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
}
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
- saveInvalidLoginAttempt = func(query *LoginUserQuery) {
+ saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
sc.saveInvalidLoginAttemptWasCalled = true
}
}
@@ -195,7 +195,7 @@ func authScenario(desc string, fn authScenarioFunc) {
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
sc := &authScenarioContext{
- loginUserQuery: &LoginUserQuery{
+ loginUserQuery: &m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
diff --git a/pkg/login/brute_force_login_protection.go b/pkg/login/brute_force_login_protection.go
index 2ea93979c7a..ca5e0a667ff 100644
--- a/pkg/login/brute_force_login_protection.go
+++ b/pkg/login/brute_force_login_protection.go
@@ -34,7 +34,7 @@ var validateLoginAttempts = func(username string) error {
return nil
}
-var saveInvalidLoginAttempt = func(query *LoginUserQuery) {
+var saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
if setting.DisableBruteForceLoginProtection {
return
}
diff --git a/pkg/login/brute_force_login_protection_test.go b/pkg/login/brute_force_login_protection_test.go
index 5375134ba88..aca100760c7 100644
--- a/pkg/login/brute_force_login_protection_test.go
+++ b/pkg/login/brute_force_login_protection_test.go
@@ -50,7 +50,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
return nil
})
- saveInvalidLoginAttempt(&LoginUserQuery{
+ saveInvalidLoginAttempt(&m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
@@ -103,7 +103,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
return nil
})
- saveInvalidLoginAttempt(&LoginUserQuery{
+ saveInvalidLoginAttempt(&m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go
new file mode 100644
index 00000000000..e1d5e3e3b48
--- /dev/null
+++ b/pkg/login/ext_user.go
@@ -0,0 +1,184 @@
+package login
+
+import (
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/log"
+ m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/quota"
+)
+
+func init() {
+ bus.AddHandler("auth", UpsertUser)
+}
+
+func UpsertUser(cmd *m.UpsertUserCommand) error {
+ extUser := cmd.ExternalUser
+
+ userQuery := &m.GetUserByAuthInfoQuery{
+ AuthModule: extUser.AuthModule,
+ AuthId: extUser.AuthId,
+ UserId: extUser.UserId,
+ Email: extUser.Email,
+ Login: extUser.Login,
+ }
+ err := bus.Dispatch(userQuery)
+ if err != m.ErrUserNotFound && err != nil {
+ return err
+ }
+
+ if err != nil {
+ if !cmd.SignupAllowed {
+ log.Warn("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule)
+ return ErrInvalidCredentials
+ }
+
+ limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
+ if err != nil {
+ log.Warn("Error getting user quota", "err", err)
+ return ErrGettingUserQuota
+ }
+ if limitReached {
+ return ErrUsersQuotaReached
+ }
+
+ cmd.Result, err = createUser(extUser)
+ if err != nil {
+ return err
+ }
+
+ if extUser.AuthModule != "" && extUser.AuthId != "" {
+ cmd2 := &m.SetAuthInfoCommand{
+ UserId: cmd.Result.Id,
+ AuthModule: extUser.AuthModule,
+ AuthId: extUser.AuthId,
+ }
+ if err := bus.Dispatch(cmd2); err != nil {
+ return err
+ }
+ }
+
+ } else {
+ cmd.Result = userQuery.Result
+
+ err = updateUser(cmd.Result, extUser)
+ if err != nil {
+ return err
+ }
+ }
+
+ return syncOrgRoles(cmd.Result, extUser)
+}
+
+func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
+ cmd := &m.CreateUserCommand{
+ Login: extUser.Login,
+ Email: extUser.Email,
+ Name: extUser.Name,
+ SkipOrgSetup: len(extUser.OrgRoles) > 0,
+ }
+ if err := bus.Dispatch(cmd); err != nil {
+ return nil, err
+ }
+
+ return &cmd.Result, nil
+}
+
+func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
+ // sync user info
+ updateCmd := &m.UpdateUserCommand{
+ UserId: user.Id,
+ }
+
+ needsUpdate := false
+ if extUser.Login != "" && extUser.Login != user.Login {
+ updateCmd.Login = extUser.Login
+ user.Login = extUser.Login
+ needsUpdate = true
+ }
+
+ if extUser.Email != "" && extUser.Email != user.Email {
+ updateCmd.Email = extUser.Email
+ user.Email = extUser.Email
+ needsUpdate = true
+ }
+
+ if extUser.Name != "" && extUser.Name != user.Name {
+ updateCmd.Name = extUser.Name
+ user.Name = extUser.Name
+ needsUpdate = true
+ }
+
+ if !needsUpdate {
+ return nil
+ }
+
+ log.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
+ return bus.Dispatch(updateCmd)
+}
+
+func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
+ // don't sync org roles if none are specified
+ if len(extUser.OrgRoles) == 0 {
+ return nil
+ }
+
+ orgsQuery := &m.GetUserOrgListQuery{UserId: user.Id}
+ if err := bus.Dispatch(orgsQuery); err != nil {
+ return err
+ }
+
+ handledOrgIds := map[int64]bool{}
+ deleteOrgIds := []int64{}
+
+ // update existing org roles
+ for _, org := range orgsQuery.Result {
+ handledOrgIds[org.OrgId] = true
+
+ if extUser.OrgRoles[org.OrgId] == "" {
+ deleteOrgIds = append(deleteOrgIds, org.OrgId)
+ } else if extUser.OrgRoles[org.OrgId] != org.Role {
+ // update role
+ cmd := &m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extUser.OrgRoles[org.OrgId]}
+ if err := bus.Dispatch(cmd); err != nil {
+ return err
+ }
+ }
+ }
+
+ // add any new org roles
+ for orgId, orgRole := range extUser.OrgRoles {
+ if _, exists := handledOrgIds[orgId]; exists {
+ continue
+ }
+
+ // add role
+ cmd := &m.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId}
+ err := bus.Dispatch(cmd)
+ if err != nil && err != m.ErrOrgNotFound {
+ return err
+ }
+ }
+
+ // delete any removed org roles
+ for _, orgId := range deleteOrgIds {
+ cmd := &m.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id}
+ if err := bus.Dispatch(cmd); err != nil {
+ return err
+ }
+ }
+
+ // update user's default org if needed
+ if _, ok := extUser.OrgRoles[user.OrgId]; !ok {
+ for orgId := range extUser.OrgRoles {
+ user.OrgId = orgId
+ break
+ }
+
+ return bus.Dispatch(&m.SetUsingOrgCommand{
+ UserId: user.Id,
+ OrgId: user.OrgId,
+ })
+ }
+
+ return nil
+}
diff --git a/pkg/login/grafana_login.go b/pkg/login/grafana_login.go
index 677ba776e4f..e8594fdd190 100644
--- a/pkg/login/grafana_login.go
+++ b/pkg/login/grafana_login.go
@@ -17,7 +17,7 @@ var validatePassword = func(providedPassword string, userPassword string, userSa
return nil
}
-var loginUsingGrafanaDB = func(query *LoginUserQuery) error {
+var loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
if err := bus.Dispatch(&userQuery); err != nil {
diff --git a/pkg/login/grafana_login_test.go b/pkg/login/grafana_login_test.go
index 88e52224113..90422678fd2 100644
--- a/pkg/login/grafana_login_test.go
+++ b/pkg/login/grafana_login_test.go
@@ -66,7 +66,7 @@ func TestGrafanaLogin(t *testing.T) {
}
type grafanaLoginScenarioContext struct {
- loginUserQuery *LoginUserQuery
+ loginUserQuery *m.LoginUserQuery
validatePasswordCalled bool
}
@@ -77,7 +77,7 @@ func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) {
origValidatePassword := validatePassword
sc := &grafanaLoginScenarioContext{
- loginUserQuery: &LoginUserQuery{
+ loginUserQuery: &m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go
index be3babac02e..ccf44f77f23 100644
--- a/pkg/login/ldap.go
+++ b/pkg/login/ldap.go
@@ -24,10 +24,9 @@ type ILdapConn interface {
}
type ILdapAuther interface {
- Login(query *LoginUserQuery) error
- SyncSignedInUser(signedInUser *m.SignedInUser) error
- GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error)
- SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error
+ Login(query *m.LoginUserQuery) error
+ SyncUser(query *m.LoginUserQuery) error
+ GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
}
type ldapAuther struct {
@@ -89,7 +88,8 @@ func (a *ldapAuther) Dial() error {
return err
}
-func (a *ldapAuther) Login(query *LoginUserQuery) error {
+func (a *ldapAuther) Login(query *m.LoginUserQuery) error {
+ // connect to ldap server
if err := a.Dial(); err != nil {
return err
}
@@ -101,206 +101,105 @@ func (a *ldapAuther) Login(query *LoginUserQuery) error {
}
// find user entry & attributes
- if ldapUser, err := a.searchForUser(query.Username); err != nil {
+ ldapUser, err := a.searchForUser(query.Username)
+ if err != nil {
return err
- } else {
- a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
+ }
- // check if a second user bind is needed
- if a.requireSecondBind {
- if err := a.secondBind(ldapUser, query.Password); err != nil {
- return err
- }
- }
+ a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
- if grafanaUser, err := a.GetGrafanaUserFor(ldapUser); err != nil {
+ // check if a second user bind is needed
+ if a.requireSecondBind {
+ err = a.secondBind(ldapUser, query.Password)
+ if err != nil {
return err
- } else {
- if syncErr := a.syncInfoAndOrgRoles(grafanaUser, ldapUser); syncErr != nil {
- return syncErr
- }
- query.User = grafanaUser
- return nil
}
}
+
+ grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
+ if err != nil {
+ return err
+ }
+
+ query.User = grafanaUser
+ return nil
}
-func (a *ldapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
- grafanaUser := m.User{
- Id: signedInUser.UserId,
- Login: signedInUser.Login,
- Email: signedInUser.Email,
- Name: signedInUser.Name,
- }
-
- if err := a.Dial(); err != nil {
+func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error {
+ // connect to ldap server
+ err := a.Dial()
+ if err != nil {
return err
}
-
defer a.conn.Close()
- if err := a.serverBind(); err != nil {
+
+ err = a.serverBind()
+ if err != nil {
return err
}
- if ldapUser, err := a.searchForUser(signedInUser.Login); err != nil {
+ // find user entry & attributes
+ ldapUser, err := a.searchForUser(query.Username)
+ if err != nil {
a.log.Error("Failed searching for user in ldap", "error", err)
-
return err
- } else {
- if err := a.syncInfoAndOrgRoles(&grafanaUser, ldapUser); err != nil {
- return err
+ }
+
+ a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
+
+ grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
+ if err != nil {
+ return err
+ }
+
+ query.User = grafanaUser
+ return nil
+}
+
+func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
+ extUser := &m.ExternalUserInfo{
+ AuthModule: "ldap",
+ AuthId: ldapUser.DN,
+ Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
+ Login: ldapUser.Username,
+ Email: ldapUser.Email,
+ OrgRoles: map[int64]m.RoleType{},
+ }
+
+ for _, group := range a.server.LdapGroups {
+ // only use the first match for each org
+ if extUser.OrgRoles[group.OrgId] != "" {
+ continue
}
- a.log.Debug("Got Ldap User Info", "user", spew.Sdump(ldapUser))
+ if ldapUser.isMemberOf(group.GroupDN) {
+ extUser.OrgRoles[group.OrgId] = group.OrgRole
+ }
}
- return nil
-}
-
-// Sync info for ldap user and grafana user
-func (a *ldapAuther) syncInfoAndOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
- // sync user details
- if err := a.syncUserInfo(user, ldapUser); err != nil {
- return err
- }
- // sync org roles
- if err := a.SyncOrgRoles(user, ldapUser); err != nil {
- return err
- }
-
- return nil
-}
-
-func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
// validate that the user has access
// if there are no ldap group mappings access is true
// otherwise a single group must match
- access := len(a.server.LdapGroups) == 0
- for _, ldapGroup := range a.server.LdapGroups {
- if ldapUser.isMemberOf(ldapGroup.GroupDN) {
- access = true
- break
- }
- }
-
- if !access {
- a.log.Info("Ldap Auth: user does not belong in any of the specified ldap groups", "username", ldapUser.Username, "groups", ldapUser.MemberOf)
+ if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 {
+ a.log.Info(
+ "Ldap Auth: user does not belong in any of the specified ldap groups",
+ "username", ldapUser.Username,
+ "groups", ldapUser.MemberOf)
return nil, ErrInvalidCredentials
}
- // get user from grafana db
- userQuery := m.GetUserByLoginQuery{LoginOrEmail: ldapUser.Username}
- if err := bus.Dispatch(&userQuery); err != nil {
- if err == m.ErrUserNotFound && setting.LdapAllowSignup {
- return a.createGrafanaUser(ldapUser)
- } else if err == m.ErrUserNotFound {
- a.log.Warn("Not allowing LDAP login, user not found in internal user database, and ldap allow signup = false")
- return nil, ErrInvalidCredentials
- } else {
- return nil, err
- }
+ // add/update user in grafana
+ userQuery := &m.UpsertUserCommand{
+ ReqContext: ctx,
+ ExternalUser: extUser,
+ SignupAllowed: setting.LdapAllowSignup,
}
-
- return userQuery.Result, nil
-
-}
-func (a *ldapAuther) createGrafanaUser(ldapUser *LdapUserInfo) (*m.User, error) {
- cmd := m.CreateUserCommand{
- Login: ldapUser.Username,
- Email: ldapUser.Email,
- Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
- }
-
- if err := bus.Dispatch(&cmd); err != nil {
+ err := bus.Dispatch(userQuery)
+ if err != nil {
return nil, err
}
- return &cmd.Result, nil
-}
-
-func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *LdapUserInfo) error {
- var name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
- if user.Email == ldapUser.Email && user.Name == name {
- return nil
- }
-
- a.log.Debug("Syncing user info", "username", ldapUser.Username)
- updateCmd := m.UpdateUserCommand{}
- updateCmd.UserId = user.Id
- updateCmd.Login = user.Login
- updateCmd.Email = ldapUser.Email
- updateCmd.Name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
- return bus.Dispatch(&updateCmd)
-}
-
-func (a *ldapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
- if len(a.server.LdapGroups) == 0 {
- a.log.Warn("No group mappings defined")
- return nil
- }
-
- orgsQuery := m.GetUserOrgListQuery{UserId: user.Id}
- if err := bus.Dispatch(&orgsQuery); err != nil {
- return err
- }
-
- handledOrgIds := map[int64]bool{}
-
- // update or remove org roles
- for _, org := range orgsQuery.Result {
- match := false
- handledOrgIds[org.OrgId] = true
-
- for _, group := range a.server.LdapGroups {
- if org.OrgId != group.OrgId {
- continue
- }
-
- if ldapUser.isMemberOf(group.GroupDN) {
- match = true
- if org.Role != group.OrgRole {
- // update role
- cmd := m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: group.OrgRole}
- if err := bus.Dispatch(&cmd); err != nil {
- return err
- }
- }
- // ignore subsequent ldap group mapping matches
- break
- }
- }
-
- // remove role if no mappings match
- if !match {
- cmd := m.RemoveOrgUserCommand{OrgId: org.OrgId, UserId: user.Id}
- if err := bus.Dispatch(&cmd); err != nil {
- return err
- }
- }
- }
-
- // add missing org roles
- for _, group := range a.server.LdapGroups {
- if !ldapUser.isMemberOf(group.GroupDN) {
- continue
- }
-
- if _, exists := handledOrgIds[group.OrgId]; exists {
- continue
- }
-
- // add role
- cmd := m.AddOrgUserCommand{UserId: user.Id, Role: group.OrgRole, OrgId: group.OrgId}
- err := bus.Dispatch(&cmd)
- if err != nil && err != m.ErrOrgNotFound {
- return err
- }
-
- // mark this group has handled so we do not process it again
- handledOrgIds[group.OrgId] = true
- }
-
- return nil
+ return userQuery.Result, nil
}
func (a *ldapAuther) serverBind() error {
@@ -403,8 +302,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
// If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups
var groupSearchResult *ldap.SearchResult
for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
- var filter_replace string
- filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
+ filter_replace := getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
if a.server.GroupSearchFilterUserAttribute == "" {
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
}
@@ -470,7 +368,3 @@ func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
}
return []string{}
}
-
-func createUserFromLdapInfo() error {
- return nil
-}
diff --git a/pkg/login/ldap_login.go b/pkg/login/ldap_login.go
index b74b69db036..5974e19d691 100644
--- a/pkg/login/ldap_login.go
+++ b/pkg/login/ldap_login.go
@@ -1,10 +1,11 @@
package login
import (
+ m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
-var loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
+var loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
if !setting.LdapEnabled {
return false, nil
}
diff --git a/pkg/login/ldap_login_test.go b/pkg/login/ldap_login_test.go
index 6af125566e8..6067a063795 100644
--- a/pkg/login/ldap_login_test.go
+++ b/pkg/login/ldap_login_test.go
@@ -79,7 +79,7 @@ func TestLdapLogin(t *testing.T) {
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
sc.withLoginResult(false)
- enabled, err := loginUsingLdap(&LoginUserQuery{
+ enabled, err := loginUsingLdap(&m.LoginUserQuery{
Username: "user",
Password: "pwd",
})
@@ -117,7 +117,7 @@ type mockLdapAuther struct {
loginCalled bool
}
-func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
+func (a *mockLdapAuther) Login(query *m.LoginUserQuery) error {
a.loginCalled = true
if !a.validLogin {
@@ -127,20 +127,16 @@ func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
return nil
}
-func (a *mockLdapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
+func (a *mockLdapAuther) SyncUser(query *m.LoginUserQuery) error {
return nil
}
-func (a *mockLdapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
+func (a *mockLdapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
return nil, nil
}
-func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
- return nil
-}
-
type ldapLoginScenarioContext struct {
- loginUserQuery *LoginUserQuery
+ loginUserQuery *m.LoginUserQuery
ldapAuthenticatorMock *mockLdapAuther
}
@@ -151,7 +147,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
origNewLdapAuthenticator := NewLdapAuthenticator
sc := &ldapLoginScenarioContext{
- loginUserQuery: &LoginUserQuery{
+ loginUserQuery: &m.LoginUserQuery{
Username: "user",
Password: "pwd",
IpAddress: "192.168.1.1:56433",
diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go
index 8677bbeae42..6085fffb638 100644
--- a/pkg/login/ldap_test.go
+++ b/pkg/login/ldap_test.go
@@ -18,7 +18,7 @@ func TestLdapAuther(t *testing.T) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{{}},
})
- _, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
+ _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
So(err, ShouldEqual, ErrInvalidCredentials)
})
@@ -34,7 +34,7 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(user1)
- result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
+ result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
So(err, ShouldBeNil)
So(result, ShouldEqual, user1)
})
@@ -48,7 +48,7 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(user1)
- result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{MemberOf: []string{"cn=users"}})
+ result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"cn=users"}})
So(err, ShouldBeNil)
So(result, ShouldEqual, user1)
})
@@ -64,7 +64,8 @@ func TestLdapAuther(t *testing.T) {
sc.userQueryReturns(nil)
- result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{
+ result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
+ DN: "torkelo",
Username: "torkelo",
Email: "my@email.com",
MemberOf: []string{"cn=editor"},
@@ -72,11 +73,6 @@ func TestLdapAuther(t *testing.T) {
So(err, ShouldBeNil)
- Convey("Should create new user", func() {
- So(sc.createUserCmd.Login, ShouldEqual, "torkelo")
- So(sc.createUserCmd.Email, ShouldEqual, "my@email.com")
- })
-
Convey("Should return new user", func() {
So(result.Login, ShouldEqual, "torkelo")
})
@@ -95,7 +91,7 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
- err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+ _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=users"},
})
@@ -114,7 +110,7 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
- err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+ _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=users"},
})
@@ -122,24 +118,29 @@ func TestLdapAuther(t *testing.T) {
So(err, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldNotBeNil)
So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
+ So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
ldapAutherScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
LdapGroups: []*LdapGroupToOrgRole{
- {GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"},
+ {GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
},
})
- sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
- err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
- MemberOf: []string{"cn=other"},
+ sc.userOrgsQueryReturns([]*m.UserOrgDTO{
+ {OrgId: 1, Role: m.ROLE_EDITOR},
+ {OrgId: 2, Role: m.ROLE_EDITOR},
+ })
+ _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
+ MemberOf: []string{"cn=users"},
})
Convey("Should remove org role", func() {
So(err, ShouldBeNil)
So(sc.removeOrgUserCmd, ShouldNotBeNil)
+ So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2)
})
})
@@ -152,7 +153,7 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
- err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+ _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=users"},
})
@@ -160,6 +161,7 @@ func TestLdapAuther(t *testing.T) {
So(err, ShouldBeNil)
So(sc.removeOrgUserCmd, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldNotBeNil)
+ So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
@@ -172,13 +174,14 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
- err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+ _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=admins"},
})
Convey("Should take first match, and ignore subsequent matches", func() {
So(err, ShouldBeNil)
So(sc.updateOrgUserCmd, ShouldBeNil)
+ So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
@@ -191,19 +194,20 @@ func TestLdapAuther(t *testing.T) {
})
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
- err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
+ _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
MemberOf: []string{"cn=admins"},
})
Convey("Should take first match, and ignore subsequent matches", func() {
So(err, ShouldBeNil)
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
+ So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
})
})
})
- Convey("When calling SyncSignedInUser", t, func() {
+ Convey("When calling SyncUser", t, func() {
mockLdapConnection := &mockLdapConn{}
ldapAuther := NewLdapAuthenticator(
@@ -243,17 +247,20 @@ func TestLdapAuther(t *testing.T) {
ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
// arrange
- signedInUser := &m.SignedInUser{
- Email: "roel@test.net",
- UserId: 1,
- Name: "Roel Gerrits",
- Login: "roelgerrits",
+ query := &m.LoginUserQuery{
+ Username: "roelgerrits",
}
+ sc.userQueryReturns(&m.User{
+ Id: 1,
+ Email: "roel@test.net",
+ Name: "Roel Gerrits",
+ Login: "roelgerrits",
+ })
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
// act
- syncErrResult := ldapAuther.SyncSignedInUser(signedInUser)
+ syncErrResult := ldapAuther.SyncUser(query)
// assert
So(dialCalled, ShouldBeTrue)
@@ -299,6 +306,19 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
sc := &scenarioContext{}
+ bus.AddHandler("test", UpsertUser)
+
+ bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
+ sc.getUserByAuthInfoQuery = cmd
+ sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *m.GetUserOrgListQuery) error {
+ sc.getUserOrgListQuery = cmd
+ return nil
+ })
+
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
sc.createUserCmd = cmd
sc.createUserCmd.Result = m.User{Login: cmd.Login}
@@ -325,20 +345,28 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
return nil
})
+ bus.AddHandler("test", func(cmd *m.SetUsingOrgCommand) error {
+ sc.setUsingOrgCmd = cmd
+ return nil
+ })
+
fn(sc)
})
}
type scenarioContext struct {
- createUserCmd *m.CreateUserCommand
- addOrgUserCmd *m.AddOrgUserCommand
- updateOrgUserCmd *m.UpdateOrgUserCommand
- removeOrgUserCmd *m.RemoveOrgUserCommand
- updateUserCmd *m.UpdateUserCommand
+ getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
+ getUserOrgListQuery *m.GetUserOrgListQuery
+ createUserCmd *m.CreateUserCommand
+ addOrgUserCmd *m.AddOrgUserCommand
+ updateOrgUserCmd *m.UpdateOrgUserCommand
+ removeOrgUserCmd *m.RemoveOrgUserCommand
+ updateUserCmd *m.UpdateUserCommand
+ setUsingOrgCmd *m.SetUsingOrgCommand
}
func (sc *scenarioContext) userQueryReturns(user *m.User) {
- bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
+ bus.AddHandler("test", func(query *m.GetUserByAuthInfoQuery) error {
if user == nil {
return m.ErrUserNotFound
} else {
@@ -346,6 +374,9 @@ func (sc *scenarioContext) userQueryReturns(user *m.User) {
return nil
}
})
+ bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error {
+ return nil
+ })
}
func (sc *scenarioContext) userOrgsQueryReturns(orgs []*m.UserOrgDTO) {
diff --git a/pkg/metrics/graphitebridge/graphite.go b/pkg/metrics/graphitebridge/graphite.go
index 68fb544fc7c..670636cedce 100644
--- a/pkg/metrics/graphitebridge/graphite.go
+++ b/pkg/metrics/graphitebridge/graphite.go
@@ -295,11 +295,7 @@ func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error
}
}
- if err = addExtentionConventionForRollups(buf, mf, m); err != nil {
- return err
- }
-
- return nil
+ return addExtentionConventionForRollups(buf, mf, m)
}
func addExtentionConventionForRollups(buf *bufio.Writer, mf *dto.MetricFamily, m model.Metric) error {
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 4d4a11d0faa..e3640378f7e 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -54,6 +54,7 @@ var (
M_Alerting_Active_Alerts prometheus.Gauge
M_StatTotal_Dashboards prometheus.Gauge
M_StatTotal_Users prometheus.Gauge
+ M_StatActive_Users prometheus.Gauge
M_StatTotal_Orgs prometheus.Gauge
M_StatTotal_Playlists prometheus.Gauge
M_Grafana_Version *prometheus.GaugeVec
@@ -253,6 +254,12 @@ func init() {
Namespace: exporterName,
})
+ M_StatActive_Users = prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "stat_active_users",
+ Help: "number of active users",
+ Namespace: exporterName,
+ })
+
M_StatTotal_Orgs = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_total_orgs",
Help: "total amount of orgs",
@@ -270,7 +277,6 @@ func init() {
Help: "Information about the Grafana",
Namespace: exporterName,
}, []string{"version"})
-
}
func initMetricVars(settings *MetricSettings) {
@@ -305,6 +311,7 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Active_Alerts,
M_StatTotal_Dashboards,
M_StatTotal_Users,
+ M_StatActive_Users,
M_StatTotal_Orgs,
M_StatTotal_Playlists,
M_Grafana_Version)
@@ -315,35 +322,36 @@ func initMetricVars(settings *MetricSettings) {
func instrumentationLoop(settings *MetricSettings) chan struct{} {
M_Instance_Start.Inc()
+ // set the total stats gauges before we publishing metrics
+ updateTotalStats()
+
onceEveryDayTick := time.NewTicker(time.Hour * 24)
- secondTicker := time.NewTicker(time.Second * time.Duration(settings.IntervalSeconds))
+ everyMinuteTicker := time.NewTicker(time.Minute)
+ defer onceEveryDayTick.Stop()
+ defer everyMinuteTicker.Stop()
for {
select {
case <-onceEveryDayTick.C:
sendUsageStats()
- case <-secondTicker.C:
+ case <-everyMinuteTicker.C:
updateTotalStats()
}
}
}
-var metricPublishCounter int64 = 0
-
func updateTotalStats() {
- metricPublishCounter++
- if metricPublishCounter == 1 || metricPublishCounter%10 == 0 {
- statsQuery := models.GetSystemStatsQuery{}
- if err := bus.Dispatch(&statsQuery); err != nil {
- metricsLogger.Error("Failed to get system stats", "error", err)
- return
- }
-
- M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
- M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
- M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
- M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
+ statsQuery := models.GetSystemStatsQuery{}
+ if err := bus.Dispatch(&statsQuery); err != nil {
+ metricsLogger.Error("Failed to get system stats", "error", err)
+ return
}
+
+ M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
+ M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
+ M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
+ M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
+ M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
}
func sendUsageStats() {
@@ -403,6 +411,6 @@ func sendUsageStats() {
out, _ := json.MarshalIndent(report, "", " ")
data := bytes.NewBuffer(out)
- client := http.Client{Timeout: time.Duration(5 * time.Second)}
+ client := http.Client{Timeout: 5 * time.Second}
go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data)
}
diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go
index adf0b7b53d5..36b059e4ae7 100644
--- a/pkg/middleware/auth_proxy.go
+++ b/pkg/middleware/auth_proxy.go
@@ -3,6 +3,7 @@ package middleware
import (
"fmt"
"net"
+ "net/mail"
"strings"
"time"
@@ -14,6 +15,8 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
+var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
+
func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
if !setting.AuthProxyEnabled {
return false
@@ -30,40 +33,104 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
return true
}
- query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
- query.OrgId = orgID
- if err := bus.Dispatch(query); err != nil {
- if err != m.ErrUserNotFound {
- ctx.Handle(500, "Failed to find user specified in auth proxy header", err)
- return true
- }
-
- if !setting.AuthProxyAutoSignUp {
- return false
- }
-
- cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
- if setting.LdapEnabled {
- cmd.SkipOrgSetup = true
- }
-
- if err := bus.Dispatch(cmd); err != nil {
- ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
- return true
- }
- query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgID}
- if err := bus.Dispatch(query); err != nil {
- ctx.Handle(500, "Failed find user after creation", err)
- return true
- }
- }
-
// initialize session
if err := ctx.Session.Start(ctx.Context); err != nil {
log.Error(3, "Failed to start session", err)
return false
}
+ query := &m.GetSignedInUserQuery{OrgId: orgID}
+
+ // if this session has already been authenticated by authProxy just load the user
+ sessProxyValue := ctx.Session.Get(AUTH_PROXY_SESSION_VAR)
+ if sessProxyValue != nil && sessProxyValue.(string) == proxyHeaderValue && getRequestUserId(ctx) > 0 {
+ // if we're using ldap, sync user periodically
+ if setting.LdapEnabled {
+ syncQuery := &m.LoginUserQuery{
+ ReqContext: ctx,
+ Username: proxyHeaderValue,
+ }
+
+ if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil {
+ if err == login.ErrInvalidCredentials {
+ ctx.Handle(500, "Unable to authenticate user", err)
+ return false
+ }
+
+ ctx.Handle(500, "Failed to sync user", err)
+ return false
+ }
+ }
+
+ query.UserId = getRequestUserId(ctx)
+ // if we're using ldap, pass authproxy login name to ldap user sync
+ } else if setting.LdapEnabled {
+ ctx.Session.Delete(session.SESS_KEY_LASTLDAPSYNC)
+
+ syncQuery := &m.LoginUserQuery{
+ ReqContext: ctx,
+ Username: proxyHeaderValue,
+ }
+
+ if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil {
+ if err == login.ErrInvalidCredentials {
+ ctx.Handle(500, "Unable to authenticate user", err)
+ return false
+ }
+
+ ctx.Handle(500, "Failed to sync user", err)
+ return false
+ }
+
+ if syncQuery.User == nil {
+ ctx.Handle(500, "Failed to sync user", nil)
+ return false
+ }
+
+ query.UserId = syncQuery.User.Id
+ // no ldap, just use the info we have
+ } else {
+ extUser := &m.ExternalUserInfo{
+ AuthModule: "authproxy",
+ AuthId: proxyHeaderValue,
+ }
+
+ if setting.AuthProxyHeaderProperty == "username" {
+ extUser.Login = proxyHeaderValue
+
+ // only set Email if it can be parsed as an email address
+ emailAddr, emailErr := mail.ParseAddress(proxyHeaderValue)
+ if emailErr == nil {
+ extUser.Email = emailAddr.Address
+ }
+ } else if setting.AuthProxyHeaderProperty == "email" {
+ extUser.Email = proxyHeaderValue
+ extUser.Login = proxyHeaderValue
+ } else {
+ ctx.Handle(500, "Auth proxy header property invalid", nil)
+ return true
+ }
+
+ // add/update user in grafana
+ cmd := &m.UpsertUserCommand{
+ ReqContext: ctx,
+ ExternalUser: extUser,
+ SignupAllowed: setting.AuthProxyAutoSignUp,
+ }
+ err := bus.Dispatch(cmd)
+ if err != nil {
+ ctx.Handle(500, "Failed to login as user specified in auth proxy header", err)
+ return true
+ }
+
+ query.UserId = cmd.Result.Id
+ }
+
+ if err := bus.Dispatch(query); err != nil {
+ ctx.Handle(500, "Failed to find user", err)
+ return true
+ }
+
// Make sure that we cannot share a session between different users!
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
// remove session
@@ -77,16 +144,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
}
}
- // When ldap is enabled, sync userinfo and org roles
- if err := syncGrafanaUserWithLdapUser(ctx, query); err != nil {
- if err == login.ErrInvalidCredentials {
- ctx.Handle(500, "Unable to authenticate user", err)
- return false
- }
-
- ctx.Handle(500, "Failed to sync user", err)
- return false
- }
+ ctx.Session.Set(AUTH_PROXY_SESSION_VAR, proxyHeaderValue)
ctx.SignedInUser = query.Result
ctx.IsSignedIn = true
@@ -95,29 +153,29 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
return true
}
-var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
- if !setting.LdapEnabled {
- return nil
- }
-
+var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
var lastLdapSync int64
- if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
+ if lastLdapSyncInSession := query.ReqContext.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
lastLdapSync = lastLdapSyncInSession.(int64)
}
if lastLdapSync < expireEpoch {
ldapCfg := login.LdapCfg
+ if len(ldapCfg.Servers) < 1 {
+ return fmt.Errorf("No LDAP servers available")
+ }
+
for _, server := range ldapCfg.Servers {
author := login.NewLdapAuthenticator(server)
- if err := author.SyncSignedInUser(query.Result); err != nil {
+ if err := author.SyncUser(query); err != nil {
return err
}
}
- ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
+ query.ReqContext.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
}
return nil
@@ -143,29 +201,3 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
}
-
-func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
- query := m.GetSignedInUserQuery{}
- if setting.AuthProxyHeaderProperty == "username" {
- query.Login = headerVal
- } else if setting.AuthProxyHeaderProperty == "email" {
- query.Email = headerVal
- } else {
- panic("Auth proxy header property invalid")
- }
- return &query
-}
-
-func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand {
- cmd := m.CreateUserCommand{}
- if setting.AuthProxyHeaderProperty == "username" {
- cmd.Login = headerVal
- cmd.Email = headerVal
- } else if setting.AuthProxyHeaderProperty == "email" {
- cmd.Email = headerVal
- cmd.Login = headerVal
- } else {
- panic("Auth proxy header property invalid")
- }
- return &cmd
-}
diff --git a/pkg/middleware/auth_proxy_test.go b/pkg/middleware/auth_proxy_test.go
index b3c011bd870..47ed2f71a79 100644
--- a/pkg/middleware/auth_proxy_test.go
+++ b/pkg/middleware/auth_proxy_test.go
@@ -26,57 +26,71 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) {
return &mockLdapAuther
}
- signedInUser := m.SignedInUser{}
- query := m.GetSignedInUserQuery{Result: &signedInUser}
-
- Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() {
+ Convey("When user logs in, call SyncUser", func() {
// arrange
- sess := mockSession{}
+ sess := newMockSession()
ctx := m.ReqContext{Session: &sess}
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
// act
- syncGrafanaUserWithLdapUser(&ctx, &query)
+ syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
+ ReqContext: &ctx,
+ Username: "test",
+ })
// assert
- So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
+ So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0)
})
Convey("When session variable not expired, don't sync and don't change session var", func() {
// arrange
- sess := mockSession{}
+ sess := newMockSession()
ctx := m.ReqContext{Session: &sess}
now := time.Now().Unix()
sess.Set(session.SESS_KEY_LASTLDAPSYNC, now)
+ sess.Set(AUTH_PROXY_SESSION_VAR, "test")
// act
- syncGrafanaUserWithLdapUser(&ctx, &query)
+ syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
+ ReqContext: &ctx,
+ Username: "test",
+ })
// assert
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldEqual, now)
- So(mockLdapAuther.syncSignedInUserCalled, ShouldBeFalse)
+ So(mockLdapAuther.syncUserCalled, ShouldBeFalse)
})
Convey("When lastldapsync is expired, session variable should be updated", func() {
// arrange
- sess := mockSession{}
+ sess := newMockSession()
ctx := m.ReqContext{Session: &sess}
expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix()
sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime)
+ sess.Set(AUTH_PROXY_SESSION_VAR, "test")
// act
- syncGrafanaUserWithLdapUser(&ctx, &query)
+ syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
+ ReqContext: &ctx,
+ Username: "test",
+ })
// assert
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
- So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
+ So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
})
})
}
type mockSession struct {
- value interface{}
+ value map[interface{}]interface{}
+}
+
+func newMockSession() mockSession {
+ session := mockSession{}
+ session.value = make(map[interface{}]interface{})
+ return session
}
func (s *mockSession) Start(c *macaron.Context) error {
@@ -84,15 +98,16 @@ func (s *mockSession) Start(c *macaron.Context) error {
}
func (s *mockSession) Set(k interface{}, v interface{}) error {
- s.value = v
+ s.value[k] = v
return nil
}
func (s *mockSession) Get(k interface{}) interface{} {
- return s.value
+ return s.value[k]
}
func (s *mockSession) Delete(k interface{}) interface{} {
+ delete(s.value, k)
return nil
}
@@ -113,21 +128,18 @@ func (s *mockSession) RegenerateId(c *macaron.Context) error {
}
type mockLdapAuthenticator struct {
- syncSignedInUserCalled bool
+ syncUserCalled bool
}
-func (a *mockLdapAuthenticator) Login(query *login.LoginUserQuery) error {
+func (a *mockLdapAuthenticator) Login(query *m.LoginUserQuery) error {
return nil
}
-func (a *mockLdapAuthenticator) SyncSignedInUser(signedInUser *m.SignedInUser) error {
- a.syncSignedInUserCalled = true
+func (a *mockLdapAuthenticator) SyncUser(query *m.LoginUserQuery) error {
+ a.syncUserCalled = true
return nil
}
-func (a *mockLdapAuthenticator) GetGrafanaUserFor(ldapUser *login.LdapUserInfo) (*m.User, error) {
+func (a *mockLdapAuthenticator) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *login.LdapUserInfo) (*m.User, error) {
return nil, nil
}
-func (a *mockLdapAuthenticator) SyncOrgRoles(user *m.User, ldapUser *login.LdapUserInfo) error {
- return nil
-}
diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go
index b5b244d5bff..93db49ed880 100644
--- a/pkg/middleware/middleware.go
+++ b/pkg/middleware/middleware.go
@@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/log"
- l "github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
@@ -165,7 +164,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
user := loginQuery.Result
- loginUserQuery := l.LoginUserQuery{Username: username, Password: password, User: user}
+ loginUserQuery := m.LoginUserQuery{Username: username, Password: password, User: user}
if err := bus.Dispatch(&loginUserQuery); err != nil {
ctx.JsonApiErr(401, "Invalid username or password", err)
return true
diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go
index f80a30de02f..072cb793d3c 100644
--- a/pkg/middleware/middleware_test.go
+++ b/pkg/middleware/middleware_test.go
@@ -9,7 +9,6 @@ import (
ms "github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
- l "github.com/grafana/grafana/pkg/login"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
@@ -72,7 +71,7 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
- bus.AddHandler("test", func(loginUserQuery *l.LoginUserQuery) error {
+ bus.AddHandler("test", func(loginUserQuery *m.LoginUserQuery) error {
return nil
})
@@ -177,12 +176,18 @@ func TestMiddlewareContext(t *testing.T) {
setting.AuthProxyEnabled = true
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
+ setting.LdapEnabled = false
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
+ bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
+ cmd.Result = &m.User{Id: 12}
+ return nil
+ })
+
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.exec()
@@ -199,6 +204,7 @@ func TestMiddlewareContext(t *testing.T) {
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyAutoSignUp = true
+ setting.LdapEnabled = false
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
if query.UserId > 0 {
@@ -209,8 +215,8 @@ func TestMiddlewareContext(t *testing.T) {
}
})
- bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
- cmd.Result = m.User{Id: 33}
+ bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
+ cmd.Result = &m.User{Id: 33}
return nil
})
@@ -271,6 +277,11 @@ func TestMiddlewareContext(t *testing.T) {
return nil
})
+ bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
+ cmd.Result = &m.User{Id: 33}
+ return nil
+ })
+
sc.fakeReq("GET", "/")
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
sc.req.RemoteAddr = "[2001::23]:12345"
@@ -289,6 +300,11 @@ func TestMiddlewareContext(t *testing.T) {
setting.AuthProxyHeaderProperty = "username"
setting.AuthProxyWhitelist = ""
+ bus.AddHandler("test", func(query *m.UpsertUserCommand) error {
+ query.Result = &m.User{Id: 32}
+ return nil
+ })
+
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
return nil
@@ -319,11 +335,17 @@ func TestMiddlewareContext(t *testing.T) {
setting.LdapEnabled = true
called := false
- syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
+ syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
called = true
+ query.User = &m.User{Id: 32}
return nil
}
+ bus.AddHandler("test", func(query *m.UpsertUserCommand) error {
+ query.Result = &m.User{Id: 32}
+ return nil
+ })
+
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
return nil
diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go
index ec289387aa4..456bc91354e 100644
--- a/pkg/middleware/recovery.go
+++ b/pkg/middleware/recovery.go
@@ -35,7 +35,7 @@ var (
slash = []byte("/")
)
-// stack returns a nicely formated stack frame, skipping skip frames
+// stack returns a nicely formatted stack frame, skipping skip frames
func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data
// As we loop, we open files and read them. These variables record the currently
diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go
index 5b91b2a70b4..4ef8061486b 100644
--- a/pkg/models/dashboard_acl.go
+++ b/pkg/models/dashboard_acl.go
@@ -56,7 +56,10 @@ type DashboardAclInfoDTO struct {
UserId int64 `json:"userId"`
UserLogin string `json:"userLogin"`
UserEmail string `json:"userEmail"`
+ UserAvatarUrl string `json:"userAvatarUrl"`
TeamId int64 `json:"teamId"`
+ TeamEmail string `json:"teamEmail"`
+ TeamAvatarUrl string `json:"teamAvatarUrl"`
Team string `json:"team"`
Role *RoleType `json:"role,omitempty"`
Permission PermissionType `json:"permission"`
diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go
index 8cd2b01811c..eb44c1bc582 100644
--- a/pkg/models/dashboards.go
+++ b/pkg/models/dashboards.go
@@ -13,26 +13,27 @@ import (
// Typed errors
var (
- ErrDashboardNotFound = errors.New("Dashboard not found")
- ErrDashboardFolderNotFound = errors.New("Folder not found")
- ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
- ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
- ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
- ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
- ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
- ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
- ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
- ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
- ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
- ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
- ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
- ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
- ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
- ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
- ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
- ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
- ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
- RootFolderName = "General"
+ ErrDashboardNotFound = errors.New("Dashboard not found")
+ ErrDashboardFolderNotFound = errors.New("Folder not found")
+ ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
+ ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
+ ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
+ ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
+ ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
+ ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
+ ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
+ ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
+ ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
+ ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
+ ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
+ ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
+ ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
+ ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
+ ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
+ ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
+ ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
+ ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboard")
+ RootFolderName = "General"
)
type UpdatePluginDashboardError struct {
@@ -157,7 +158,7 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
return dash
}
-// GetDashboardModel turns the command into the savable model
+// GetDashboardModel turns the command into the saveable model
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash := NewDashboardFromJson(cmd.Dashboard)
userId := cmd.UserId
@@ -224,6 +225,10 @@ func GetFolderUrl(folderUid string, slug string) string {
return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
}
+type ValidateDashboardBeforeSaveResult struct {
+ IsParentFolderChanged bool
+}
+
//
// COMMANDS
//
@@ -268,6 +273,7 @@ type ValidateDashboardBeforeSaveCommand struct {
OrgId int64
Dashboard *Dashboard
Overwrite bool
+ Result *ValidateDashboardBeforeSaveResult
}
//
@@ -317,6 +323,12 @@ type GetDashboardSlugByIdQuery struct {
Result string
}
+type IsDashboardProvisionedQuery struct {
+ DashboardId int64
+
+ Result bool
+}
+
type GetProvisionedDashboardDataQuery struct {
Name string
diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go
index b4a4e7f8a4d..66ba66e4d39 100644
--- a/pkg/models/datasource_cache.go
+++ b/pkg/models/datasource_cache.go
@@ -33,7 +33,7 @@ func (ds *DataSource) GetHttpClient() (*http.Client, error) {
}
return &http.Client{
- Timeout: time.Duration(30 * time.Second),
+ Timeout: 30 * time.Second,
Transport: transport,
}, nil
}
diff --git a/pkg/models/folders.go b/pkg/models/folders.go
index c61620a11fc..0c876edcfd7 100644
--- a/pkg/models/folders.go
+++ b/pkg/models/folders.go
@@ -32,7 +32,7 @@ type Folder struct {
HasAcl bool
}
-// GetDashboardModel turns the command into the savable model
+// GetDashboardModel turns the command into the saveable model
func (cmd *CreateFolderCommand) GetDashboardModel(orgId int64, userId int64) *Dashboard {
dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title))
dashFolder.OrgId = orgId
diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go
index ca32cc50060..9231d18cfd6 100644
--- a/pkg/models/org_user.go
+++ b/pkg/models/org_user.go
@@ -48,9 +48,9 @@ func (r *RoleType) UnmarshalJSON(data []byte) error {
*r = RoleType(str)
- if (*r).IsValid() == false {
+ if !(*r).IsValid() {
if (*r) != "" {
- return errors.New(fmt.Sprintf("JSON validation error: invalid role value: %s", *r))
+ return fmt.Errorf("JSON validation error: invalid role value: %s", *r)
}
*r = ROLE_VIEWER
diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go
new file mode 100644
index 00000000000..0ecd144d52c
--- /dev/null
+++ b/pkg/models/user_auth.go
@@ -0,0 +1,72 @@
+package models
+
+import (
+ "time"
+)
+
+type UserAuth struct {
+ Id int64
+ UserId int64
+ AuthModule string
+ AuthId string
+ Created time.Time
+}
+
+type ExternalUserInfo struct {
+ AuthModule string
+ AuthId string
+ UserId int64
+ Email string
+ Login string
+ Name string
+ OrgRoles map[int64]RoleType
+}
+
+// ---------------------
+// COMMANDS
+
+type UpsertUserCommand struct {
+ ReqContext *ReqContext
+ ExternalUser *ExternalUserInfo
+ SignupAllowed bool
+
+ Result *User
+}
+
+type SetAuthInfoCommand struct {
+ AuthModule string
+ AuthId string
+ UserId int64
+}
+
+type DeleteAuthInfoCommand struct {
+ UserAuth *UserAuth
+}
+
+// ----------------------
+// QUERIES
+
+type LoginUserQuery struct {
+ ReqContext *ReqContext
+ Username string
+ Password string
+ User *User
+ IpAddress string
+}
+
+type GetUserByAuthInfoQuery struct {
+ AuthModule string
+ AuthId string
+ UserId int64
+ Email string
+ Login string
+
+ Result *User
+}
+
+type GetAuthInfoQuery struct {
+ AuthModule string
+ AuthId string
+
+ Result *UserAuth
+}
diff --git a/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go b/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go
index 834e8238e3a..7ada6fb6b03 100644
--- a/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go
+++ b/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go
@@ -74,7 +74,7 @@ func TestMappingRowValue(t *testing.T) {
boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
haveBool, ok := boolRowValue.(bool)
- if !ok || haveBool != true {
+ if !ok || !haveBool {
t.Fatalf("Expected true, was %v", haveBool)
}
diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go
index 541b37c8a8a..9677c21ef04 100644
--- a/pkg/plugins/models.go
+++ b/pkg/plugins/models.go
@@ -69,7 +69,7 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error {
for _, include := range pb.Includes {
if include.Role == "" {
- include.Role = m.RoleType(m.ROLE_VIEWER)
+ include.Role = m.ROLE_VIEWER
}
}
diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go
index 4d3ccb4502b..00329b4a8a1 100644
--- a/pkg/plugins/plugins_test.go
+++ b/pkg/plugins/plugins_test.go
@@ -12,7 +12,7 @@ import (
func TestPluginScans(t *testing.T) {
- Convey("When scaning for plugins", t, func() {
+ Convey("When scanning for plugins", t, func() {
setting.StaticRootPath, _ = filepath.Abs("../../public/")
setting.Cfg = ini.Empty()
err := initPlugins(context.Background())
diff --git a/pkg/plugins/queries.go b/pkg/plugins/queries.go
index 5ae1825a88f..5bd412d2cc9 100644
--- a/pkg/plugins/queries.go
+++ b/pkg/plugins/queries.go
@@ -37,7 +37,7 @@ func GetPluginSettings(orgId int64) (map[string]*m.PluginSettingInfoDTO, error)
// if it's included in app check app settings
if pluginDef.IncludedInAppId != "" {
- // app componets are by default disabled
+ // app components are by default disabled
opt.Enabled = false
if appSettings, ok := pluginMap[pluginDef.IncludedInAppId]; ok {
diff --git a/pkg/plugins/update_checker.go b/pkg/plugins/update_checker.go
index 68ccdeaf840..946d215b1c2 100644
--- a/pkg/plugins/update_checker.go
+++ b/pkg/plugins/update_checker.go
@@ -13,7 +13,7 @@ import (
)
var (
- httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)}
+ httpClient http.Client = http.Client{Timeout: 10 * time.Second}
)
type GrafanaNetPlugin struct {
diff --git a/pkg/services/alerting/conditions/evaluator.go b/pkg/services/alerting/conditions/evaluator.go
index 1b8fb952f65..dfc058940cf 100644
--- a/pkg/services/alerting/conditions/evaluator.go
+++ b/pkg/services/alerting/conditions/evaluator.go
@@ -20,7 +20,7 @@ type AlertEvaluator interface {
type NoValueEvaluator struct{}
func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
- return reducedValue.Valid == false
+ return !reducedValue.Valid
}
type ThresholdEvaluator struct {
@@ -45,7 +45,7 @@ func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvalua
}
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
- if reducedValue.Valid == false {
+ if !reducedValue.Valid {
return false
}
@@ -88,7 +88,7 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
}
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
- if reducedValue.Valid == false {
+ if !reducedValue.Valid {
return false
}
diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go
index d499c5e8532..7d1a276c42e 100644
--- a/pkg/services/alerting/conditions/query.go
+++ b/pkg/services/alerting/conditions/query.go
@@ -53,7 +53,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue)
- if reducedValue.Valid == false {
+ if !reducedValue.Valid {
emptySerieCount++
}
diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go
index a6f97333d1b..0945a2a5330 100644
--- a/pkg/services/alerting/engine.go
+++ b/pkg/services/alerting/engine.go
@@ -15,8 +15,8 @@ import (
)
type Engine struct {
- execQueue chan *Job
- clock clock.Clock
+ execQueue chan *Job
+ //clock clock.Clock
ticker *Ticker
scheduler Scheduler
evalHandler EvalHandler
diff --git a/pkg/services/alerting/engine_test.go b/pkg/services/alerting/engine_test.go
index 64f954c6dd5..63108bbb9aa 100644
--- a/pkg/services/alerting/engine_test.go
+++ b/pkg/services/alerting/engine_test.go
@@ -10,7 +10,7 @@ import (
)
type FakeEvalHandler struct {
- SuccessCallID int // 0 means never sucess
+ SuccessCallID int // 0 means never success
CallNb int
}
@@ -87,7 +87,7 @@ func TestEngineProcessJob(t *testing.T) {
Convey("Should trigger as many retries as needed", func() {
- Convey("never sucess -> max retries number", func() {
+ Convey("never success -> max retries number", func() {
expectedAttempts := alertMaxAttempts
evalHandler := NewFakeEvalHandler(0)
engine.evalHandler = evalHandler
@@ -96,7 +96,7 @@ func TestEngineProcessJob(t *testing.T) {
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
})
- Convey("always sucess -> never retry", func() {
+ Convey("always success -> never retry", func() {
expectedAttempts := 1
evalHandler := NewFakeEvalHandler(1)
engine.evalHandler = evalHandler
@@ -105,7 +105,7 @@ func TestEngineProcessJob(t *testing.T) {
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
})
- Convey("some errors before sucess -> some retries", func() {
+ Convey("some errors before success -> some retries", func() {
expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2))
evalHandler := NewFakeEvalHandler(expectedAttempts)
engine.evalHandler = evalHandler
diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go
index edd872b8fce..e1c1bfacb2e 100644
--- a/pkg/services/alerting/extractor.go
+++ b/pkg/services/alerting/extractor.go
@@ -104,7 +104,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
// backward compatibility check, can be removed later
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
- if hasEnabled && enabled.MustBool() == false {
+ if hasEnabled && !enabled.MustBool() {
continue
}
diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go
index e32b9d34f91..14eacef5831 100644
--- a/pkg/services/alerting/notifiers/dingding.go
+++ b/pkg/services/alerting/notifiers/dingding.go
@@ -72,7 +72,10 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
}
- body, _ := bodyJSON.MarshalJSON()
+ body, err := bodyJSON.MarshalJSON()
+ if err != nil {
+ return err
+ }
cmd := &m.SendWebhookSync{
Url: this.Url,
diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go
index f1f63d42a04..58e1b7bd71e 100644
--- a/pkg/services/alerting/notifiers/hipchat.go
+++ b/pkg/services/alerting/notifiers/hipchat.go
@@ -111,7 +111,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
}
message := ""
- if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok.
+ if evalContext.Rule.State != models.AlertStateOK { //don't add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
}
diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go
index 4fbaa2d543e..4814662f3a9 100644
--- a/pkg/services/alerting/notifiers/line.go
+++ b/pkg/services/alerting/notifiers/line.go
@@ -90,7 +90,7 @@ func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- this.log.Error("Failed to send notification to LINE", "error", err, "body", string(body))
+ this.log.Error("Failed to send notification to LINE", "error", err, "body", body)
return err
}
diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go
index e051a71740a..a8139b62726 100644
--- a/pkg/services/alerting/notifiers/slack.go
+++ b/pkg/services/alerting/notifiers/slack.go
@@ -129,7 +129,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
}
message := this.Mention
- if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
+ if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
}
image_url := ""
diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go
index 9a9e93dbc47..7f62340d0e1 100644
--- a/pkg/services/alerting/notifiers/teams.go
+++ b/pkg/services/alerting/notifiers/teams.go
@@ -13,7 +13,7 @@ func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "teams",
Name: "Microsoft Teams",
- Description: "Sends notifications using Incomming Webhook connector to Microsoft Teams",
+ Description: "Sends notifications using Incoming Webhook connector to Microsoft Teams",
Factory: NewTeamsNotifier,
OptionsTemplate: `
Teams settings
@@ -76,7 +76,7 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
}
message := this.Mention
- if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
+ if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
} else {
message += " " // summary must not be empty
diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go
index 5cbdad60906..0b8efe808d5 100644
--- a/pkg/services/alerting/notifiers/telegram.go
+++ b/pkg/services/alerting/notifiers/telegram.go
@@ -219,7 +219,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
var cmd *m.SendWebhookSync
- if evalContext.ImagePublicUrl == "" && this.UploadImage == true {
+ if evalContext.ImagePublicUrl == "" && this.UploadImage {
cmd = this.buildMessage(evalContext, true)
} else {
cmd = this.buildMessage(evalContext, false)
diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go
index 05be787dced..98c8d884ad0 100644
--- a/pkg/services/alerting/notifiers/telegram_test.go
+++ b/pkg/services/alerting/notifiers/telegram_test.go
@@ -100,7 +100,7 @@ func TestTelegramNotifier(t *testing.T) {
So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
})
- Convey("Metrics should be skipped if they dont fit", func() {
+ Convey("Metrics should be skipped if they don't fit", func() {
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go
index 45f0c65d4fb..627159c286b 100644
--- a/pkg/services/alerting/reader.go
+++ b/pkg/services/alerting/reader.go
@@ -16,7 +16,7 @@ type RuleReader interface {
type DefaultRuleReader struct {
sync.RWMutex
- serverID string
+ //serverID string
serverPosition int
clusterSize int
log log.Logger
diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go
index 8f9deb758a6..0c92fc32110 100644
--- a/pkg/services/alerting/result_handler.go
+++ b/pkg/services/alerting/result_handler.go
@@ -56,7 +56,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
if err := bus.Dispatch(cmd); err != nil {
if err == m.ErrCannotChangeStateOnPausedAlert {
- handler.log.Error("Cannot change state on alert thats pause", "error", err)
+ handler.log.Error("Cannot change state on alert that's paused", "error", err)
return err
}
@@ -77,7 +77,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
Text: "",
NewState: string(evalContext.Rule.State),
PrevState: string(evalContext.PrevAlertState),
- Epoch: time.Now().Unix(),
+ Epoch: time.Now().UnixNano() / int64(time.Millisecond),
Data: annotationData,
}
diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go
index bdf53798e34..027ff96d6c0 100644
--- a/pkg/services/alerting/rule.go
+++ b/pkg/services/alerting/rule.go
@@ -55,8 +55,8 @@ func (e ValidationError) Error() string {
}
var (
- ValueFormatRegex = regexp.MustCompile("^\\d+")
- UnitFormatRegex = regexp.MustCompile("\\w{1}$")
+ ValueFormatRegex = regexp.MustCompile(`^\d+`)
+ UnitFormatRegex = regexp.MustCompile(`\w{1}$`)
)
var unitMultiplier = map[string]int{
diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go
index 151f802ec15..b7555ae8d89 100644
--- a/pkg/services/alerting/scheduler.go
+++ b/pkg/services/alerting/scheduler.go
@@ -15,7 +15,7 @@ type SchedulerImpl struct {
func NewScheduler() Scheduler {
return &SchedulerImpl{
- jobs: make(map[int64]*Job, 0),
+ jobs: make(map[int64]*Job),
log: log.New("alerting.scheduler"),
}
}
@@ -23,7 +23,7 @@ func NewScheduler() Scheduler {
func (s *SchedulerImpl) Update(rules []*Rule) {
s.log.Debug("Scheduling update", "ruleCount", len(rules))
- jobs := make(map[int64]*Job, 0)
+ jobs := make(map[int64]*Job)
for i, rule := range rules {
var job *Job
@@ -58,7 +58,7 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
if job.OffsetWait && now%job.Offset == 0 {
job.OffsetWait = false
- s.enque(job, execQueue)
+ s.enqueue(job, execQueue)
continue
}
@@ -66,13 +66,13 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
if job.Offset > 0 {
job.OffsetWait = true
} else {
- s.enque(job, execQueue)
+ s.enqueue(job, execQueue)
}
}
}
}
-func (s *SchedulerImpl) enque(job *Job, execQueue chan *Job) {
+func (s *SchedulerImpl) enqueue(job *Job, execQueue chan *Job) {
s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.Id)
execQueue <- job
}
diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go
index a6cd7a33318..5cebb3d2df9 100644
--- a/pkg/services/annotations/annotations.go
+++ b/pkg/services/annotations/annotations.go
@@ -13,6 +13,7 @@ type ItemQuery struct {
OrgId int64 `json:"orgId"`
From int64 `json:"from"`
To int64 `json:"to"`
+ UserId int64 `json:"userId"`
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
@@ -63,6 +64,8 @@ type Item struct {
PrevState string `json:"prevState"`
NewState string `json:"newState"`
Epoch int64 `json:"epoch"`
+ Created int64 `json:"created"`
+ Updated int64 `json:"updated"`
Tags []string `json:"tags"`
Data *simplejson.Json `json:"data"`
@@ -80,6 +83,8 @@ type ItemDTO struct {
UserId int64 `json:"userId"`
NewState string `json:"newState"`
PrevState string `json:"prevState"`
+ Created int64 `json:"created"`
+ Updated int64 `json:"updated"`
Time int64 `json:"time"`
Text string `json:"text"`
RegionId int64 `json:"regionId"`
diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go
index 02a6ffc8330..278421e6be7 100644
--- a/pkg/services/dashboards/dashboard_service.go
+++ b/pkg/services/dashboards/dashboard_service.go
@@ -57,7 +57,7 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
return cmd.Result, nil
}
-func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) {
+func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
dash := dto.Dashboard
dash.Title = strings.TrimSpace(dash.Title)
@@ -103,6 +103,29 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return nil, err
}
+ if validateBeforeSaveCmd.Result.IsParentFolderChanged {
+ folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User)
+ if canSave, err := folderGuardian.CanSave(); err != nil || !canSave {
+ if err != nil {
+ return nil, err
+ }
+ return nil, models.ErrDashboardUpdateAccessDenied
+ }
+ }
+
+ if validateProvisionedDashboard {
+ isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id}
+ err := bus.Dispatch(isDashboardProvisioned)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if isDashboardProvisioned.Result {
+ return nil, models.ErrDashboardCannotSaveProvisionedDashboard
+ }
+ }
+
guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
if canSave, err := guard.CanSave(); err != nil || !canSave {
if err != nil {
@@ -148,7 +171,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
- cmd, err := dr.buildSaveDashboardCommand(dto, true)
+ cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
if err != nil {
return nil, err
}
@@ -178,7 +201,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
- cmd, err := dr.buildSaveDashboardCommand(dto, false)
+ cmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return nil, err
}
@@ -197,7 +220,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
}
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
- cmd, err := dr.buildSaveDashboardCommand(dto, true)
+ cmd, err := dr.buildSaveDashboardCommand(dto, true, true)
if err != nil {
return nil, err
}
@@ -216,7 +239,7 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da
}
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
- cmd, err := dr.buildSaveDashboardCommand(dto, false)
+ cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
if err != nil {
return nil, err
}
diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go
index 965b10655b3..f9d487f625c 100644
--- a/pkg/services/dashboards/dashboard_service_test.go
+++ b/pkg/services/dashboards/dashboard_service_test.go
@@ -14,7 +14,9 @@ import (
func TestDashboardService(t *testing.T) {
Convey("Dashboard service tests", t, func() {
- service := dashboardServiceImpl{}
+ bus.ClearBusHandlers()
+
+ service := &dashboardServiceImpl{}
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
@@ -51,6 +53,12 @@ func TestDashboardService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+ cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+ cmd.Result = false
return nil
})
@@ -72,12 +80,42 @@ func TestDashboardService(t *testing.T) {
dto.Dashboard.SetUid(tc.Uid)
dto.User = &models.SignedInUser{}
- _, err := service.buildSaveDashboardCommand(dto, true)
+ _, err := service.buildSaveDashboardCommand(dto, true, false)
So(err, ShouldEqual, tc.Error)
}
})
+ Convey("Should return validation error if dashboard is provisioned", func() {
+ provisioningValidated := false
+ bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+ provisioningValidated = true
+ cmd.Result = true
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+ cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
+ return nil
+ })
+
+ dto.Dashboard = models.NewDashboard("Dash")
+ dto.Dashboard.SetId(3)
+ dto.User = &models.SignedInUser{UserId: 1}
+ _, err := service.SaveDashboard(dto)
+ So(provisioningValidated, ShouldBeTrue)
+ So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
+ })
+
Convey("Should return validation error if alert data is invalid", func() {
+ bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+ cmd.Result = false
+ return nil
+ })
+
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return errors.New("error")
})
@@ -88,6 +126,80 @@ func TestDashboardService(t *testing.T) {
})
})
+ Convey("Save provisioned dashboard validation", func() {
+ dto := &SaveDashboardDTO{}
+
+ Convey("Should not return validation error if dashboard is provisioned", func() {
+ provisioningValidated := false
+ bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+ provisioningValidated = true
+ cmd.Result = true
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+ cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
+ return nil
+ })
+
+ dto.Dashboard = models.NewDashboard("Dash")
+ dto.Dashboard.SetId(3)
+ dto.User = &models.SignedInUser{UserId: 1}
+ _, err := service.SaveProvisionedDashboard(dto, nil)
+ So(err, ShouldBeNil)
+ So(provisioningValidated, ShouldBeFalse)
+ })
+ })
+
+ Convey("Import dashboard validation", func() {
+ dto := &SaveDashboardDTO{}
+
+ Convey("Should return validation error if dashboard is provisioned", func() {
+ provisioningValidated := false
+ bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+ provisioningValidated = true
+ cmd.Result = true
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+ cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
+ return nil
+ })
+
+ bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
+ return nil
+ })
+
+ dto.Dashboard = models.NewDashboard("Dash")
+ dto.Dashboard.SetId(3)
+ dto.User = &models.SignedInUser{UserId: 1}
+ _, err := service.ImportDashboard(dto)
+ So(provisioningValidated, ShouldBeTrue)
+ So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
+ })
+ })
+
Reset(func() {
guardian.New = origNewDashboardGuardian
})
diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go
index ae92952056e..b521b0e5213 100644
--- a/pkg/services/dashboards/folder_service.go
+++ b/pkg/services/dashboards/folder_service.go
@@ -104,7 +104,7 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er
User: dr.user,
}
- saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
+ saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return toFolderError(err)
}
@@ -141,7 +141,7 @@ func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.Upd
Overwrite: cmd.Overwrite,
}
- saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
+ saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return toFolderError(err)
}
diff --git a/pkg/services/dashboards/folder_service_test.go b/pkg/services/dashboards/folder_service_test.go
index 6c0413d1878..4c9cecd3352 100644
--- a/pkg/services/dashboards/folder_service_test.go
+++ b/pkg/services/dashboards/folder_service_test.go
@@ -32,6 +32,7 @@ func TestFolderService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+ cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return models.ErrDashboardUpdateAccessDenied
})
@@ -92,6 +93,7 @@ func TestFolderService(t *testing.T) {
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
+ cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
})
@@ -108,11 +110,19 @@ func TestFolderService(t *testing.T) {
return nil
})
+ provisioningValidated := false
+
+ bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error {
+ provisioningValidated = true
+ return nil
+ })
+
Convey("When creating folder should not return access denied error", func() {
err := service.CreateFolder(&models.CreateFolderCommand{
Title: "Folder",
})
So(err, ShouldBeNil)
+ So(provisioningValidated, ShouldBeFalse)
})
Convey("When updating folder should not return access denied error", func() {
@@ -121,6 +131,7 @@ func TestFolderService(t *testing.T) {
Title: "Folder",
})
So(err, ShouldBeNil)
+ So(provisioningValidated, ShouldBeFalse)
})
Convey("When deleting folder by uid should not return access denied error", func() {
diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go
index 811b38cac86..700f22d8d26 100644
--- a/pkg/services/guardian/guardian.go
+++ b/pkg/services/guardian/guardian.go
@@ -113,7 +113,7 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D
return false, err
}
- // evalute team rules
+ // evaluate team rules
for _, p := range acl {
for _, ug := range teams {
if ug.Id == p.TeamId && p.Permission >= permission {
@@ -173,7 +173,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss
return true, nil
}
- return g.checkAcl(permission, acl)
+ return g.checkAcl(permission, existingPermissions)
}
// GetAcl returns dashboard acl
diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go
index bb7e6bd1a72..9de12c60fea 100644
--- a/pkg/services/guardian/guardian_test.go
+++ b/pkg/services/guardian/guardian_test.go
@@ -2,710 +2,663 @@ package guardian
import (
"fmt"
+ "runtime"
"testing"
- "github.com/grafana/grafana/pkg/bus"
-
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
-func TestGuardian(t *testing.T) {
- Convey("Guardian permission tests", t, func() {
- orgRoleScenario("Given user has admin org role", m.ROLE_ADMIN, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeTrue)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
-
- Convey("When trying to update permissions", func() {
- Convey("With duplicate user permissions should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
- {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianPermissionExists)
- })
-
- Convey("With duplicate team permissions should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
- {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianPermissionExists)
- })
-
- Convey("With duplicate everyone with editor role permission should return error", func() {
- r := m.ROLE_EDITOR
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
- {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianPermissionExists)
- })
-
- Convey("With duplicate everyone with viewer role permission should return error", func() {
- r := m.ROLE_VIEWER
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
- {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianPermissionExists)
- })
-
- Convey("With everyone with admin role permission should return error", func() {
- r := m.ROLE_ADMIN
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianPermissionExists)
- })
- })
-
- Convey("Given default permissions", func() {
- editor := m.ROLE_EDITOR
- viewer := m.ROLE_VIEWER
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: -1, Role: &editor, Permission: m.PERMISSION_EDIT},
- {OrgId: 1, DashboardId: -1, Role: &viewer, Permission: m.PERMISSION_VIEW},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions without everyone with role editor can edit should be allowed", func() {
- r := m.ROLE_VIEWER
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions without everyone with role viewer can view should be allowed", func() {
- r := m.ROLE_EDITOR
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_EDIT},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
- })
-
- Convey("Given parent folder has user admin permission", func() {
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions with admin user permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
-
- Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
-
- Convey("When trying to update dashboard permissions with view user permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
- })
-
- Convey("Given parent folder has user edit permission", func() {
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
-
- Convey("When trying to update dashboard permissions with view user permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
- })
-
- Convey("Given parent folder has user view permission", func() {
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with edit user permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with view user permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
- })
-
- Convey("Given parent folder has team admin permission", func() {
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_ADMIN},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions with admin team permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
-
- Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
-
- Convey("When trying to update dashboard permissions with view team permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
- })
-
- Convey("Given parent folder has team edit permission", func() {
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_EDIT},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
-
- Convey("When trying to update dashboard permissions with view team permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
- })
-
- Convey("Given parent folder has team view permission", func() {
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_VIEW},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with edit team permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with view team permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
- })
-
- Convey("Given parent folder has editor role with edit permission", func() {
- r := m.ROLE_EDITOR
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_EDIT},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions with everyone with editor role can admin permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with everyone with editor role can edit permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
-
- Convey("When trying to update dashboard permissions with everyone with editor role can view permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
- })
-
- Convey("Given parent folder has editor role with view permission", func() {
- r := m.ROLE_EDITOR
- existingPermissions := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_VIEW},
- }
-
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = existingPermissions
- return nil
- })
-
- Convey("When trying to update dashboard permissions with everyone with viewer role can admin permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with everyone with viewer role can edit permission should be allowed", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeTrue)
- })
-
- Convey("When trying to update dashboard permissions with everyone with viewer role can view permission should return error", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
- }
- _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(err, ShouldEqual, ErrGuardianOverride)
- })
- })
- })
-
- orgRoleScenario("Given user has editor org role", m.ROLE_EDITOR, func(sc *scenarioContext) {
- everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeTrue)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeTrue)
- })
-
- everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeFalse)
- })
-
- everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeFalse)
- })
-
- everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeFalse)
- })
-
- userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeTrue)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeTrue)
- })
-
- teamWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeTrue)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- teamWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- teamWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeTrue)
- })
-
- Convey("When trying to update permissions should return false", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
- {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeFalse)
- })
- })
-
- orgRoleScenario("Given user has viewer org role", m.ROLE_VIEWER, func(sc *scenarioContext) {
- everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeFalse)
- })
-
- everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeFalse)
- })
-
- everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeFalse)
- })
-
- everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeTrue)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeTrue)
- })
-
- userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeTrue)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeTrue)
- So(canSave, ShouldBeTrue)
- So(canView, ShouldBeTrue)
- })
-
- userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
- canAdmin, _ := sc.g.CanAdmin()
- canEdit, _ := sc.g.CanEdit()
- canSave, _ := sc.g.CanSave()
- canView, _ := sc.g.CanView()
- So(canAdmin, ShouldBeFalse)
- So(canEdit, ShouldBeFalse)
- So(canSave, ShouldBeFalse)
- So(canView, ShouldBeTrue)
- })
-
- Convey("When trying to update permissions should return false", func() {
- p := []*m.DashboardAcl{
- {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
- {OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
- }
- ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
- So(ok, ShouldBeFalse)
- })
+var (
+ orgID = int64(1)
+ defaultDashboardID = int64(-1)
+ dashboardID = int64(1)
+ parentFolderID = int64(2)
+ childDashboardID = int64(3)
+ userID = int64(1)
+ otherUserID = int64(2)
+ teamID = int64(1)
+ otherTeamID = int64(2)
+ adminRole = m.ROLE_ADMIN
+ editorRole = m.ROLE_EDITOR
+ viewerRole = m.ROLE_VIEWER
+)
+
+func TestGuardianAdmin(t *testing.T) {
+ Convey("Guardian admin org role tests", t, func() {
+ orgRoleScenario("Given user has admin org role", t, m.ROLE_ADMIN, func(sc *scenarioContext) {
+ // dashboard has default permissions
+ sc.defaultPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+
+ // dashboard has user with permission
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_EDIT, FULL_ACCESS)
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_VIEW, FULL_ACCESS)
+
+ // dashboard has team with permission
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_EDIT, FULL_ACCESS)
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_VIEW, FULL_ACCESS)
+
+ // dashboard has editor role with permission
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_EDIT, FULL_ACCESS)
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_VIEW, FULL_ACCESS)
+
+ // dashboard has viewer role with permission
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_EDIT, FULL_ACCESS)
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_VIEW, FULL_ACCESS)
+
+ // parent folder has user with permission
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_EDIT, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_VIEW, FULL_ACCESS)
+
+ // parent folder has team with permission
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_EDIT, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_VIEW, FULL_ACCESS)
+
+ // parent folder has editor role with permission
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_EDIT, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_VIEW, FULL_ACCESS)
+
+ // parent folder has viweer role with permission
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, FULL_ACCESS)
})
})
}
-type scenarioContext struct {
- g DashboardGuardian
+func TestGuardianEditor(t *testing.T) {
+ Convey("Guardian editor org role tests", t, func() {
+ orgRoleScenario("Given user has editor org role", t, m.ROLE_EDITOR, func(sc *scenarioContext) {
+ // dashboard has user with permission
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_VIEW, CAN_VIEW)
+
+ // dashboard has team with permission
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_VIEW, CAN_VIEW)
+
+ // dashboard has editor role with permission
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+ // dashboard has viewer role with permission
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_ADMIN, NO_ACCESS)
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_EDIT, NO_ACCESS)
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_VIEW, NO_ACCESS)
+
+ // parent folder has user with permission
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+ // parent folder has team with permission
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+ // parent folder has editor role with permission
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+ // parent folder has viweer role with permission
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_ADMIN, NO_ACCESS)
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, NO_ACCESS)
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, NO_ACCESS)
+ })
+ })
}
-type scenarioFunc func(c *scenarioContext)
+func TestGuardianViewer(t *testing.T) {
+ Convey("Guardian viewer org role tests", t, func() {
+ orgRoleScenario("Given user has viewer org role", t, m.ROLE_VIEWER, func(sc *scenarioContext) {
+ // dashboard has user with permission
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.dashboardPermissionScenario(USER, m.PERMISSION_VIEW, VIEWER_ACCESS)
-func orgRoleScenario(desc string, role m.RoleType, fn scenarioFunc) {
- user := &m.SignedInUser{
- UserId: 1,
- OrgId: 1,
- OrgRole: role,
- }
- guard := New(1, 1, user)
- sc := &scenarioContext{
- g: guard,
+ // dashboard has team with permission
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.dashboardPermissionScenario(TEAM, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+ // dashboard has editor role with permission
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_ADMIN, NO_ACCESS)
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_EDIT, NO_ACCESS)
+ sc.dashboardPermissionScenario(EDITOR, m.PERMISSION_VIEW, NO_ACCESS)
+
+ // dashboard has viewer role with permission
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.dashboardPermissionScenario(VIEWER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+ // parent folder has user with permission
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.parentFolderPermissionScenario(USER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+ // parent folder has team with permission
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.parentFolderPermissionScenario(TEAM, m.PERMISSION_VIEW, VIEWER_ACCESS)
+
+ // parent folder has editor role with permission
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_ADMIN, NO_ACCESS)
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_EDIT, NO_ACCESS)
+ sc.parentFolderPermissionScenario(EDITOR, m.PERMISSION_VIEW, NO_ACCESS)
+
+ // parent folder has viweer role with permission
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_ADMIN, FULL_ACCESS)
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_EDIT, EDITOR_ACCESS)
+ sc.parentFolderPermissionScenario(VIEWER, m.PERMISSION_VIEW, VIEWER_ACCESS)
+ })
+ })
+}
+
+func (sc *scenarioContext) defaultPermissionScenario(pt permissionType, permission m.PermissionType, flag permissionFlags) {
+ _, callerFile, callerLine, _ := runtime.Caller(1)
+ sc.callerFile = callerFile
+ sc.callerLine = callerLine
+ existingPermissions := []*m.DashboardAclInfoDTO{
+ toDto(newEditorRolePermission(defaultDashboardID, m.PERMISSION_EDIT)),
+ toDto(newViewerRolePermission(defaultDashboardID, m.PERMISSION_VIEW)),
}
- Convey(desc, func() {
- fn(sc)
+ permissionScenario("and existing permissions is the default permissions (everyone with editor role can edit, everyone with viewer role can view)", dashboardID, sc, existingPermissions, func(sc *scenarioContext) {
+ sc.expectedFlags = flag
+ sc.verifyExpectedPermissionsFlags()
+ sc.verifyDuplicatePermissionsShouldNotBeAllowed()
+ sc.verifyUpdateDashboardPermissionsShouldBeAllowed(pt)
+ sc.verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt)
})
}
-func permissionScenario(desc string, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
- bus.ClearBusHandlers()
+func (sc *scenarioContext) dashboardPermissionScenario(pt permissionType, permission m.PermissionType, flag permissionFlags) {
+ _, callerFile, callerLine, _ := runtime.Caller(1)
+ sc.callerFile = callerFile
+ sc.callerLine = callerLine
+ var existingPermissions []*m.DashboardAclInfoDTO
- bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
- query.Result = permissions
- return nil
+ switch pt {
+ case USER:
+ existingPermissions = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: dashboardID, UserId: userID, Permission: permission}}
+ case TEAM:
+ existingPermissions = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: dashboardID, TeamId: teamID, Permission: permission}}
+ case EDITOR:
+ existingPermissions = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: dashboardID, Role: &editorRole, Permission: permission}}
+ case VIEWER:
+ existingPermissions = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: dashboardID, Role: &viewerRole, Permission: permission}}
+ }
+
+ permissionScenario(fmt.Sprintf("and %s has permission to %s dashboard", pt.String(), permission.String()), dashboardID, sc, existingPermissions, func(sc *scenarioContext) {
+ sc.expectedFlags = flag
+ sc.verifyExpectedPermissionsFlags()
+ sc.verifyDuplicatePermissionsShouldNotBeAllowed()
+ sc.verifyUpdateDashboardPermissionsShouldBeAllowed(pt)
+ sc.verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt)
})
+}
- teams := []*m.Team{}
+func (sc *scenarioContext) parentFolderPermissionScenario(pt permissionType, permission m.PermissionType, flag permissionFlags) {
+ _, callerFile, callerLine, _ := runtime.Caller(1)
+ sc.callerFile = callerFile
+ sc.callerLine = callerLine
+ var folderPermissionList []*m.DashboardAclInfoDTO
- for _, p := range permissions {
- if p.TeamId > 0 {
- teams = append(teams, &m.Team{Id: p.TeamId})
+ switch pt {
+ case USER:
+ folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, UserId: userID, Permission: permission}}
+ case TEAM:
+ folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, TeamId: teamID, Permission: permission}}
+ case EDITOR:
+ folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &editorRole, Permission: permission}}
+ case VIEWER:
+ folderPermissionList = []*m.DashboardAclInfoDTO{{OrgId: orgID, DashboardId: parentFolderID, Role: &viewerRole, Permission: permission}}
+ }
+
+ permissionScenario(fmt.Sprintf("and parent folder has %s with permission to %s", pt.String(), permission.String()), childDashboardID, sc, folderPermissionList, func(sc *scenarioContext) {
+ sc.expectedFlags = flag
+ sc.verifyExpectedPermissionsFlags()
+ sc.verifyDuplicatePermissionsShouldNotBeAllowed()
+ sc.verifyUpdateChildDashboardPermissionsShouldBeAllowed(pt, permission)
+ sc.verifyUpdateChildDashboardPermissionsShouldNotBeAllowed(pt, permission)
+ sc.verifyUpdateChildDashboardPermissionsWithOverrideShouldBeAllowed(pt, permission)
+ sc.verifyUpdateChildDashboardPermissionsWithOverrideShouldNotBeAllowed(pt, permission)
+ })
+}
+
+func (sc *scenarioContext) verifyExpectedPermissionsFlags() {
+ canAdmin, _ := sc.g.CanAdmin()
+ canEdit, _ := sc.g.CanEdit()
+ canSave, _ := sc.g.CanSave()
+ canView, _ := sc.g.CanView()
+
+ tc := fmt.Sprintf("should have permissions to %s", sc.expectedFlags.String())
+ Convey(tc, func() {
+ var actualFlag permissionFlags
+
+ if canAdmin {
+ actualFlag |= CAN_ADMIN
}
- }
- bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
- query.Result = teams
- return nil
- })
+ if canEdit {
+ actualFlag |= CAN_EDIT
+ }
- Convey(desc, func() {
- fn(sc)
+ if canSave {
+ actualFlag |= CAN_SAVE
+ }
+
+ if canView {
+ actualFlag |= CAN_VIEW
+ }
+
+ if actualFlag.noAccess() {
+ actualFlag = NO_ACCESS
+ }
+
+ if sc.expectedFlags&actualFlag != sc.expectedFlags {
+ sc.reportFailure(tc, sc.expectedFlags.String(), actualFlag.String())
+ }
+
+ sc.reportSuccess()
})
}
-func userWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
- p := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 1, UserId: 1, Permission: permission},
+func (sc *scenarioContext) verifyDuplicatePermissionsShouldNotBeAllowed() {
+ if !sc.expectedFlags.canAdmin() {
+ return
}
- permissionScenario(fmt.Sprintf("and user has permission to %s item", permission), sc, p, fn)
+
+ tc := "When updating dashboard permissions with duplicate permission for user should not be allowed"
+ Convey(tc, func() {
+ p := []*m.DashboardAcl{
+ newDefaultUserPermission(dashboardID, m.PERMISSION_VIEW),
+ newDefaultUserPermission(dashboardID, m.PERMISSION_ADMIN),
+ }
+ sc.updatePermissions = p
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+
+ if err != ErrGuardianPermissionExists {
+ sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+ }
+ sc.reportSuccess()
+ })
+
+ tc = "When updating dashboard permissions with duplicate permission for team should not be allowed"
+ Convey(tc, func() {
+ p := []*m.DashboardAcl{
+ newDefaultTeamPermission(dashboardID, m.PERMISSION_VIEW),
+ newDefaultTeamPermission(dashboardID, m.PERMISSION_ADMIN),
+ }
+ sc.updatePermissions = p
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+
+ if err != ErrGuardianPermissionExists {
+ sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+ }
+ sc.reportSuccess()
+ })
+
+ tc = "When updating dashboard permissions with duplicate permission for editor role should not be allowed"
+ Convey(tc, func() {
+ p := []*m.DashboardAcl{
+ newEditorRolePermission(dashboardID, m.PERMISSION_VIEW),
+ newEditorRolePermission(dashboardID, m.PERMISSION_ADMIN),
+ }
+ sc.updatePermissions = p
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+
+ if err != ErrGuardianPermissionExists {
+ sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+ }
+ sc.reportSuccess()
+ })
+
+ tc = "When updating dashboard permissions with duplicate permission for viewer role should not be allowed"
+ Convey(tc, func() {
+ p := []*m.DashboardAcl{
+ newViewerRolePermission(dashboardID, m.PERMISSION_VIEW),
+ newViewerRolePermission(dashboardID, m.PERMISSION_ADMIN),
+ }
+ sc.updatePermissions = p
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+
+ if err != ErrGuardianPermissionExists {
+ sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+ }
+ sc.reportSuccess()
+ })
+
+ tc = "When updating dashboard permissions with duplicate permission for admin role should not be allowed"
+ Convey(tc, func() {
+ p := []*m.DashboardAcl{
+ newAdminRolePermission(dashboardID, m.PERMISSION_ADMIN),
+ }
+ sc.updatePermissions = p
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
+
+ if err != ErrGuardianPermissionExists {
+ sc.reportFailure(tc, ErrGuardianPermissionExists, err)
+ }
+ sc.reportSuccess()
+ })
}
-func teamWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
- p := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: permission},
+func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldBeAllowed(pt permissionType) {
+ if !sc.expectedFlags.canAdmin() {
+ return
+ }
+
+ for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+ tc := fmt.Sprintf("When updating dashboard permissions with %s permissions should be allowed", p.String())
+
+ Convey(tc, func() {
+ permissionList := []*m.DashboardAcl{}
+ switch pt {
+ case USER:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(dashboardID, p),
+ newViewerRolePermission(dashboardID, p),
+ newCustomUserPermission(dashboardID, otherUserID, p),
+ newDefaultTeamPermission(dashboardID, p),
+ }
+ case TEAM:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(dashboardID, p),
+ newViewerRolePermission(dashboardID, p),
+ newDefaultUserPermission(dashboardID, p),
+ newCustomTeamPermission(dashboardID, otherTeamID, p),
+ }
+ case EDITOR, VIEWER:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(dashboardID, p),
+ newViewerRolePermission(dashboardID, p),
+ newDefaultUserPermission(dashboardID, p),
+ newDefaultTeamPermission(dashboardID, p),
+ }
+ }
+
+ sc.updatePermissions = permissionList
+ ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+ if err != nil {
+ sc.reportFailure(tc, nil, err)
+ }
+ if !ok {
+ sc.reportFailure(tc, false, true)
+ }
+ sc.reportSuccess()
+ })
}
- permissionScenario(fmt.Sprintf("and team has permission to %s item", permission), sc, p, fn)
}
-func everyoneWithRoleScenario(role m.RoleType, permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
- p := []*m.DashboardAclInfoDTO{
- {OrgId: 1, DashboardId: 1, UserId: -1, Role: &role, Permission: permission},
+func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt permissionType) {
+ if sc.expectedFlags.canAdmin() {
+ return
+ }
+
+ for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+ tc := fmt.Sprintf("When updating dashboard permissions with %s permissions should NOT be allowed", p.String())
+
+ Convey(tc, func() {
+ permissionList := []*m.DashboardAcl{
+ newEditorRolePermission(dashboardID, p),
+ newViewerRolePermission(dashboardID, p),
+ }
+ switch pt {
+ case USER:
+ permissionList = append(permissionList, []*m.DashboardAcl{
+ newCustomUserPermission(dashboardID, otherUserID, p),
+ newDefaultTeamPermission(dashboardID, p),
+ }...)
+ case TEAM:
+ permissionList = append(permissionList, []*m.DashboardAcl{
+ newDefaultUserPermission(dashboardID, p),
+ newCustomTeamPermission(dashboardID, otherTeamID, p),
+ }...)
+ }
+
+ sc.updatePermissions = permissionList
+ ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+ if err != nil {
+ sc.reportFailure(tc, nil, err)
+ }
+ if ok {
+ sc.reportFailure(tc, true, false)
+ }
+ sc.reportSuccess()
+ })
+ }
+}
+
+func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldBeAllowed(pt permissionType, parentFolderPermission m.PermissionType) {
+ if !sc.expectedFlags.canAdmin() {
+ return
+ }
+
+ for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+ tc := fmt.Sprintf("When updating child dashboard permissions with %s permissions should be allowed", p.String())
+
+ Convey(tc, func() {
+ permissionList := []*m.DashboardAcl{}
+ switch pt {
+ case USER:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(childDashboardID, p),
+ newViewerRolePermission(childDashboardID, p),
+ newCustomUserPermission(childDashboardID, otherUserID, p),
+ newDefaultTeamPermission(childDashboardID, p),
+ }
+ case TEAM:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(childDashboardID, p),
+ newViewerRolePermission(childDashboardID, p),
+ newDefaultUserPermission(childDashboardID, p),
+ newCustomTeamPermission(childDashboardID, otherTeamID, p),
+ }
+ case EDITOR:
+ permissionList = []*m.DashboardAcl{
+ newViewerRolePermission(childDashboardID, p),
+ newDefaultUserPermission(childDashboardID, p),
+ newDefaultTeamPermission(childDashboardID, p),
+ }
+
+ // permission to update is higher than parent folder permission
+ if p > parentFolderPermission {
+ permissionList = append(permissionList, newEditorRolePermission(childDashboardID, p))
+ }
+ case VIEWER:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(childDashboardID, p),
+ newDefaultUserPermission(childDashboardID, p),
+ newDefaultTeamPermission(childDashboardID, p),
+ }
+
+ // permission to update is higher than parent folder permission
+ if p > parentFolderPermission {
+ permissionList = append(permissionList, newViewerRolePermission(childDashboardID, p))
+ }
+ }
+
+ sc.updatePermissions = permissionList
+ ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+ if err != nil {
+ sc.reportFailure(tc, nil, err)
+ }
+ if !ok {
+ sc.reportFailure(tc, false, true)
+ }
+ sc.reportSuccess()
+ })
+ }
+}
+
+func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldNotBeAllowed(pt permissionType, parentFolderPermission m.PermissionType) {
+ if sc.expectedFlags.canAdmin() {
+ return
+ }
+
+ for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+ tc := fmt.Sprintf("When updating child dashboard permissions with %s permissions should NOT be allowed", p.String())
+
+ Convey(tc, func() {
+ permissionList := []*m.DashboardAcl{}
+ switch pt {
+ case USER:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(childDashboardID, p),
+ newViewerRolePermission(childDashboardID, p),
+ newCustomUserPermission(childDashboardID, otherUserID, p),
+ newDefaultTeamPermission(childDashboardID, p),
+ }
+ case TEAM:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(childDashboardID, p),
+ newViewerRolePermission(childDashboardID, p),
+ newDefaultUserPermission(childDashboardID, p),
+ newCustomTeamPermission(childDashboardID, otherTeamID, p),
+ }
+ case EDITOR:
+ permissionList = []*m.DashboardAcl{
+ newViewerRolePermission(childDashboardID, p),
+ newDefaultUserPermission(childDashboardID, p),
+ newDefaultTeamPermission(childDashboardID, p),
+ }
+
+ // perminssion to update is higher than parent folder permission
+ if p > parentFolderPermission {
+ permissionList = append(permissionList, newEditorRolePermission(childDashboardID, p))
+ }
+ case VIEWER:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(childDashboardID, p),
+ newDefaultUserPermission(childDashboardID, p),
+ newDefaultTeamPermission(childDashboardID, p),
+ }
+
+ // perminssion to update is higher than parent folder permission
+ if p > parentFolderPermission {
+ permissionList = append(permissionList, newViewerRolePermission(childDashboardID, p))
+ }
+ }
+
+ sc.updatePermissions = permissionList
+ ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+ if err != nil {
+ sc.reportFailure(tc, nil, err)
+ }
+ if ok {
+ sc.reportFailure(tc, true, false)
+ }
+ sc.reportSuccess()
+ })
+ }
+}
+
+func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldBeAllowed(pt permissionType, parentFolderPermission m.PermissionType) {
+ if !sc.expectedFlags.canAdmin() {
+ return
+ }
+
+ for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+ // perminssion to update is higher tban parent folder permission
+ if p > parentFolderPermission {
+ continue
+ }
+
+ tc := fmt.Sprintf("When updating child dashboard permissions overriding parent %s permission with %s permission should NOT be allowed", pt.String(), p.String())
+
+ Convey(tc, func() {
+ permissionList := []*m.DashboardAcl{}
+ switch pt {
+ case USER:
+ permissionList = []*m.DashboardAcl{
+ newDefaultUserPermission(childDashboardID, p),
+ }
+ case TEAM:
+ permissionList = []*m.DashboardAcl{
+ newDefaultTeamPermission(childDashboardID, p),
+ }
+ case EDITOR:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(childDashboardID, p),
+ }
+ case VIEWER:
+ permissionList = []*m.DashboardAcl{
+ newViewerRolePermission(childDashboardID, p),
+ }
+ }
+
+ sc.updatePermissions = permissionList
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+ if err != ErrGuardianOverride {
+ sc.reportFailure(tc, ErrGuardianOverride, err)
+ }
+ sc.reportSuccess()
+ })
+ }
+}
+
+func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldNotBeAllowed(pt permissionType, parentFolderPermission m.PermissionType) {
+ if !sc.expectedFlags.canAdmin() {
+ return
+ }
+
+ for _, p := range []m.PermissionType{m.PERMISSION_ADMIN, m.PERMISSION_EDIT, m.PERMISSION_VIEW} {
+ // perminssion to update is lower than/equal parent folder permission
+ if p <= parentFolderPermission {
+ continue
+ }
+
+ tc := fmt.Sprintf("When updating child dashboard permissions overriding parent %s permission with %s permission should be allowed", pt.String(), p.String())
+
+ Convey(tc, func() {
+ permissionList := []*m.DashboardAcl{}
+ switch pt {
+ case USER:
+ permissionList = []*m.DashboardAcl{
+ newDefaultUserPermission(childDashboardID, p),
+ }
+ case TEAM:
+ permissionList = []*m.DashboardAcl{
+ newDefaultTeamPermission(childDashboardID, p),
+ }
+ case EDITOR:
+ permissionList = []*m.DashboardAcl{
+ newEditorRolePermission(childDashboardID, p),
+ }
+ case VIEWER:
+ permissionList = []*m.DashboardAcl{
+ newViewerRolePermission(childDashboardID, p),
+ }
+ }
+
+ _, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+ sc.updatePermissions = permissionList
+ ok, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, permissionList)
+
+ if err != nil {
+ sc.reportFailure(tc, nil, err)
+ }
+ if !ok {
+ sc.reportFailure(tc, false, true)
+ }
+ sc.reportSuccess()
+ })
}
- permissionScenario(fmt.Sprintf("and everyone with %s role can %s item", role, permission), sc, p, fn)
}
diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go
new file mode 100644
index 00000000000..b065c4194ad
--- /dev/null
+++ b/pkg/services/guardian/guardian_util_test.go
@@ -0,0 +1,256 @@
+package guardian
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/grafana/grafana/pkg/bus"
+ m "github.com/grafana/grafana/pkg/models"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+type scenarioContext struct {
+ t *testing.T
+ orgRoleScenario string
+ permissionScenario string
+ g DashboardGuardian
+ givenUser *m.SignedInUser
+ givenDashboardID int64
+ givenPermissions []*m.DashboardAclInfoDTO
+ givenTeams []*m.Team
+ updatePermissions []*m.DashboardAcl
+ expectedFlags permissionFlags
+ callerFile string
+ callerLine int
+}
+
+type scenarioFunc func(c *scenarioContext)
+
+func orgRoleScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc) {
+ user := &m.SignedInUser{
+ UserId: userID,
+ OrgId: orgID,
+ OrgRole: role,
+ }
+ guard := New(dashboardID, orgID, user)
+ sc := &scenarioContext{
+ t: t,
+ orgRoleScenario: desc,
+ givenUser: user,
+ givenDashboardID: dashboardID,
+ g: guard,
+ }
+
+ Convey(desc, func() {
+ fn(sc)
+ })
+}
+
+func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
+ bus.ClearBusHandlers()
+
+ bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
+ if query.OrgId != sc.givenUser.OrgId {
+ sc.reportFailure("Invalid organization id for GetDashboardAclInfoListQuery", sc.givenUser.OrgId, query.OrgId)
+ }
+ if query.DashboardId != sc.givenDashboardID {
+ sc.reportFailure("Invalid dashboard id for GetDashboardAclInfoListQuery", sc.givenDashboardID, query.DashboardId)
+ }
+
+ query.Result = permissions
+ return nil
+ })
+
+ teams := []*m.Team{}
+
+ for _, p := range permissions {
+ if p.TeamId > 0 {
+ teams = append(teams, &m.Team{Id: p.TeamId})
+ }
+ }
+
+ bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
+ if query.OrgId != sc.givenUser.OrgId {
+ sc.reportFailure("Invalid organization id for GetTeamsByUserQuery", sc.givenUser.OrgId, query.OrgId)
+ }
+ if query.UserId != sc.givenUser.UserId {
+ sc.reportFailure("Invalid user id for GetTeamsByUserQuery", sc.givenUser.UserId, query.UserId)
+ }
+
+ query.Result = teams
+ return nil
+ })
+
+ sc.permissionScenario = desc
+ sc.g = New(dashboardID, sc.givenUser.OrgId, sc.givenUser)
+ sc.givenDashboardID = dashboardID
+ sc.givenPermissions = permissions
+ sc.givenTeams = teams
+
+ Convey(desc, func() {
+ fn(sc)
+ })
+}
+
+type permissionType uint8
+
+const (
+ USER permissionType = 1 << iota
+ TEAM
+ EDITOR
+ VIEWER
+)
+
+func (p permissionType) String() string {
+ names := map[uint8]string{
+ uint8(USER): "user",
+ uint8(TEAM): "team",
+ uint8(EDITOR): "editor role",
+ uint8(VIEWER): "viewer role",
+ }
+ return names[uint8(p)]
+}
+
+type permissionFlags uint8
+
+const (
+ NO_ACCESS permissionFlags = 1 << iota
+ CAN_ADMIN
+ CAN_EDIT
+ CAN_SAVE
+ CAN_VIEW
+ FULL_ACCESS = CAN_ADMIN | CAN_EDIT | CAN_SAVE | CAN_VIEW
+ EDITOR_ACCESS = CAN_EDIT | CAN_SAVE | CAN_VIEW
+ VIEWER_ACCESS = CAN_VIEW
+)
+
+func (flag permissionFlags) canAdmin() bool {
+ return flag&CAN_ADMIN != 0
+}
+
+func (flag permissionFlags) canEdit() bool {
+ return flag&CAN_EDIT != 0
+}
+
+func (flag permissionFlags) canSave() bool {
+ return flag&CAN_SAVE != 0
+}
+
+func (flag permissionFlags) canView() bool {
+ return flag&CAN_VIEW != 0
+}
+
+func (flag permissionFlags) noAccess() bool {
+ return flag&(CAN_ADMIN|CAN_EDIT|CAN_SAVE|CAN_VIEW) == 0
+}
+
+func (f permissionFlags) String() string {
+ r := []string{}
+
+ if f.canAdmin() {
+ r = append(r, "admin")
+ }
+
+ if f.canEdit() {
+ r = append(r, "edit")
+ }
+
+ if f.canSave() {
+ r = append(r, "save")
+ }
+
+ if f.canView() {
+ r = append(r, "view")
+ }
+
+ if f.noAccess() {
+ r = append(r, "")
+ }
+
+ return strings.Join(r[:], ", ")
+}
+
+func (sc *scenarioContext) reportSuccess() {
+ So(true, ShouldBeTrue)
+}
+
+func (sc *scenarioContext) reportFailure(desc string, expected interface{}, actual interface{}) {
+ var buf bytes.Buffer
+ buf.WriteString("\n")
+ buf.WriteString(sc.orgRoleScenario)
+ buf.WriteString(" ")
+ buf.WriteString(sc.permissionScenario)
+ buf.WriteString("\n ")
+ buf.WriteString(desc)
+ buf.WriteString("\n")
+ buf.WriteString(fmt.Sprintf("Source test: %s:%d\n", sc.callerFile, sc.callerLine))
+ buf.WriteString(fmt.Sprintf("Expected: %v\n", expected))
+ buf.WriteString(fmt.Sprintf("Actual: %v\n", actual))
+ buf.WriteString("Context:")
+ buf.WriteString(fmt.Sprintf("\n Given user: orgRole=%s, id=%d, orgId=%d", sc.givenUser.OrgRole, sc.givenUser.UserId, sc.givenUser.OrgId))
+ buf.WriteString(fmt.Sprintf("\n Given dashboard id: %d", sc.givenDashboardID))
+
+ for i, p := range sc.givenPermissions {
+ r := ""
+ if p.Role != nil {
+ r = string(*p.Role)
+ }
+ buf.WriteString(fmt.Sprintf("\n Given permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
+ }
+
+ for i, t := range sc.givenTeams {
+ buf.WriteString(fmt.Sprintf("\n Given team (%d): id=%d", i, t.Id))
+ }
+
+ for i, p := range sc.updatePermissions {
+ r := ""
+ if p.Role != nil {
+ r = string(*p.Role)
+ }
+ buf.WriteString(fmt.Sprintf("\n Update permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
+ }
+
+ sc.t.Fatalf(buf.String())
+}
+
+func newCustomUserPermission(dashboardID int64, userID int64, permission m.PermissionType) *m.DashboardAcl {
+ return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, UserId: userID, Permission: permission}
+}
+
+func newDefaultUserPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+ return newCustomUserPermission(dashboardID, userID, permission)
+}
+
+func newCustomTeamPermission(dashboardID int64, teamID int64, permission m.PermissionType) *m.DashboardAcl {
+ return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, TeamId: teamID, Permission: permission}
+}
+
+func newDefaultTeamPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+ return newCustomTeamPermission(dashboardID, teamID, permission)
+}
+
+func newAdminRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+ return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &adminRole, Permission: permission}
+}
+
+func newEditorRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+ return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &editorRole, Permission: permission}
+}
+
+func newViewerRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
+ return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &viewerRole, Permission: permission}
+}
+
+func toDto(acl *m.DashboardAcl) *m.DashboardAclInfoDTO {
+ return &m.DashboardAclInfoDTO{
+ OrgId: acl.OrgId,
+ DashboardId: acl.DashboardId,
+ UserId: acl.UserId,
+ TeamId: acl.TeamId,
+ Role: acl.Role,
+ Permission: acl.Permission,
+ PermissionName: acl.Permission.String(),
+ }
+}
diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go
index 05c2e53c748..1bac5025244 100644
--- a/pkg/services/notifications/mailer.go
+++ b/pkg/services/notifications/mailer.go
@@ -7,7 +7,6 @@ package notifications
import (
"bytes"
"crypto/tls"
- "errors"
"fmt"
"html/template"
"net"
@@ -135,7 +134,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
subjectText, hasSubject := subjectData["value"]
if !hasSubject {
- return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
+ return nil, fmt.Errorf("Missing subject in Template %s", cmd.Template)
}
subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go
index dff5aa4924a..0636a6adadc 100644
--- a/pkg/services/notifications/webhook.go
+++ b/pkg/services/notifications/webhook.go
@@ -101,7 +101,3 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
webhookLog.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body))
return fmt.Errorf("Webhook response status %v", resp.Status)
}
-
-var addToWebhookQueue = func(msg *Webhook) {
- webhookQueue <- msg
-}
diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go
index 9030ba609b9..8ac79df0fac 100644
--- a/pkg/services/provisioning/dashboards/config_reader.go
+++ b/pkg/services/provisioning/dashboards/config_reader.go
@@ -58,7 +58,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
files, err := ioutil.ReadDir(cr.path)
if err != nil {
- cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path)
+ cr.log.Error("can't read dashboard provisioning files from directory", "path", cr.path)
return dashboards, nil
}
diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go
index f742b321552..4a55351d3e4 100644
--- a/pkg/services/provisioning/dashboards/types.go
+++ b/pkg/services/provisioning/dashboards/types.go
@@ -55,9 +55,6 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
dash.OrgId = cfg.OrgId
dash.Dashboard.OrgId = cfg.OrgId
dash.Dashboard.FolderId = folderId
- if !cfg.Editable {
- dash.Dashboard.Data.Set("editable", cfg.Editable)
- }
if dash.Dashboard.Title == "" {
return nil, models.ErrDashboardTitleEmpty
diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go
index 58ed5472a6b..4b8931f0ed3 100644
--- a/pkg/services/provisioning/datasources/config_reader.go
+++ b/pkg/services/provisioning/datasources/config_reader.go
@@ -19,7 +19,7 @@ func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error)
files, err := ioutil.ReadDir(path)
if err != nil {
- cr.log.Error("cant read datasource provisioning files from directory", "path", path)
+ cr.log.Error("can't read datasource provisioning files from directory", "path", path)
return datasources, nil
}
diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go
index 3198329e0ae..7d621ffe70f 100644
--- a/pkg/services/provisioning/datasources/config_reader_test.go
+++ b/pkg/services/provisioning/datasources/config_reader_test.go
@@ -12,7 +12,6 @@ import (
var (
logger log.Logger = log.New("fake.log")
- oneDatasourcesConfig string = ""
twoDatasourcesConfig string = "./test-configs/two-datasources"
twoDatasourcesConfigPurgeOthers string = "./test-configs/insert-two-delete-two"
doubleDatasourcesConfig string = "./test-configs/double-default"
diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go
index b41ec37b797..71385994bb5 100644
--- a/pkg/services/provisioning/provisioning.go
+++ b/pkg/services/provisioning/provisioning.go
@@ -20,11 +20,7 @@ func Init(ctx context.Context, homePath string, cfg *ini.File) error {
dashboardPath := path.Join(provisioningPath, "dashboards")
_, err := dashboards.Provision(ctx, dashboardPath)
- if err != nil {
- return err
- }
-
- return nil
+ return err
}
func makeAbsolute(path string, root string) string {
diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go
index ae691c7166c..651241f7714 100644
--- a/pkg/services/sqlstore/alert_notification.go
+++ b/pkg/services/sqlstore/alert_notification.go
@@ -23,12 +23,7 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
return inTransaction(func(sess *DBSession) error {
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
-
- if err != nil {
- return err
- }
-
- return nil
+ return err
})
}
diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go
index d37062fb58f..761114978a8 100644
--- a/pkg/services/sqlstore/alert_notification_test.go
+++ b/pkg/services/sqlstore/alert_notification_test.go
@@ -21,7 +21,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
}
err := GetAlertNotifications(cmd)
- fmt.Printf("errror %v", err)
+ fmt.Printf("error %v", err)
So(err, ShouldBeNil)
So(cmd.Result, ShouldBeNil)
})
diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go
index 76f1819a18c..1066be0ef74 100644
--- a/pkg/services/sqlstore/annotation.go
+++ b/pkg/services/sqlstore/annotation.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strings"
+ "time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
@@ -17,6 +18,12 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
return inTransaction(func(sess *DBSession) error {
tags := models.ParseTagPairs(item.Tags)
item.Tags = models.JoinTagPairs(tags)
+ item.Created = time.Now().UnixNano() / int64(time.Millisecond)
+ item.Updated = item.Created
+ if item.Epoch == 0 {
+ item.Epoch = item.Created
+ }
+
if _, err := sess.Table("annotation").Insert(item); err != nil {
return err
}
@@ -79,6 +86,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
return errors.New("Annotation not found")
}
+ existing.Updated = time.Now().UnixNano() / int64(time.Millisecond)
existing.Epoch = item.Epoch
existing.Text = item.Text
if item.RegionId != 0 {
@@ -102,11 +110,8 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
existing.Tags = item.Tags
- if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
- return err
- }
-
- return nil
+ _, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "updated", "tags").Update(existing)
+ return err
})
}
@@ -127,6 +132,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
annotation.text,
annotation.tags,
annotation.data,
+ annotation.created,
+ annotation.updated,
usr.email,
usr.login,
alert.name as alert_name
@@ -164,6 +171,11 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
params = append(params, query.PanelId)
}
+ if query.UserId != 0 {
+ sql.WriteString(` AND annotation.user_id = ?`)
+ params = append(params, query.UserId)
+ }
+
if query.From > 0 && query.To > 0 {
sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`)
params = append(params, query.From, query.To)
@@ -171,6 +183,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
if query.Type == "alert" {
sql.WriteString(` AND annotation.alert_id > 0`)
+ } else if query.Type == "annotation" {
+ sql.WriteString(` AND annotation.alert_id = 0`)
}
if len(query.Tags) > 0 {
@@ -202,7 +216,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
}
if query.Limit == 0 {
- query.Limit = 10
+ query.Limit = 100
}
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go
index d5cee110b9a..5af5f271993 100644
--- a/pkg/services/sqlstore/annotation_test.go
+++ b/pkg/services/sqlstore/annotation_test.go
@@ -79,6 +79,12 @@ func TestAnnotations(t *testing.T) {
Convey("Can read tags", func() {
So(items[0].Tags, ShouldResemble, []string{"outage", "error", "type:outage", "server:server-1"})
})
+
+ Convey("Has created and updated values", func() {
+ So(items[0].Created, ShouldBeGreaterThan, 0)
+ So(items[0].Updated, ShouldBeGreaterThan, 0)
+ So(items[0].Updated, ShouldEqual, items[0].Created)
+ })
})
Convey("Can query for annotation by id", func() {
@@ -231,6 +237,10 @@ func TestAnnotations(t *testing.T) {
So(items[0].Tags, ShouldResemble, []string{"newtag1", "newtag2"})
So(items[0].Text, ShouldEqual, "something new")
})
+
+ Convey("Updated time has increased", func() {
+ So(items[0].Updated, ShouldBeGreaterThan, items[0].Created)
+ })
})
Convey("Can delete annotation", func() {
diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go
index 0532f636625..9d41b5c809e 100644
--- a/pkg/services/sqlstore/apikey.go
+++ b/pkg/services/sqlstore/apikey.go
@@ -55,7 +55,7 @@ func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrInvalidApiKey
}
@@ -69,7 +69,7 @@ func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrInvalidApiKey
}
diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go
index 8a89c3d942c..c0848f08863 100644
--- a/pkg/services/sqlstore/dashboard.go
+++ b/pkg/services/sqlstore/dashboard.go
@@ -63,7 +63,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
}
// do not allow plugin dashboard updates without overwrite flag
- if existing.PluginId != "" && cmd.Overwrite == false {
+ if existing.PluginId != "" && !cmd.Overwrite {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
}
@@ -172,7 +172,7 @@ func GetDashboard(query *m.GetDashboardQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrDashboardNotFound
}
@@ -308,7 +308,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
has, err := sess.Get(&dashboard)
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrDashboardNotFound
}
@@ -347,12 +347,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
err := x.In("id", query.DashboardIds).Find(&dashboards)
query.Result = dashboards
-
- if err != nil {
- return err
- }
-
- return nil
+ return err
}
// GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s)
@@ -431,12 +426,7 @@ func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
query.Result = dashboards
-
- if err != nil {
- return err
- }
-
- return nil
+ return err
}
type DashboardSlugDTO struct {
@@ -451,7 +441,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
if err != nil {
return err
- } else if exists == false {
+ } else if !exists {
return m.ErrDashboardNotFound
}
@@ -479,7 +469,7 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
if err != nil {
return err
- } else if exists == false {
+ } else if !exists {
return m.ErrDashboardNotFound
}
@@ -544,6 +534,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
dash.SetId(existingByUid.Id)
dash.SetUid(existingByUid.Uid)
existing = existingByUid
+
+ if !dash.IsFolder {
+ cmd.Result.IsParentFolderChanged = true
+ }
}
if (existing.IsFolder && !dash.IsFolder) ||
@@ -551,6 +545,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
return m.ErrDashboardTypeMismatch
}
+ if !dash.IsFolder && dash.FolderId != existing.FolderId {
+ cmd.Result.IsParentFolderChanged = true
+ }
+
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
@@ -561,7 +559,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
}
// do not allow plugin dashboard updates without overwrite flag
- if existing.PluginId != "" && cmd.Overwrite == false {
+ if existing.PluginId != "" && !cmd.Overwrite {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
@@ -586,6 +584,10 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
return m.ErrDashboardFolderWithSameNameAsDashboard
}
+ if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) {
+ cmd.Result.IsParentFolderChanged = true
+ }
+
if cmd.Overwrite {
dash.SetId(existing.Id)
dash.SetUid(existing.Uid)
@@ -599,6 +601,7 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
}
func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
+ cmd.Result = &m.ValidateDashboardBeforeSaveResult{}
return inTransaction(func(sess *DBSession) error {
if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
return err
diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go
index ae91d1d41f3..2034ccf0d30 100644
--- a/pkg/services/sqlstore/dashboard_acl.go
+++ b/pkg/services/sqlstore/dashboard_acl.go
@@ -35,10 +35,8 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
// Update dashboard HasAcl flag
dashboard := m.Dashboard{HasAcl: true}
- if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
- return err
- }
- return nil
+ _, err = sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard)
+ return err
})
}
@@ -92,6 +90,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
u.login AS user_login,
u.email AS user_email,
ug.name AS team,
+ ug.email AS team_email,
d.title,
d.slug,
d.uid,
diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go
index 69409c3b873..33fbb01c5b7 100644
--- a/pkg/services/sqlstore/dashboard_provisioning.go
+++ b/pkg/services/sqlstore/dashboard_provisioning.go
@@ -8,6 +8,7 @@ import (
func init() {
bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
bus.AddHandler("sql", SaveProvisionedDashboard)
+ bus.AddHandler("sql", GetProvisionedDataByDashboardId)
}
type DashboardExtras struct {
@@ -17,6 +18,19 @@ type DashboardExtras struct {
Value string
}
+func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error {
+ result := &models.DashboardProvisioning{}
+
+ exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result)
+ if err != nil {
+ return err
+ }
+
+ cmd.Result = exist
+
+ return nil
+}
+
func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error {
return inTransaction(func(sess *DBSession) error {
err := saveDashboard(sess, cmd.DashboardCmd)
diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go
index b752173b67d..7ef45df3152 100644
--- a/pkg/services/sqlstore/dashboard_provisioning_test.go
+++ b/pkg/services/sqlstore/dashboard_provisioning_test.go
@@ -50,6 +50,23 @@ func TestDashboardProvisioningTest(t *testing.T) {
So(query.Result[0].DashboardId, ShouldEqual, dashId)
So(query.Result[0].Updated, ShouldEqual, now.Unix())
})
+
+ Convey("Can query for one provisioned dashboard", func() {
+ query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
+
+ err := GetProvisionedDataByDashboardId(query)
+ So(err, ShouldBeNil)
+
+ So(query.Result, ShouldBeTrue)
+ })
+
+ Convey("Can query for none provisioned dashboard", func() {
+ query := &models.IsDashboardProvisionedQuery{DashboardId: 3000}
+
+ err := GetProvisionedDataByDashboardId(query)
+ So(err, ShouldBeNil)
+ So(query.Result, ShouldBeFalse)
+ })
})
})
}
diff --git a/pkg/services/sqlstore/dashboard_service_integration_test.go b/pkg/services/sqlstore/dashboard_service_integration_test.go
index d005270c33c..a9658f7ab76 100644
--- a/pkg/services/sqlstore/dashboard_service_integration_test.go
+++ b/pkg/services/sqlstore/dashboard_service_integration_test.go
@@ -19,7 +19,6 @@ func TestIntegratedDashboardService(t *testing.T) {
var testOrgId int64 = 1
Convey("Given saved folders and dashboards in organization A", func() {
-
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
@@ -28,6 +27,11 @@ func TestIntegratedDashboardService(t *testing.T) {
return nil
})
+ bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
+ cmd.Result = false
+ return nil
+ })
+
savedFolder := saveTestFolder("Saved folder", testOrgId)
savedDashInFolder := saveTestDashboard("Saved dash in folder", testOrgId, savedFolder.Id)
saveTestDashboard("Other saved dash in folder", testOrgId, savedFolder.Id)
@@ -74,7 +78,7 @@ func TestIntegratedDashboardService(t *testing.T) {
Convey("Given organization B", func() {
var otherOrgId int64 = 2
- Convey("When saving a dashboard with id that are saved in organization A", func() {
+ Convey("When creating a dashboard with same id as dashboard in organization A", func() {
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -93,7 +97,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
- Convey("When saving a dashboard with uid that are saved in organization A", func() {
+ Convey("When creating a dashboard with same uid as dashboard in organization A", func() {
var otherOrgId int64 = 2
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
@@ -106,7 +110,7 @@ func TestIntegratedDashboardService(t *testing.T) {
res := callSaveWithResult(cmd)
- Convey("It should create dashboard in other organization", func() {
+ Convey("It should create a new dashboard in organization B", func() {
So(res, ShouldNotBeNil)
query := models.GetDashboardQuery{OrgId: otherOrgId, Uid: savedDashInFolder.Uid}
@@ -126,7 +130,7 @@ func TestIntegratedDashboardService(t *testing.T) {
permissionScenario("Given user has no permission to save", false, func(sc *dashboardPermissionScenarioContext) {
- Convey("When trying to create a new dashboard in the General folder", func() {
+ Convey("When creating a new dashboard in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -138,7 +142,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
- Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
+ Convey("It should create dashboard guardian for General Folder with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@@ -148,7 +152,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to create a new dashboard in other folder", func() {
+ Convey("When creating a new dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -161,7 +165,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
- Convey("It should call dashboard guardian with correct arguments and rsult in access denied error", func() {
+ Convey("It should create dashboard guardian for other folder with correct arguments and rsult in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@@ -171,7 +175,54 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to update a dashboard by existing id in the General folder", func() {
+ Convey("When creating a new dashboard by existing title in folder", func() {
+ cmd := models.SaveDashboardCommand{
+ OrgId: testOrgId,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "title": savedDashInFolder.Title,
+ }),
+ FolderId: savedFolder.Id,
+ UserId: 10000,
+ Overwrite: true,
+ }
+
+ err := callSaveWithError(cmd)
+
+ Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
+ So(err, ShouldNotBeNil)
+ So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+ So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
+ So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+ So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+ })
+ })
+
+ Convey("When creating a new dashboard by existing uid in folder", func() {
+ cmd := models.SaveDashboardCommand{
+ OrgId: testOrgId,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "uid": savedDashInFolder.Uid,
+ "title": "New dash",
+ }),
+ FolderId: savedFolder.Id,
+ UserId: 10000,
+ Overwrite: true,
+ }
+
+ err := callSaveWithError(cmd)
+
+ Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
+ So(err, ShouldNotBeNil)
+ So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+ So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
+ So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+ So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+ })
+ })
+
+ Convey("When updating a dashboard by existing id in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -185,7 +236,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
- Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
+ Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@@ -195,7 +246,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to update a dashboard by existing id in other folder", func() {
+ Convey("When updating a dashboard by existing id in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -209,7 +260,7 @@ func TestIntegratedDashboardService(t *testing.T) {
err := callSaveWithError(cmd)
- Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
+ Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
@@ -218,6 +269,102 @@ func TestIntegratedDashboardService(t *testing.T) {
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
+
+ Convey("When moving a dashboard by existing id to other folder from General folder", func() {
+ cmd := models.SaveDashboardCommand{
+ OrgId: testOrgId,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "id": savedDashInGeneralFolder.Id,
+ "title": "Dash",
+ }),
+ FolderId: otherSavedFolder.Id,
+ UserId: 10000,
+ Overwrite: true,
+ }
+
+ err := callSaveWithError(cmd)
+
+ Convey("It should create dashboard guardian for other folder with correct arguments and result in access denied error", func() {
+ So(err, ShouldNotBeNil)
+ So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+ So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
+ So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+ So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+ })
+ })
+
+ Convey("When moving a dashboard by existing id to the General folder from other folder", func() {
+ cmd := models.SaveDashboardCommand{
+ OrgId: testOrgId,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "id": savedDashInFolder.Id,
+ "title": "Dash",
+ }),
+ FolderId: 0,
+ UserId: 10000,
+ Overwrite: true,
+ }
+
+ err := callSaveWithError(cmd)
+
+ Convey("It should create dashboard guardian for General folder with correct arguments and result in access denied error", func() {
+ So(err, ShouldNotBeNil)
+ So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+ So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
+ So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+ So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+ })
+ })
+
+ Convey("When moving a dashboard by existing uid to other folder from General folder", func() {
+ cmd := models.SaveDashboardCommand{
+ OrgId: testOrgId,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "uid": savedDashInGeneralFolder.Uid,
+ "title": "Dash",
+ }),
+ FolderId: otherSavedFolder.Id,
+ UserId: 10000,
+ Overwrite: true,
+ }
+
+ err := callSaveWithError(cmd)
+
+ Convey("It should create dashboard guardian for other folder with correct arguments and result in access denied error", func() {
+ So(err, ShouldNotBeNil)
+ So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+ So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
+ So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+ So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+ })
+ })
+
+ Convey("When moving a dashboard by existing uid to the General folder from other folder", func() {
+ cmd := models.SaveDashboardCommand{
+ OrgId: testOrgId,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "uid": savedDashInFolder.Uid,
+ "title": "Dash",
+ }),
+ FolderId: 0,
+ UserId: 10000,
+ Overwrite: true,
+ }
+
+ err := callSaveWithError(cmd)
+
+ Convey("It should create dashboard guardian for General folder with correct arguments and result in access denied error", func() {
+ So(err, ShouldNotBeNil)
+ So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
+
+ So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
+ So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
+ So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
+ })
+ })
})
// Given user has permission to save
@@ -668,7 +815,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to update existing folder to a dashboard using id", func() {
+ Convey("When updating existing folder to a dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -687,7 +834,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to update existing dashboard to a folder using id", func() {
+ Convey("When updating existing dashboard to a folder using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -706,7 +853,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to update existing folder to a dashboard using uid", func() {
+ Convey("When updating existing folder to a dashboard using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -725,7 +872,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to update existing dashboard to a folder using uid", func() {
+ Convey("When updating existing dashboard to a folder using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -744,7 +891,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to update existing folder to a dashboard using title", func() {
+ Convey("When updating existing folder to a dashboard using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -762,7 +909,7 @@ func TestIntegratedDashboardService(t *testing.T) {
})
})
- Convey("When trying to update existing dashboard to a folder using title", func() {
+ Convey("When updating existing dashboard to a folder using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@@ -850,23 +997,6 @@ func callSaveWithError(cmd models.SaveDashboardCommand) error {
return err
}
-func dashboardServiceScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
- Convey(desc, func() {
- origNewDashboardGuardian := guardian.New
- guardian.MockDashboardGuardian(mock)
-
- sc := &scenarioContext{
- dashboardGuardianMock: mock,
- }
-
- defer func() {
- guardian.New = origNewDashboardGuardian
- }()
-
- fn(sc)
- })
-}
-
func saveTestDashboard(title string, orgId int64, folderId int64) *models.Dashboard {
cmd := models.SaveDashboardCommand{
OrgId: orgId,
diff --git a/pkg/services/sqlstore/dashboard_snapshot.go b/pkg/services/sqlstore/dashboard_snapshot.go
index 9e82bbb2c83..2e2ea8a4783 100644
--- a/pkg/services/sqlstore/dashboard_snapshot.go
+++ b/pkg/services/sqlstore/dashboard_snapshot.go
@@ -80,7 +80,7 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrDashboardSnapshotNotFound
}
diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go
index 8d2bf94bc42..7fac0001e5b 100644
--- a/pkg/services/sqlstore/migrations/annotation_mig.go
+++ b/pkg/services/sqlstore/migrations/annotation_mig.go
@@ -90,4 +90,29 @@ func addAnnotationMig(mg *Migrator) {
Sqlite(updateTextFieldSql).
Postgres(updateTextFieldSql).
Mysql(updateTextFieldSql))
+
+ //
+ // Add a 'created' & 'updated' column
+ //
+ mg.AddMigration("Add created time to annotation table", NewAddColumnMigration(table, &Column{
+ Name: "created", Type: DB_BigInt, Nullable: true, Default: "0",
+ }))
+ mg.AddMigration("Add updated time to annotation table", NewAddColumnMigration(table, &Column{
+ Name: "updated", Type: DB_BigInt, Nullable: true, Default: "0",
+ }))
+ mg.AddMigration("Add index for created in annotation table", NewAddIndexMigration(table, &Index{
+ Cols: []string{"org_id", "created"}, Type: IndexType,
+ }))
+ mg.AddMigration("Add index for updated in annotation table", NewAddIndexMigration(table, &Index{
+ Cols: []string{"org_id", "updated"}, Type: IndexType,
+ }))
+
+ //
+ // Convert epoch saved as seconds to miliseconds
+ //
+ updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999"
+ mg.AddMigration("Convert existing annotations from seconds to milliseconds", new(RawSqlMigration).
+ Sqlite(updateEpochSql).
+ Postgres(updateEpochSql).
+ Mysql(updateEpochSql))
}
diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go
index 282f98e7318..58ac6256f41 100644
--- a/pkg/services/sqlstore/migrations/migrations.go
+++ b/pkg/services/sqlstore/migrations/migrations.go
@@ -30,6 +30,7 @@ func AddMigrations(mg *Migrator) {
addDashboardAclMigrations(mg)
addTagMigration(mg)
addLoginAttemptMigrations(mg)
+ addUserAuthMigrations(mg)
}
func addMigrationLogMigrations(mg *Migrator) {
diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go
index 51aea0bbdef..8e8e4af824f 100644
--- a/pkg/services/sqlstore/migrations/migrations_test.go
+++ b/pkg/services/sqlstore/migrations/migrations_test.go
@@ -8,11 +8,8 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
. "github.com/smartystreets/goconvey/convey"
- //"github.com/grafana/grafana/pkg/log"
)
-var indexTypes = []string{"Unknown", "INDEX", "UNIQUE INDEX"}
-
func TestMigrations(t *testing.T) {
testDBs := []sqlutil.TestDB{
sqlutil.TestDB_Sqlite3,
diff --git a/pkg/services/sqlstore/migrations/user_auth_mig.go b/pkg/services/sqlstore/migrations/user_auth_mig.go
new file mode 100644
index 00000000000..4d8a18ce33e
--- /dev/null
+++ b/pkg/services/sqlstore/migrations/user_auth_mig.go
@@ -0,0 +1,24 @@
+package migrations
+
+import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addUserAuthMigrations(mg *Migrator) {
+ userAuthV1 := Table{
+ Name: "user_auth",
+ Columns: []*Column{
+ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+ {Name: "user_id", Type: DB_BigInt, Nullable: false},
+ {Name: "auth_module", Type: DB_NVarchar, Length: 190, Nullable: false},
+ {Name: "auth_id", Type: DB_NVarchar, Length: 100, Nullable: false},
+ {Name: "created", Type: DB_DateTime, Nullable: false},
+ },
+ Indices: []*Index{
+ {Cols: []string{"auth_module", "auth_id"}},
+ },
+ }
+
+ // create table
+ mg.AddMigration("create user auth table", NewAddTableMigration(userAuthV1))
+ // add indices
+ addTableIndicesMigrations(mg, "v1", userAuthV1)
+}
diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go
index 064b5981063..dadc7248844 100644
--- a/pkg/services/sqlstore/migrator/dialect.go
+++ b/pkg/services/sqlstore/migrator/dialect.go
@@ -84,8 +84,7 @@ func (db *BaseDialect) DateTimeFunc(value string) string {
}
func (b *BaseDialect) CreateTableSql(table *Table) string {
- var sql string
- sql = "CREATE TABLE IF NOT EXISTS "
+ sql := "CREATE TABLE IF NOT EXISTS "
sql += b.dialect.Quote(table.Name) + " (\n"
pkList := table.PrimaryKeys
@@ -162,8 +161,7 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string {
func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
quote := db.dialect.Quote
- var name string
- name = index.XName(tableName)
+ name := index.XName(tableName)
return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
}
diff --git a/pkg/services/sqlstore/migrator/migrations.go b/pkg/services/sqlstore/migrator/migrations.go
index 2fec8825fa4..4e6402a14e5 100644
--- a/pkg/services/sqlstore/migrator/migrations.go
+++ b/pkg/services/sqlstore/migrator/migrations.go
@@ -1,7 +1,6 @@
package migrator
import (
- "fmt"
"strings"
)
@@ -113,7 +112,7 @@ func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration {
func (m *DropIndexMigration) Sql(dialect Dialect) string {
if m.index.Name == "" {
- m.index.Name = fmt.Sprintf("%s", strings.Join(m.index.Cols, "_"))
+ m.index.Name = strings.Join(m.index.Cols, "_")
}
return dialect.DropIndexSql(m.tableName, m.index)
}
@@ -180,7 +179,7 @@ type CopyTableDataMigration struct {
targetTable string
sourceCols []string
targetCols []string
- colMap map[string]string
+ //colMap map[string]string
}
func NewCopyTableDataMigration(targetTable string, sourceTable string, colMap map[string]string) *CopyTableDataMigration {
diff --git a/pkg/services/sqlstore/migrator/types.go b/pkg/services/sqlstore/migrator/types.go
index d42eba0f58a..62ec74e7b9f 100644
--- a/pkg/services/sqlstore/migrator/types.go
+++ b/pkg/services/sqlstore/migrator/types.go
@@ -46,7 +46,7 @@ type Index struct {
func (index *Index) XName(tableName string) string {
if index.Name == "" {
- index.Name = fmt.Sprintf("%s", strings.Join(index.Cols, "_"))
+ index.Name = strings.Join(index.Cols, "_")
}
if !strings.HasPrefix(index.Name, "UQE_") &&
diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go
index 172995872eb..312a3bda523 100644
--- a/pkg/services/sqlstore/plugin_setting.go
+++ b/pkg/services/sqlstore/plugin_setting.go
@@ -36,7 +36,7 @@ func GetPluginSettingById(query *m.GetPluginSettingByIdQuery) error {
has, err := x.Get(&pluginSetting)
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrPluginSettingNotFound
}
query.Result = &pluginSetting
diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go
index 3db3fc2657e..539555ddc50 100644
--- a/pkg/services/sqlstore/quota.go
+++ b/pkg/services/sqlstore/quota.go
@@ -31,7 +31,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
has, err := x.Get("a)
if err != nil {
return err
- } else if has == false {
+ } else if !has {
quota.Limit = query.Default
}
@@ -108,7 +108,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
return err
}
quota.Limit = cmd.Limit
- if has == false {
+ if !has {
quota.Created = time.Now()
//No quota in the DB for this target, so create a new one.
if _, err := sess.Insert("a); err != nil {
@@ -133,7 +133,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
has, err := x.Get("a)
if err != nil {
return err
- } else if has == false {
+ } else if !has {
quota.Limit = query.Default
}
@@ -210,7 +210,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
return err
}
quota.Limit = cmd.Limit
- if has == false {
+ if !has {
quota.Created = time.Now()
//No quota in the DB for this target, so create a new one.
if _, err := sess.Insert("a); err != nil {
diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go
index 6aace350193..782318fa188 100644
--- a/pkg/services/sqlstore/sqlstore.go
+++ b/pkg/services/sqlstore/sqlstore.go
@@ -258,7 +258,7 @@ func InitTestDB(t *testing.T) *xorm.Engine {
// x.ShowSQL()
if err != nil {
- t.Fatalf("Failed to init in memory sqllite3 db %v", err)
+ t.Fatalf("Failed to init test database: %v", err)
}
sqlutil.CleanDB(x)
@@ -269,3 +269,19 @@ func InitTestDB(t *testing.T) *xorm.Engine {
return x
}
+
+func IsTestDbMySql() bool {
+ if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
+ return db == dbMySql
+ }
+
+ return false
+}
+
+func IsTestDbPostgres() bool {
+ if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
+ return db == dbPostgres
+ }
+
+ return false
+}
diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go
index cfe2d88c82c..47020d1a6f7 100644
--- a/pkg/services/sqlstore/stats.go
+++ b/pkg/services/sqlstore/stats.go
@@ -19,10 +19,6 @@ func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
query.Result = make([]*m.DataSourceStats, 0)
err := x.SQL(rawSql).Find(&query.Result)
- if err != nil {
- return err
- }
-
return err
}
@@ -68,6 +64,7 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
}
query.Result = &stats
+
return err
}
diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go
index d238301c7ce..b3ff4c81e7c 100644
--- a/pkg/services/sqlstore/team.go
+++ b/pkg/services/sqlstore/team.go
@@ -210,11 +210,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
err := sess.Find(&query.Result)
- if err != nil {
- return err
- }
-
- return nil
+ return err
}
// AddTeamMember adds a user to a team
diff --git a/pkg/services/sqlstore/temp_user.go b/pkg/services/sqlstore/temp_user.go
index 43e1f027057..e93ba2fd641 100644
--- a/pkg/services/sqlstore/temp_user.go
+++ b/pkg/services/sqlstore/temp_user.go
@@ -126,7 +126,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrTempUserNotFound
}
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index f42ff5fb2ed..1546fb83b21 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -154,7 +154,7 @@ func GetUserById(query *m.GetUserByIdQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrUserNotFound
}
@@ -179,7 +179,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
return err
}
- if has == false && strings.Contains(query.LoginOrEmail, "@") {
+ if !has && strings.Contains(query.LoginOrEmail, "@") {
// If the user wasn't found, and it contains an "@" fallback to finding the
// user by email.
user = &m.User{Email: query.LoginOrEmail}
@@ -188,7 +188,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrUserNotFound
}
@@ -209,7 +209,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrUserNotFound
}
@@ -253,11 +253,8 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
Updated: time.Now(),
}
- if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
- return err
- }
-
- return nil
+ _, err := sess.Id(cmd.UserId).Update(&user)
+ return err
})
}
@@ -271,11 +268,8 @@ func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
LastSeenAt: time.Now(),
}
- if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
- return err
- }
-
- return nil
+ _, err := sess.Id(cmd.UserId).Update(&user)
+ return err
})
}
@@ -295,11 +289,12 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
}
return inTransaction(func(sess *DBSession) error {
- user := m.User{}
- sess.Id(cmd.UserId).Get(&user)
+ user := m.User{
+ Id: cmd.UserId,
+ OrgId: cmd.OrgId,
+ }
- user.OrgId = cmd.OrgId
- _, err := sess.Id(user.Id).Update(&user)
+ _, err := sess.Id(cmd.UserId).Update(&user)
return err
})
}
@@ -310,7 +305,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
if err != nil {
return err
- } else if has == false {
+ } else if !has {
return m.ErrUserNotFound
}
@@ -333,6 +328,7 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
sess.Join("INNER", "org", "org_user.org_id=org.id")
sess.Where("org_user.user_id=?", query.UserId)
sess.Cols("org.name", "org_user.role", "org_user.org_id")
+ sess.OrderBy("org.name")
err := sess.Find(&query.Result)
return err
}
@@ -444,6 +440,7 @@ func DeleteUser(cmd *m.DeleteUserCommand) error {
"DELETE FROM dashboard_acl WHERE user_id = ?",
"DELETE FROM preferences WHERE user_id = ?",
"DELETE FROM team_member WHERE user_id = ?",
+ "DELETE FROM user_auth WHERE user_id = ?",
}
for _, sql := range deletes {
@@ -478,10 +475,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
Updated: time.Now(),
}
- if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
- return err
- }
-
- return nil
+ _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user)
+ return err
})
}
diff --git a/pkg/services/sqlstore/user_auth.go b/pkg/services/sqlstore/user_auth.go
new file mode 100644
index 00000000000..aec828451a4
--- /dev/null
+++ b/pkg/services/sqlstore/user_auth.go
@@ -0,0 +1,148 @@
+package sqlstore
+
+import (
+ "time"
+
+ "github.com/grafana/grafana/pkg/bus"
+ m "github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+ bus.AddHandler("sql", GetUserByAuthInfo)
+ bus.AddHandler("sql", GetAuthInfo)
+ bus.AddHandler("sql", SetAuthInfo)
+ bus.AddHandler("sql", DeleteAuthInfo)
+}
+
+func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
+ user := &m.User{}
+ has := false
+ var err error
+ authQuery := &m.GetAuthInfoQuery{}
+
+ // Try to find the user by auth module and id first
+ if query.AuthModule != "" && query.AuthId != "" {
+ authQuery.AuthModule = query.AuthModule
+ authQuery.AuthId = query.AuthId
+
+ err = GetAuthInfo(authQuery)
+ if err != m.ErrUserNotFound {
+ if err != nil {
+ return err
+ }
+
+ // if user id was specified and doesn't match the user_auth entry, remove it
+ if query.UserId != 0 && query.UserId != authQuery.Result.UserId {
+ err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
+ UserAuth: authQuery.Result,
+ })
+ if err != nil {
+ sqlog.Error("Error removing user_auth entry", "error", err)
+ }
+
+ authQuery.Result = nil
+ } else {
+ has, err = x.Id(authQuery.Result.UserId).Get(user)
+ if err != nil {
+ return err
+ }
+
+ if !has {
+ // if the user has been deleted then remove the entry
+ err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
+ UserAuth: authQuery.Result,
+ })
+ if err != nil {
+ sqlog.Error("Error removing user_auth entry", "error", err)
+ }
+
+ authQuery.Result = nil
+ }
+ }
+ }
+ }
+
+ // If not found, try to find the user by id
+ if !has && query.UserId != 0 {
+ has, err = x.Id(query.UserId).Get(user)
+ if err != nil {
+ return err
+ }
+ }
+
+ // If not found, try to find the user by email address
+ if !has && query.Email != "" {
+ user = &m.User{Email: query.Email}
+ has, err = x.Get(user)
+ if err != nil {
+ return err
+ }
+ }
+
+ // If not found, try to find the user by login
+ if !has && query.Login != "" {
+ user = &m.User{Login: query.Login}
+ has, err = x.Get(user)
+ if err != nil {
+ return err
+ }
+ }
+
+ // No user found
+ if !has {
+ return m.ErrUserNotFound
+ }
+
+ // create authInfo record to link accounts
+ if authQuery.Result == nil && query.AuthModule != "" && query.AuthId != "" {
+ cmd2 := &m.SetAuthInfoCommand{
+ UserId: user.Id,
+ AuthModule: query.AuthModule,
+ AuthId: query.AuthId,
+ }
+ if err := SetAuthInfo(cmd2); err != nil {
+ return err
+ }
+ }
+
+ query.Result = user
+ return nil
+}
+
+func GetAuthInfo(query *m.GetAuthInfoQuery) error {
+ userAuth := &m.UserAuth{
+ AuthModule: query.AuthModule,
+ AuthId: query.AuthId,
+ }
+ has, err := x.Get(userAuth)
+ if err != nil {
+ return err
+ }
+ if !has {
+ return m.ErrUserNotFound
+ }
+
+ query.Result = userAuth
+ return nil
+}
+
+func SetAuthInfo(cmd *m.SetAuthInfoCommand) error {
+ return inTransaction(func(sess *DBSession) error {
+ authUser := &m.UserAuth{
+ UserId: cmd.UserId,
+ AuthModule: cmd.AuthModule,
+ AuthId: cmd.AuthId,
+ Created: time.Now(),
+ }
+
+ _, err := sess.Insert(authUser)
+ return err
+ })
+}
+
+func DeleteAuthInfo(cmd *m.DeleteAuthInfoCommand) error {
+ return inTransaction(func(sess *DBSession) error {
+ _, err := sess.Delete(cmd.UserAuth)
+ return err
+ })
+}
diff --git a/pkg/services/sqlstore/user_auth_test.go b/pkg/services/sqlstore/user_auth_test.go
new file mode 100644
index 00000000000..279fd7aa0f5
--- /dev/null
+++ b/pkg/services/sqlstore/user_auth_test.go
@@ -0,0 +1,131 @@
+package sqlstore
+
+import (
+ "fmt"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+
+ m "github.com/grafana/grafana/pkg/models"
+)
+
+func TestUserAuth(t *testing.T) {
+ InitTestDB(t)
+
+ Convey("Given 5 users", t, func() {
+ var err error
+ var cmd *m.CreateUserCommand
+ users := []m.User{}
+ for i := 0; i < 5; i++ {
+ cmd = &m.CreateUserCommand{
+ Email: fmt.Sprint("user", i, "@test.com"),
+ Name: fmt.Sprint("user", i),
+ Login: fmt.Sprint("loginuser", i),
+ }
+ err = CreateUser(cmd)
+ So(err, ShouldBeNil)
+ users = append(users, cmd.Result)
+ }
+
+ Reset(func() {
+ _, err := x.Exec("DELETE FROM org_user WHERE 1=1")
+ So(err, ShouldBeNil)
+ _, err = x.Exec("DELETE FROM org WHERE 1=1")
+ So(err, ShouldBeNil)
+ _, err = x.Exec("DELETE FROM user WHERE 1=1")
+ So(err, ShouldBeNil)
+ _, err = x.Exec("DELETE FROM user_auth WHERE 1=1")
+ So(err, ShouldBeNil)
+ })
+
+ Convey("Can find existing user", func() {
+ // By Login
+ login := "loginuser0"
+
+ query := &m.GetUserByAuthInfoQuery{Login: login}
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldBeNil)
+ So(query.Result.Login, ShouldEqual, login)
+
+ // By ID
+ id := query.Result.Id
+
+ query = &m.GetUserByAuthInfoQuery{UserId: id}
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldBeNil)
+ So(query.Result.Id, ShouldEqual, id)
+
+ // By Email
+ email := "user1@test.com"
+
+ query = &m.GetUserByAuthInfoQuery{Email: email}
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldBeNil)
+ So(query.Result.Email, ShouldEqual, email)
+
+ // Don't find nonexistent user
+ email = "nonexistent@test.com"
+
+ query = &m.GetUserByAuthInfoQuery{Email: email}
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldEqual, m.ErrUserNotFound)
+ So(query.Result, ShouldBeNil)
+ })
+
+ Convey("Can set & locate by AuthModule and AuthId", func() {
+ // get nonexistent user_auth entry
+ query := &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldEqual, m.ErrUserNotFound)
+ So(query.Result, ShouldBeNil)
+
+ // create user_auth entry
+ login := "loginuser0"
+
+ query.Login = login
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldBeNil)
+ So(query.Result.Login, ShouldEqual, login)
+
+ // get via user_auth
+ query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldBeNil)
+ So(query.Result.Login, ShouldEqual, login)
+
+ // get with non-matching id
+ id := query.Result.Id
+
+ query.UserId = id + 1
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldBeNil)
+ So(query.Result.Login, ShouldEqual, "loginuser1")
+
+ // get via user_auth
+ query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldBeNil)
+ So(query.Result.Login, ShouldEqual, "loginuser1")
+
+ // remove user
+ _, err = x.Exec("DELETE FROM user WHERE id=?", query.Result.Id)
+ So(err, ShouldBeNil)
+
+ // get via user_auth for deleted user
+ query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
+ err = GetUserByAuthInfo(query)
+
+ So(err, ShouldEqual, m.ErrUserNotFound)
+ So(query.Result, ShouldBeNil)
+ })
+ })
+}
diff --git a/pkg/social/generic_oauth.go b/pkg/social/generic_oauth.go
index b92d64ad9fc..8c02076096d 100644
--- a/pkg/social/generic_oauth.go
+++ b/pkg/social/generic_oauth.go
@@ -182,7 +182,7 @@ func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token)
var data UserInfoJson
var err error
- if s.extractToken(&data, token) != true {
+ if !s.extractToken(&data, token) {
response, err := HttpGet(client, s.apiUrl)
if err != nil {
return nil, fmt.Errorf("Error getting user info: %s", err)
diff --git a/pkg/social/grafana_com_oauth.go b/pkg/social/grafana_com_oauth.go
index d3614520d61..87601788c3f 100644
--- a/pkg/social/grafana_com_oauth.go
+++ b/pkg/social/grafana_com_oauth.go
@@ -51,6 +51,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
+ Id int `json:"id"`
Name string `json:"name"`
Login string `json:"username"`
Email string `json:"email"`
@@ -69,6 +70,7 @@ func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*
}
userInfo := &BasicUserInfo{
+ Id: fmt.Sprintf("%d", data.Id),
Name: data.Name,
Login: data.Login,
Email: data.Email,
diff --git a/pkg/social/social.go b/pkg/social/social.go
index b763e2d71b2..8f0618b7f74 100644
--- a/pkg/social/social.go
+++ b/pkg/social/social.go
@@ -14,6 +14,7 @@ import (
)
type BasicUserInfo struct {
+ Id string
Name string
Email string
Login string
diff --git a/pkg/tsdb/cloudwatch/annotation_query.go b/pkg/tsdb/cloudwatch/annotation_query.go
index 287f4e770ef..e0d9158435e 100644
--- a/pkg/tsdb/cloudwatch/annotation_query.go
+++ b/pkg/tsdb/cloudwatch/annotation_query.go
@@ -72,7 +72,7 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo
MetricName: aws.String(metricName),
Dimensions: qd,
Statistic: aws.String(s),
- Period: aws.Int64(int64(period)),
+ Period: aws.Int64(period),
}
resp, err := svc.DescribeAlarmsForMetric(params)
if err != nil {
@@ -88,7 +88,7 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo
MetricName: aws.String(metricName),
Dimensions: qd,
ExtendedStatistic: aws.String(s),
- Period: aws.Int64(int64(period)),
+ Period: aws.Int64(period),
}
resp, err := svc.DescribeAlarmsForMetric(params)
if err != nil {
diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go
index 3879dce4ea6..d98805c661d 100644
--- a/pkg/tsdb/cloudwatch/cloudwatch.go
+++ b/pkg/tsdb/cloudwatch/cloudwatch.go
@@ -71,15 +71,12 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
switch queryType {
case "metricFindQuery":
result, err = e.executeMetricFindQuery(ctx, queryContext)
- break
case "annotationQuery":
result, err = e.executeAnnotationQuery(ctx, queryContext)
- break
case "timeSeriesQuery":
fallthrough
default:
result, err = e.executeTimeSeriesQuery(ctx, queryContext)
- break
}
return result, err
diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go
index c82cff390c3..d73516ca88f 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query.go
@@ -175,25 +175,18 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
switch subType {
case "regions":
data, err = e.handleGetRegions(ctx, parameters, queryContext)
- break
case "namespaces":
data, err = e.handleGetNamespaces(ctx, parameters, queryContext)
- break
case "metrics":
data, err = e.handleGetMetrics(ctx, parameters, queryContext)
- break
case "dimension_keys":
data, err = e.handleGetDimensions(ctx, parameters, queryContext)
- break
case "dimension_values":
data, err = e.handleGetDimensionValues(ctx, parameters, queryContext)
- break
case "ebs_volume_ids":
data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
- break
case "ec2_instance_attribute":
data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
- break
}
transformToTable(data, queryResult)
@@ -261,7 +254,7 @@ func (e *CloudWatchExecutor) handleGetNamespaces(ctx context.Context, parameters
keys = append(keys, strings.Split(customNamespaces, ",")...)
}
- sort.Sort(sort.StringSlice(keys))
+ sort.Strings(keys)
result := make([]suggestData, 0)
for _, key := range keys {
@@ -290,7 +283,7 @@ func (e *CloudWatchExecutor) handleGetMetrics(ctx context.Context, parameters *s
return nil, errors.New("Unable to call AWS API")
}
}
- sort.Sort(sort.StringSlice(namespaceMetrics))
+ sort.Strings(namespaceMetrics)
result := make([]suggestData, 0)
for _, name := range namespaceMetrics {
@@ -319,7 +312,7 @@ func (e *CloudWatchExecutor) handleGetDimensions(ctx context.Context, parameters
return nil, errors.New("Unable to call AWS API")
}
}
- sort.Sort(sort.StringSlice(dimensionValues))
+ sort.Strings(dimensionValues)
result := make([]suggestData, 0)
for _, name := range dimensionValues {
@@ -573,11 +566,7 @@ func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error)
}
return !lastPage
})
- if err != nil {
- return resp, err
- }
-
- return resp, nil
+ return resp, err
}
var metricsCacheLock sync.Mutex
diff --git a/pkg/tsdb/cloudwatch/metric_find_query_test.go b/pkg/tsdb/cloudwatch/metric_find_query_test.go
index bf87e7b7d41..e3903e8027e 100644
--- a/pkg/tsdb/cloudwatch/metric_find_query_test.go
+++ b/pkg/tsdb/cloudwatch/metric_find_query_test.go
@@ -181,10 +181,7 @@ func TestCloudWatchMetrics(t *testing.T) {
}
func TestParseMultiSelectValue(t *testing.T) {
-
- var values []string
-
- values = parseMultiSelectValue(" i-someInstance ")
+ values := parseMultiSelectValue(" i-someInstance ")
assert.Equal(t, []string{"i-someInstance"}, values)
values = parseMultiSelectValue("{i-05}")
diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go
index 9d41cd03255..bb9489cd654 100644
--- a/pkg/tsdb/mssql/macros.go
+++ b/pkg/tsdb/mssql/macros.go
@@ -82,11 +82,11 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s >= DATEADD(s, %d, '1970-01-01') AND %s <= DATEADD(s, %d, '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%s >= DATEADD(s, %d, '1970-01-01') AND %s <= DATEADD(s, %d, '1970-01-01')", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
case "__timeFrom":
- return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", m.TimeRange.GetFromAsSecondsEpoch()), nil
case "__timeTo":
- return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", m.TimeRange.GetToAsSecondsEpoch()), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
@@ -113,11 +113,11 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
case "__unixEpochFrom":
- return fmt.Sprintf("%d", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
case "__unixEpochTo":
- return fmt.Sprintf("%d", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
default:
return "", fmt.Errorf("Unknown macro %v", name)
}
diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go
index 12a9b0d82be..ae0d4f67d2b 100644
--- a/pkg/tsdb/mssql/macros_test.go
+++ b/pkg/tsdb/mssql/macros_test.go
@@ -1,6 +1,8 @@
package mssql
import (
+ "fmt"
+ "strconv"
"testing"
"time"
@@ -13,112 +15,213 @@ import (
func TestMacroEngine(t *testing.T) {
Convey("MacroEngine", t, func() {
engine := &MsSqlMacroEngine{}
- timeRange := &tsdb.TimeRange{From: "5m", To: "now"}
query := &tsdb.Query{
Model: simplejson.New(),
}
- Convey("interpolate __time function", func() {
- sql, err := engine.Interpolate(query, nil, "select $__time(time_column)")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
+ from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
+ to := from.Add(5 * time.Minute)
+ timeRange := tsdb.NewFakeTimeRange("5m", "now", to)
- So(sql, ShouldEqual, "select time_column AS time")
+ Convey("interpolate __time function", func() {
+ sql, err := engine.Interpolate(query, nil, "select $__time(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select time_column AS time")
+ })
+
+ Convey("interpolate __timeEpoch function", func() {
+ sql, err := engine.Interpolate(query, nil, "select $__timeEpoch(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select DATEDIFF(second, '1970-01-01', time_column) AS time")
+ })
+
+ Convey("interpolate __timeEpoch function wrapped in aggregation", func() {
+ sql, err := engine.Interpolate(query, nil, "select min($__timeEpoch(time_column))")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select min(DATEDIFF(second, '1970-01-01', time_column) AS time)")
+ })
+
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= DATEADD(s, %d, '1970-01-01') AND time_column <= DATEADD(s, %d, '1970-01-01')", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __timeGroup function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
+ })
+
+ Convey("interpolate __timeGroup function with spaces around arguments", func() {
+ sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
+ })
+
+ Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
+ _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)")
+
+ fill := query.Model.Get("fill").MustBool()
+ fillNull := query.Model.Get("fillNull").MustBool()
+ fillInterval := query.Model.Get("fillInterval").MustInt()
+
+ So(err, ShouldBeNil)
+ So(fill, ShouldBeTrue)
+ So(fillNull, ShouldBeTrue)
+ So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
+ })
+
+ Convey("interpolate __timeGroup function with fill (value = float)", func() {
+ _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', 1.5)")
+
+ fill := query.Model.Get("fill").MustBool()
+ fillValue := query.Model.Get("fillValue").MustFloat64()
+ fillInterval := query.Model.Get("fillInterval").MustInt()
+
+ So(err, ShouldBeNil)
+ So(fill, ShouldBeTrue)
+ So(fillValue, ShouldEqual, 1.5)
+ So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
+ })
+
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", from.Unix()))
+ })
+
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
+
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
- Convey("interpolate __timeEpoch function", func() {
- sql, err := engine.Interpolate(query, nil, "select $__timeEpoch(time_column)")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
+ from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
+ to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
+ timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
- So(sql, ShouldEqual, "select DATEDIFF(second, '1970-01-01', time_column) AS time")
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= DATEADD(s, %d, '1970-01-01') AND time_column <= DATEADD(s, %d, '1970-01-01')", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", from.Unix()))
+ })
+
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
+
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
- Convey("interpolate __timeEpoch function wrapped in aggregation", func() {
- sql, err := engine.Interpolate(query, nil, "select min($__timeEpoch(time_column))")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 1960-02-01 07:00 and 1980-02-03 08:00", func() {
+ from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
+ to := time.Date(1980, 2, 3, 8, 0, 0, 0, time.UTC)
+ timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
- So(sql, ShouldEqual, "select min(DATEDIFF(second, '1970-01-01', time_column) AS time)")
- })
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
- Convey("interpolate __timeFilter function", func() {
- sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
- So(err, ShouldBeNil)
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= DATEADD(s, %d, '1970-01-01') AND time_column <= DATEADD(s, %d, '1970-01-01')", from.Unix(), to.Unix()))
+ })
- So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038, '1970-01-01')")
- })
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
- Convey("interpolate __timeGroup function", func() {
- sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
- So(err, ShouldBeNil)
+ So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", from.Unix()))
+ })
- So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
- })
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
- Convey("interpolate __timeGroup function with spaces around arguments", func() {
- sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
- So(err, ShouldBeNil)
+ So(sql, ShouldEqual, fmt.Sprintf("select DATEADD(second, %d, '1970-01-01')", to.Unix()))
+ })
- So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
- })
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
+ So(err, ShouldBeNil)
- Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
- _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)")
+ So(sql, ShouldEqual, fmt.Sprintf("select time_column >= %d AND time_column <= %d", from.Unix(), to.Unix()))
+ })
- fill := query.Model.Get("fill").MustBool()
- fillNull := query.Model.Get("fillNull").MustBool()
- fillInterval := query.Model.Get("fillInterval").MustInt()
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
- So(err, ShouldBeNil)
- So(fill, ShouldBeTrue)
- So(fillNull, ShouldBeTrue)
- So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
- })
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
- Convey("interpolate __timeGroup function with fill (value = float)", func() {
- _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', 1.5)")
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
- fill := query.Model.Get("fill").MustBool()
- fillValue := query.Model.Get("fillValue").MustFloat64()
- fillInterval := query.Model.Get("fillInterval").MustInt()
-
- So(err, ShouldBeNil)
- So(fill, ShouldBeTrue)
- So(fillValue, ShouldEqual, 1.5)
- So(fillInterval, ShouldEqual, 5*time.Minute.Seconds())
- })
-
- Convey("interpolate __timeFrom function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738, '1970-01-01')")
- })
-
- Convey("interpolate __timeTo function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038, '1970-01-01')")
- })
-
- Convey("interpolate __unixEpochFilter function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select time_column >= 18446744066914186738 AND time_column <= 18446744066914187038")
- })
-
- Convey("interpolate __unixEpochFrom function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select 18446744066914186738")
- })
-
- Convey("interpolate __unixEpochTo function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select 18446744066914187038")
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
})
}
diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go
index 2638fd8bb40..a598b7239ed 100644
--- a/pkg/tsdb/mssql/mssql.go
+++ b/pkg/tsdb/mssql/mssql.go
@@ -8,8 +8,6 @@ import (
"strconv"
"strings"
- "time"
-
"math"
_ "github.com/denisenkom/go-mssqldb"
@@ -147,7 +145,7 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
// convert types not handled by denisenkom/go-mssqldb
// unhandled types are returned as []byte
for i := 0; i < len(types); i++ {
- if value, ok := values[i].([]byte); ok == true {
+ if value, ok := values[i].([]byte); ok {
switch types[i].DatabaseTypeName() {
case "MONEY", "SMALLMONEY", "DECIMAL":
if v, err := strconv.ParseFloat(string(value), 64); err == nil {
@@ -211,7 +209,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
fillValue := null.Float{}
if fillMissing {
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
- if query.Model.Get("fillNull").MustBool(false) == false {
+ if !query.Model.Get("fillNull").MustBool(false) {
fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
fillValue.Valid = true
}
@@ -231,19 +229,22 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
return err
}
+ // converts column named time to unix timestamp in milliseconds to make
+ // native mysql datetime types and epoch dates work in
+ // annotation and table queries.
+ tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
+
switch columnValue := values[timeIndex].(type) {
case int64:
- timestamp = float64(columnValue * 1000)
+ timestamp = float64(columnValue)
case float64:
- timestamp = columnValue * 1000
- case time.Time:
- timestamp = (float64(columnValue.Unix()) * 1000) + float64(columnValue.Nanosecond()/1e6) // in case someone is trying to map times beyond 2262 :D
+ timestamp = columnValue
default:
- return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp")
+ return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
}
if metricIndex >= 0 {
- if columnValue, ok := values[metricIndex].(string); ok == true {
+ if columnValue, ok := values[metricIndex].(string); ok {
metric = columnValue
} else {
return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex])
@@ -270,7 +271,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
}
series, exist := pointsBySeries[metric]
- if exist == false {
+ if !exist {
series = &tsdb.TimeSeries{Name: metric}
pointsBySeries[metric] = series
seriesByQueryOrder.PushBack(metric)
@@ -278,7 +279,7 @@ func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
if fillMissing {
var intervalStart float64
- if exist == false {
+ if !exist {
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
} else {
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go
index 4bd1e3a8ad7..599f4869f6a 100644
--- a/pkg/tsdb/mssql/mssql_test.go
+++ b/pkg/tsdb/mssql/mssql_test.go
@@ -16,10 +16,10 @@ import (
)
// To run this test, remove the Skip from SkipConvey
-// and set up a MSSQL db named grafanatest and a user/password grafana/Password!
+// The tests require a MSSQL db named grafanatest and a user/password grafana/Password!
// Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a
// preconfigured MSSQL server suitable for running these tests.
-// Thers's also a dashboard.json in same directory that you can import to Grafana
+// There is also a dashboard.json in same directory that you can import to Grafana
// once you've created a datasource for the test server/database.
// If needed, change the variable below to the IP address of the database.
var serverIP string = "localhost"
@@ -188,10 +188,8 @@ func TestMSSQL(t *testing.T) {
})
}
- for _, s := range series {
- _, err = sess.Insert(s)
- So(err, ShouldBeNil)
- }
+ _, err = sess.InsertMulti(series)
+ So(err, ShouldBeNil)
Convey("When doing a metric query using timeGroup", func() {
query := &tsdb.TsdbQuery{
@@ -312,10 +310,18 @@ func TestMSSQL(t *testing.T) {
Convey("Given a table with metrics having multiple values and measurements", func() {
type metric_values struct {
- Time time.Time
- Measurement string
- ValueOne int64 `xorm:"integer 'valueOne'"`
- ValueTwo int64 `xorm:"integer 'valueTwo'"`
+ Time time.Time
+ TimeInt64 int64 `xorm:"bigint 'timeInt64' not null"`
+ TimeInt64Nullable *int64 `xorm:"bigint 'timeInt64Nullable' null"`
+ TimeFloat64 float64 `xorm:"float 'timeFloat64' not null"`
+ TimeFloat64Nullable *float64 `xorm:"float 'timeFloat64Nullable' null"`
+ TimeInt32 int32 `xorm:"int(11) 'timeInt32' not null"`
+ TimeInt32Nullable *int32 `xorm:"int(11) 'timeInt32Nullable' null"`
+ TimeFloat32 float32 `xorm:"float(11) 'timeFloat32' not null"`
+ TimeFloat32Nullable *float32 `xorm:"float(11) 'timeFloat32Nullable' null"`
+ Measurement string
+ ValueOne int64 `xorm:"integer 'valueOne'"`
+ ValueTwo int64 `xorm:"integer 'valueTwo'"`
}
if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
@@ -330,26 +336,219 @@ func TestMSSQL(t *testing.T) {
return rand.Int63n(max-min) + min
}
+ var tInitial time.Time
+
series := []*metric_values{}
- for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
- series = append(series, &metric_values{
- Time: t,
- Measurement: "Metric A",
- ValueOne: rnd(0, 100),
- ValueTwo: rnd(0, 100),
- })
- series = append(series, &metric_values{
- Time: t,
- Measurement: "Metric B",
- ValueOne: rnd(0, 100),
- ValueTwo: rnd(0, 100),
- })
+ for i, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
+ if i == 0 {
+ tInitial = t
+ }
+ tSeconds := t.Unix()
+ tSecondsInt32 := int32(tSeconds)
+ tSecondsFloat32 := float32(tSeconds)
+ tMilliseconds := tSeconds * 1e3
+ tMillisecondsFloat := float64(tMilliseconds)
+ first := metric_values{
+ Time: t,
+ TimeInt64: tMilliseconds,
+ TimeInt64Nullable: &(tMilliseconds),
+ TimeFloat64: tMillisecondsFloat,
+ TimeFloat64Nullable: &tMillisecondsFloat,
+ TimeInt32: tSecondsInt32,
+ TimeInt32Nullable: &tSecondsInt32,
+ TimeFloat32: tSecondsFloat32,
+ TimeFloat32Nullable: &tSecondsFloat32,
+ Measurement: "Metric A",
+ ValueOne: rnd(0, 100),
+ ValueTwo: rnd(0, 100),
+ }
+ second := first
+ second.Measurement = "Metric B"
+ second.ValueOne = rnd(0, 100)
+ second.ValueTwo = rnd(0, 100)
+
+ series = append(series, &first)
+ series = append(series, &second)
}
- for _, s := range series {
- _, err = sess.Insert(s)
+ _, err = sess.InsertMulti(series)
+ So(err, ShouldBeNil)
+
+ Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT TOP 1 timeInt64 as time, valueOne FROM metric_values ORDER BY time`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
So(err, ShouldBeNil)
- }
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT TOP 1 timeInt64Nullable as time, valueOne FROM metric_values ORDER BY time`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT TOP 1 timeFloat64 as time, valueOne FROM metric_values ORDER BY time`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT TOP 1 timeFloat64Nullable as time, valueOne FROM metric_values ORDER BY time`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT TOP 1 timeInt32 as time, valueOne FROM metric_values ORDER BY time`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT TOP 1 timeInt32Nullable as time, valueOne FROM metric_values ORDER BY time`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT TOP 1 timeFloat32 as time, valueOne FROM metric_values ORDER BY time`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
+ })
+
+ Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT TOP 1 timeFloat32Nullable as time, valueOne FROM metric_values ORDER BY time`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
+ })
Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
query := &tsdb.TsdbQuery{
@@ -476,7 +675,6 @@ func TestMSSQL(t *testing.T) {
resp, err := endpoint.Query(nil, nil, query)
queryResult := resp.Results["A"]
So(err, ShouldBeNil)
- fmt.Println("query", "sql", queryResult.Meta)
So(queryResult.Error, ShouldBeNil)
So(len(queryResult.Series), ShouldEqual, 4)
@@ -696,7 +894,7 @@ func TestMSSQL(t *testing.T) {
columns := queryResult.Tables[0].Rows[0]
//Should be in milliseconds
- So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
+ So(columns[0].(float64), ShouldEqual, float64(dt.UnixNano()/1e6))
})
Convey("When doing an annotation query with a time column in epoch second format should return ms", func() {
@@ -850,15 +1048,15 @@ func TestMSSQL(t *testing.T) {
func InitMSSQLTestDB(t *testing.T) *xorm.Engine {
x, err := xorm.NewEngine(sqlutil.TestDB_Mssql.DriverName, strings.Replace(sqlutil.TestDB_Mssql.ConnStr, "localhost", serverIP, 1))
+ if err != nil {
+ t.Fatalf("Failed to init mssql db %v", err)
+ }
+
x.DatabaseTZ = time.UTC
x.TZLocation = time.UTC
// x.ShowSQL()
- if err != nil {
- t.Fatalf("Failed to init mssql db %v", err)
- }
-
return x
}
diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go
index a292f209429..fadcbe4edbc 100644
--- a/pkg/tsdb/mysql/macros.go
+++ b/pkg/tsdb/mysql/macros.go
@@ -77,11 +77,11 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s >= FROM_UNIXTIME(%d) AND %s <= FROM_UNIXTIME(%d)", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%s >= FROM_UNIXTIME(%d) AND %s <= FROM_UNIXTIME(%d)", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
case "__timeFrom":
- return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("FROM_UNIXTIME(%d)", m.TimeRange.GetFromAsSecondsEpoch()), nil
case "__timeTo":
- return fmt.Sprintf("FROM_UNIXTIME(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("FROM_UNIXTIME(%d)", m.TimeRange.GetToAsSecondsEpoch()), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
@@ -108,11 +108,11 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
case "__unixEpochFrom":
- return fmt.Sprintf("%d", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
case "__unixEpochTo":
- return fmt.Sprintf("%d", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
default:
return "", fmt.Errorf("Unknown macro %v", name)
}
diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go
index a89ba16ab78..66ec143eac8 100644
--- a/pkg/tsdb/mysql/macros_test.go
+++ b/pkg/tsdb/mysql/macros_test.go
@@ -1,7 +1,10 @@
package mysql
import (
+ "fmt"
+ "strconv"
"testing"
+ "time"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
@@ -11,79 +14,179 @@ func TestMacroEngine(t *testing.T) {
Convey("MacroEngine", t, func() {
engine := &MySqlMacroEngine{}
query := &tsdb.Query{}
- timeRange := &tsdb.TimeRange{From: "5m", To: "now"}
- Convey("interpolate __time function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__time(time_column)")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
+ from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
+ to := from.Add(5 * time.Minute)
+ timeRange := tsdb.NewFakeTimeRange("5m", "now", to)
- So(sql, ShouldEqual, "select UNIX_TIMESTAMP(time_column) as time_sec")
+ Convey("interpolate __time function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__time(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select UNIX_TIMESTAMP(time_column) as time_sec")
+ })
+
+ Convey("interpolate __time function wrapped in aggregation", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select min($__time(time_column))")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select min(UNIX_TIMESTAMP(time_column) as time_sec)")
+ })
+
+ Convey("interpolate __timeGroup function", func() {
+
+ sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
+ })
+
+ Convey("interpolate __timeGroup function with spaces around arguments", func() {
+
+ sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
+ })
+
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= FROM_UNIXTIME(%d) AND time_column <= FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()))
+ })
+
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
+
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
- Convey("interpolate __time function wrapped in aggregation", func() {
- sql, err := engine.Interpolate(query, timeRange, "select min($__time(time_column))")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
+ from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
+ to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
+ timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
- So(sql, ShouldEqual, "select min(UNIX_TIMESTAMP(time_column) as time_sec)")
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= FROM_UNIXTIME(%d) AND time_column <= FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()))
+ })
+
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
+
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
- Convey("interpolate __timeFilter function", func() {
- sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 1960-02-01 07:00 and 1980-02-03 08:00", func() {
+ from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
+ to := time.Date(1980, 2, 3, 8, 0, 0, 0, time.UTC)
+ timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
- So(sql, ShouldEqual, "WHERE time_column >= FROM_UNIXTIME(18446744066914186738) AND time_column <= FROM_UNIXTIME(18446744066914187038)")
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column >= FROM_UNIXTIME(%d) AND time_column <= FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()))
+ })
+
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
+
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
-
- Convey("interpolate __timeFrom function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select FROM_UNIXTIME(18446744066914186738)")
- })
-
- Convey("interpolate __timeGroup function", func() {
-
- sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
- })
-
- Convey("interpolate __timeGroup function with spaces around arguments", func() {
-
- sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
- })
-
- Convey("interpolate __timeTo function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select FROM_UNIXTIME(18446744066914187038)")
- })
-
- Convey("interpolate __unixEpochFilter function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(18446744066914186738)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select 18446744066914186738 >= 18446744066914186738 AND 18446744066914186738 <= 18446744066914187038")
- })
-
- Convey("interpolate __unixEpochFrom function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select 18446744066914186738")
- })
-
- Convey("interpolate __unixEpochTo function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select 18446744066914187038")
- })
-
})
}
diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go
index 483974c55a4..4f5cd1b0784 100644
--- a/pkg/tsdb/mysql/mysql.go
+++ b/pkg/tsdb/mysql/mysql.go
@@ -8,7 +8,6 @@ import (
"math"
"reflect"
"strconv"
- "time"
"github.com/go-sql-driver/mysql"
"github.com/go-xorm/core"
@@ -219,7 +218,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
fillValue := null.Float{}
if fillMissing {
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
- if query.Model.Get("fillNull").MustBool(false) == false {
+ if !query.Model.Get("fillNull").MustBool(false) {
fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
fillValue.Valid = true
}
@@ -239,19 +238,22 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
return err
}
+ // converts column named time to unix timestamp in milliseconds to make
+ // native mysql datetime types and epoch dates work in
+ // annotation and table queries.
+ tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
+
switch columnValue := values[timeIndex].(type) {
case int64:
- timestamp = float64(columnValue * 1000)
+ timestamp = float64(columnValue)
case float64:
- timestamp = columnValue * 1000
- case time.Time:
- timestamp = float64(columnValue.UnixNano() / 1e6)
+ timestamp = columnValue
default:
- return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
+ return fmt.Errorf("Invalid type for column time/time_sec, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
}
if metricIndex >= 0 {
- if columnValue, ok := values[metricIndex].(string); ok == true {
+ if columnValue, ok := values[metricIndex].(string); ok {
metric = columnValue
} else {
return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
@@ -278,7 +280,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
}
series, exist := pointsBySeries[metric]
- if exist == false {
+ if !exist {
series = &tsdb.TimeSeries{Name: metric}
pointsBySeries[metric] = series
seriesByQueryOrder.PushBack(metric)
@@ -286,7 +288,7 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
if fillMissing {
var intervalStart float64
- if exist == false {
+ if !exist {
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
} else {
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go
index 750704c9965..74cedea803a 100644
--- a/pkg/tsdb/mysql/mysql_test.go
+++ b/pkg/tsdb/mysql/mysql_test.go
@@ -3,25 +3,36 @@ package mysql
import (
"fmt"
"math/rand"
+ "strings"
"testing"
"time"
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
)
-// To run this test, remove the Skip from SkipConvey
-// and set up a MySQL db named grafana_tests and a user/password grafana/password
+// To run this test, set runMySqlTests=true
+// Or from the commandline: GRAFANA_TEST_DB=mysql go test -v ./pkg/tsdb/mysql
+// The tests require a MySQL db named grafana_ds_tests and a user/password grafana/password
// Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a
// preconfigured MySQL server suitable for running these tests.
-// Thers's also a dashboard.json in same directory that you can import to Grafana
+// There is also a dashboard.json in same directory that you can import to Grafana
// once you've created a datasource for the test server/database.
func TestMySQL(t *testing.T) {
- SkipConvey("MySQL", t, func() {
+ // change to true to run the MySQL tests
+ runMySqlTests := false
+ // runMySqlTests := true
+
+ if !(sqlstore.IsTestDbMySql() || runMySqlTests) {
+ t.Skip()
+ }
+
+ Convey("MySQL", t, func() {
x := InitMySQLTestDB(t)
endpoint := &MysqlQueryEndpoint{
@@ -35,7 +46,7 @@ func TestMySQL(t *testing.T) {
sess := x.NewSession()
defer sess.Close()
- fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.Local)
+ fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC)
Convey("Given a table with different native data types", func() {
if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists {
@@ -121,9 +132,8 @@ func TestMySQL(t *testing.T) {
So(column[7].(float64), ShouldEqual, 1.11)
So(column[8].(float64), ShouldEqual, 2.22)
So(*column[9].(*float32), ShouldEqual, 3.33)
- _, offset := time.Now().Zone()
- So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
- So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
+ So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now())
+ So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now())
So(column[12].(string), ShouldEqual, "11:11:11")
So(column[13].(int64), ShouldEqual, 2018)
So(*column[14].(*[]byte), ShouldHaveSameTypeAs, []byte{1})
@@ -137,8 +147,8 @@ func TestMySQL(t *testing.T) {
So(column[22].(string), ShouldEqual, "longblob")
So(column[23].(string), ShouldEqual, "val2")
So(column[24].(string), ShouldEqual, "a,b")
- So(column[25].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z"))
- So(column[26].(float64), ShouldEqual, float64(1514764861000))
+ So(column[25].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().UTC().Format("2006-01-02T00:00:00Z"))
+ So(column[26].(float64), ShouldEqual, float64(1.514764861123456*1e12))
So(column[27], ShouldEqual, nil)
So(column[28], ShouldEqual, nil)
So(column[29], ShouldEqual, "")
@@ -177,10 +187,8 @@ func TestMySQL(t *testing.T) {
})
}
- for _, s := range series {
- _, err = sess.Insert(s)
- So(err, ShouldBeNil)
- }
+ _, err = sess.InsertMulti(series)
+ So(err, ShouldBeNil)
Convey("When doing a metric query using timeGroup", func() {
query := &tsdb.TsdbQuery{
@@ -301,10 +309,19 @@ func TestMySQL(t *testing.T) {
Convey("Given a table with metrics having multiple values and measurements", func() {
type metric_values struct {
- Time time.Time
- Measurement string
- ValueOne int64 `xorm:"integer 'valueOne'"`
- ValueTwo int64 `xorm:"integer 'valueTwo'"`
+ Time time.Time `xorm:"datetime 'time' not null"`
+ TimeNullable *time.Time `xorm:"datetime(6) 'timeNullable' null"`
+ TimeInt64 int64 `xorm:"bigint(20) 'timeInt64' not null"`
+ TimeInt64Nullable *int64 `xorm:"bigint(20) 'timeInt64Nullable' null"`
+ TimeFloat64 float64 `xorm:"double 'timeFloat64' not null"`
+ TimeFloat64Nullable *float64 `xorm:"double 'timeFloat64Nullable' null"`
+ TimeInt32 int32 `xorm:"int(11) 'timeInt32' not null"`
+ TimeInt32Nullable *int32 `xorm:"int(11) 'timeInt32Nullable' null"`
+ TimeFloat32 float32 `xorm:"double 'timeFloat32' not null"`
+ TimeFloat32Nullable *float32 `xorm:"double 'timeFloat32Nullable' null"`
+ Measurement string
+ ValueOne int64 `xorm:"integer 'valueOne'"`
+ ValueTwo int64 `xorm:"integer 'valueTwo'"`
}
if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
@@ -319,26 +336,265 @@ func TestMySQL(t *testing.T) {
return rand.Int63n(max-min) + min
}
+ var tInitial time.Time
+
series := []*metric_values{}
- for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
- series = append(series, &metric_values{
- Time: t,
- Measurement: "Metric A",
- ValueOne: rnd(0, 100),
- ValueTwo: rnd(0, 100),
- })
- series = append(series, &metric_values{
- Time: t,
- Measurement: "Metric B",
- ValueOne: rnd(0, 100),
- ValueTwo: rnd(0, 100),
- })
+ for i, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
+ if i == 0 {
+ tInitial = t
+ }
+ tSeconds := t.Unix()
+ tSecondsInt32 := int32(tSeconds)
+ tSecondsFloat32 := float32(tSeconds)
+ tMilliseconds := tSeconds * 1e3
+ tMillisecondsFloat := float64(tMilliseconds)
+ t2 := t
+ first := metric_values{
+ Time: t,
+ TimeNullable: &t2,
+ TimeInt64: tMilliseconds,
+ TimeInt64Nullable: &(tMilliseconds),
+ TimeFloat64: tMillisecondsFloat,
+ TimeFloat64Nullable: &tMillisecondsFloat,
+ TimeInt32: tSecondsInt32,
+ TimeInt32Nullable: &tSecondsInt32,
+ TimeFloat32: tSecondsFloat32,
+ TimeFloat32Nullable: &tSecondsFloat32,
+ Measurement: "Metric A",
+ ValueOne: rnd(0, 100),
+ ValueTwo: rnd(0, 100),
+ }
+ second := first
+ second.Measurement = "Metric B"
+ second.ValueOne = rnd(0, 100)
+ second.ValueTwo = rnd(0, 100)
+
+ series = append(series, &first)
+ series = append(series, &second)
}
- for _, s := range series {
- _, err := sess.Insert(s)
+ _, err = sess.InsertMulti(series)
+ So(err, ShouldBeNil)
+
+ Convey("When doing a metric query using time as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
So(err, ShouldBeNil)
- }
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using time (nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeNullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeInt64 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeInt64Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeFloat64 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeFloat64Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeInt32 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeInt32Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeFloat32 as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
+ })
+
+ Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT timeFloat32Nullable as time, valueOne FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
+ })
Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
query := &tsdb.TsdbQuery{
@@ -647,16 +903,16 @@ func TestMySQL(t *testing.T) {
}
func InitMySQLTestDB(t *testing.T) *xorm.Engine {
- x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr+"&parseTime=true")
- x.DatabaseTZ = time.Local
- x.TZLocation = time.Local
-
- // x.ShowSQL()
-
+ x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, strings.Replace(sqlutil.TestDB_Mysql.ConnStr, "/grafana_tests", "/grafana_ds_tests", 1))
if err != nil {
t.Fatalf("Failed to init mysql db %v", err)
}
+ x.DatabaseTZ = time.UTC
+ x.TZLocation = time.UTC
+
+ // x.ShowSQL()
+
return x
}
diff --git a/pkg/tsdb/opentsdb/opentsdb_test.go b/pkg/tsdb/opentsdb/opentsdb_test.go
index 094deb9e8ec..fe03599f54d 100644
--- a/pkg/tsdb/opentsdb/opentsdb_test.go
+++ b/pkg/tsdb/opentsdb/opentsdb_test.go
@@ -35,7 +35,7 @@ func TestOpenTsdbExecutor(t *testing.T) {
})
- Convey("Build metric with downsampling diabled", func() {
+ Convey("Build metric with downsampling disabled", func() {
query := &tsdb.Query{
Model: simplejson.New(),
diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go
index 23daeebec5a..05e39f2c762 100644
--- a/pkg/tsdb/postgres/macros.go
+++ b/pkg/tsdb/postgres/macros.go
@@ -79,15 +79,15 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
}
return fmt.Sprintf("extract(epoch from %s) as \"time\"", args[0]), nil
case "__timeFilter":
- // dont use to_timestamp in this macro for redshift compatibility #9566
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("extract(epoch from %s) BETWEEN %d AND %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+
+ return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
- return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
- return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)
@@ -114,11 +114,11 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
- return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil
case "__unixEpochFrom":
- return fmt.Sprintf("%d", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil
case "__unixEpochTo":
- return fmt.Sprintf("%d", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+ return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil
default:
return "", fmt.Errorf("Unknown macro %v", name)
}
diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go
index b18acced963..c3c15691e42 100644
--- a/pkg/tsdb/postgres/macros_test.go
+++ b/pkg/tsdb/postgres/macros_test.go
@@ -1,7 +1,10 @@
package postgres
import (
+ "fmt"
+ "strconv"
"testing"
+ "time"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
@@ -9,81 +12,181 @@ import (
func TestMacroEngine(t *testing.T) {
Convey("MacroEngine", t, func() {
- engine := &PostgresMacroEngine{}
+ engine := NewPostgresMacroEngine()
query := &tsdb.Query{}
- timeRange := &tsdb.TimeRange{From: "5m", To: "now"}
- Convey("interpolate __time function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__time(time_column)")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
+ from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC)
+ to := from.Add(5 * time.Minute)
+ timeRange := tsdb.NewFakeTimeRange("5m", "now", to)
- So(sql, ShouldEqual, "select time_column AS \"time\"")
+ Convey("interpolate __time function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__time(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select time_column AS \"time\"")
+ })
+
+ Convey("interpolate __time function wrapped in aggregation", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select min($__time(time_column))")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "select min(time_column AS \"time\")")
+ })
+
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __timeGroup function", func() {
+
+ sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time")
+ })
+
+ Convey("interpolate __timeGroup function with spaces between args", func() {
+
+ sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time")
+ })
+
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
+
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
- Convey("interpolate __time function wrapped in aggregation", func() {
- sql, err := engine.Interpolate(query, timeRange, "select min($__time(time_column))")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() {
+ from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
+ to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
+ timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
- So(sql, ShouldEqual, "select min(time_column AS \"time\")")
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
+
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
- Convey("interpolate __timeFilter function", func() {
- sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
- So(err, ShouldBeNil)
+ Convey("Given a time range between 1960-02-01 07:00 and 1980-02-03 08:00", func() {
+ from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
+ to := time.Date(1980, 2, 3, 8, 0, 0, 0, time.UTC)
+ timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
- So(sql, ShouldEqual, "WHERE extract(epoch from time_column) BETWEEN 18446744066914186738 AND 18446744066914187038")
+ Convey("interpolate __timeFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __timeFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __timeTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
+ })
+
+ Convey("interpolate __unixEpochFilter function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix()))
+ })
+
+ Convey("interpolate __unixEpochFrom function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", from.Unix()))
+ })
+
+ Convey("interpolate __unixEpochTo function", func() {
+ sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
+ So(err, ShouldBeNil)
+
+ So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix()))
+ })
})
-
- Convey("interpolate __timeFrom function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select to_timestamp(18446744066914186738)")
- })
-
- Convey("interpolate __timeGroup function", func() {
-
- sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time")
- })
-
- Convey("interpolate __timeGroup function with spaces between args", func() {
-
- sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time")
- })
-
- Convey("interpolate __timeTo function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select to_timestamp(18446744066914187038)")
- })
-
- Convey("interpolate __unixEpochFilter function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(18446744066914186738)")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select 18446744066914186738 >= 18446744066914186738 AND 18446744066914186738 <= 18446744066914187038")
- })
-
- Convey("interpolate __unixEpochFrom function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFrom()")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select 18446744066914186738")
- })
-
- Convey("interpolate __unixEpochTo function", func() {
- sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochTo()")
- So(err, ShouldBeNil)
-
- So(sql, ShouldEqual, "select 18446744066914187038")
- })
-
})
}
diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go
index 5f6b56ebcf1..72d50b32d04 100644
--- a/pkg/tsdb/postgres/postgres.go
+++ b/pkg/tsdb/postgres/postgres.go
@@ -7,7 +7,6 @@ import (
"math"
"net/url"
"strconv"
- "time"
"github.com/go-xorm/core"
"github.com/grafana/grafana/pkg/components/null"
@@ -132,7 +131,7 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues,
// convert types not handled by lib/pq
// unhandled types are returned as []byte
for i := 0; i < len(types); i++ {
- if value, ok := values[i].([]byte); ok == true {
+ if value, ok := values[i].([]byte); ok {
switch types[i].DatabaseTypeName() {
case "NUMERIC":
if v, err := strconv.ParseFloat(string(value), 64); err == nil {
@@ -199,7 +198,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
fillValue := null.Float{}
if fillMissing {
fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000
- if query.Model.Get("fillNull").MustBool(false) == false {
+ if !query.Model.Get("fillNull").MustBool(false) {
fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
fillValue.Valid = true
}
@@ -219,19 +218,22 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
return err
}
+ // converts column named time to unix timestamp in milliseconds to make
+ // native mysql datetime types and epoch dates work in
+ // annotation and table queries.
+ tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
+
switch columnValue := values[timeIndex].(type) {
case int64:
- timestamp = float64(columnValue * 1000)
+ timestamp = float64(columnValue)
case float64:
- timestamp = columnValue * 1000
- case time.Time:
- timestamp = float64(columnValue.UnixNano() / 1e6)
+ timestamp = columnValue
default:
return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
}
if metricIndex >= 0 {
- if columnValue, ok := values[metricIndex].(string); ok == true {
+ if columnValue, ok := values[metricIndex].(string); ok {
metric = columnValue
} else {
return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
@@ -258,7 +260,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
}
series, exist := pointsBySeries[metric]
- if exist == false {
+ if !exist {
series = &tsdb.TimeSeries{Name: metric}
pointsBySeries[metric] = series
seriesByQueryOrder.PushBack(metric)
@@ -266,7 +268,7 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
if fillMissing {
var intervalStart float64
- if exist == false {
+ if !exist {
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
} else {
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go
index 3f2203ac7a4..d18251bac7d 100644
--- a/pkg/tsdb/postgres/postgres_test.go
+++ b/pkg/tsdb/postgres/postgres_test.go
@@ -3,26 +3,37 @@ package postgres
import (
"fmt"
"math/rand"
+ "strings"
"testing"
"time"
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/tsdb"
_ "github.com/lib/pq"
. "github.com/smartystreets/goconvey/convey"
)
-// To run this test, remove the Skip from SkipConvey
-// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest!
+// To run this test, set runPostgresTests=true
+// Or from the commandline: GRAFANA_TEST_DB=postgres go test -v ./pkg/tsdb/postgres
+// The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest!
// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a
// preconfigured Postgres server suitable for running these tests.
-// Thers's also a dashboard.json in same directory that you can import to Grafana
+// There is also a dashboard.json in same directory that you can import to Grafana
// once you've created a datasource for the test server/database.
func TestPostgres(t *testing.T) {
- SkipConvey("PostgreSQL", t, func() {
+ // change to true to run the MySQL tests
+ runPostgresTests := false
+ // runPostgresTests := true
+
+ if !(sqlstore.IsTestDbPostgres() || runPostgresTests) {
+ t.Skip()
+ }
+
+ Convey("PostgreSQL", t, func() {
x := InitPostgresTestDB(t)
endpoint := &PostgresQueryEndpoint{
@@ -156,10 +167,8 @@ func TestPostgres(t *testing.T) {
})
}
- for _, s := range series {
- _, err = sess.Insert(s)
- So(err, ShouldBeNil)
- }
+ _, err = sess.InsertMulti(series)
+ So(err, ShouldBeNil)
Convey("When doing a metric query using timeGroup", func() {
query := &tsdb.TsdbQuery{
@@ -280,10 +289,18 @@ func TestPostgres(t *testing.T) {
Convey("Given a table with metrics having multiple values and measurements", func() {
type metric_values struct {
- Time time.Time
- Measurement string
- ValueOne int64 `xorm:"integer 'valueOne'"`
- ValueTwo int64 `xorm:"integer 'valueTwo'"`
+ Time time.Time
+ TimeInt64 int64 `xorm:"bigint 'timeInt64' not null"`
+ TimeInt64Nullable *int64 `xorm:"bigint 'timeInt64Nullable' null"`
+ TimeFloat64 float64 `xorm:"double 'timeFloat64' not null"`
+ TimeFloat64Nullable *float64 `xorm:"double 'timeFloat64Nullable' null"`
+ TimeInt32 int32 `xorm:"int(11) 'timeInt32' not null"`
+ TimeInt32Nullable *int32 `xorm:"int(11) 'timeInt32Nullable' null"`
+ TimeFloat32 float32 `xorm:"double 'timeFloat32' not null"`
+ TimeFloat32Nullable *float32 `xorm:"double 'timeFloat32Nullable' null"`
+ Measurement string
+ ValueOne int64 `xorm:"integer 'valueOne'"`
+ ValueTwo int64 `xorm:"integer 'valueTwo'"`
}
if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
@@ -298,26 +315,219 @@ func TestPostgres(t *testing.T) {
return rand.Int63n(max-min) + min
}
+ var tInitial time.Time
+
series := []*metric_values{}
- for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
- series = append(series, &metric_values{
- Time: t,
- Measurement: "Metric A",
- ValueOne: rnd(0, 100),
- ValueTwo: rnd(0, 100),
- })
- series = append(series, &metric_values{
- Time: t,
- Measurement: "Metric B",
- ValueOne: rnd(0, 100),
- ValueTwo: rnd(0, 100),
- })
+ for i, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
+ if i == 0 {
+ tInitial = t
+ }
+ tSeconds := t.Unix()
+ tSecondsInt32 := int32(tSeconds)
+ tSecondsFloat32 := float32(tSeconds)
+ tMilliseconds := tSeconds * 1e3
+ tMillisecondsFloat := float64(tMilliseconds)
+ first := metric_values{
+ Time: t,
+ TimeInt64: tMilliseconds,
+ TimeInt64Nullable: &(tMilliseconds),
+ TimeFloat64: tMillisecondsFloat,
+ TimeFloat64Nullable: &tMillisecondsFloat,
+ TimeInt32: tSecondsInt32,
+ TimeInt32Nullable: &tSecondsInt32,
+ TimeFloat32: tSecondsFloat32,
+ TimeFloat32Nullable: &tSecondsFloat32,
+ Measurement: "Metric A",
+ ValueOne: rnd(0, 100),
+ ValueTwo: rnd(0, 100),
+ }
+ second := first
+ second.Measurement = "Metric B"
+ second.ValueOne = rnd(0, 100)
+ second.ValueTwo = rnd(0, 100)
+
+ series = append(series, &first)
+ series = append(series, &second)
}
- for _, s := range series {
- _, err := sess.Insert(s)
+ _, err = sess.InsertMulti(series)
+ So(err, ShouldBeNil)
+
+ Convey("When doing a metric query using epoch (int64) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT "timeInt64" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
So(err, ShouldBeNil)
- }
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int64 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT "timeInt64Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float64) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT "timeFloat64" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float64 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT "timeFloat64Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int32) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT "timeInt32" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (int32 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT "timeInt32Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(tInitial.UnixNano()/1e6))
+ })
+
+ Convey("When doing a metric query using epoch (float32) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT "timeFloat32" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
+ })
+
+ Convey("When doing a metric query using epoch (float32 nullable) as time column should return metric with time in milliseconds", func() {
+ query := &tsdb.TsdbQuery{
+ Queries: []*tsdb.Query{
+ {
+ Model: simplejson.NewFromAny(map[string]interface{}{
+ "rawSql": `SELECT "timeFloat32Nullable" as time, "valueOne" FROM metric_values ORDER BY time LIMIT 1`,
+ "format": "time_series",
+ }),
+ RefId: "A",
+ },
+ },
+ }
+
+ resp, err := endpoint.Query(nil, nil, query)
+ So(err, ShouldBeNil)
+ queryResult := resp.Results["A"]
+ So(queryResult.Error, ShouldBeNil)
+
+ So(len(queryResult.Series), ShouldEqual, 1)
+ So(queryResult.Series[0].Points[0][1].Float64, ShouldEqual, float64(float64(float32(tInitial.Unix())))*1e3)
+ })
Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
query := &tsdb.TsdbQuery{
@@ -473,7 +683,7 @@ func TestPostgres(t *testing.T) {
columns := queryResult.Tables[0].Rows[0]
//Should be in milliseconds
- So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
+ So(columns[0].(float64), ShouldEqual, float64(dt.UnixNano()/1e6))
})
Convey("When doing an annotation query with a time column in epoch second format should return ms", func() {
@@ -626,16 +836,16 @@ func TestPostgres(t *testing.T) {
}
func InitPostgresTestDB(t *testing.T) *xorm.Engine {
- x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
+ x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, strings.Replace(sqlutil.TestDB_Postgres.ConnStr, "dbname=grafanatest", "dbname=grafanadstest", 1))
+ if err != nil {
+ t.Fatalf("Failed to init postgres db %v", err)
+ }
+
x.DatabaseTZ = time.UTC
x.TZLocation = time.UTC
// x.ShowSQL()
- if err != nil {
- t.Fatalf("Failed to init postgres db %v", err)
- }
-
return x
}
diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go
index 1186fccbbf9..bf9fe9f152c 100644
--- a/pkg/tsdb/prometheus/prometheus.go
+++ b/pkg/tsdb/prometheus/prometheus.go
@@ -108,8 +108,8 @@ func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus")
span.SetTag("expr", query.Expr)
- span.SetTag("start_unixnano", int64(query.Start.UnixNano()))
- span.SetTag("stop_unixnano", int64(query.End.UnixNano()))
+ span.SetTag("start_unixnano", query.Start.UnixNano())
+ span.SetTag("stop_unixnano", query.End.UnixNano())
defer span.Finish()
value, err := client.QueryRange(ctx, query.Expr, timeRange)
diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go
index 16370a4ea7f..56ed2cd3cb6 100644
--- a/pkg/tsdb/sql_engine.go
+++ b/pkg/tsdb/sql_engine.go
@@ -51,7 +51,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
defer engineCache.Unlock()
if engine, present := engineCache.cache[dsInfo.Id]; present {
- if version, _ := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
+ if version := engineCache.versions[dsInfo.Id]; version == dsInfo.Version {
e.XormEngine = engine
return nil
}
@@ -135,16 +135,16 @@ func (e *DefaultSqlEngine) Query(
return result, nil
}
-// ConvertTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds
+// ConvertSqlTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds
// to make native datetime types and epoch dates work in annotation and table queries.
func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) {
if timeIndex >= 0 {
switch value := values[timeIndex].(type) {
case time.Time:
- values[timeIndex] = EpochPrecisionToMs(float64(value.Unix()))
+ values[timeIndex] = EpochPrecisionToMs(float64(value.UnixNano()))
case *time.Time:
if value != nil {
- values[timeIndex] = EpochPrecisionToMs(float64((*value).Unix()))
+ values[timeIndex] = EpochPrecisionToMs(float64((*value).UnixNano()))
}
case int64:
values[timeIndex] = int64(EpochPrecisionToMs(float64(value)))
@@ -152,12 +152,36 @@ func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) {
if value != nil {
values[timeIndex] = int64(EpochPrecisionToMs(float64(*value)))
}
+ case uint64:
+ values[timeIndex] = int64(EpochPrecisionToMs(float64(value)))
+ case *uint64:
+ if value != nil {
+ values[timeIndex] = int64(EpochPrecisionToMs(float64(*value)))
+ }
+ case int32:
+ values[timeIndex] = int64(EpochPrecisionToMs(float64(value)))
+ case *int32:
+ if value != nil {
+ values[timeIndex] = int64(EpochPrecisionToMs(float64(*value)))
+ }
+ case uint32:
+ values[timeIndex] = int64(EpochPrecisionToMs(float64(value)))
+ case *uint32:
+ if value != nil {
+ values[timeIndex] = int64(EpochPrecisionToMs(float64(*value)))
+ }
case float64:
values[timeIndex] = EpochPrecisionToMs(value)
case *float64:
if value != nil {
values[timeIndex] = EpochPrecisionToMs(*value)
}
+ case float32:
+ values[timeIndex] = EpochPrecisionToMs(float64(value))
+ case *float32:
+ if value != nil {
+ values[timeIndex] = EpochPrecisionToMs(float64(*value))
+ }
}
}
}
diff --git a/pkg/tsdb/sql_engine_test.go b/pkg/tsdb/sql_engine_test.go
index 48aac2c4d45..4c6951a0196 100644
--- a/pkg/tsdb/sql_engine_test.go
+++ b/pkg/tsdb/sql_engine_test.go
@@ -1,6 +1,7 @@
package tsdb
import (
+ "fmt"
"testing"
"time"
@@ -9,37 +10,177 @@ import (
func TestSqlEngine(t *testing.T) {
Convey("SqlEngine", t, func() {
- Convey("Given row values with time columns when converting them", func() {
- dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
- fixtures := make([]interface{}, 8)
- fixtures[0] = dt
- fixtures[1] = dt.Unix() * 1000
- fixtures[2] = dt.Unix()
- fixtures[3] = float64(dt.Unix() * 1000)
- fixtures[4] = float64(dt.Unix())
+ dt := time.Date(2018, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC)
- var nilDt *time.Time
- var nilInt64 *int64
- var nilFloat64 *float64
- fixtures[5] = nilDt
- fixtures[6] = nilInt64
- fixtures[7] = nilFloat64
+ Convey("Given row values with time.Time as time columns", func() {
+ var nilPointer *time.Time
+
+ fixtures := make([]interface{}, 3)
+ fixtures[0] = dt
+ fixtures[1] = &dt
+ fixtures[2] = nilPointer
for i := range fixtures {
ConvertSqlTimeColumnToEpochMs(fixtures, i)
}
- Convey("Should convert sql time columns to epoch time in ms ", func() {
- expected := float64(dt.Unix() * 1000)
+ Convey("When converting them should return epoch time with millisecond precision ", func() {
+ expected := float64(dt.UnixNano()) / float64(time.Millisecond)
So(fixtures[0].(float64), ShouldEqual, expected)
- So(fixtures[1].(int64), ShouldEqual, expected)
- So(fixtures[2].(int64), ShouldEqual, expected)
- So(fixtures[3].(float64), ShouldEqual, expected)
- So(fixtures[4].(float64), ShouldEqual, expected)
+ So(fixtures[1].(float64), ShouldEqual, expected)
+ So(fixtures[2], ShouldBeNil)
+ })
+ })
- So(fixtures[5], ShouldBeNil)
+ Convey("Given row values with int64 as time columns", func() {
+ tSeconds := dt.Unix()
+ tMilliseconds := dt.UnixNano() / 1e6
+ tNanoSeconds := dt.UnixNano()
+ var nilPointer *int64
+
+ fixtures := make([]interface{}, 7)
+ fixtures[0] = tSeconds
+ fixtures[1] = &tSeconds
+ fixtures[2] = tMilliseconds
+ fixtures[3] = &tMilliseconds
+ fixtures[4] = tNanoSeconds
+ fixtures[5] = &tNanoSeconds
+ fixtures[6] = nilPointer
+
+ for i := range fixtures {
+ ConvertSqlTimeColumnToEpochMs(fixtures, i)
+ }
+
+ Convey("When converting them should return epoch time with millisecond precision ", func() {
+ So(fixtures[0].(int64), ShouldEqual, tSeconds*1e3)
+ So(fixtures[1].(int64), ShouldEqual, tSeconds*1e3)
+ So(fixtures[2].(int64), ShouldEqual, tMilliseconds)
+ So(fixtures[3].(int64), ShouldEqual, tMilliseconds)
+ So(fixtures[4].(int64), ShouldEqual, tMilliseconds)
+ So(fixtures[5].(int64), ShouldEqual, tMilliseconds)
So(fixtures[6], ShouldBeNil)
- So(fixtures[7], ShouldBeNil)
+ })
+ })
+
+ Convey("Given row values with uin64 as time columns", func() {
+ tSeconds := uint64(dt.Unix())
+ tMilliseconds := uint64(dt.UnixNano() / 1e6)
+ tNanoSeconds := uint64(dt.UnixNano())
+ var nilPointer *uint64
+
+ fixtures := make([]interface{}, 7)
+ fixtures[0] = tSeconds
+ fixtures[1] = &tSeconds
+ fixtures[2] = tMilliseconds
+ fixtures[3] = &tMilliseconds
+ fixtures[4] = tNanoSeconds
+ fixtures[5] = &tNanoSeconds
+ fixtures[6] = nilPointer
+
+ for i := range fixtures {
+ ConvertSqlTimeColumnToEpochMs(fixtures, i)
+ }
+
+ Convey("When converting them should return epoch time with millisecond precision ", func() {
+ So(fixtures[0].(int64), ShouldEqual, tSeconds*1e3)
+ So(fixtures[1].(int64), ShouldEqual, tSeconds*1e3)
+ So(fixtures[2].(int64), ShouldEqual, tMilliseconds)
+ So(fixtures[3].(int64), ShouldEqual, tMilliseconds)
+ So(fixtures[4].(int64), ShouldEqual, tMilliseconds)
+ So(fixtures[5].(int64), ShouldEqual, tMilliseconds)
+ So(fixtures[6], ShouldBeNil)
+ })
+ })
+
+ Convey("Given row values with int32 as time columns", func() {
+ tSeconds := int32(dt.Unix())
+ var nilInt *int32
+
+ fixtures := make([]interface{}, 3)
+ fixtures[0] = tSeconds
+ fixtures[1] = &tSeconds
+ fixtures[2] = nilInt
+
+ for i := range fixtures {
+ ConvertSqlTimeColumnToEpochMs(fixtures, i)
+ }
+
+ Convey("When converting them should return epoch time with millisecond precision ", func() {
+ So(fixtures[0].(int64), ShouldEqual, dt.Unix()*1e3)
+ So(fixtures[1].(int64), ShouldEqual, dt.Unix()*1e3)
+ So(fixtures[2], ShouldBeNil)
+ })
+ })
+
+ Convey("Given row values with uint32 as time columns", func() {
+ tSeconds := uint32(dt.Unix())
+ var nilInt *uint32
+
+ fixtures := make([]interface{}, 3)
+ fixtures[0] = tSeconds
+ fixtures[1] = &tSeconds
+ fixtures[2] = nilInt
+
+ for i := range fixtures {
+ ConvertSqlTimeColumnToEpochMs(fixtures, i)
+ }
+
+ Convey("When converting them should return epoch time with millisecond precision ", func() {
+ So(fixtures[0].(int64), ShouldEqual, dt.Unix()*1e3)
+ So(fixtures[1].(int64), ShouldEqual, dt.Unix()*1e3)
+ So(fixtures[2], ShouldBeNil)
+ })
+ })
+
+ Convey("Given row values with float64 as time columns", func() {
+ tSeconds := float64(dt.UnixNano()) / float64(time.Second)
+ tMilliseconds := float64(dt.UnixNano()) / float64(time.Millisecond)
+ tNanoSeconds := float64(dt.UnixNano())
+ var nilPointer *float64
+
+ fixtures := make([]interface{}, 7)
+ fixtures[0] = tSeconds
+ fixtures[1] = &tSeconds
+ fixtures[2] = tMilliseconds
+ fixtures[3] = &tMilliseconds
+ fixtures[4] = tNanoSeconds
+ fixtures[5] = &tNanoSeconds
+ fixtures[6] = nilPointer
+
+ for i := range fixtures {
+ ConvertSqlTimeColumnToEpochMs(fixtures, i)
+ }
+
+ Convey("When converting them should return epoch time with millisecond precision ", func() {
+ So(fixtures[0].(float64), ShouldEqual, tMilliseconds)
+ So(fixtures[1].(float64), ShouldEqual, tMilliseconds)
+ So(fixtures[2].(float64), ShouldEqual, tMilliseconds)
+ So(fixtures[3].(float64), ShouldEqual, tMilliseconds)
+ fmt.Println(fixtures[4].(float64))
+ fmt.Println(tMilliseconds)
+ So(fixtures[4].(float64), ShouldEqual, tMilliseconds)
+ So(fixtures[5].(float64), ShouldEqual, tMilliseconds)
+ So(fixtures[6], ShouldBeNil)
+ })
+ })
+
+ Convey("Given row values with float32 as time columns", func() {
+ tSeconds := float32(dt.Unix())
+ var nilInt *float32
+
+ fixtures := make([]interface{}, 3)
+ fixtures[0] = tSeconds
+ fixtures[1] = &tSeconds
+ fixtures[2] = nilInt
+
+ for i := range fixtures {
+ ConvertSqlTimeColumnToEpochMs(fixtures, i)
+ }
+
+ Convey("When converting them should return epoch time with millisecond precision ", func() {
+ So(fixtures[0].(float64), ShouldEqual, float32(dt.Unix()*1e3))
+ So(fixtures[1].(float64), ShouldEqual, float32(dt.Unix()*1e3))
+ So(fixtures[2], ShouldBeNil)
})
})
})
diff --git a/pkg/tsdb/time_range.go b/pkg/tsdb/time_range.go
index fd0cb3f8e82..777fd15907e 100644
--- a/pkg/tsdb/time_range.go
+++ b/pkg/tsdb/time_range.go
@@ -15,6 +15,14 @@ func NewTimeRange(from, to string) *TimeRange {
}
}
+func NewFakeTimeRange(from, to string, now time.Time) *TimeRange {
+ return &TimeRange{
+ From: from,
+ To: to,
+ now: now,
+ }
+}
+
type TimeRange struct {
From string
To string
@@ -25,10 +33,26 @@ func (tr *TimeRange) GetFromAsMsEpoch() int64 {
return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
}
+func (tr *TimeRange) GetFromAsSecondsEpoch() int64 {
+ return tr.GetFromAsMsEpoch() / 1000
+}
+
+func (tr *TimeRange) GetFromAsTimeUTC() time.Time {
+ return tr.MustGetFrom().UTC()
+}
+
func (tr *TimeRange) GetToAsMsEpoch() int64 {
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
}
+func (tr *TimeRange) GetToAsSecondsEpoch() int64 {
+ return tr.GetToAsMsEpoch() / 1000
+}
+
+func (tr *TimeRange) GetToAsTimeUTC() time.Time {
+ return tr.MustGetTo().UTC()
+}
+
func (tr *TimeRange) MustGetFrom() time.Time {
if res, err := tr.ParseFrom(); err != nil {
return time.Unix(0, 0)
@@ -92,9 +116,14 @@ func (tr *TimeRange) ParseTo() (time.Time, error) {
// EpochPrecisionToMs converts epoch precision to millisecond, if needed.
// Only seconds to milliseconds supported right now
func EpochPrecisionToMs(value float64) float64 {
- if int64(value)/1e10 == 0 {
- return float64(value * 1e3)
+ s := strconv.FormatFloat(value, 'e', -1, 64)
+ if strings.HasSuffix(s, "e+09") {
+ return value * float64(1e3)
}
- return float64(value)
+ if strings.HasSuffix(s, "e+18") {
+ return value / float64(time.Millisecond)
+ }
+
+ return value
}
diff --git a/pkg/util/shortid_generator.go b/pkg/util/shortid_generator.go
index d87b6f70fe6..f900cb8275e 100644
--- a/pkg/util/shortid_generator.go
+++ b/pkg/util/shortid_generator.go
@@ -17,11 +17,7 @@ func init() {
// IsValidShortUid checks if short unique identifier contains valid characters
func IsValidShortUid(uid string) bool {
- if !validUidPattern(uid) {
- return false
- }
-
- return true
+ return validUidPattern(uid)
}
// GenerateShortUid generates a short unique identifier.
diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx
index 07ccfdbbef5..4dcd07ffb48 100644
--- a/public/app/core/components/Permissions/AddPermissions.tsx
+++ b/public/app/core/components/Permissions/AddPermissions.tsx
@@ -39,7 +39,7 @@ class AddPermissions extends Component {
permissions.newItem.setUser(null, null);
return;
}
- return permissions.newItem.setUser(user.id, user.login);
+ return permissions.newItem.setUser(user.id, user.login, user.avatarUrl);
}
teamPicked(team: Team) {
@@ -48,7 +48,7 @@ class AddPermissions extends Component {
permissions.newItem.setTeam(null, null);
return;
}
- return permissions.newItem.setTeam(team.id, team.name);
+ return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl);
}
permissionPicked(permission: OptionWithDescription) {
diff --git a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx
index db45714136e..5e2497d983e 100644
--- a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx
+++ b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { Component } from 'react';
import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
@@ -12,9 +12,12 @@ export default class DisabledPermissionListItem extends Component {
return (
+
+ This dashboard cannot be saved from Grafana's UI since it has been provisioned from another source.
+ Copy the JSON or save it to a file below. Then you can update your dashboard in corresponding provisioning source.
+ See
+ documentation for more information about provisioning.
+
+
Each organization contains their own dashboards, data sources and configuration, and cannot be shared between orgs. While users may belong to more than one, mutiple organization are most frequently used in multi-tenant deployments.
+
Each organization contains their own dashboards, data sources and configuration, and cannot be shared between orgs. While users may belong to more than one, multiple organization are most frequently used in multi-tenant deployments.
-
diff --git a/public/app/features/templating/datasource_variable.ts b/public/app/features/templating/datasource_variable.ts
index 0c5b226c372..4c326a94e3b 100644
--- a/public/app/features/templating/datasource_variable.ts
+++ b/public/app/features/templating/datasource_variable.ts
@@ -29,7 +29,7 @@ export class DatasourceVariable implements Variable {
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
- // dont persist options
+ // don't persist options
this.model.options = [];
return this.model;
}
diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts
index b87167ad646..54bd7bb660c 100644
--- a/public/app/features/templating/query_variable.ts
+++ b/public/app/features/templating/query_variable.ts
@@ -198,7 +198,9 @@ export class QueryVariable implements Variable {
}
});
} else if (sortType === 3) {
- options = _.sortBy(options, opt => { return _.toLower(opt.text); });
+ options = _.sortBy(options, opt => {
+ return _.toLower(opt.text);
+ });
}
if (reverseSort) {
diff --git a/public/app/features/templating/specs/adhoc_variable.jest.ts b/public/app/features/templating/specs/adhoc_variable.jest.ts
index 863c8401c50..a7b20e8d029 100644
--- a/public/app/features/templating/specs/adhoc_variable.jest.ts
+++ b/public/app/features/templating/specs/adhoc_variable.jest.ts
@@ -2,7 +2,7 @@ import { AdhocVariable } from '../adhoc_variable';
describe('AdhocVariable', function() {
describe('when serializing to url', function() {
- it('should set return key value and op seperated by pipe', function() {
+ it('should set return key value and op separated by pipe', function() {
var variable = new AdhocVariable({
filters: [
{ key: 'key1', operator: '=', value: 'value1' },
diff --git a/public/app/features/templating/specs/query_variable.jest.ts b/public/app/features/templating/specs/query_variable.jest.ts
index ce753a4b205..39c51874586 100644
--- a/public/app/features/templating/specs/query_variable.jest.ts
+++ b/public/app/features/templating/specs/query_variable.jest.ts
@@ -91,7 +91,6 @@ describe('QueryVariable', () => {
it('should return in same order', () => {
var i = 0;
- console.log(result);
expect(result.length).toBe(11);
expect(result[i++].text).toBe('');
expect(result[i++].text).toBe('0');
diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts
index f28fbf9ac64..5290a883c48 100644
--- a/public/app/features/templating/specs/template_srv.jest.ts
+++ b/public/app/features/templating/specs/template_srv.jest.ts
@@ -282,7 +282,7 @@ describe('templateSrv', function() {
});
});
- describe('can hightlight variables in string', function() {
+ describe('can highlight variables in string', function() {
beforeEach(function() {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts
index 5b31072d140..f6274a80165 100644
--- a/public/app/features/templating/template_srv.ts
+++ b/public/app/features/templating/template_srv.ts
@@ -204,7 +204,7 @@ export class TemplateSrv {
value = variable.current.value;
if (this.isAllValue(value)) {
value = this.getAllValue(variable);
- // skip formating of custom all values
+ // skip formatting of custom all values
if (variable.allValue) {
return value;
}
diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html
index 210275d2200..9506587c515 100644
--- a/public/app/partials/dashboard.html
+++ b/public/app/partials/dashboard.html
@@ -1,18 +1,18 @@
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
diff --git a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json
index c56d9e9216f..5a4459cd62c 100644
--- a/public/app/plugins/app/testdata/dashboards/graph_last_1h.json
+++ b/public/app/plugins/app/testdata/dashboards/graph_last_1h.json
@@ -392,7 +392,7 @@
"thresholds": [],
"timeFrom": null,
"timeShift": null,
- "title": "2 yaxis and axis lables",
+ "title": "2 yaxis and axis labels",
"tooltip": {
"msResolution": false,
"shared": true,
@@ -894,7 +894,7 @@
"thresholds": [],
"timeFrom": null,
"timeShift": null,
- "title": "Legend Table Single Series Should Take Minium Height",
+ "title": "Legend Table Single Series Should Take Minimum Height",
"tooltip": {
"shared": true,
"sort": 0,
diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts
index ede5cb0ba3a..a378ab8b55f 100644
--- a/public/app/plugins/datasource/elasticsearch/elastic_response.ts
+++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts
@@ -175,7 +175,7 @@ export class ElasticResponse {
}
// This is quite complex
- // neeed to recurise down the nested buckets to build series
+ // need to recurise down the nested buckets to build series
processBuckets(aggs, target, seriesList, table, props, depth) {
var bucket, aggDef, esAgg, aggId;
var maxDepth = target.bucketAggs.length - 1;
diff --git a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html
index d4e1e7d1b1c..a2e903f231c 100644
--- a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html
+++ b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html
@@ -27,7 +27,7 @@
+ Setting the database for this datasource does not deny access to other databases. The InfluxDB query syntax allows
+ switching the database in the query. For example:
+ SHOW MEASUREMENTS ON _internal or SELECT * FROM "_internal".."database" LIMIT 10
+
+ To support data isolation and security, make sure appropriate permissions are configured in InfluxDB.
+
+
+
+
diff --git a/public/app/plugins/datasource/influxdb/query_help.md b/public/app/plugins/datasource/influxdb/query_help.md
index 0d4fd941ca5..4930ccbc83f 100644
--- a/public/app/plugins/datasource/influxdb/query_help.md
+++ b/public/app/plugins/datasource/influxdb/query_help.md
@@ -10,7 +10,7 @@
- When stacking is enabled it is important that points align
- If there are missing points for one series it can cause gaps or missing bars
- You must use fill(0), and select a group by time low limit
-- Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
+- Use the group by time option below your queries and specify for example 10s if your metrics are written every 10 seconds
- This will insert zeros for series that are missing measurements and will make stacking work properly
#### Group by time
@@ -18,8 +18,7 @@
- Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
- If you use fill(0) or fill(null) set a low limit for the auto group by time interval
- The low limit can only be set in the group by time option below your queries
-- You set a low limit by adding a greater sign before the interval
-- Example: >60s if you write metrics to InfluxDB every 60 seconds
+- Example: 60s if you write metrics to InfluxDB every 60 seconds
#### Documentation links:
diff --git a/public/app/plugins/datasource/influxdb/specs/influx_series.jest.ts b/public/app/plugins/datasource/influxdb/specs/influx_series.jest.ts
index 8ae4a335828..8c8fee9ab9f 100644
--- a/public/app/plugins/datasource/influxdb/specs/influx_series.jest.ts
+++ b/public/app/plugins/datasource/influxdb/specs/influx_series.jest.ts
@@ -195,10 +195,34 @@ describe('when generating timeseries from influxdb response', function() {
expect(table.type).toBe('table');
expect(table.columns.length).toBe(5);
+ expect(table.columns[0].text).toEqual('Time');
expect(table.rows[0]).toEqual([1431946625000, 'Africa', 'server2', 23, 10]);
});
});
+ describe('given table response from SHOW CARDINALITY', function() {
+ var options = {
+ alias: '',
+ series: [
+ {
+ name: 'cpu',
+ columns: ['count'],
+ values: [[37]],
+ },
+ ],
+ };
+
+ it('should return table', function() {
+ var series = new InfluxSeries(options);
+ var table = series.getTable();
+
+ expect(table.type).toBe('table');
+ expect(table.columns.length).toBe(1);
+ expect(table.columns[0].text).toEqual('count');
+ expect(table.rows[0]).toEqual([37]);
+ });
+ });
+
describe('given annotation response', function() {
describe('with empty tagsColumn', function() {
var options = {
diff --git a/public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts b/public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts
index 439bf7b1fc5..eeae987b139 100644
--- a/public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts
+++ b/public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts
@@ -97,7 +97,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG VALUES FROM "one_week"."cpu" WITH KEY = "app" WHERE "host" = \'server1\'');
});
- it('should not includ policy when policy is default', function() {
+ it('should not include policy when policy is default', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
policy: 'default',
diff --git a/public/app/plugins/datasource/mssql/partials/annotations.editor.html b/public/app/plugins/datasource/mssql/partials/annotations.editor.html
index 8a94c470379..b2c0d7b97a6 100644
--- a/public/app/plugins/datasource/mssql/partials/annotations.editor.html
+++ b/public/app/plugins/datasource/mssql/partials/annotations.editor.html
@@ -18,7 +18,7 @@
Annotation Query Format
-An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
+An annotation is an event that is overlaid on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: time for the annotation event time. Use epoch time or any native date data type.
- column with alias: text for the annotation text.
diff --git a/public/app/plugins/datasource/mysql/partials/annotations.editor.html b/public/app/plugins/datasource/mysql/partials/annotations.editor.html
index d142e091fed..23ec726a9f0 100644
--- a/public/app/plugins/datasource/mysql/partials/annotations.editor.html
+++ b/public/app/plugins/datasource/mysql/partials/annotations.editor.html
@@ -18,7 +18,7 @@
Annotation Query Format
-An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time or time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
+An annotation is an event that is overlaid on top of graphs. The query can have up to three columns per row, the time or time_sec column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: time or time_sec for the annotation event time. Use epoch time or any native date data type.
- column with alias: text for the annotation text
diff --git a/public/app/plugins/datasource/postgres/img/postgresql_logo.svg b/public/app/plugins/datasource/postgres/img/postgresql_logo.svg
index d98e3659c39..40a39970070 100644
--- a/public/app/plugins/datasource/postgres/img/postgresql_logo.svg
+++ b/public/app/plugins/datasource/postgres/img/postgresql_logo.svg
@@ -3,7 +3,7 @@
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
\ No newline at end of file
+
diff --git a/public/app/plugins/datasource/postgres/module.ts b/public/app/plugins/datasource/postgres/module.ts
index acd23318b6d..a24971fa1a1 100644
--- a/public/app/plugins/datasource/postgres/module.ts
+++ b/public/app/plugins/datasource/postgres/module.ts
@@ -8,7 +8,7 @@ class PostgresConfigCtrl {
/** @ngInject **/
constructor($scope) {
- this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'require';
+ this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full';
}
}
diff --git a/public/app/plugins/datasource/postgres/partials/annotations.editor.html b/public/app/plugins/datasource/postgres/partials/annotations.editor.html
index 09232d6f8ed..b83f5a14832 100644
--- a/public/app/plugins/datasource/postgres/partials/annotations.editor.html
+++ b/public/app/plugins/datasource/postgres/partials/annotations.editor.html
@@ -18,7 +18,7 @@
Annotation Query Format
-An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
+An annotation is an event that is overlaid on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: time for the annotation event time. Use epoch time or any native date data type.
- column with alias: text for the annotation text
@@ -28,12 +28,12 @@ An annotation is an event that is overlayed on top of graphs. The query can have
Macros:
- $__time(column) -> column as "time"
- $__timeEpoch -> extract(epoch from column) as "time"
-- $__timeFilter(column) -> column ≥ to_timestamp(1492750877) AND column ≤ to_timestamp(1492750877)
-- $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877
+- $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
+- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
Or build your own conditionals using these macros which just return the values:
-- $__timeFrom() -> to_timestamp(1492750877)
-- $__timeTo() -> to_timestamp(1492750877)
+- $__timeFrom() -> '2017-04-21T05:01:17Z'
+- $__timeTo() -> '2017-04-21T05:01:17Z'
- $__unixEpochFrom() -> 1492750877
- $__unixEpochTo() -> 1492750877
diff --git a/public/app/plugins/datasource/postgres/partials/query.editor.html b/public/app/plugins/datasource/postgres/partials/query.editor.html
index 163970a9ad5..26392c17356 100644
--- a/public/app/plugins/datasource/postgres/partials/query.editor.html
+++ b/public/app/plugins/datasource/postgres/partials/query.editor.html
@@ -48,8 +48,8 @@ Table:
Macros:
- $__time(column) -> column as "time"
- $__timeEpoch -> extract(epoch from column) as "time"
-- $__timeFilter(column) -> extract(epoch from column) BETWEEN 1492750877 AND 1492750877
-- $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877
+- $__timeFilter(column) -> column BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:01:17Z'
+- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS time
Example of group by and order by with $__timeGroup:
@@ -61,8 +61,8 @@ GROUP BY time
ORDER BY time
Or build your own conditionals using these macros which just return the values:
-- $__timeFrom() -> to_timestamp(1492750877)
-- $__timeTo() -> to_timestamp(1492750877)
+- $__timeFrom() -> '2017-04-21T05:01:17Z'
+- $__timeTo() -> '2017-04-21T05:01:17Z'
- $__unixEpochFrom() -> 1492750877
- $__unixEpochTo() -> 1492750877
diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html
index 8d6e89c3406..68791d96c19 100644
--- a/public/app/plugins/datasource/prometheus/partials/query.editor.html
+++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html
@@ -14,7 +14,7 @@
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
- Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for
+ Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for
the label hostname.
diff --git a/public/app/plugins/panel/graph/jquery.flot.events.js b/public/app/plugins/panel/graph/jquery.flot.events.js
index 1aa79c5056f..3ea3ca8f330 100644
--- a/public/app/plugins/panel/graph/jquery.flot.events.js
+++ b/public/app/plugins/panel/graph/jquery.flot.events.js
@@ -52,14 +52,14 @@ function ($, _, angular, Drop) {
var eventManager = plot.getOptions().events.manager;
if (eventManager.editorOpen) {
// update marker element to attach to (needed in case of legend on the right
- // when there is a double render pass and the inital marker element is removed)
+ // when there is a double render pass and the initial marker element is removed)
markerElementToAttachTo = element;
return;
}
// mark as openend
eventManager.editorOpened();
- // set marker elment to attache to
+ // set marker element to attache to
markerElementToAttachTo = element;
// wait for element to be attached and positioned
diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts
index 4dfeb75ff55..6b6c89444dc 100644
--- a/public/app/plugins/panel/graph/legend.ts
+++ b/public/app/plugins/panel/graph/legend.ts
@@ -1,7 +1,7 @@
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
-import PerfectScrollbar from 'perfect-scrollbar';
+import baron from 'baron';
var module = angular.module('grafana.directives');
@@ -16,11 +16,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
var i;
var legendScrollbar;
const legendRightDefaultWidth = 10;
+ let legendElem = elem.parent();
scope.$on('$destroy', function() {
- if (legendScrollbar) {
- legendScrollbar.destroy();
- }
+ destroyScrollbar();
});
ctrl.events.on('render-legend', () => {
@@ -112,7 +111,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
}
function render() {
- let legendWidth = elem.width();
+ let legendWidth = legendElem.width();
if (!ctrl.panel.legend.show) {
elem.empty();
firstRender = true;
@@ -130,12 +129,12 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
elem.empty();
- // Set min-width if side style and there is a value, otherwise remove the CSS propery
+ // Set min-width if side style and there is a value, otherwise remove the CSS property
// Set width so it works with IE11
var width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
var ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : '';
- elem.css('min-width', width);
- elem.css('width', ieWidth);
+ legendElem.css('min-width', width);
+ legendElem.css('width', ieWidth);
elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
@@ -241,8 +240,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
tbodyElem.append(tableHeaderElem);
tbodyElem.append(seriesElements);
elem.append(tbodyElem);
+ tbodyElem.wrap('');
} else {
- elem.append(seriesElements);
+ elem.append('');
+ elem.find('.graph-legend-scroll').append(seriesElements);
}
if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) {
@@ -253,23 +254,45 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
}
function addScrollbar() {
- const scrollbarOptions = {
- // Number of pixels the content height can surpass the container height without enabling the scroll bar.
- scrollYMarginOffset: 2,
- suppressScrollX: true,
- wheelPropagation: true,
+ const scrollRootClass = 'baron baron__root';
+ const scrollerClass = 'baron__scroller';
+ const scrollBarHTML = `
+
+
+
+ `;
+
+ let scrollRoot = elem;
+ let scroller = elem.find('.graph-legend-scroll');
+
+ // clear existing scroll bar track to prevent duplication
+ scrollRoot.find('.baron__track').remove();
+
+ scrollRoot.addClass(scrollRootClass);
+ $(scrollBarHTML).appendTo(scrollRoot);
+ scroller.addClass(scrollerClass);
+
+ let scrollbarParams = {
+ root: scrollRoot[0],
+ scroller: scroller[0],
+ bar: '.baron__bar',
+ track: '.baron__track',
+ barOnCls: '_scrollbar',
+ scrollingCls: '_scrolling',
};
if (!legendScrollbar) {
- legendScrollbar = new PerfectScrollbar(elem[0], scrollbarOptions);
+ legendScrollbar = baron(scrollbarParams);
} else {
- legendScrollbar.update();
+ destroyScrollbar();
+ legendScrollbar = baron(scrollbarParams);
}
+ legendScrollbar.scroll();
}
function destroyScrollbar() {
if (legendScrollbar) {
- legendScrollbar.destroy();
+ legendScrollbar.dispose();
legendScrollbar = undefined;
}
}
diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts
index 703c4648716..ecf79a8a4fb 100644
--- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts
+++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts
@@ -31,7 +31,7 @@ export class SeriesOverridesCtrl {
$scope.override[item.propertyName] = subItem.value;
- // automatically disable lines for this series and the fill bellow to series
+ // automatically disable lines for this series and the fill below to series
// can be removed by the user if they still want lines
if (item.propertyName === 'fillBelowTo') {
$scope.override['lines'] = false;
diff --git a/public/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts
index 0b9eb8227df..c897327fe1a 100644
--- a/public/app/plugins/panel/graph/template.ts
+++ b/public/app/plugins/panel/graph/template.ts
@@ -3,7 +3,9 @@ var template = `