Merge branch 'main' into encryption/use-secrets-service

This commit is contained in:
Tania B
2021-10-21 15:25:56 +03:00
128 changed files with 3339 additions and 2962 deletions
+72 -116
View File
@@ -28,10 +28,11 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- ./bin/grabpl verify-drone
- make gen-go
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- yarn install --immutable
image: grafana/build-container:1.4.3
name: initialize
@@ -145,20 +146,12 @@ steps:
image: grafana/build-container:1.4.3
name: ensure-cuetsified
- commands:
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- . scripts/build/gpg-test-vars.sh && ./bin/grabpl package --jobs 8 --edition oss
--build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise --variants linux-x64,linux-x64-musl,osx64,win64,armv6
depends_on:
- build-plugins
- build-backend
- build-frontend
- codespell
- shellcheck
image: grafana/build-container:1.4.3
name: gen-version
- commands:
- . scripts/build/gpg-test-vars.sh && ./bin/grabpl package --jobs 8 --edition oss
--build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise --variants linux-x64,linux-x64-musl,osx64,win64,armv6
depends_on:
- gen-version
environment: null
image: grafana/build-container:1.4.3
name: package
@@ -184,7 +177,7 @@ steps:
- yarn storybook:build
- ./bin/grabpl verify-storybook
depends_on:
- package
- build-frontend
environment:
NODE_OPTIONS: --max_old_space_size=4096
image: grafana/build-container:1.4.3
@@ -220,7 +213,7 @@ steps:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
depends_on:
- end-to-end-tests-server
- package
image: grafana/build-container:1.4.3
name: copy-packages-for-docker
- depends_on:
@@ -298,10 +291,11 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- ./bin/grabpl verify-drone
- make gen-go
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- yarn install --immutable
image: grafana/build-container:1.4.3
name: initialize
@@ -428,20 +422,12 @@ steps:
image: grafana/build-container:1.4.3
name: ensure-cuetsified
- commands:
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise
--sign
depends_on:
- build-plugins
- build-backend
- build-frontend
- codespell
- shellcheck
image: grafana/build-container:1.4.3
name: gen-version
- commands:
- ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise
--sign
depends_on:
- gen-version
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -477,7 +463,7 @@ steps:
- yarn storybook:build
- ./bin/grabpl verify-storybook
depends_on:
- package
- build-frontend
environment:
NODE_OPTIONS: --max_old_space_size=4096
image: grafana/build-container:1.4.3
@@ -527,7 +513,7 @@ steps:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
depends_on:
- end-to-end-tests-server
- package
image: grafana/build-container:1.4.3
name: copy-packages-for-docker
- depends_on:
@@ -637,7 +623,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -684,7 +670,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- ./bin/grabpl verify-drone
- make gen-go
@@ -772,11 +758,12 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- ./bin/grabpl verify-drone
- make gen-go
- ./bin/grabpl verify-version ${DRONE_TAG}
- ./bin/grabpl gen-version ${DRONE_TAG}
- yarn install --immutable
image: grafana/build-container:1.4.3
name: initialize
@@ -894,20 +881,12 @@ steps:
image: grafana/build-container:1.4.3
name: ensure-cuetsified
- commands:
- ./bin/grabpl gen-version ${DRONE_TAG}
- ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise
--sign ${DRONE_TAG}
depends_on:
- build-plugins
- build-backend
- build-frontend
- codespell
- shellcheck
image: grafana/build-container:1.4.3
name: gen-version
- commands:
- ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise
--sign ${DRONE_TAG}
depends_on:
- gen-version
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -943,7 +922,7 @@ steps:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
depends_on:
- end-to-end-tests-server
- package
image: grafana/build-container:1.4.3
name: copy-packages-for-docker
- depends_on:
@@ -1006,7 +985,7 @@ steps:
- yarn storybook:build
- ./bin/grabpl verify-storybook
depends_on:
- package
- build-frontend
environment:
NODE_OPTIONS: --max_old_space_size=4096
image: grafana/build-container:1.4.3
@@ -1076,7 +1055,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -1143,7 +1122,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -1164,6 +1143,7 @@ steps:
- ./bin/grabpl verify-drone
- make gen-go
- ./bin/grabpl verify-version ${DRONE_TAG}
- ./bin/grabpl gen-version ${DRONE_TAG}
- yarn install --immutable
depends_on:
- clone
@@ -1315,22 +1295,14 @@ steps:
image: grafana/build-container:1.4.3
name: build-backend-enterprise2
- commands:
- ./bin/grabpl gen-version ${DRONE_TAG}
- ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN}
--no-pull-enterprise --sign ${DRONE_TAG}
depends_on:
- build-plugins
- build-backend
- build-frontend
- codespell
- shellcheck
- build-backend-enterprise2
- test-backend-enterprise2
image: grafana/build-container:1.4.3
name: gen-version
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN}
--no-pull-enterprise --sign ${DRONE_TAG}
depends_on:
- gen-version
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -1368,7 +1340,7 @@ steps:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
depends_on:
- end-to-end-tests-server
- package
image: grafana/build-container:1.4.3
name: copy-packages-for-docker
- depends_on:
@@ -1473,7 +1445,11 @@ steps:
- ./bin/grabpl package --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN}
--no-pull-enterprise --sign ${DRONE_TAG}
depends_on:
- gen-version
- build-plugins
- build-backend
- build-frontend
- build-backend-enterprise2
- test-backend-enterprise2
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -1554,7 +1530,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -1619,7 +1595,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- ./bin/grabpl verify-drone
- make gen-go
@@ -1726,11 +1702,12 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- ./bin/grabpl verify-drone
- make gen-go
- ./bin/grabpl verify-version v7.3.0-test
- ./bin/grabpl gen-version v7.3.0-test
- yarn install --immutable
image: grafana/build-container:1.4.3
name: initialize
@@ -1848,20 +1825,12 @@ steps:
image: grafana/build-container:1.4.3
name: ensure-cuetsified
- commands:
- ./bin/grabpl gen-version v7.3.0-test
- ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise
--sign v7.3.0-test
depends_on:
- build-plugins
- build-backend
- build-frontend
- codespell
- shellcheck
image: grafana/build-container:1.4.3
name: gen-version
- commands:
- ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise
--sign v7.3.0-test
depends_on:
- gen-version
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -1897,7 +1866,7 @@ steps:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
depends_on:
- end-to-end-tests-server
- package
image: grafana/build-container:1.4.3
name: copy-packages-for-docker
- depends_on:
@@ -1952,7 +1921,7 @@ steps:
- yarn storybook:build
- ./bin/grabpl verify-storybook
depends_on:
- package
- build-frontend
environment:
NODE_OPTIONS: --max_old_space_size=4096
image: grafana/build-container:1.4.3
@@ -2018,7 +1987,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -2086,7 +2055,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -2107,6 +2076,7 @@ steps:
- ./bin/grabpl verify-drone
- make gen-go
- ./bin/grabpl verify-version v7.3.0-test
- ./bin/grabpl gen-version v7.3.0-test
- yarn install --immutable
depends_on:
- clone
@@ -2258,22 +2228,14 @@ steps:
image: grafana/build-container:1.4.3
name: build-backend-enterprise2
- commands:
- ./bin/grabpl gen-version v7.3.0-test
- ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN}
--no-pull-enterprise --sign v7.3.0-test
depends_on:
- build-plugins
- build-backend
- build-frontend
- codespell
- shellcheck
- build-backend-enterprise2
- test-backend-enterprise2
image: grafana/build-container:1.4.3
name: gen-version
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN}
--no-pull-enterprise --sign v7.3.0-test
depends_on:
- gen-version
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -2311,7 +2273,7 @@ steps:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
depends_on:
- end-to-end-tests-server
- package
image: grafana/build-container:1.4.3
name: copy-packages-for-docker
- depends_on:
@@ -2408,7 +2370,11 @@ steps:
- ./bin/grabpl package --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN}
--no-pull-enterprise --sign v7.3.0-test
depends_on:
- gen-version
- build-plugins
- build-backend
- build-frontend
- build-backend-enterprise2
- test-backend-enterprise2
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -2489,7 +2455,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -2555,7 +2521,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- ./bin/grabpl verify-drone
- make gen-go
@@ -2667,10 +2633,11 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- ./bin/grabpl verify-drone
- make gen-go
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- yarn install --immutable
image: grafana/build-container:1.4.3
name: initialize
@@ -2786,20 +2753,12 @@ steps:
image: grafana/build-container:1.4.3
name: ensure-cuetsified
- commands:
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise
--sign
depends_on:
- build-plugins
- build-backend
- build-frontend
- codespell
- shellcheck
image: grafana/build-container:1.4.3
name: gen-version
- commands:
- ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise
--sign
depends_on:
- gen-version
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -2835,7 +2794,7 @@ steps:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
depends_on:
- end-to-end-tests-server
- package
image: grafana/build-container:1.4.3
name: copy-packages-for-docker
- depends_on:
@@ -2890,7 +2849,7 @@ steps:
- yarn storybook:build
- ./bin/grabpl verify-storybook
depends_on:
- package
- build-frontend
environment:
NODE_OPTIONS: --max_old_space_size=4096
image: grafana/build-container:1.4.3
@@ -2936,7 +2895,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -2999,7 +2958,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/grabpl
- chmod +x bin/grabpl
- git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -3019,6 +2978,7 @@ steps:
- mv /tmp/grabpl bin/
- ./bin/grabpl verify-drone
- make gen-go
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- yarn install --immutable
depends_on:
- clone
@@ -3166,22 +3126,14 @@ steps:
image: grafana/build-container:1.4.3
name: build-backend-enterprise2
- commands:
- ./bin/grabpl gen-version --build-id ${DRONE_BUILD_NUMBER}
- ./bin/grabpl package --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise --sign
depends_on:
- build-plugins
- build-backend
- build-frontend
- codespell
- shellcheck
- build-backend-enterprise2
- test-backend-enterprise2
image: grafana/build-container:1.4.3
name: gen-version
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise --sign
depends_on:
- gen-version
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -3219,7 +3171,7 @@ steps:
- ls dist/*.tar.gz*
- cp dist/*.tar.gz* packaging/docker/
depends_on:
- end-to-end-tests-server
- package
image: grafana/build-container:1.4.3
name: copy-packages-for-docker
- depends_on:
@@ -3274,7 +3226,7 @@ steps:
- yarn storybook:build
- ./bin/grabpl verify-storybook
depends_on:
- package
- build-frontend
environment:
NODE_OPTIONS: --max_old_space_size=4096
image: grafana/build-container:1.4.3
@@ -3325,7 +3277,11 @@ steps:
- ./bin/grabpl package --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise --variants linux-x64 --sign
depends_on:
- gen-version
- build-plugins
- build-backend
- build-frontend
- build-backend-enterprise2
- test-backend-enterprise2
environment:
GITHUB_TOKEN:
from_secret: github_token
@@ -3406,7 +3362,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.2/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.5.5/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -3529,6 +3485,6 @@ kind: secret
name: drone_token
---
kind: signature
hmac: 96d23b144a0a27bce871f7faf782827c17cc132492fe058a7c56768c543078c9
hmac: 0d26eda19dc6e530d02c734704c8a9d01beb5082c7e14b2609577b8695ed06f5
...
+1
View File
@@ -63,6 +63,7 @@ public/css/*.min.css
!.vscode/launch.json
.vs/
.eslintcache
.stylelintcache
/data/*
/bin/*
+30
View File
@@ -1,3 +1,33 @@
<!-- 8.2.2 START -->
# 8.2.2 (2021-10-21)
### Features and enhancements
- **Annotations:** We have improved tag search performance. [#40567](https://github.com/grafana/grafana/pull/40567), [@ashharrison90](https://github.com/ashharrison90)
- **Application:** You can now configure an error-template title. [#40310](https://github.com/grafana/grafana/pull/40310), [@benrubson](https://github.com/benrubson)
- **AzureMonitor:** We removed a restriction from the resource filter query. [#40690](https://github.com/grafana/grafana/pull/40690), [@andresmgot](https://github.com/andresmgot)
- **Caching:** Make cache size metric collection optional. (Enterprise)
- **Packaging:** We removed the ProcSubset option in systemd. This option prevented Grafana from starting in LXC environments. [#40339](https://github.com/grafana/grafana/pull/40339), [@kminehart](https://github.com/kminehart)
- **Prometheus:** We removed the autocomplete limit for metrics. [#39363](https://github.com/grafana/grafana/pull/39363), [@ivanahuckova](https://github.com/ivanahuckova)
- **Request interceptor:** Allow MSSQL's named instances. (Enterprise)
- **Table:** We improved the styling of the type icons to make them more distinct from column / field name. [#40596](https://github.com/grafana/grafana/pull/40596), [@torkelo](https://github.com/torkelo)
- **ValueMappings:** You can now use value mapping in stat, gauge, bar gauge, and pie chart visualizations. [#40612](https://github.com/grafana/grafana/pull/40612), [@torkelo](https://github.com/torkelo)
### Bug fixes
- **Alerting:** Fix panic when Slack's API sends unexpected response. [#40721](https://github.com/grafana/grafana/pull/40721), [@santihernandezc](https://github.com/santihernandezc)
- **Alerting:** The Create Alert button now appears on the dashboard panel when you are working with a default datasource. [#40334](https://github.com/grafana/grafana/pull/40334), [@domasx2](https://github.com/domasx2)
- **Explore:** We fixed the problem where the Explore log panel disappears when an Elasticsearch logs query returns no results. [#40217](https://github.com/grafana/grafana/pull/40217), [@Elfo404](https://github.com/Elfo404)
- **Graph:** You can now see annotation descriptions on hover. [#40581](https://github.com/grafana/grafana/pull/40581), [@axelavargas](https://github.com/axelavargas)
- **Logs:** The system now uses the JSON parser only if the line is parsed to an object. [#40507](https://github.com/grafana/grafana/pull/40507), [@ivanahuckova](https://github.com/ivanahuckova)
- **Prometheus:** We fixed the issue where the system did not reuse TCP connections when querying from Grafana alerting. [#40349](https://github.com/grafana/grafana/pull/40349), [@kminehart](https://github.com/kminehart)
- **Prometheus:** We fixed the problem that resulted in an error when a user created a query with a $\_\_interval min step. [#40525](https://github.com/grafana/grafana/pull/40525), [@ivanahuckova](https://github.com/ivanahuckova)
- **RowsToFields:** We fixed the issue where the system was not properly interpreting number values. [#40580](https://github.com/grafana/grafana/pull/40580), [@torkelo](https://github.com/torkelo)
- **Scale:** We fixed how the system handles NaN percent when data min = data max. [#40622](https://github.com/grafana/grafana/pull/40622), [@torkelo](https://github.com/torkelo)
- **Table panel:** You can now create a filter that includes special characters. [#40458](https://github.com/grafana/grafana/pull/40458), [@dprokop](https://github.com/dprokop)
<!-- 8.2.2 END -->
<!-- 8.2.1 START -->
# 8.2.1 (2021-10-11)
@@ -0,0 +1,27 @@
+++
title = "Alert groups"
description = "Alert groups"
keywords = ["grafana", "alerting", "alerts", "groups"]
weight = 400
+++
# View alert groups
Alert groups shows grouped alerts from an alertmanager instance. Alertmanager will group alerts based on common label values. This prevents duplicate alerts from being fired by grouping common alerts into a single alert group. By default, the alerts are grouped by the label keys for the root policy in [notification policies]({{< relref "./notification-policies.md" >}}).
## Show alert groups for an external Alertmanager
Grafana alerting UI supports alert groups from external Alertmanager data sources. Once you add an [Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}), a dropdown displays at the top of the page where you can select either `Grafana` or an external Alertmanager as your data source.
## View different alert groupings
To view a grouping other than the default use the **custom group by** dropdown to select combinations of labels to group alerts by. This is useful for debugging and verifying your notification policies grouping.
If an alert does not contain labels specified in the grouping of the route policy or the custom grouping it will be added to a catch all group with a header of `No grouping`.
## Filter alerts
You can use the following filters to view only alerts that match specific criteria:
- **Filter alerts by label -** Search by alert labels using label selectors in the **Search** input. eg: `environment=production,region=~US|EU,severity!=warning`
- **Filter alerts by state -** In **States** Select which alert states you want to see. All others are hidden.
@@ -179,7 +179,6 @@ consists of an array of alerts.
#### Removed fields related to dashboards
Alerts are not coupled to dashboards anymore therefore the fields related to dashboards `dashboardId` and `panelId` have been removed.
where removed. The removed fields are `dashboardId` and `panelId`.
## Manage contact points for an external Alertmanager
@@ -33,9 +33,14 @@ To add a silence:
Alert instances that have labels that match all of the "Matching Labels" specified in the silence will have their notifications suppressed.
- The **Label** field is the name of the label to match. It must exactly match the label name.
- The **Value** field matches against the corresponding value for the specified **Label** name. How it matches depends on the **Regex** and **Equal** checkboxes.
- The **Regex** checkbox specifies if the inputted **Value** should be matched against labels as a regular expression. The regular expression is always anchored. If not selected it is an exact string match.
- The **Equal** checkbox specifies if the match should include alert instances that match or do not match. If not checked, the silence includes alert instances _do not_ match.
- The **Operator** field is the operator to match against the label value. The available operators are:
- `=`: Select labels that are exactly equal to the provided string.
- `!=`: Select labels that are not equal to the provided string.
- `=~`: Select labels that regex-match the provided string.
- `!~`: Select labels that do not regex-match the provided string.
- The **Value** field matches against the corresponding value for the specified **Label** name. How it matches depends on the **Operator** value.
## Viewing and editing silences
@@ -52,6 +57,6 @@ Grafana alerting UI supports managing external Alertmanager silences. Once you a
When linking to a silence form, provide the default matching labels and comment via `matchers` and `comment` query parameters. The `matchers` parameter requires one more matching labels of the type `[label][operator][value]` joined by a comma while the `operator` parameter can be one of the following: `=` (equals, not regex), `!=` (not equals, not regex), `=~` (equals, regex), `!~` (not equals, regex).
For example, to link to silence form with matching labels `severity=critical` & `cluster!~europe-.*` and comment `Silence critical EU alerts`, create a URL `https://mygrafana/aleting/silence/new?matchers=severity%3Dcritical%2Ccluster!~europe-*&comment=Silence%20critical%20EU%20alert`.
For example, to link to silence form with matching labels `severity=critical` & `cluster!~europe-.*` and comment `Silence critical EU alerts`, create a URL `https://mygrafana/alerting/silence/new?matchers=severity%3Dcritical%2Ccluster!~europe-*&comment=Silence%20critical%20EU%20alert`.
To link to a new silence page for an [external Alertmanager]({{< relref "../../datasources/alertmanager.md" >}}), add a `alertmanager` query parameter with the Alertmanager data source name.
@@ -40,7 +40,7 @@ You can use the [Flux query and scripting language](https://www.influxdata.com/p
## Supported macros
The macros support copying and pasting from [Chronograph](https://www.influxdata.com/time-series-platform/chronograf/).
The macros support copying and pasting from [Chronograf](https://www.influxdata.com/time-series-platform/chronograf/).
| Macro example | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+1 -1
View File
@@ -69,7 +69,7 @@ Status Codes:
`GET /api/dashboards/id/:dashboardId/versions/:id`
Get the dashboard version with the given id, for the dashboard with the given id.
Get the dashboard version with the given version, for the dashboard with the given id.
**Example request for getting a dashboard version**:
+1
View File
@@ -8,6 +8,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 8.2.2]({{< relref "release-notes-8-2-2" >}})
- [Release notes for 8.2.1]({{< relref "release-notes-8-2-1" >}})
- [Release notes for 8.2.0]({{< relref "release-notes-8-2-0" >}})
- [Release notes for 8.2.0-beta2]({{< relref "release-notes-8-2-0-beta2" >}})
@@ -0,0 +1,34 @@
+++
title = "Release notes for Grafana 8.2.2"
[_build]
list = false
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 8.2.2
### Features and enhancements
- **Annotations:** We have improved tag search performance. [#40567](https://github.com/grafana/grafana/pull/40567), [@ashharrison90](https://github.com/ashharrison90)
- **Application:** You can now configure an error-template title. [#40310](https://github.com/grafana/grafana/pull/40310), [@benrubson](https://github.com/benrubson)
- **AzureMonitor:** We removed a restriction from the resource filter query. [#40690](https://github.com/grafana/grafana/pull/40690), [@andresmgot](https://github.com/andresmgot)
- **Caching:** Make cache size metric collection optional. (Enterprise)
- **Packaging:** We removed the ProcSubset option in systemd. This option prevented Grafana from starting in LXC environments. [#40339](https://github.com/grafana/grafana/pull/40339), [@kminehart](https://github.com/kminehart)
- **Prometheus:** We removed the autocomplete limit for metrics. [#39363](https://github.com/grafana/grafana/pull/39363), [@ivanahuckova](https://github.com/ivanahuckova)
- **Request interceptor:** Allow MSSQL's named instances. (Enterprise)
- **Table:** We improved the styling of the type icons to make them more distinct from column / field name. [#40596](https://github.com/grafana/grafana/pull/40596), [@torkelo](https://github.com/torkelo)
- **ValueMappings:** You can now use value mapping in stat, gauge, bar gauge, and pie chart visualizations. [#40612](https://github.com/grafana/grafana/pull/40612), [@torkelo](https://github.com/torkelo)
### Bug fixes
- **Alerting:** Fix panic when Slack's API sends unexpected response. [#40721](https://github.com/grafana/grafana/pull/40721), [@santihernandezc](https://github.com/santihernandezc)
- **Alerting:** The Create Alert button now appears on the dashboard panel when you are working with a default datasource. [#40334](https://github.com/grafana/grafana/pull/40334), [@domasx2](https://github.com/domasx2)
- **Explore:** We fixed the problem where the Explore log panel disappears when an Elasticsearch logs query returns no results. [#40217](https://github.com/grafana/grafana/pull/40217), [@Elfo404](https://github.com/Elfo404)
- **Graph:** You can now see annotation descriptions on hover. [#40581](https://github.com/grafana/grafana/pull/40581), [@axelavargas](https://github.com/axelavargas)
- **Logs:** The system now uses the JSON parser only if the line is parsed to an object. [#40507](https://github.com/grafana/grafana/pull/40507), [@ivanahuckova](https://github.com/ivanahuckova)
- **Prometheus:** We fixed the issue where the system did not reuse TCP connections when querying from Grafana alerting. [#40349](https://github.com/grafana/grafana/pull/40349), [@kminehart](https://github.com/kminehart)
- **Prometheus:** We fixed the problem that resulted in an error when a user created a query with a $\_\_interval min step. [#40525](https://github.com/grafana/grafana/pull/40525), [@ivanahuckova](https://github.com/ivanahuckova)
- **RowsToFields:** We fixed the issue where the system was not properly interpreting number values. [#40580](https://github.com/grafana/grafana/pull/40580), [@torkelo](https://github.com/torkelo)
- **Scale:** We fixed how the system handles NaN percent when data min = data max. [#40622](https://github.com/grafana/grafana/pull/40622), [@torkelo](https://github.com/torkelo)
- **Table panel:** You can now create a filter that includes special characters. [#40458](https://github.com/grafana/grafana/pull/40458), [@dprokop](https://github.com/dprokop)
+6 -4
View File
@@ -19,7 +19,7 @@
"test:accessibility-report": "./scripts/generate-a11y-report.sh",
"lint": "yarn run lint:ts && yarn run lint:sass",
"lint:ts": "eslint . --ext .js,.tsx,.ts --cache",
"lint:sass": "yarn run sass-lint -c public/sass/.sass-lint.yml 'public/sass/**/*.scss, packages/**/*.scss' -v -i '**/node_modules/**/*.scss'",
"lint:sass": "yarn stylelint '{public/sass,packages}/**/*.scss' --cache",
"test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%}",
"lint:fix": "yarn lint --fix",
"packages:build": "lerna run clean && lerna run build --ignore @grafana-plugins/input-datasource",
@@ -90,7 +90,7 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-rc.6",
"@testing-library/jest-dom": "5.11.5",
"@testing-library/react": "12.1.2",
"@testing-library/react-hooks": "^3.2.1",
"@testing-library/react-hooks": "7.0.2",
"@testing-library/user-event": "13.3.0",
"@types/angular": "1.6.56",
"@types/angular-route": "1.7.0",
@@ -120,7 +120,7 @@
"@types/rc-time-picker": "^3",
"@types/react": "17.0.30",
"@types/react-beautiful-dnd": "13.1.1",
"@types/react-dom": "16.9.9",
"@types/react-dom": "17.0.10",
"@types/react-grid-layout": "1.1.1",
"@types/react-highlight-words": "^0.16.2",
"@types/react-loadable": "5.5.2",
@@ -207,10 +207,12 @@
"rimraf": "3.0.1",
"rxjs-spy": "8.0.0",
"sass": "1.32.13",
"sass-lint": "1.12.1",
"sass-loader": "12.1.0",
"sinon": "8.1.1",
"style-loader": "3.3.0",
"stylelint": "13.13.1",
"stylelint-config-prettier": "9.0.3",
"stylelint-config-sass-guidelines": "8.0.0",
"terser-webpack-plugin": "5.2.4",
"testing-library-selector": "^0.1.3",
"ts-jest": "26.4.4",
+1 -1
View File
@@ -45,7 +45,7 @@
"@types/node": "10.14.1",
"@types/papaparse": "5.2.0",
"@types/react": "17.0.30",
"@types/rollup-plugin-visualizer": "2.6.0",
"@types/rollup-plugin-visualizer": "4.2.1",
"@types/sinon": "^10.0.4",
"rollup": "2.33.3",
"rollup-plugin-sourcemaps": "0.6.3",
+10 -4
View File
@@ -47,8 +47,8 @@ export const onUpdateDatasourceResetOption = (props: DataSourcePluginOptionsEdit
updateDatasourcePluginResetOption(props, key);
};
export function updateDatasourcePluginOption(
props: DataSourcePluginOptionsEditorProps,
export function updateDatasourcePluginOption<J, S extends {} = KeyValue>(
props: DataSourcePluginOptionsEditorProps<J, S>,
key: keyof DataSourceSettings,
val: any
) {
@@ -82,21 +82,27 @@ export const updateDatasourcePluginSecureJsonDataOption = <J, S extends {} = Key
val: any
) => {
const config = props.options;
if (!config.secureJsonData) {
return;
}
props.onOptionsChange({
...config,
secureJsonData: {
...config.secureJsonData!,
...config.secureJsonData,
[key]: val,
},
});
};
export const updateDatasourcePluginResetOption = <J, S extends {} = KeyValue>(
props: DataSourcePluginOptionsEditorProps,
props: DataSourcePluginOptionsEditorProps<J, S>,
key: string
) => {
const config = props.options;
if (!config.secureJsonData) {
return;
}
props.onOptionsChange({
...config,
+1 -1
View File
@@ -27,7 +27,7 @@
"@rollup/plugin-commonjs": "16.0.0",
"@rollup/plugin-node-resolve": "10.0.0",
"@types/node": "13.7.7",
"@types/rollup-plugin-visualizer": "2.6.0",
"@types/rollup-plugin-visualizer": "4.2.1",
"@types/systemjs": "^0.20.6",
"rollup": "2.33.3",
"rollup-plugin-sourcemaps": "0.6.3",
+1 -1
View File
@@ -33,7 +33,7 @@
"@rollup/plugin-commonjs": "16.0.0",
"@rollup/plugin-node-resolve": "10.0.0",
"@types/node": "13.7.7",
"@types/rollup-plugin-visualizer": "2.6.0",
"@types/rollup-plugin-visualizer": "4.2.1",
"date-fns": "2.21.3",
"rollup": "2.33.3",
"rollup-plugin-copy": "3.3.0",
+1 -1
View File
@@ -37,7 +37,7 @@
"@rollup/plugin-node-resolve": "10.0.0",
"@types/history": "^4.7.8",
"@types/jest": "26.0.15",
"@types/rollup-plugin-visualizer": "2.6.0",
"@types/rollup-plugin-visualizer": "4.2.1",
"@types/systemjs": "^0.20.6",
"lodash": "4.17.21",
"rollup": "2.33.3",
@@ -0,0 +1,59 @@
import { BusEventBase, BusEventWithPayload, EventBus, GrafanaTheme2, PanelModel, TimeRange } from '@grafana/data';
/**
* Called when a dashboard is refreshed
*
* @public
*/
export class RefreshEvent extends BusEventBase {
static type = 'refresh';
}
/**
* Called when the theme settings change
*
* @public
*/
export class ThemeChangedEvent extends BusEventWithPayload<GrafanaTheme2> {
static type = 'theme-changed';
}
/**
* Called when time range is updated
*
* @public
*/
export class TimeRangeUpdatedEvent extends BusEventWithPayload<TimeRange> {
static type = 'time-range-updated';
}
/**
* Called to copy a panel JSON into local storage
*
* @public
*/
export class CopyPanelEvent extends BusEventWithPayload<PanelModel> {
static type = 'copy-panel';
}
// Internal singleton instance
let singletonInstance: EventBus;
/**
* Used during startup by Grafana to set the LocationSrv so it is available
* via the {@link getLocationSrv} to the rest of the application.
*
* @internal
*/
export function setAppEvents(instance: EventBus) {
singletonInstance = instance;
}
/**
* Used to retrieve an event bus that manages application level events
*
* @public
*/
export function getAppEvents(): EventBus {
return singletonInstance;
}
@@ -7,3 +7,4 @@ export * from './templateSrv';
export * from './legacyAngularInjector';
export * from './live';
export * from './LocationService';
export * from './appEvents';
+1 -1
View File
@@ -26,7 +26,7 @@
"@rollup/plugin-commonjs": "16.0.0",
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "10.0.0",
"@types/rollup-plugin-visualizer": "2.6.0",
"@types/rollup-plugin-visualizer": "4.2.1",
"rollup": "2.33.3",
"rollup-plugin-sourcemaps": "0.6.3",
"rollup-plugin-terser": "7.0.2",
+2 -2
View File
@@ -39,8 +39,8 @@ export const parameters = {
container: ThemedDocsContainer,
},
darkMode: {
dark: GrafanaDark,
light: GrafanaLight,
dark: { ...GrafanaDark, base: 'dark' },
light: { ...GrafanaLight, base: 'light' },
},
layout: 'fullscreen',
actions: { argTypesRegex: '^on[A-Z].*' },
@@ -5,8 +5,6 @@ import '../src/components/Icon/iconBundle';
const createStorybookTheme = (theme: GrafanaTheme2) => {
return create({
base: theme.name.includes('Light') ? 'light' : 'dark',
colorPrimary: theme.colors.primary.main,
colorSecondary: theme.colors.error.main,
+1 -1
View File
@@ -111,7 +111,7 @@
"@types/react-table": "7.7.2",
"@types/react-test-renderer": "17.0.1",
"@types/react-transition-group": "4.4.0",
"@types/rollup-plugin-visualizer": "2.6.0",
"@types/rollup-plugin-visualizer": "4.2.1",
"@types/slate": "0.47.2",
"@types/slate-react": "0.22.5",
"@types/tinycolor2": "1.4.1",
@@ -13,6 +13,7 @@ export interface IconProps extends React.HTMLAttributes<HTMLDivElement> {
name: IconName;
size?: IconSize;
type?: IconType;
title?: string;
}
const getIconStyles = stylesFactory((theme: GrafanaTheme) => {
@@ -44,7 +45,7 @@ function getIconSubDir(name: IconName, type: string): string {
}
export const Icon = React.forwardRef<HTMLDivElement, IconProps>(
({ size = 'md', type = 'default', name, className, style, ...divElementProps }, ref) => {
({ size = 'md', type = 'default', name, className, style, title = '', ...divElementProps }, ref) => {
const theme = useTheme();
/* Temporary solution to display also font awesome icons */
@@ -73,6 +74,7 @@ export const Icon = React.forwardRef<HTMLDivElement, IconProps>(
src={svgPath}
width={svgWid}
height={svgHgt}
title={title}
className={cx(styles.icon, className, type === 'mono' ? { [styles.orange]: name === 'favorite' } : '')}
style={style}
/>
@@ -7,6 +7,7 @@ import { SlideOutTransition } from '../transitions/SlideOutTransition';
import { FadeTransition } from '../transitions/FadeTransition';
import { Spinner } from '../Spinner/Spinner';
import { GrafanaTheme2 } from '@grafana/data';
import tinycolor from 'tinycolor2';
const getStyles = (theme: GrafanaTheme2) => {
const singleValue = css`
@@ -35,7 +36,7 @@ const getStyles = (theme: GrafanaTheme2) => {
`;
const disabled = css`
color: ${theme.colors.action.disabledText};
color: ${tinycolor(theme.colors.text.disabled).setAlpha(0.64).toString()};
`;
return { singleValue, container, item, disabled };
@@ -95,8 +95,9 @@ $height-lg: ${theme.v1.height.lg};
// Typography
// -------------------------
/* stylelint-disable-next-line string-quotes */
$font-family-sans-serif: ${theme.v1.typography.fontFamily.sansSerif};
/* stylelint-disable-next-line string-quotes */
$font-family-monospace: ${theme.v1.typography.fontFamily.monospace};
$font-size-base: ${theme.v1.typography.size.base} !default;
+3 -1
View File
@@ -27,7 +27,9 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
WORKDIR $GF_PATHS_HOME
RUN apk add --no-cache ca-certificates bash tzdata && \
apk add --no-cache openssl musl-utils
apk add --no-cache musl-utils
RUN apk add --no-cache openssl --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
# Oracle Support for x86_64 only
RUN if [ `arch` = "x86_64" ]; then \
+11
View File
@@ -72,6 +72,17 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext, cmd models.AddApiKeyComman
}
}
cmd.OrgId = c.OrgId
var err error
var serviceAccount *models.User = &models.User{Id: -1}
if hs.Cfg.FeatureToggles["service-accounts"] {
if cmd.CreateNewServiceAccount {
serviceAccount, err = hs.AccessControl.CloneUserToServiceAccount(c.Req.Context(), c.SignedInUser)
if err != nil {
return response.Error(500, "Unable to clone user to service account", err)
}
cmd.ServiceAccountId = serviceAccount.Id
}
}
newKeyInfo, err := apikeygen.New(cmd.OrgId, cmd.Name)
if err != nil {
+12 -5
View File
@@ -199,16 +199,19 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
navTree = append(navTree, hs.getProfileNode(c))
}
if setting.AlertingEnabled || hs.Cfg.UnifiedAlerting.Enabled {
_, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgId]
uaVisibleForOrg := hs.Cfg.UnifiedAlerting.Enabled && !uaIsDisabledForOrg
if setting.AlertingEnabled || uaVisibleForOrg {
alertChildNavs := []*dtos.NavLink{
{Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul"},
}
if hs.Cfg.UnifiedAlerting.Enabled {
if uaVisibleForOrg {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Alert groups", Id: "groups", Url: hs.Cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
}
if c.OrgRole == models.ROLE_ADMIN || c.OrgRole == models.ROLE_EDITOR {
if hs.Cfg.UnifiedAlerting.Enabled {
if uaVisibleForOrg {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Contact points", Id: "receivers", Url: hs.Cfg.AppSubURL + "/alerting/notifications",
Icon: "comment-alt-share",
@@ -221,7 +224,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
})
}
}
if c.OrgRole == models.ROLE_ADMIN && hs.Cfg.UnifiedAlerting.Enabled {
if c.OrgRole == models.ROLE_ADMIN && uaVisibleForOrg {
alertChildNavs = append(alertChildNavs, &dtos.NavLink{
Text: "Admin", Id: "alerting-admin", Url: hs.Cfg.AppSubURL + "/alerting/admin",
Icon: "cog",
@@ -380,7 +383,11 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink
Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "import",
Url: hs.Cfg.AppSubURL + "/dashboard/import",
})
if setting.AlertingEnabled || hs.Cfg.UnifiedAlerting.Enabled {
_, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgId]
uaVisibleForOrg := hs.Cfg.UnifiedAlerting.Enabled && !uaIsDisabledForOrg
if setting.AlertingEnabled || uaVisibleForOrg {
children = append(children, &dtos.NavLink{
Text: "Alert rule", SubTitle: "Create an alert rule", Id: "alert",
Icon: "bell", Url: hs.Cfg.AppSubURL + "/alerting/new",
+16 -13
View File
@@ -13,24 +13,27 @@ var (
)
type ApiKey struct {
Id int64
OrgId int64
Name string
Key string
Role RoleType
Created time.Time
Updated time.Time
Expires *int64
Id int64
OrgId int64
Name string
Key string
Role RoleType
Created time.Time
Updated time.Time
Expires *int64
ServiceAccountId int64
}
// ---------------------
// COMMANDS
type AddApiKeyCommand struct {
Name string `json:"name" binding:"Required"`
Role RoleType `json:"role" binding:"Required"`
OrgId int64 `json:"-"`
Key string `json:"-"`
SecondsToLive int64 `json:"secondsToLive"`
Name string `json:"name" binding:"Required"`
Role RoleType `json:"role" binding:"Required"`
OrgId int64 `json:"-"`
Key string `json:"-"`
SecondsToLive int64 `json:"secondsToLive"`
ServiceAccountId int64 `json:"serviceAccount"`
CreateNewServiceAccount bool `json:"createServiceAccount"`
Result *ApiKey `json:"-"`
}
+13 -12
View File
@@ -56,18 +56,19 @@ func (u *User) NameOrFallback() string {
// COMMANDS
type CreateUserCommand struct {
Email string
Login string
Name string
Company string
OrgId int64
OrgName string
Password string
EmailVerified bool
IsAdmin bool
IsDisabled bool
SkipOrgSetup bool
DefaultOrgRole string
Email string
Login string
Name string
Company string
OrgId int64
OrgName string
Password string
EmailVerified bool
IsAdmin bool
IsDisabled bool
SkipOrgSetup bool
DefaultOrgRole string
IsServiceAccount bool
Result User
}
@@ -14,6 +14,15 @@ type AccessControl interface {
// GetUserPermissions returns user permissions.
GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*Permission, error)
// GetUserRoles returns user roles.
GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*RoleDTO, error)
// CloneUserToServiceAccount Creates a new service account and assigns it the same roles as the user has
CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error)
// LinkAPIKeyToServiceAccount Connects an APIkey to a service account. Multiple API keys may be linked to one account.
LinkAPIKeyToServiceAccount(ctx context.Context, ApiKey *models.ApiKey, serviceAccount *models.User) error
//IsDisabled returns if access control is enabled or not
IsDisabled() bool
+52 -12
View File
@@ -14,17 +14,24 @@ type fullAccessControl interface {
}
type Calls struct {
Evaluate []interface{}
GetUserPermissions []interface{}
IsDisabled []interface{}
DeclareFixedRoles []interface{}
GetUserBuiltInRoles []interface{}
RegisterFixedRoles []interface{}
CloneUserToServiceAccount []interface{}
Evaluate []interface{}
GetUserPermissions []interface{}
GetUserRoles []interface{}
IsDisabled []interface{}
DeclareFixedRoles []interface{}
GetUserBuiltInRoles []interface{}
RegisterFixedRoles []interface{}
LinkAPIKeyToServiceAccount []interface{}
}
type Mock struct {
// Unless an override is provided, user will be returned by CloneUserToServiceAccount
createduser *models.User
// Unless an override is provided, permissions will be returned by GetUserPermissions
permissions []*accesscontrol.Permission
// Unless an override is provided, roles will be returned by GetUserRoles
roles []*accesscontrol.RoleDTO
// Unless an override is provided, disabled will be returned by IsDisabled
disabled bool
// Unless an override is provided, builtInRoles will be returned by GetUserBuiltInRoles
@@ -34,12 +41,15 @@ type Mock struct {
Calls Calls
// Override functions
EvaluateFunc func(context.Context, *models.SignedInUser, accesscontrol.Evaluator) (bool, error)
GetUserPermissionsFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.Permission, error)
IsDisabledFunc func() bool
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
GetUserBuiltInRolesFunc func(user *models.SignedInUser) []string
RegisterFixedRolesFunc func() error
CloneUserToServiceAccountFunc func(context.Context, *models.SignedInUser) (*models.User, error)
LinkAPIKeyToServiceAccountFunc func(context.Context, *models.ApiKey, *models.User) error
EvaluateFunc func(context.Context, *models.SignedInUser, accesscontrol.Evaluator) (bool, error)
GetUserPermissionsFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.Permission, error)
GetUserRolesFunc func(context.Context, *models.SignedInUser) ([]*accesscontrol.RoleDTO, error)
IsDisabledFunc func() bool
DeclareFixedRolesFunc func(...accesscontrol.RoleRegistration) error
GetUserBuiltInRolesFunc func(user *models.SignedInUser) []string
RegisterFixedRolesFunc func() error
}
// Ensure the mock stays in line with the interface
@@ -99,6 +109,36 @@ func (m *Mock) GetUserPermissions(ctx context.Context, user *models.SignedInUser
return m.permissions, nil
}
func (m *Mock) GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.RoleDTO, error) {
m.Calls.GetUserRoles = append(m.Calls.GetUserRoles, []interface{}{ctx, user})
// Use override if provided
if m.GetUserRolesFunc != nil {
return m.GetUserRolesFunc(ctx, user)
}
// Otherwise return the Roles list
return m.roles, nil
}
func (m *Mock) CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error) {
m.Calls.CloneUserToServiceAccount = append(m.Calls.CloneUserToServiceAccount, []interface{}{ctx, user})
// Use override if provided
if m.CloneUserToServiceAccountFunc != nil {
return m.CloneUserToServiceAccountFunc(ctx, user)
}
// Otherwise return the user
return m.createduser, nil
}
func (m *Mock) LinkAPIKeyToServiceAccount(ctx context.Context, apikey *models.ApiKey, service_account *models.User) error {
m.Calls.LinkAPIKeyToServiceAccount = append(m.Calls.LinkAPIKeyToServiceAccount, []interface{}{ctx, apikey, service_account})
// Use override if provided
if m.LinkAPIKeyToServiceAccountFunc != nil {
return m.LinkAPIKeyToServiceAccountFunc(ctx, apikey, service_account)
}
// Otherwise return the default
return nil
}
// Middleware checks if service disabled or not to switch to fallback authorization.
// This mock return m.disabled unless an override is provided.
func (m *Mock) IsDisabled() bool {
@@ -2,6 +2,7 @@ package ossaccesscontrol
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
@@ -71,6 +72,21 @@ func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.Si
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
}
// GetUserRoles returns user permissions based on built-in roles
func (ac *OSSAccessControlService) GetUserRoles(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.RoleDTO, error) {
return nil, errors.New("unsupported function") //OSS users will continue to use builtin roles via GetUserPermissions
}
// CloneUserToServiceAccount creates a service account with permissions based on a user
func (ac *OSSAccessControlService) CloneUserToServiceAccount(ctx context.Context, user *models.SignedInUser) (*models.User, error) {
return nil, errors.New("clone user not implemented yet in service accounts") //Please switch on Enterprise to test this
}
// Link creates a service account with permissions based on a user
func (ac *OSSAccessControlService) LinkAPIKeyToServiceAccount(context.Context, *models.ApiKey, *models.User) error {
return errors.New("link SA not implemented yet in service accounts") //Please switch on Enterprise to test this
}
// GetUserPermissions returns user permissions based on built-in roles
func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user *models.SignedInUser) ([]*accesscontrol.Permission, error) {
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
+38
View File
@@ -3,6 +3,8 @@ package accesscontrol
import (
"fmt"
"strings"
"github.com/grafana/grafana/pkg/models"
)
// Scope builds scope from parts
@@ -29,3 +31,39 @@ func Parameter(key string) string {
func Field(key string) string {
return fmt.Sprintf(`{{ .%s }}`, key)
}
type KeywordScopeResolveFunc func(*models.SignedInUser) (string, error)
// ScopeResolver contains a map of functions to resolve scope keywords such as `self` or `current` into `id` based scopes
type ScopeResolver struct {
keywordResolvers map[string]KeywordScopeResolveFunc
}
func NewScopeResolver() ScopeResolver {
return ScopeResolver{
keywordResolvers: map[string]KeywordScopeResolveFunc{
"orgs:current": resolveCurrentOrg,
"users:self": resolveUserSelf,
},
}
}
func resolveCurrentOrg(u *models.SignedInUser) (string, error) {
return Scope("orgs", "id", fmt.Sprintf("%v", u.OrgId)), nil
}
func resolveUserSelf(u *models.SignedInUser) (string, error) {
return Scope("users", "id", fmt.Sprintf("%v", u.UserId)), nil
}
// ResolveKeyword resolves scope with keywords such as `self` or `current` into `id` based scopes
func (s *ScopeResolver) ResolveKeyword(user *models.SignedInUser, permission Permission) (*Permission, error) {
if fn, ok := s.keywordResolvers[permission.Scope]; ok {
resolvedScope, err := fn(user)
if err != nil {
return nil, fmt.Errorf("could not resolve %v: %v", permission.Scope, err)
}
permission.Scope = resolvedScope
}
return &permission, nil
}
@@ -1,43 +0,0 @@
package accesscontrol
import (
"fmt"
"github.com/grafana/grafana/pkg/models"
)
type KeywordScopeResolveFunc func(*models.SignedInUser) (string, error)
// ScopeResolver contains a map of functions to resolve scope keywords such as `self` or `current` into `id` based scopes
type ScopeResolver struct {
keywordResolvers map[string]KeywordScopeResolveFunc
}
func NewScopeResolver() ScopeResolver {
return ScopeResolver{
keywordResolvers: map[string]KeywordScopeResolveFunc{
"orgs:current": resolveCurrentOrg,
"users:self": resolveUserSelf,
},
}
}
func resolveCurrentOrg(u *models.SignedInUser) (string, error) {
return Scope("orgs", "id", fmt.Sprintf("%v", u.OrgId)), nil
}
func resolveUserSelf(u *models.SignedInUser) (string, error) {
return Scope("users", "id", fmt.Sprintf("%v", u.UserId)), nil
}
// ResolveKeyword resolves scope with keywords such as `self` or `current` into `id` based scopes
func (s *ScopeResolver) ResolveKeyword(user *models.SignedInUser, permission Permission) (*Permission, error) {
if fn, ok := s.keywordResolvers[permission.Scope]; ok {
resolvedScope, err := fn(user)
if err != nil {
return nil, fmt.Errorf("could not resolve %v: %v", permission.Scope, err)
}
permission.Scope = resolvedScope
}
return &permission, nil
}
+8 -6
View File
@@ -382,15 +382,17 @@ func (sn *SlackNotifier) sendRequest(ctx context.Context, data []byte) error {
return fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode/100 == 2 {
var rslt map[string]interface{}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
// Slack responds to some requests with a JSON document, that might contain an error
rslt := struct {
Ok bool `json:"ok"`
Err string `json:"error"`
}{}
if err := json.Unmarshal(body, &rslt); err == nil {
if !rslt["ok"].(bool) {
errMsg := rslt["error"].(string)
if !rslt.Ok && rslt.Err != "" {
sn.log.Warn("Sending Slack API request failed", "url", sn.url.String(), "statusCode", resp.Status,
"err", errMsg)
return fmt.Errorf("failed to make Slack API request: %s", errMsg)
"err", rslt.Err)
return fmt.Errorf("failed to make Slack API request: %s", rslt.Err)
}
}
@@ -2,6 +2,9 @@ package notifiers
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -223,3 +226,102 @@ func TestSlackNotifier(t *testing.T) {
assert.Equal(t, "1ABCDE", slackNotifier.recipient)
})
}
func TestSendSlackRequest(t *testing.T) {
tests := []struct {
name string
slackResponse string
statusCode int
expectError bool
}{
{
name: "Example error",
slackResponse: `{
"ok": false,
"error": "too_many_attachments"
}`,
statusCode: http.StatusBadRequest,
expectError: true,
},
{
name: "Non 200 status code, no response body",
statusCode: http.StatusMovedPermanently,
expectError: true,
},
{
name: "Success case, normal response body",
slackResponse: `{
"ok": true,
"channel": "C1H9RESGL",
"ts": "1503435956.000247",
"message": {
"text": "Here's a message for you",
"username": "ecto1",
"bot_id": "B19LU7CSY",
"attachments": [
{
"text": "This is an attachment",
"id": 1,
"fallback": "This is an attachment's fallback"
}
],
"type": "message",
"subtype": "bot_message",
"ts": "1503435956.000247"
}
}`,
statusCode: http.StatusOK,
expectError: false,
},
{
name: "Success case, no response body",
statusCode: http.StatusOK,
expectError: false,
},
{
name: "Success case, unexpected response body",
statusCode: http.StatusOK,
slackResponse: "{}",
expectError: false,
},
{
name: "Success case, ok: true",
statusCode: http.StatusOK,
slackResponse: "{\"ok\": true}",
expectError: false,
},
{
name: "200 status code, error in body",
statusCode: http.StatusOK,
slackResponse: `{"ok": false, "error": "test error"}`,
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.statusCode)
_, err := w.Write([]byte(test.slackResponse))
require.NoError(tt, err)
}))
settingsJSON, err := simplejson.NewJson([]byte(fmt.Sprintf(`{"url": %q}`, server.URL)))
require.NoError(t, err)
model := &models.AlertNotification{
Settings: settingsJSON,
}
not, err := NewSlackNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.NoError(t, err)
slackNotifier := not.(*SlackNotifier)
err = slackNotifier.sendRequest(context.TODO(), []byte("test"))
if !test.expectError {
require.NoError(tt, err)
} else {
require.Error(tt, err)
}
})
}
}
+23 -27
View File
@@ -5,37 +5,33 @@ import (
"time"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestCleanUpTmpFiles(t *testing.T) {
Convey("Cleanup service tests", t, func() {
cfg := setting.Cfg{}
cfg.TempDataLifetime, _ = time.ParseDuration("24h")
service := CleanUpService{
Cfg: &cfg,
}
now := time.Now()
secondAgo := now.Add(-time.Second)
twoDaysAgo := now.Add(-time.Second * 3600 * 24 * 2)
weekAgo := now.Add(-time.Second * 3600 * 24 * 7)
cfg := setting.Cfg{}
cfg.TempDataLifetime, _ = time.ParseDuration("24h")
service := CleanUpService{
Cfg: &cfg,
}
now := time.Now()
secondAgo := now.Add(-time.Second)
twoDaysAgo := now.Add(-time.Second * 3600 * 24 * 2)
weekAgo := now.Add(-time.Second * 3600 * 24 * 7)
t.Run("Should not cleanup recent files", func(t *testing.T) {
require.False(t, service.shouldCleanupTempFile(secondAgo, now))
})
t.Run("Should cleanup older files", func(t *testing.T) {
require.True(t, service.shouldCleanupTempFile(twoDaysAgo, now))
})
Convey("Should not cleanup recent files", func() {
So(service.shouldCleanupTempFile(secondAgo, now), ShouldBeFalse)
})
t.Run("After increasing temporary files lifetime, older files should be kept", func(t *testing.T) {
cfg.TempDataLifetime, _ = time.ParseDuration("1000h")
require.False(t, service.shouldCleanupTempFile(weekAgo, now))
})
Convey("Should cleanup older files", func() {
So(service.shouldCleanupTempFile(twoDaysAgo, now), ShouldBeTrue)
})
Convey("After increasing temporary files lifetime, older files should be kept", func() {
cfg.TempDataLifetime, _ = time.ParseDuration("1000h")
So(service.shouldCleanupTempFile(weekAgo, now), ShouldBeFalse)
})
Convey("If lifetime is 0, files should never be cleaned up", func() {
cfg.TempDataLifetime = 0
So(service.shouldCleanupTempFile(weekAgo, now), ShouldBeFalse)
})
t.Run("If lifetime is 0, files should never be cleaned up", func(t *testing.T) {
cfg.TempDataLifetime = 0
require.False(t, service.shouldCleanupTempFile(weekAgo, now))
})
}
+26 -4
View File
@@ -218,11 +218,33 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo
return true
}
if apikey.ServiceAccountId < 1 { //There is no service account attached to the apikey
//Use the old APIkey method. This provides backwards compatibility.
reqContext.SignedInUser = &models.SignedInUser{}
reqContext.OrgRole = apikey.Role
reqContext.ApiKeyId = apikey.Id
reqContext.OrgId = apikey.OrgId
reqContext.IsSignedIn = true
return true
}
//There is a service account attached to the API key
//Use service account linked to API key as the signed in user
query := models.GetSignedInUserQuery{UserId: apikey.ServiceAccountId, OrgId: apikey.OrgId}
if err := bus.Dispatch(&query); err != nil {
reqContext.Logger.Error(
"Failed to link API key to service account in",
"id", query.UserId,
"org", query.OrgId,
"err", err,
)
reqContext.JsonApiErr(500, "Unable to link API key to service account", err)
return true
}
reqContext.IsSignedIn = true
reqContext.SignedInUser = &models.SignedInUser{}
reqContext.OrgRole = apikey.Role
reqContext.ApiKeyId = apikey.Id
reqContext.OrgId = apikey.OrgId
reqContext.SignedInUser = query.Result
return true
}
+370 -371
View File
@@ -6,443 +6,442 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ldap"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestMultiLDAP(t *testing.T) {
Convey("Multildap", t, func() {
Convey("Ping()", func() {
Convey("Should return error for absent config list", func() {
setup()
t.Run("Ping()", func(t *testing.T) {
t.Run("Should return error for absent config list", func(t *testing.T) {
setup()
multi := New([]*ldap.ServerConfig{})
_, err := multi.Ping()
multi := New([]*ldap.ServerConfig{})
_, err := multi.Ping()
So(err, ShouldBeError)
So(err, ShouldEqual, ErrNoLDAPServers)
require.Error(t, err)
require.Equal(t, ErrNoLDAPServers, err)
teardown()
})
Convey("Should return an unavailable status on dial error", func() {
mock := setup()
expectedErr := errors.New("Dial error")
mock.dialErrReturn = expectedErr
multi := New([]*ldap.ServerConfig{
{Host: "10.0.0.1", Port: 361},
})
statuses, err := multi.Ping()
So(err, ShouldBeNil)
So(statuses[0].Host, ShouldEqual, "10.0.0.1")
So(statuses[0].Port, ShouldEqual, 361)
So(statuses[0].Available, ShouldBeFalse)
So(statuses[0].Error, ShouldEqual, expectedErr)
So(mock.closeCalledTimes, ShouldEqual, 0)
teardown()
})
Convey("Should get the LDAP server statuses", func() {
mock := setup()
multi := New([]*ldap.ServerConfig{
{Host: "10.0.0.1", Port: 361},
})
statuses, err := multi.Ping()
So(err, ShouldBeNil)
So(statuses[0].Host, ShouldEqual, "10.0.0.1")
So(statuses[0].Port, ShouldEqual, 361)
So(statuses[0].Available, ShouldBeTrue)
So(statuses[0].Error, ShouldBeNil)
So(mock.closeCalledTimes, ShouldEqual, 1)
teardown()
})
teardown()
})
Convey("Login()", func() {
Convey("Should return error for absent config list", func() {
setup()
t.Run("Should return an unavailable status on dial error", func(t *testing.T) {
mock := setup()
multi := New([]*ldap.ServerConfig{})
_, err := multi.Login(&models.LoginUserQuery{})
expectedErr := errors.New("Dial error")
mock.dialErrReturn = expectedErr
So(err, ShouldBeError)
So(err, ShouldEqual, ErrNoLDAPServers)
teardown()
multi := New([]*ldap.ServerConfig{
{Host: "10.0.0.1", Port: 361},
})
Convey("Should return a dial error", func() {
mock := setup()
statuses, err := multi.Ping()
expected := errors.New("Dial error")
mock.dialErrReturn = expected
require.Nil(t, err)
require.Equal(t, "10.0.0.1", statuses[0].Host)
require.Equal(t, 361, statuses[0].Port)
require.False(t, statuses[0].Available)
require.Equal(t, expectedErr, statuses[0].Error)
require.Equal(t, 0, mock.closeCalledTimes)
multi := New([]*ldap.ServerConfig{
{}, {},
})
teardown()
})
t.Run("Should get the LDAP server statuses", func(t *testing.T) {
mock := setup()
_, err := multi.Login(&models.LoginUserQuery{})
So(err, ShouldBeError)
So(err, ShouldEqual, expected)
teardown()
multi := New([]*ldap.ServerConfig{
{Host: "10.0.0.1", Port: 361},
})
Convey("Should call underlying LDAP methods", func() {
mock := setup()
statuses, err := multi.Ping()
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
require.Nil(t, err)
require.Equal(t, "10.0.0.1", statuses[0].Host)
require.Equal(t, 361, statuses[0].Port)
require.True(t, statuses[0].Available)
require.Nil(t, statuses[0].Error)
require.Equal(t, 1, mock.closeCalledTimes)
So(mock.dialCalledTimes, ShouldEqual, 2)
So(mock.loginCalledTimes, ShouldEqual, 2)
So(mock.closeCalledTimes, ShouldEqual, 2)
teardown()
})
})
t.Run("Login()", func(t *testing.T) {
t.Run("Should return error for absent config list", func(t *testing.T) {
setup()
So(err, ShouldEqual, ErrInvalidCredentials)
multi := New([]*ldap.ServerConfig{})
_, err := multi.Login(&models.LoginUserQuery{})
teardown()
})
require.Error(t, err)
require.Equal(t, ErrNoLDAPServers, err)
Convey("Should get login result", func() {
mock := setup()
mock.loginReturn = &models.ExternalUserInfo{
Login: "killa",
}
multi := New([]*ldap.ServerConfig{
{}, {},
})
result, err := multi.Login(&models.LoginUserQuery{})
So(mock.dialCalledTimes, ShouldEqual, 1)
So(mock.loginCalledTimes, ShouldEqual, 1)
So(mock.closeCalledTimes, ShouldEqual, 1)
So(result.Login, ShouldEqual, "killa")
So(err, ShouldBeNil)
teardown()
})
Convey("Should still call a second error for invalid not found error", func() {
mock := setup()
mock.loginErrReturn = ErrCouldNotFindUser
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
So(mock.dialCalledTimes, ShouldEqual, 2)
So(mock.loginCalledTimes, ShouldEqual, 2)
So(mock.closeCalledTimes, ShouldEqual, 2)
So(err, ShouldEqual, ErrInvalidCredentials)
teardown()
})
Convey("Should still try to auth with the second server after receiving an invalid credentials error from the first", func() {
mock := setup()
mock.loginErrReturn = ErrInvalidCredentials
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
So(mock.dialCalledTimes, ShouldEqual, 2)
So(mock.loginCalledTimes, ShouldEqual, 2)
So(mock.closeCalledTimes, ShouldEqual, 2)
So(err, ShouldEqual, ErrInvalidCredentials)
teardown()
})
Convey("Should still try to auth with the second server after receiving a dial error from the first", func() {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
So(mock.dialCalledTimes, ShouldEqual, 2)
So(err, ShouldEqual, expectedError)
teardown()
})
Convey("Should return unknown error", func() {
mock := setup()
expected := errors.New("Something unknown")
mock.loginErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
So(mock.dialCalledTimes, ShouldEqual, 1)
So(mock.loginCalledTimes, ShouldEqual, 1)
So(mock.closeCalledTimes, ShouldEqual, 1)
So(err, ShouldEqual, expected)
teardown()
})
teardown()
})
Convey("User()", func() {
Convey("Should return error for absent config list", func() {
setup()
t.Run("Should return a dial error", func(t *testing.T) {
mock := setup()
multi := New([]*ldap.ServerConfig{})
_, _, err := multi.User("test")
expected := errors.New("Dial error")
mock.dialErrReturn = expected
So(err, ShouldBeError)
So(err, ShouldEqual, ErrNoLDAPServers)
teardown()
multi := New([]*ldap.ServerConfig{
{}, {},
})
Convey("Should return a dial error", func() {
mock := setup()
_, err := multi.Login(&models.LoginUserQuery{})
expected := errors.New("Dial error")
mock.dialErrReturn = expected
require.Error(t, err)
require.Equal(t, expected, err)
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
So(err, ShouldBeError)
So(err, ShouldEqual, expected)
teardown()
})
Convey("Should call underlying LDAP methods", func() {
mock := setup()
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
So(mock.dialCalledTimes, ShouldEqual, 2)
So(mock.usersCalledTimes, ShouldEqual, 2)
So(mock.closeCalledTimes, ShouldEqual, 2)
So(err, ShouldEqual, ErrDidNotFindUser)
teardown()
})
Convey("Should return some error", func() {
mock := setup()
expected := errors.New("Killa Gorilla")
mock.usersErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
So(mock.dialCalledTimes, ShouldEqual, 1)
So(mock.usersCalledTimes, ShouldEqual, 1)
So(mock.closeCalledTimes, ShouldEqual, 1)
So(err, ShouldEqual, expected)
teardown()
})
Convey("Should get only one user", func() {
mock := setup()
mock.usersFirstReturn = []*models.ExternalUserInfo{
{
Login: "one",
},
{
Login: "two",
},
}
multi := New([]*ldap.ServerConfig{
{}, {},
})
user, _, err := multi.User("test")
So(mock.dialCalledTimes, ShouldEqual, 1)
So(mock.usersCalledTimes, ShouldEqual, 1)
So(mock.closeCalledTimes, ShouldEqual, 1)
So(err, ShouldBeNil)
So(user.Login, ShouldEqual, "one")
teardown()
})
Convey("Should still try to auth with the second server after receiving a dial error from the first", func() {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
So(mock.dialCalledTimes, ShouldEqual, 2)
So(err, ShouldEqual, expectedError)
teardown()
})
teardown()
})
Convey("Users()", func() {
Convey("Should still try to auth with the second server after receiving a dial error from the first", func() {
mock := setup()
t.Run("Should call underlying LDAP methods", func(t *testing.T) {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
So(mock.dialCalledTimes, ShouldEqual, 2)
So(err, ShouldEqual, expectedError)
teardown()
multi := New([]*ldap.ServerConfig{
{}, {},
})
Convey("Should return error for absent config list", func() {
setup()
_, err := multi.Login(&models.LoginUserQuery{})
multi := New([]*ldap.ServerConfig{})
_, err := multi.Users([]string{"test"})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.loginCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
So(err, ShouldBeError)
So(err, ShouldEqual, ErrNoLDAPServers)
require.Equal(t, ErrInvalidCredentials, err)
teardown()
teardown()
})
t.Run("Should get login result", func(t *testing.T) {
mock := setup()
mock.loginReturn = &models.ExternalUserInfo{
Login: "killa",
}
multi := New([]*ldap.ServerConfig{
{}, {},
})
result, err := multi.Login(&models.LoginUserQuery{})
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.loginCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Equal(t, "killa", result.Login)
require.Nil(t, err)
teardown()
})
t.Run("Should still call a second error for invalid not found error", func(t *testing.T) {
mock := setup()
mock.loginErrReturn = ErrCouldNotFindUser
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.loginCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Equal(t, ErrInvalidCredentials, err)
teardown()
})
t.Run("Should still try to auth with the second server after receiving an invalid credentials error from the first", func(t *testing.T) {
mock := setup()
mock.loginErrReturn = ErrInvalidCredentials
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.loginCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Equal(t, ErrInvalidCredentials, err)
teardown()
})
t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, expectedError, err)
teardown()
})
t.Run("Should return unknown error", func(t *testing.T) {
mock := setup()
expected := errors.New("Something unknown")
mock.loginErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&models.LoginUserQuery{})
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.loginCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Equal(t, expected, err)
teardown()
})
})
t.Run("User()", func(t *testing.T) {
t.Run("Should return error for absent config list", func(t *testing.T) {
setup()
multi := New([]*ldap.ServerConfig{})
_, _, err := multi.User("test")
require.Error(t, err)
require.Equal(t, ErrNoLDAPServers, err)
teardown()
})
t.Run("Should return a dial error", func(t *testing.T) {
mock := setup()
expected := errors.New("Dial error")
mock.dialErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
Convey("Should return a dial error", func() {
mock := setup()
_, _, err := multi.User("test")
expected := errors.New("Dial error")
mock.dialErrReturn = expected
require.Error(t, err)
require.Equal(t, expected, err)
multi := New([]*ldap.ServerConfig{
{}, {},
})
teardown()
})
_, err := multi.Users([]string{"test"})
t.Run("Should call underlying LDAP methods", func(t *testing.T) {
mock := setup()
So(err, ShouldBeError)
So(err, ShouldEqual, expected)
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
teardown()
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.usersCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Equal(t, ErrDidNotFindUser, err)
teardown()
})
t.Run("Should return some error", func(t *testing.T) {
mock := setup()
expected := errors.New("Killa Gorilla")
mock.usersErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.usersCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Equal(t, expected, err)
teardown()
})
t.Run("Should get only one user", func(t *testing.T) {
mock := setup()
mock.usersFirstReturn = []*models.ExternalUserInfo{
{
Login: "one",
},
{
Login: "two",
},
}
multi := New([]*ldap.ServerConfig{
{}, {},
})
user, _, err := multi.User("test")
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.usersCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Nil(t, err)
require.Equal(t, "one", user.Login)
teardown()
})
t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, expectedError, err)
teardown()
})
})
t.Run("Users()", func(t *testing.T) {
t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, expectedError, err)
teardown()
})
t.Run("Should return error for absent config list", func(t *testing.T) {
setup()
multi := New([]*ldap.ServerConfig{})
_, err := multi.Users([]string{"test"})
require.Error(t, err)
require.Equal(t, ErrNoLDAPServers, err)
teardown()
})
t.Run("Should return a dial error", func(t *testing.T) {
mock := setup()
expected := errors.New("Dial error")
mock.dialErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
Convey("Should call underlying LDAP methods", func() {
mock := setup()
_, err := multi.Users([]string{"test"})
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
require.Error(t, err)
require.Equal(t, expected, err)
So(mock.dialCalledTimes, ShouldEqual, 2)
So(mock.usersCalledTimes, ShouldEqual, 2)
So(mock.closeCalledTimes, ShouldEqual, 2)
teardown()
})
So(err, ShouldBeNil)
t.Run("Should call underlying LDAP methods", func(t *testing.T) {
mock := setup()
teardown()
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
Convey("Should return some error", func() {
mock := setup()
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.usersCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
expected := errors.New("Killa Gorilla")
mock.usersErrReturn = expected
require.Nil(t, err)
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
teardown()
})
So(mock.dialCalledTimes, ShouldEqual, 1)
So(mock.usersCalledTimes, ShouldEqual, 1)
So(mock.closeCalledTimes, ShouldEqual, 1)
t.Run("Should return some error", func(t *testing.T) {
mock := setup()
So(err, ShouldEqual, expected)
expected := errors.New("Killa Gorilla")
mock.usersErrReturn = expected
teardown()
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
Convey("Should get users", func() {
mock := setup()
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.usersCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
mock.usersFirstReturn = []*models.ExternalUserInfo{
{
Login: "one",
},
require.Equal(t, expected, err)
{
Login: "two",
},
}
teardown()
})
mock.usersRestReturn = []*models.ExternalUserInfo{
{
Login: "three",
},
}
t.Run("Should get users", func(t *testing.T) {
mock := setup()
multi := New([]*ldap.ServerConfig{
{}, {},
})
users, err := multi.Users([]string{"test"})
mock.usersFirstReturn = []*models.ExternalUserInfo{
{
Login: "one",
},
So(mock.dialCalledTimes, ShouldEqual, 2)
So(mock.usersCalledTimes, ShouldEqual, 2)
So(mock.closeCalledTimes, ShouldEqual, 2)
{
Login: "two",
},
}
So(err, ShouldBeNil)
So(users[0].Login, ShouldEqual, "one")
So(users[1].Login, ShouldEqual, "two")
So(users[2].Login, ShouldEqual, "three")
mock.usersRestReturn = []*models.ExternalUserInfo{
{
Login: "three",
},
}
teardown()
multi := New([]*ldap.ServerConfig{
{}, {},
})
users, err := multi.Users([]string{"test"})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.usersCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Nil(t, err)
require.Equal(t, "one", users[0].Login)
require.Equal(t, "two", users[1].Login)
require.Equal(t, "three", users[2].Login)
teardown()
})
})
}
@@ -219,19 +219,21 @@ var sendSlackRequest = func(request *http.Request, logger log.Logger) error {
return fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode/100 != 2 {
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
logger.Warn("Slack API request failed", "url", request.URL.String(), "statusCode", resp.Status, "body", string(body))
return fmt.Errorf("request to Slack API failed with status code %d", resp.StatusCode)
}
var rslt map[string]interface{}
// Slack responds to some requests with a JSON document, that might contain an error
rslt := struct {
Ok bool `json:"ok"`
Err string `json:"error"`
}{}
if err := json.Unmarshal(body, &rslt); err == nil {
if !rslt["ok"].(bool) {
errMsg := rslt["error"].(string)
if !rslt.Ok && rslt.Err != "" {
logger.Warn("Sending Slack API request failed", "url", request.URL.String(), "statusCode", resp.Status,
"err", errMsg)
return fmt.Errorf("failed to make Slack API request: %s", errMsg)
"err", rslt.Err)
return fmt.Errorf("failed to make Slack API request: %s", rslt.Err)
}
}
@@ -5,6 +5,7 @@ import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
@@ -221,3 +222,94 @@ func TestSlackNotifier(t *testing.T) {
})
}
}
func TestSendSlackRequest(t *testing.T) {
tests := []struct {
name string
slackResponse string
statusCode int
expectError bool
}{
{
name: "Example error",
slackResponse: `{
"ok": false,
"error": "too_many_attachments"
}`,
statusCode: http.StatusBadRequest,
expectError: true,
},
{
name: "Non 200 status code, no response body",
statusCode: http.StatusMovedPermanently,
expectError: true,
},
{
name: "Success case, normal response body",
slackResponse: `{
"ok": true,
"channel": "C1H9RESGL",
"ts": "1503435956.000247",
"message": {
"text": "Here's a message for you",
"username": "ecto1",
"bot_id": "B19LU7CSY",
"attachments": [
{
"text": "This is an attachment",
"id": 1,
"fallback": "This is an attachment's fallback"
}
],
"type": "message",
"subtype": "bot_message",
"ts": "1503435956.000247"
}
}`,
statusCode: http.StatusOK,
expectError: false,
},
{
name: "Success case, no response body",
statusCode: http.StatusOK,
expectError: false,
},
{
name: "Success case, unexpected response body",
statusCode: http.StatusOK,
slackResponse: "{}",
expectError: false,
},
{
name: "Success case, ok: true",
statusCode: http.StatusOK,
slackResponse: "{\"ok\": true}",
expectError: false,
},
{
name: "200 status code, error in body",
statusCode: http.StatusOK,
slackResponse: `{"ok": false, "error": "test error"}`,
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.statusCode)
_, err := w.Write([]byte(test.slackResponse))
require.NoError(tt, err)
}))
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
require.NoError(tt, err)
err = sendSlackRequest(req, log.New("test"))
if !test.expectError {
require.NoError(tt, err)
} else {
require.Error(tt, err)
}
})
}
}
+411 -401
View File
@@ -13,400 +13,423 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestAlertNotificationSQLAccess(t *testing.T) {
Convey("Testing Alert notification sql access", t, func() {
var sqlStore *SQLStore
setup := func() {
sqlStore := InitTestDB(t)
// Set up bus handlers
bus.AddHandler("deleteAlertNotification", func(cmd *models.DeleteAlertNotificationCommand) error {
return sqlStore.DeleteAlertNotification(cmd)
})
}
Convey("Alert notification state", func() {
var alertID int64 = 7
var orgID int64 = 5
var notifierID int64 = 10
oldTimeNow := timeNow
now := time.Date(2018, 9, 30, 0, 0, 0, 0, time.UTC)
timeNow = func() time.Time { return now }
t.Run("Alert notification state", func(t *testing.T) {
setup()
var alertID int64 = 7
var orgID int64 = 5
var notifierID int64 = 10
oldTimeNow := timeNow
now := time.Date(2018, 9, 30, 0, 0, 0, 0, time.UTC)
timeNow = func() time.Time { return now }
Convey("Get no existing state should create a new state", func() {
query := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err := sqlStore.GetOrCreateAlertNotificationState(context.Background(), query)
So(err, ShouldBeNil)
So(query.Result, ShouldNotBeNil)
So(query.Result.State, ShouldEqual, "unknown")
So(query.Result.Version, ShouldEqual, 0)
So(query.Result.UpdatedAt, ShouldEqual, now.Unix())
defer func() { timeNow = oldTimeNow }()
Convey("Get existing state should not create a new state", func() {
query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err := sqlStore.GetOrCreateAlertNotificationState(context.Background(), query2)
So(err, ShouldBeNil)
So(query2.Result, ShouldNotBeNil)
So(query2.Result.Id, ShouldEqual, query.Result.Id)
So(query2.Result.UpdatedAt, ShouldEqual, now.Unix())
t.Run("Get no existing state should create a new state", func(t *testing.T) {
query := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err := sqlStore.GetOrCreateAlertNotificationState(context.Background(), query)
require.Nil(t, err)
require.NotNil(t, query.Result)
require.Equal(t, models.AlertNotificationStateUnknown, query.Result.State)
require.Equal(t, int64(0), query.Result.Version)
require.Equal(t, now.Unix(), query.Result.UpdatedAt)
t.Run("Get existing state should not create a new state", func(t *testing.T) {
query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err := sqlStore.GetOrCreateAlertNotificationState(context.Background(), query2)
require.Nil(t, err)
require.NotNil(t, query2.Result)
require.Equal(t, query.Result.Id, query2.Result.Id)
require.Equal(t, now.Unix(), query2.Result.UpdatedAt)
})
t.Run("Update existing state to pending with correct version should update database", func(t *testing.T) {
s := *query.Result
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
require.Nil(t, err)
require.Equal(t, int64(1), cmd.ResultVersion)
query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query2)
require.Nil(t, err)
require.Equal(t, int64(1), query2.Result.Version)
require.Equal(t, models.AlertNotificationStatePending, query2.Result.State)
require.Equal(t, now.Unix(), query2.Result.UpdatedAt)
t.Run("Update existing state to completed should update database", func(t *testing.T) {
s := *query.Result
setStateCmd := models.SetAlertNotificationStateToCompleteCommand{
Id: s.Id,
Version: cmd.ResultVersion,
}
err := sqlStore.SetAlertNotificationStateToCompleteCommand(context.Background(), &setStateCmd)
require.Nil(t, err)
query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query3)
require.Nil(t, err)
require.Equal(t, int64(2), query3.Result.Version)
require.Equal(t, models.AlertNotificationStateCompleted, query3.Result.State)
require.Equal(t, now.Unix(), query3.Result.UpdatedAt)
})
Convey("Update existing state to pending with correct version should update database", func() {
t.Run("Update existing state to completed should update database. regardless of version", func(t *testing.T) {
s := *query.Result
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
unknownVersion := int64(1000)
cmd := models.SetAlertNotificationStateToCompleteCommand{
Id: s.Id,
Version: unknownVersion,
}
err := sqlStore.SetAlertNotificationStateToCompleteCommand(context.Background(), &cmd)
require.Nil(t, err)
err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
So(err, ShouldBeNil)
So(cmd.ResultVersion, ShouldEqual, 1)
query2 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query2)
So(err, ShouldBeNil)
So(query2.Result.Version, ShouldEqual, 1)
So(query2.Result.State, ShouldEqual, models.AlertNotificationStatePending)
So(query2.Result.UpdatedAt, ShouldEqual, now.Unix())
Convey("Update existing state to completed should update database", func() {
s := *query.Result
setStateCmd := models.SetAlertNotificationStateToCompleteCommand{
Id: s.Id,
Version: cmd.ResultVersion,
}
err := sqlStore.SetAlertNotificationStateToCompleteCommand(context.Background(), &setStateCmd)
So(err, ShouldBeNil)
query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query3)
So(err, ShouldBeNil)
So(query3.Result.Version, ShouldEqual, 2)
So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted)
So(query3.Result.UpdatedAt, ShouldEqual, now.Unix())
})
Convey("Update existing state to completed should update database. regardless of version", func() {
s := *query.Result
unknownVersion := int64(1000)
cmd := models.SetAlertNotificationStateToCompleteCommand{
Id: s.Id,
Version: unknownVersion,
}
err := sqlStore.SetAlertNotificationStateToCompleteCommand(context.Background(), &cmd)
So(err, ShouldBeNil)
query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query3)
So(err, ShouldBeNil)
So(query3.Result.Version, ShouldEqual, unknownVersion+1)
So(query3.Result.State, ShouldEqual, models.AlertNotificationStateCompleted)
So(query3.Result.UpdatedAt, ShouldEqual, now.Unix())
})
})
Convey("Update existing state to pending with incorrect version should return version mismatch error", func() {
s := *query.Result
s.Version = 1000
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.NotifierId,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
So(err, ShouldEqual, models.ErrAlertNotificationStateVersionConflict)
})
Convey("Updating existing state to pending with incorrect version since alert rule state update version is higher", func() {
s := *query.Result
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: 1000,
}
err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
So(err, ShouldBeNil)
So(cmd.ResultVersion, ShouldEqual, 1)
})
Convey("different version and same alert state change version should return error", func() {
s := *query.Result
s.Version = 1000
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
So(err, ShouldNotBeNil)
query3 := &models.GetOrCreateNotificationStateQuery{AlertId: alertID, OrgId: orgID, NotifierId: notifierID}
err = sqlStore.GetOrCreateAlertNotificationState(context.Background(), query3)
require.Nil(t, err)
require.Equal(t, unknownVersion+1, query3.Result.Version)
require.Equal(t, models.AlertNotificationStateCompleted, query3.Result.State)
require.Equal(t, now.Unix(), query3.Result.UpdatedAt)
})
})
Reset(func() {
timeNow = oldTimeNow
t.Run("Update existing state to pending with incorrect version should return version mismatch error", func(t *testing.T) {
s := *query.Result
s.Version = 1000
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.NotifierId,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
require.Equal(t, models.ErrAlertNotificationStateVersionConflict, err)
})
t.Run("Updating existing state to pending with incorrect version since alert rule state update version is higher", func(t *testing.T) {
s := *query.Result
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: 1000,
}
err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
require.Nil(t, err)
require.Equal(t, int64(1), cmd.ResultVersion)
})
t.Run("different version and same alert state change version should return error", func(t *testing.T) {
s := *query.Result
s.Version = 1000
cmd := models.SetAlertNotificationStateToPendingCommand{
Id: s.Id,
Version: s.Version,
AlertRuleStateUpdatedVersion: s.AlertRuleStateUpdatedVersion,
}
err := sqlStore.SetAlertNotificationStateToPendingCommand(context.Background(), &cmd)
require.Error(t, err)
})
})
})
Convey("Alert notifications should be empty", func() {
cmd := &models.GetAlertNotificationsQuery{
OrgId: 2,
Name: "email",
}
t.Run("Alert notifications should be empty", func(t *testing.T) {
setup()
cmd := &models.GetAlertNotificationsQuery{
OrgId: 2,
Name: "email",
}
err := sqlStore.GetAlertNotifications(cmd)
So(err, ShouldBeNil)
So(cmd.Result, ShouldBeNil)
err := sqlStore.GetAlertNotifications(cmd)
require.Nil(t, err)
require.Nil(t, cmd.Result)
})
t.Run("Cannot save alert notifier with send reminder = true", func(t *testing.T) {
setup()
cmd := &models.CreateAlertNotificationCommand{
Name: "ops",
Type: "email",
OrgId: 1,
SendReminder: true,
Settings: simplejson.New(),
}
t.Run("and missing frequency", func(t *testing.T) {
err := sqlStore.CreateAlertNotificationCommand(cmd)
require.Equal(t, models.ErrNotificationFrequencyNotFound, err)
})
Convey("Cannot save alert notifier with send reminder = true", func() {
cmd := &models.CreateAlertNotificationCommand{
Name: "ops",
Type: "email",
t.Run("invalid frequency", func(t *testing.T) {
cmd.Frequency = "invalid duration"
err := sqlStore.CreateAlertNotificationCommand(cmd)
require.True(t, regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString(
err.Error()))
})
})
t.Run("Cannot update alert notifier with send reminder = false", func(t *testing.T) {
setup()
cmd := &models.CreateAlertNotificationCommand{
Name: "ops update",
Type: "email",
OrgId: 1,
SendReminder: false,
Settings: simplejson.New(),
}
err := sqlStore.CreateAlertNotificationCommand(cmd)
require.Nil(t, err)
updateCmd := &models.UpdateAlertNotificationCommand{
Id: cmd.Result.Id,
SendReminder: true,
}
t.Run("and missing frequency", func(t *testing.T) {
err := sqlStore.UpdateAlertNotification(updateCmd)
require.Equal(t, models.ErrNotificationFrequencyNotFound, err)
})
t.Run("invalid frequency", func(t *testing.T) {
updateCmd.Frequency = "invalid duration"
err := sqlStore.UpdateAlertNotification(updateCmd)
require.Error(t, err)
require.True(t, regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString(
err.Error()))
})
})
t.Run("Can save Alert Notification", func(t *testing.T) {
setup()
cmd := &models.CreateAlertNotificationCommand{
Name: "ops",
Type: "email",
OrgId: 1,
SendReminder: true,
Frequency: "10s",
Settings: simplejson.New(),
}
err := sqlStore.CreateAlertNotificationCommand(cmd)
require.Nil(t, err)
require.NotEqual(t, 0, cmd.Result.Id)
require.NotEqual(t, 0, cmd.Result.OrgId)
require.Equal(t, "email", cmd.Result.Type)
require.Equal(t, 10*time.Second, cmd.Result.Frequency)
require.False(t, cmd.Result.DisableResolveMessage)
require.NotEmpty(t, cmd.Result.Uid)
t.Run("Cannot save Alert Notification with the same name", func(t *testing.T) {
err = sqlStore.CreateAlertNotificationCommand(cmd)
require.Error(t, err)
})
t.Run("Cannot save Alert Notification with the same name and another uid", func(t *testing.T) {
anotherUidCmd := &models.CreateAlertNotificationCommand{
Name: cmd.Name,
Type: cmd.Type,
OrgId: 1,
SendReminder: true,
Settings: simplejson.New(),
SendReminder: cmd.SendReminder,
Frequency: cmd.Frequency,
Settings: cmd.Settings,
Uid: "notifier1",
}
Convey("and missing frequency", func() {
err := sqlStore.CreateAlertNotificationCommand(cmd)
So(err, ShouldEqual, models.ErrNotificationFrequencyNotFound)
})
Convey("invalid frequency", func() {
cmd.Frequency = "invalid duration"
err := sqlStore.CreateAlertNotificationCommand(cmd)
So(regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString(
err.Error()), ShouldBeTrue)
})
err = sqlStore.CreateAlertNotificationCommand(anotherUidCmd)
require.Error(t, err)
})
t.Run("Can save Alert Notification with another name and another uid", func(t *testing.T) {
anotherUidCmd := &models.CreateAlertNotificationCommand{
Name: "another ops",
Type: cmd.Type,
OrgId: 1,
SendReminder: cmd.SendReminder,
Frequency: cmd.Frequency,
Settings: cmd.Settings,
Uid: "notifier2",
}
err = sqlStore.CreateAlertNotificationCommand(anotherUidCmd)
require.Nil(t, err)
})
Convey("Cannot update alert notifier with send reminder = false", func() {
cmd := &models.CreateAlertNotificationCommand{
Name: "ops update",
Type: "email",
OrgId: 1,
t.Run("Can update alert notification", func(t *testing.T) {
newCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: cmd.Result.OrgId,
SendReminder: true,
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Id: cmd.Result.Id,
}
err := sqlStore.UpdateAlertNotification(newCmd)
require.Nil(t, err)
require.Equal(t, "NewName", newCmd.Result.Name)
require.Equal(t, 60*time.Second, newCmd.Result.Frequency)
require.True(t, newCmd.Result.DisableResolveMessage)
})
t.Run("Can update alert notification to disable sending of reminders", func(t *testing.T) {
newCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: cmd.Result.OrgId,
SendReminder: false,
Settings: simplejson.New(),
}
err := sqlStore.CreateAlertNotificationCommand(cmd)
So(err, ShouldBeNil)
updateCmd := &models.UpdateAlertNotificationCommand{
Id: cmd.Result.Id,
SendReminder: true,
}
err := sqlStore.UpdateAlertNotification(newCmd)
require.Nil(t, err)
require.False(t, newCmd.Result.SendReminder)
})
})
t.Run("Can search using an array of ids", func(t *testing.T) {
setup()
cmd1 := models.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd2 := models.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd3 := models.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd4 := models.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
otherOrg := models.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
require.Nil(t, sqlStore.CreateAlertNotificationCommand(&cmd1))
require.Nil(t, sqlStore.CreateAlertNotificationCommand(&cmd2))
require.Nil(t, sqlStore.CreateAlertNotificationCommand(&cmd3))
require.Nil(t, sqlStore.CreateAlertNotificationCommand(&cmd4))
require.Nil(t, sqlStore.CreateAlertNotificationCommand(&otherOrg))
t.Run("search", func(t *testing.T) {
query := &models.GetAlertNotificationsWithUidToSendQuery{
Uids: []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"},
OrgId: 1,
}
Convey("and missing frequency", func() {
err := sqlStore.UpdateAlertNotification(updateCmd)
So(err, ShouldEqual, models.ErrNotificationFrequencyNotFound)
})
Convey("invalid frequency", func() {
updateCmd.Frequency = "invalid duration"
err := sqlStore.UpdateAlertNotification(updateCmd)
So(err, ShouldNotBeNil)
So(regexp.MustCompile(`^time: invalid duration "?invalid duration"?$`).MatchString(
err.Error()), ShouldBeTrue)
})
err := sqlStore.GetAlertNotificationsWithUidToSend(query)
require.Nil(t, err)
require.Equal(t, 3, len(query.Result))
})
Convey("Can save Alert Notification", func() {
cmd := &models.CreateAlertNotificationCommand{
Name: "ops",
Type: "email",
OrgId: 1,
SendReminder: true,
Frequency: "10s",
Settings: simplejson.New(),
t.Run("all", func(t *testing.T) {
query := &models.GetAllAlertNotificationsQuery{
OrgId: 1,
}
err := sqlStore.CreateAlertNotificationCommand(cmd)
So(err, ShouldBeNil)
So(cmd.Result.Id, ShouldNotEqual, 0)
So(cmd.Result.OrgId, ShouldNotEqual, 0)
So(cmd.Result.Type, ShouldEqual, "email")
So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
So(cmd.Result.Uid, ShouldNotBeEmpty)
Convey("Cannot save Alert Notification with the same name", func() {
err = sqlStore.CreateAlertNotificationCommand(cmd)
So(err, ShouldNotBeNil)
})
Convey("Cannot save Alert Notification with the same name and another uid", func() {
anotherUidCmd := &models.CreateAlertNotificationCommand{
Name: cmd.Name,
Type: cmd.Type,
OrgId: 1,
SendReminder: cmd.SendReminder,
Frequency: cmd.Frequency,
Settings: cmd.Settings,
Uid: "notifier1",
}
err = sqlStore.CreateAlertNotificationCommand(anotherUidCmd)
So(err, ShouldNotBeNil)
})
Convey("Can save Alert Notification with another name and another uid", func() {
anotherUidCmd := &models.CreateAlertNotificationCommand{
Name: "another ops",
Type: cmd.Type,
OrgId: 1,
SendReminder: cmd.SendReminder,
Frequency: cmd.Frequency,
Settings: cmd.Settings,
Uid: "notifier2",
}
err = sqlStore.CreateAlertNotificationCommand(anotherUidCmd)
So(err, ShouldBeNil)
})
Convey("Can update alert notification", func() {
newCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: cmd.Result.OrgId,
SendReminder: true,
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Id: cmd.Result.Id,
}
err := sqlStore.UpdateAlertNotification(newCmd)
So(err, ShouldBeNil)
So(newCmd.Result.Name, ShouldEqual, "NewName")
So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
So(newCmd.Result.DisableResolveMessage, ShouldBeTrue)
})
Convey("Can update alert notification to disable sending of reminders", func() {
newCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: cmd.Result.OrgId,
SendReminder: false,
Settings: simplejson.New(),
Id: cmd.Result.Id,
}
err := sqlStore.UpdateAlertNotification(newCmd)
So(err, ShouldBeNil)
So(newCmd.Result.SendReminder, ShouldBeFalse)
})
err := sqlStore.GetAllAlertNotifications(query)
require.Nil(t, err)
require.Equal(t, 4, len(query.Result))
require.Equal(t, cmd4.Name, query.Result[0].Name)
require.Equal(t, cmd1.Name, query.Result[1].Name)
require.Equal(t, cmd3.Name, query.Result[2].Name)
require.Equal(t, cmd2.Name, query.Result[3].Name)
})
})
Convey("Can search using an array of ids", func() {
cmd1 := models.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd2 := models.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd3 := models.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
cmd4 := models.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
t.Run("Notification Uid by Id Caching", func(t *testing.T) {
setup()
ss := InitTestDB(t)
otherOrg := models.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()}
notification := &models.CreateAlertNotificationCommand{Uid: "aNotificationUid", OrgId: 1, Name: "aNotificationUid"}
err := sqlStore.CreateAlertNotificationCommand(notification)
require.Nil(t, err)
So(sqlStore.CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
So(sqlStore.CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
So(sqlStore.CreateAlertNotificationCommand(&cmd3), ShouldBeNil)
So(sqlStore.CreateAlertNotificationCommand(&cmd4), ShouldBeNil)
So(sqlStore.CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
byUidQuery := &models.GetAlertNotificationsWithUidQuery{
Uid: notification.Uid,
OrgId: notification.OrgId,
}
Convey("search", func() {
query := &models.GetAlertNotificationsWithUidToSendQuery{
Uids: []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"},
OrgId: 1,
}
notificationByUidErr := sqlStore.GetAlertNotificationsWithUid(byUidQuery)
require.Nil(t, notificationByUidErr)
err := sqlStore.GetAlertNotificationsWithUidToSend(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 3)
})
Convey("all", func() {
query := &models.GetAllAlertNotificationsQuery{
OrgId: 1,
}
err := sqlStore.GetAllAlertNotifications(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 4)
So(query.Result[0].Name, ShouldEqual, cmd4.Name)
So(query.Result[1].Name, ShouldEqual, cmd1.Name)
So(query.Result[2].Name, ShouldEqual, cmd3.Name)
So(query.Result[3].Name, ShouldEqual, cmd2.Name)
})
})
Convey("Notification Uid by Id Caching", func() {
ss := InitTestDB(t)
notification := &models.CreateAlertNotificationCommand{Uid: "aNotificationUid", OrgId: 1, Name: "aNotificationUid"}
err := sqlStore.CreateAlertNotificationCommand(notification)
So(err, ShouldBeNil)
byUidQuery := &models.GetAlertNotificationsWithUidQuery{
Uid: notification.Uid,
OrgId: notification.OrgId,
t.Run("Can cache notification Uid", func(t *testing.T) {
byIdQuery := &models.GetAlertNotificationUidQuery{
Id: byUidQuery.Result.Id,
OrgId: byUidQuery.Result.OrgId,
}
notificationByUidErr := sqlStore.GetAlertNotificationsWithUid(byUidQuery)
So(notificationByUidErr, ShouldBeNil)
cacheKey := newAlertNotificationUidCacheKey(byIdQuery.OrgId, byIdQuery.Id)
Convey("Can cache notification Uid", func() {
byIdQuery := &models.GetAlertNotificationUidQuery{
Id: byUidQuery.Result.Id,
OrgId: byUidQuery.Result.OrgId,
}
resultBeforeCaching, foundBeforeCaching := ss.CacheService.Get(cacheKey)
require.False(t, foundBeforeCaching)
require.Nil(t, resultBeforeCaching)
cacheKey := newAlertNotificationUidCacheKey(byIdQuery.OrgId, byIdQuery.Id)
notificationByIdErr := ss.GetAlertNotificationUidWithId(byIdQuery)
require.Nil(t, notificationByIdErr)
resultBeforeCaching, foundBeforeCaching := ss.CacheService.Get(cacheKey)
So(foundBeforeCaching, ShouldBeFalse)
So(resultBeforeCaching, ShouldBeNil)
notificationByIdErr := ss.GetAlertNotificationUidWithId(byIdQuery)
So(notificationByIdErr, ShouldBeNil)
resultAfterCaching, foundAfterCaching := ss.CacheService.Get(cacheKey)
So(foundAfterCaching, ShouldBeTrue)
So(resultAfterCaching, ShouldEqual, notification.Uid)
})
Convey("Retrieves from cache when exists", func() {
query := &models.GetAlertNotificationUidQuery{
Id: 999,
OrgId: 100,
}
cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id)
ss.CacheService.Set(cacheKey, "a-cached-uid", -1)
err := ss.GetAlertNotificationUidWithId(query)
So(err, ShouldBeNil)
So(query.Result, ShouldEqual, "a-cached-uid")
})
Convey("Returns an error without populating cache when the notification doesn't exist in the database", func() {
query := &models.GetAlertNotificationUidQuery{
Id: -1,
OrgId: 100,
}
err := ss.GetAlertNotificationUidWithId(query)
So(query.Result, ShouldEqual, "")
So(err, ShouldNotBeNil)
So(errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID), ShouldBeTrue)
cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id)
result, found := ss.CacheService.Get(cacheKey)
So(found, ShouldBeFalse)
So(result, ShouldBeNil)
})
resultAfterCaching, foundAfterCaching := ss.CacheService.Get(cacheKey)
require.True(t, foundAfterCaching)
require.Equal(t, notification.Uid, resultAfterCaching)
})
Convey("Cannot update non-existing Alert Notification", func() {
updateCmd := &models.UpdateAlertNotificationCommand{
t.Run("Retrieves from cache when exists", func(t *testing.T) {
query := &models.GetAlertNotificationUidQuery{
Id: 999,
OrgId: 100,
}
cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id)
ss.CacheService.Set(cacheKey, "a-cached-uid", -1)
err := ss.GetAlertNotificationUidWithId(query)
require.Nil(t, err)
require.Equal(t, "a-cached-uid", query.Result)
})
t.Run("Returns an error without populating cache when the notification doesn't exist in the database", func(t *testing.T) {
query := &models.GetAlertNotificationUidQuery{
Id: -1,
OrgId: 100,
}
err := ss.GetAlertNotificationUidWithId(query)
require.Equal(t, "", query.Result)
require.Error(t, err)
require.True(t, errors.Is(err, models.ErrAlertNotificationFailedTranslateUniqueID))
cacheKey := newAlertNotificationUidCacheKey(query.OrgId, query.Id)
result, found := ss.CacheService.Get(cacheKey)
require.False(t, found)
require.Nil(t, result)
})
})
t.Run("Cannot update non-existing Alert Notification", func(t *testing.T) {
setup()
updateCmd := &models.UpdateAlertNotificationCommand{
Name: "NewName",
Type: "webhook",
OrgId: 1,
SendReminder: true,
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Id: 1,
}
err := sqlStore.UpdateAlertNotification(updateCmd)
require.Equal(t, models.ErrAlertNotificationNotFound, err)
t.Run("using UID", func(t *testing.T) {
updateWithUidCmd := &models.UpdateAlertNotificationWithUidCommand{
Name: "NewName",
Type: "webhook",
OrgId: 1,
@@ -414,78 +437,65 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Id: 1,
Uid: "uid",
NewUid: "newUid",
}
err := sqlStore.UpdateAlertNotification(updateCmd)
So(err, ShouldEqual, models.ErrAlertNotificationNotFound)
Convey("using UID", func() {
updateWithUidCmd := &models.UpdateAlertNotificationWithUidCommand{
Name: "NewName",
Type: "webhook",
OrgId: 1,
SendReminder: true,
DisableResolveMessage: true,
Frequency: "60s",
Settings: simplejson.New(),
Uid: "uid",
NewUid: "newUid",
}
err := sqlStore.UpdateAlertNotificationWithUid(updateWithUidCmd)
So(err, ShouldEqual, models.ErrAlertNotificationNotFound)
})
err := sqlStore.UpdateAlertNotificationWithUid(updateWithUidCmd)
require.Equal(t, models.ErrAlertNotificationNotFound, err)
})
})
Convey("Can delete Alert Notification", func() {
cmd := &models.CreateAlertNotificationCommand{
Name: "ops update",
Type: "email",
OrgId: 1,
SendReminder: false,
Settings: simplejson.New(),
}
t.Run("Can delete Alert Notification", func(t *testing.T) {
setup()
cmd := &models.CreateAlertNotificationCommand{
Name: "ops update",
Type: "email",
OrgId: 1,
SendReminder: false,
Settings: simplejson.New(),
}
err := sqlStore.CreateAlertNotificationCommand(cmd)
require.Nil(t, err)
deleteCmd := &models.DeleteAlertNotificationCommand{
Id: cmd.Result.Id,
OrgId: 1,
}
err = sqlStore.DeleteAlertNotification(deleteCmd)
require.Nil(t, err)
t.Run("using UID", func(t *testing.T) {
err := sqlStore.CreateAlertNotificationCommand(cmd)
So(err, ShouldBeNil)
require.Nil(t, err)
deleteCmd := &models.DeleteAlertNotificationCommand{
Id: cmd.Result.Id,
deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{
Uid: cmd.Result.Uid,
OrgId: 1,
}
err = sqlStore.DeleteAlertNotification(deleteCmd)
So(err, ShouldBeNil)
Convey("using UID", func() {
err := sqlStore.CreateAlertNotificationCommand(cmd)
So(err, ShouldBeNil)
deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{
Uid: cmd.Result.Uid,
OrgId: 1,
}
err = sqlStore.DeleteAlertNotificationWithUid(deleteWithUidCmd)
So(err, ShouldBeNil)
So(deleteWithUidCmd.DeletedAlertNotificationId, ShouldEqual, cmd.Result.Id)
})
err = sqlStore.DeleteAlertNotificationWithUid(deleteWithUidCmd)
require.Nil(t, err)
require.Equal(t, cmd.Result.Id, deleteWithUidCmd.DeletedAlertNotificationId)
})
})
Convey("Cannot delete non-existing Alert Notification", func() {
deleteCmd := &models.DeleteAlertNotificationCommand{
Id: 1,
t.Run("Cannot delete non-existing Alert Notification", func(t *testing.T) {
setup()
deleteCmd := &models.DeleteAlertNotificationCommand{
Id: 1,
OrgId: 1,
}
err := sqlStore.DeleteAlertNotification(deleteCmd)
require.Equal(t, models.ErrAlertNotificationNotFound, err)
t.Run("using UID", func(t *testing.T) {
deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{
Uid: "uid",
OrgId: 1,
}
err := sqlStore.DeleteAlertNotification(deleteCmd)
So(err, ShouldEqual, models.ErrAlertNotificationNotFound)
Convey("using UID", func() {
deleteWithUidCmd := &models.DeleteAlertNotificationWithUidCommand{
Uid: "uid",
OrgId: 1,
}
err = sqlStore.DeleteAlertNotificationWithUid(deleteWithUidCmd)
So(err, ShouldEqual, models.ErrAlertNotificationNotFound)
})
err = sqlStore.DeleteAlertNotificationWithUid(deleteWithUidCmd)
require.Equal(t, models.ErrAlertNotificationNotFound, err)
})
})
}
+215 -206
View File
@@ -10,7 +10,8 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func mockTimeNow() {
@@ -31,13 +32,18 @@ func TestAlertingDataAccess(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
Convey("Testing Alerting data access", t, func() {
sqlStore := InitTestDB(t)
var sqlStore *SQLStore
var testDash *models.Dashboard
var cmd models.SaveAlertsCommand
var items []*models.Alert
testDash := insertTestDashboard(t, sqlStore, "dashboard with alerts", 1, 0, false, "alert")
setup := func(t *testing.T) {
sqlStore = InitTestDB(t)
testDash = insertTestDashboard(t, sqlStore, "dashboard with alerts", 1, 0, false, "alert")
evalData, err := simplejson.NewJson([]byte(`{"test": "test"}`))
So(err, ShouldBeNil)
items := []*models.Alert{
require.Nil(t, err)
items = []*models.Alert{
{
PanelId: 1,
DashboardId: testDash.Id,
@@ -50,7 +56,7 @@ func TestAlertingDataAccess(t *testing.T) {
},
}
cmd := models.SaveAlertsCommand{
cmd = models.SaveAlertsCommand{
Alerts: items,
DashboardId: testDash.Id,
OrgId: 1,
@@ -58,234 +64,237 @@ func TestAlertingDataAccess(t *testing.T) {
}
err = SaveAlerts(&cmd)
require.Nil(t, err)
}
Convey("Can create one alert", func() {
So(err, ShouldBeNil)
t.Run("Can set new states", func(t *testing.T) {
setup(t)
// Get alert so we can use its ID in tests
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&alertQuery)
require.Nil(t, err2)
insertedAlert := alertQuery.Result[0]
t.Run("new state ok", func(t *testing.T) {
cmd := &models.SetAlertStateCommand{
AlertId: insertedAlert.Id,
State: models.AlertStateOK,
}
err := SetAlertState(cmd)
require.Nil(t, err)
})
Convey("Can set new states", func() {
alert, _ := getAlertById(t, insertedAlert.Id)
stateDateBeforePause := alert.NewStateDate
// Get alert so we can use its ID in tests
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&alertQuery)
So(err2, ShouldBeNil)
t.Run("can pause all alerts", func(t *testing.T) {
err := pauseAllAlerts(t, true)
require.Nil(t, err)
insertedAlert := alertQuery.Result[0]
Convey("new state ok", func() {
t.Run("cannot updated paused alert", func(t *testing.T) {
cmd := &models.SetAlertStateCommand{
AlertId: insertedAlert.Id,
State: models.AlertStateOK,
}
err = SetAlertState(cmd)
So(err, ShouldBeNil)
require.Error(t, err)
})
alert, _ := getAlertById(insertedAlert.Id)
stateDateBeforePause := alert.NewStateDate
t.Run("alert is paused", func(t *testing.T) {
alert, _ = getAlertById(t, insertedAlert.Id)
currentState := alert.State
require.Equal(t, models.AlertStatePaused, currentState)
})
Convey("can pause all alerts", func() {
err := pauseAllAlerts(true)
So(err, ShouldBeNil)
t.Run("pausing alerts should update their NewStateDate", func(t *testing.T) {
alert, _ = getAlertById(t, insertedAlert.Id)
stateDateAfterPause := alert.NewStateDate
require.True(t, stateDateBeforePause.Before(stateDateAfterPause))
})
Convey("cannot updated paused alert", func() {
cmd := &models.SetAlertStateCommand{
AlertId: insertedAlert.Id,
State: models.AlertStateOK,
}
t.Run("unpausing alerts should update their NewStateDate again", func(t *testing.T) {
err := pauseAllAlerts(t, false)
require.Nil(t, err)
alert, _ = getAlertById(t, insertedAlert.Id)
stateDateAfterUnpause := alert.NewStateDate
require.True(t, stateDateBeforePause.Before(stateDateAfterUnpause))
})
})
})
err = SetAlertState(cmd)
So(err, ShouldNotBeNil)
})
t.Run("Can read properties", func(t *testing.T) {
setup(t)
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&alertQuery)
Convey("alert is paused", func() {
alert, _ = getAlertById(insertedAlert.Id)
currentState := alert.State
So(currentState, ShouldEqual, "paused")
})
alert := alertQuery.Result[0]
require.Nil(t, err2)
require.Greater(t, alert.Id, int64(0))
require.Equal(t, testDash.Id, alert.DashboardId)
require.Equal(t, int64(1), alert.PanelId)
require.Equal(t, "Alerting title", alert.Name)
require.Equal(t, models.AlertStateUnknown, alert.State)
require.NotNil(t, alert.NewStateDate)
require.NotNil(t, alert.EvalData)
require.Equal(t, "test", alert.EvalData.Get("test").MustString())
require.NotNil(t, alert.EvalDate)
require.Equal(t, "", alert.ExecutionError)
require.NotNil(t, alert.DashboardUid)
require.Equal(t, "dashboard-with-alerts", alert.DashboardSlug)
})
Convey("pausing alerts should update their NewStateDate", func() {
alert, _ = getAlertById(insertedAlert.Id)
stateDateAfterPause := alert.NewStateDate
So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause)
})
t.Run("Viewer can read alerts", func(t *testing.T) {
setup(t)
viewerUser := &models.SignedInUser{OrgRole: models.ROLE_VIEWER, OrgId: 1}
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser}
err2 := HandleAlertsQuery(&alertQuery)
Convey("unpausing alerts should update their NewStateDate again", func() {
err := pauseAllAlerts(false)
So(err, ShouldBeNil)
alert, _ = getAlertById(insertedAlert.Id)
stateDateAfterUnpause := alert.NewStateDate
So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterUnpause)
})
require.Nil(t, err2)
require.Equal(t, 1, len(alertQuery.Result))
})
t.Run("Alerts with same dashboard id and panel id should update", func(t *testing.T) {
setup(t)
modifiedItems := items
modifiedItems[0].Name = "Name"
modifiedCmd := models.SaveAlertsCommand{
DashboardId: testDash.Id,
OrgId: 1,
UserId: 1,
Alerts: modifiedItems,
}
err := SaveAlerts(&modifiedCmd)
t.Run("Can save alerts with same dashboard and panel id", func(t *testing.T) {
require.Nil(t, err)
})
t.Run("Alerts should be updated", func(t *testing.T) {
query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&query)
require.Nil(t, err2)
require.Equal(t, 1, len(query.Result))
require.Equal(t, "Name", query.Result[0].Name)
t.Run("Alert state should not be updated", func(t *testing.T) {
require.Equal(t, models.AlertStateUnknown, query.Result[0].State)
})
})
Convey("Can read properties", func() {
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&alertQuery)
alert := alertQuery.Result[0]
So(err2, ShouldBeNil)
So(alert.Id, ShouldBeGreaterThan, 0)
So(alert.DashboardId, ShouldEqual, testDash.Id)
So(alert.PanelId, ShouldEqual, 1)
So(alert.Name, ShouldEqual, "Alerting title")
So(alert.State, ShouldEqual, models.AlertStateUnknown)
So(alert.NewStateDate, ShouldNotBeNil)
So(alert.EvalData, ShouldNotBeNil)
So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
So(alert.EvalDate, ShouldNotBeNil)
So(alert.ExecutionError, ShouldEqual, "")
So(alert.DashboardUid, ShouldNotBeNil)
So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts")
t.Run("Updates without changes should be ignored", func(t *testing.T) {
err3 := SaveAlerts(&modifiedCmd)
require.Nil(t, err3)
})
})
Convey("Viewer can read alerts", func() {
viewerUser := &models.SignedInUser{OrgRole: models.ROLE_VIEWER, OrgId: 1}
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser}
err2 := HandleAlertsQuery(&alertQuery)
So(err2, ShouldBeNil)
So(alertQuery.Result, ShouldHaveLength, 1)
})
Convey("Alerts with same dashboard id and panel id should update", func() {
modifiedItems := items
modifiedItems[0].Name = "Name"
modifiedCmd := models.SaveAlertsCommand{
t.Run("Multiple alerts per dashboard", func(t *testing.T) {
setup(t)
multipleItems := []*models.Alert{
{
DashboardId: testDash.Id,
PanelId: 1,
Name: "1",
OrgId: 1,
UserId: 1,
Alerts: modifiedItems,
}
Settings: simplejson.New(),
},
{
DashboardId: testDash.Id,
PanelId: 2,
Name: "2",
OrgId: 1,
Settings: simplejson.New(),
},
{
DashboardId: testDash.Id,
PanelId: 3,
Name: "3",
OrgId: 1,
Settings: simplejson.New(),
},
}
err := SaveAlerts(&modifiedCmd)
cmd.Alerts = multipleItems
err := SaveAlerts(&cmd)
Convey("Can save alerts with same dashboard and panel id", func() {
So(err, ShouldBeNil)
})
t.Run("Should save 3 dashboards", func(t *testing.T) {
require.Nil(t, err)
Convey("Alerts should be updated", func() {
query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&query)
queryForDashboard := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&queryForDashboard)
So(err2, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Name, ShouldEqual, "Name")
Convey("Alert state should not be updated", func() {
So(query.Result[0].State, ShouldEqual, models.AlertStateUnknown)
})
})
Convey("Updates without changes should be ignored", func() {
err3 := SaveAlerts(&modifiedCmd)
So(err3, ShouldBeNil)
})
require.Nil(t, err2)
require.Equal(t, 3, len(queryForDashboard.Result))
})
Convey("Multiple alerts per dashboard", func() {
multipleItems := []*models.Alert{
{
DashboardId: testDash.Id,
PanelId: 1,
Name: "1",
OrgId: 1,
Settings: simplejson.New(),
},
{
DashboardId: testDash.Id,
PanelId: 2,
Name: "2",
OrgId: 1,
Settings: simplejson.New(),
},
{
DashboardId: testDash.Id,
PanelId: 3,
Name: "3",
OrgId: 1,
Settings: simplejson.New(),
},
}
t.Run("should updated two dashboards and delete one", func(t *testing.T) {
missingOneAlert := multipleItems[:2]
cmd.Alerts = multipleItems
cmd.Alerts = missingOneAlert
err = SaveAlerts(&cmd)
Convey("Should save 3 dashboards", func() {
So(err, ShouldBeNil)
queryForDashboard := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&queryForDashboard)
So(err2, ShouldBeNil)
So(len(queryForDashboard.Result), ShouldEqual, 3)
})
Convey("should updated two dashboards and delete one", func() {
missingOneAlert := multipleItems[:2]
cmd.Alerts = missingOneAlert
err = SaveAlerts(&cmd)
Convey("should delete the missing alert", func() {
query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&query)
So(err2, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
})
})
})
Convey("When dashboard is removed", func() {
items := []*models.Alert{
{
PanelId: 1,
DashboardId: testDash.Id,
Name: "Alerting title",
Message: "Alerting message",
},
}
cmd := models.SaveAlertsCommand{
Alerts: items,
DashboardId: testDash.Id,
OrgId: 1,
UserId: 1,
}
err = SaveAlerts(&cmd)
So(err, ShouldBeNil)
err = DeleteDashboard(context.Background(), &models.DeleteDashboardCommand{
OrgId: 1,
Id: testDash.Id,
})
So(err, ShouldBeNil)
Convey("Alerts should be removed", func() {
t.Run("should delete the missing alert", func(t *testing.T) {
query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&query)
So(err2, ShouldBeNil)
So(len(query.Result), ShouldEqual, 0)
require.Nil(t, err2)
require.Equal(t, 2, len(query.Result))
})
})
})
t.Run("When dashboard is removed", func(t *testing.T) {
setup(t)
items := []*models.Alert{
{
PanelId: 1,
DashboardId: testDash.Id,
Name: "Alerting title",
Message: "Alerting message",
},
}
cmd := models.SaveAlertsCommand{
Alerts: items,
DashboardId: testDash.Id,
OrgId: 1,
UserId: 1,
}
err := SaveAlerts(&cmd)
require.Nil(t, err)
err = DeleteDashboard(context.Background(), &models.DeleteDashboardCommand{
OrgId: 1,
Id: testDash.Id,
})
require.Nil(t, err)
t.Run("Alerts should be removed", func(t *testing.T) {
query := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&query)
require.Nil(t, err2)
require.Equal(t, 0, len(query.Result))
})
})
}
func TestPausingAlerts(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
Convey("Given an alert", t, func() {
t.Run("Given an alert", func(t *testing.T) {
sqlStore := InitTestDB(t)
testDash := insertTestDashboard(t, sqlStore, "dashboard with alerts", 1, 0, false, "alert")
alert, err := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New())
So(err, ShouldBeNil)
require.Nil(t, err)
stateDateBeforePause := alert.NewStateDate
stateDateAfterPause := stateDateBeforePause
@@ -293,45 +302,45 @@ func TestPausingAlerts(t *testing.T) {
// Get alert so we can use its ID in tests
alertQuery := models.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &models.SignedInUser{OrgRole: models.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&alertQuery)
So(err2, ShouldBeNil)
require.Nil(t, err2)
insertedAlert := alertQuery.Result[0]
Convey("when paused", func() {
_, err := pauseAlert(testDash.OrgId, insertedAlert.Id, true)
So(err, ShouldBeNil)
t.Run("when paused", func(t *testing.T) {
_, err := pauseAlert(t, testDash.OrgId, insertedAlert.Id, true)
require.Nil(t, err)
Convey("the NewStateDate should be updated", func() {
alert, err := getAlertById(insertedAlert.Id)
So(err, ShouldBeNil)
t.Run("the NewStateDate should be updated", func(t *testing.T) {
alert, err := getAlertById(t, insertedAlert.Id)
require.Nil(t, err)
stateDateAfterPause = alert.NewStateDate
So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause)
require.True(t, stateDateBeforePause.Before(stateDateAfterPause))
})
})
Convey("when unpaused", func() {
_, err := pauseAlert(testDash.OrgId, insertedAlert.Id, false)
So(err, ShouldBeNil)
t.Run("when unpaused", func(t *testing.T) {
_, err := pauseAlert(t, testDash.OrgId, insertedAlert.Id, false)
require.Nil(t, err)
Convey("the NewStateDate should be updated again", func() {
alert, err := getAlertById(insertedAlert.Id)
So(err, ShouldBeNil)
t.Run("the NewStateDate should be updated again", func(t *testing.T) {
alert, err := getAlertById(t, insertedAlert.Id)
require.Nil(t, err)
stateDateAfterUnpause := alert.NewStateDate
So(stateDateAfterPause, ShouldHappenBefore, stateDateAfterUnpause)
require.True(t, stateDateAfterPause.Before(stateDateAfterUnpause))
})
})
})
}
func pauseAlert(orgId int64, alertId int64, pauseState bool) (int64, error) {
func pauseAlert(t *testing.T, orgId int64, alertId int64, pauseState bool) (int64, error) {
cmd := &models.PauseAlertCommand{
OrgId: orgId,
AlertIds: []int64{alertId},
Paused: pauseState,
}
err := PauseAlert(cmd)
So(err, ShouldBeNil)
require.Nil(t, err)
return cmd.ResultCount, err
}
func insertTestAlert(title string, message string, orgId int64, dashId int64, settings *simplejson.Json) (*models.Alert, error) {
@@ -358,20 +367,20 @@ func insertTestAlert(title string, message string, orgId int64, dashId int64, se
return cmd.Alerts[0], err
}
func getAlertById(id int64) (*models.Alert, error) {
func getAlertById(t *testing.T, id int64) (*models.Alert, error) {
q := &models.GetAlertByIdQuery{
Id: id,
}
err := GetAlertById(q)
So(err, ShouldBeNil)
require.Nil(t, err)
return q.Result, err
}
func pauseAllAlerts(pauseState bool) error {
func pauseAllAlerts(t *testing.T, pauseState bool) error {
cmd := &models.PauseAllAlertCommand{
Paused: pauseState,
}
err := PauseAllAlerts(cmd)
So(err, ShouldBeNil)
require.Nil(t, err)
return err
}
+9 -7
View File
@@ -76,14 +76,16 @@ func AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand) error {
} else if cmd.SecondsToLive < 0 {
return models.ErrInvalidApiKeyExpiration
}
t := models.ApiKey{
OrgId: cmd.OrgId,
Name: cmd.Name,
Role: cmd.Role,
Key: cmd.Key,
Created: updated,
Updated: updated,
Expires: expires,
OrgId: cmd.OrgId,
Name: cmd.Name,
Role: cmd.Role,
Key: cmd.Key,
Created: updated,
Updated: updated,
Expires: expires,
ServiceAccountId: cmd.ServiceAccountId,
}
if _, err := sess.Insert(&t); err != nil {
+212 -207
View File
@@ -8,236 +8,241 @@ import (
"testing"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestDashboardAclDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
sqlStore := InitTestDB(t)
var sqlStore *SQLStore
var currentUser models.User
var savedFolder, childDash *models.Dashboard
Convey("Given a dashboard folder and a user", func() {
currentUser := createUser(t, sqlStore, "viewer", "Viewer", false)
savedFolder := insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
childDash := insertTestDashboard(t, sqlStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp")
setup := func(t *testing.T) {
sqlStore = InitTestDB(t)
currentUser = createUser(t, sqlStore, "viewer", "Viewer", false)
savedFolder = insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
childDash = insertTestDashboard(t, sqlStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp")
}
Convey("When adding dashboard permission with userId and teamId set to 0", func() {
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_EDIT,
})
So(err, ShouldEqual, models.ErrDashboardAclInfoMissing)
t.Run("Dashboard permission with userId and teamId set to 0", func(t *testing.T) {
setup(t)
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_EDIT,
})
require.Equal(t, models.ErrDashboardAclInfoMissing, err)
})
t.Run("Folder acl should include default acl", func(t *testing.T) {
setup(t)
query := models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 2, len(query.Result))
defaultPermissionsId := int64(-1)
require.Equal(t, defaultPermissionsId, query.Result[0].DashboardId)
require.Equal(t, models.ROLE_VIEWER, *query.Result[0].Role)
require.False(t, query.Result[0].Inherited)
require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId)
require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role)
require.False(t, query.Result[1].Inherited)
})
t.Run("Dashboard acl should include acl for parent folder", func(t *testing.T) {
setup(t)
query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 2, len(query.Result))
defaultPermissionsId := int64(-1)
require.Equal(t, defaultPermissionsId, query.Result[0].DashboardId)
require.Equal(t, models.ROLE_VIEWER, *query.Result[0].Role)
require.True(t, query.Result[0].Inherited)
require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId)
require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role)
require.True(t, query.Result[1].Inherited)
})
t.Run("Folder with removed default permissions returns no acl items", func(t *testing.T) {
setup(t)
err := sqlStore.UpdateDashboardACL(savedFolder.Id, nil)
require.Nil(t, err)
query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 0, len(query.Result))
})
t.Run("Given a dashboard folder and a user", func(t *testing.T) {
t.Run("Given dashboard folder permission", func(t *testing.T) {
setup(t)
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
UserID: currentUser.Id,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_EDIT,
})
require.Nil(t, err)
t.Run("When reading dashboard acl should include acl for parent folder", func(t *testing.T) {
query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 1, len(query.Result))
require.Equal(t, savedFolder.Id, query.Result[0].DashboardId)
})
Convey("Given dashboard folder with default permissions", func() {
Convey("When reading folder acl should include default acl", func() {
query := models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, models.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeFalse)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, models.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeFalse)
})
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, models.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, models.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeTrue)
})
})
Convey("Given dashboard folder with removed default permissions", func() {
err := sqlStore.UpdateDashboardACL(savedFolder.Id, nil)
So(err, ShouldBeNil)
Convey("When reading dashboard acl should return no acl items", func() {
query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 0)
})
})
Convey("Given dashboard folder permission", func() {
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
UserID: currentUser.Id,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include acl for parent folder", func() {
query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
})
Convey("Given child dashboard permission", func() {
err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{
OrgID: 1,
UserID: currentUser.Id,
DashboardID: childDash.Id,
Permission: models.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
Convey("When reading dashboard acl should include acl for parent folder and child", func() {
query := models.GetDashboardAclInfoListQuery{OrgID: 1, DashboardID: childDash.Id}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, childDash.Id)
So(query.Result[1].Inherited, ShouldBeFalse)
})
})
})
Convey("Given child dashboard permission in folder with no permissions", func() {
t.Run("Given child dashboard permission", func(t *testing.T) {
err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{
OrgID: 1,
UserID: currentUser.Id,
DashboardID: childDash.Id,
Permission: models.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
require.Nil(t, err)
Convey("When reading dashboard acl should include default acl for parent folder and the child acl", func() {
t.Run("When reading dashboard acl should include acl for parent folder and child", func(t *testing.T) {
query := models.GetDashboardAclInfoListQuery{OrgID: 1, DashboardID: childDash.Id}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
So(err, ShouldBeNil)
require.Nil(t, err)
defaultPermissionsId := -1
So(len(query.Result), ShouldEqual, 3)
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, models.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeTrue)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, models.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeTrue)
So(query.Result[2].DashboardId, ShouldEqual, childDash.Id)
So(query.Result[2].Inherited, ShouldBeFalse)
})
})
Convey("Should be able to add dashboard permission", func() {
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
UserID: currentUser.Id,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permission, ShouldEqual, models.PERMISSION_EDIT)
So(q1.Result[0].PermissionName, ShouldEqual, "Edit")
So(q1.Result[0].UserId, ShouldEqual, currentUser.Id)
So(q1.Result[0].UserLogin, ShouldEqual, currentUser.Login)
So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email)
Convey("Should be able to delete an existing permission", func() {
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id)
So(err, ShouldBeNil)
q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 0)
})
})
Convey("Given a team", func() {
team1, err := sqlStore.CreateTeam("group1 name", "", 1)
So(err, ShouldBeNil)
Convey("Should be able to add a user permission for a team", func() {
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
TeamID: team1.Id,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_EDIT,
})
So(err, ShouldBeNil)
q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), q1)
So(err, ShouldBeNil)
So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q1.Result[0].Permission, ShouldEqual, models.PERMISSION_EDIT)
So(q1.Result[0].TeamId, ShouldEqual, team1.Id)
})
Convey("Should be able to update an existing permission for a team", func() {
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
TeamID: team1.Id,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_ADMIN,
})
So(err, ShouldBeNil)
q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), q3)
So(err, ShouldBeNil)
So(len(q3.Result), ShouldEqual, 1)
So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
So(q3.Result[0].Permission, ShouldEqual, models.PERMISSION_ADMIN)
So(q3.Result[0].TeamId, ShouldEqual, team1.Id)
require.Equal(t, 2, len(query.Result))
require.Equal(t, savedFolder.Id, query.Result[0].DashboardId)
require.True(t, query.Result[0].Inherited)
require.Equal(t, childDash.Id, query.Result[1].DashboardId)
require.False(t, query.Result[1].Inherited)
})
})
})
Convey("Given a root folder", func() {
var rootFolderId int64 = 0
Convey("When reading dashboard acl should return default permissions", func() {
query := models.GetDashboardAclInfoListQuery{DashboardID: rootFolderId, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
defaultPermissionsId := -1
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[0].Role, ShouldEqual, models.ROLE_VIEWER)
So(query.Result[0].Inherited, ShouldBeFalse)
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
So(*query.Result[1].Role, ShouldEqual, models.ROLE_EDITOR)
So(query.Result[1].Inherited, ShouldBeFalse)
t.Run("Reading dashboard acl should include default acl for parent folder and the child acl", func(t *testing.T) {
setup(t)
err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{
OrgID: 1,
UserID: currentUser.Id,
DashboardID: childDash.Id,
Permission: models.PERMISSION_EDIT,
})
require.Nil(t, err)
query := models.GetDashboardAclInfoListQuery{OrgID: 1, DashboardID: childDash.Id}
err = sqlStore.GetDashboardAclInfoList(context.Background(), &query)
require.Nil(t, err)
defaultPermissionsId := int64(-1)
require.Equal(t, 3, len(query.Result))
require.Equal(t, defaultPermissionsId, query.Result[0].DashboardId)
require.Equal(t, models.ROLE_VIEWER, *query.Result[0].Role)
require.True(t, query.Result[0].Inherited)
require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId)
require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role)
require.True(t, query.Result[1].Inherited)
require.Equal(t, childDash.Id, query.Result[2].DashboardId)
require.False(t, query.Result[2].Inherited)
})
t.Run("Add and delete dashboard permission", func(t *testing.T) {
setup(t)
err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
UserID: currentUser.Id,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_EDIT,
})
require.Nil(t, err)
q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), q1)
require.Nil(t, err)
require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId)
require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission)
require.Equal(t, "Edit", q1.Result[0].PermissionName)
require.Equal(t, currentUser.Id, q1.Result[0].UserId)
require.Equal(t, currentUser.Login, q1.Result[0].UserLogin)
require.Equal(t, currentUser.Email, q1.Result[0].UserEmail)
err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id)
require.Nil(t, err)
q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), q3)
require.Nil(t, err)
require.Equal(t, 0, len(q3.Result))
})
t.Run("Should be able to add a user permission for a team", func(t *testing.T) {
setup(t)
team1, err := sqlStore.CreateTeam("group1 name", "", 1)
require.Nil(t, err)
err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
TeamID: team1.Id,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_EDIT,
})
require.Nil(t, err)
q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), q1)
require.Nil(t, err)
require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId)
require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission)
require.Equal(t, team1.Id, q1.Result[0].TeamId)
})
t.Run("Should be able to update an existing permission for a team", func(t *testing.T) {
setup(t)
team1, err := sqlStore.CreateTeam("group1 name", "", 1)
require.Nil(t, err)
err = testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{
OrgID: 1,
TeamID: team1.Id,
DashboardID: savedFolder.Id,
Permission: models.PERMISSION_ADMIN,
})
require.Nil(t, err)
q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1}
err = sqlStore.GetDashboardAclInfoList(context.Background(), q3)
require.Nil(t, err)
require.Equal(t, 1, len(q3.Result))
require.Equal(t, savedFolder.Id, q3.Result[0].DashboardId)
require.Equal(t, models.PERMISSION_ADMIN, q3.Result[0].Permission)
require.Equal(t, team1.Id, q3.Result[0].TeamId)
})
})
t.Run("Default permissions for root folder dashboards", func(t *testing.T) {
setup(t)
var rootFolderId int64 = 0
sqlStore := InitTestDB(t)
query := models.GetDashboardAclInfoListQuery{DashboardID: rootFolderId, OrgID: 1}
err := sqlStore.GetDashboardAclInfoList(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 2, len(query.Result))
defaultPermissionsId := int64(-1)
require.Equal(t, defaultPermissionsId, query.Result[0].DashboardId)
require.Equal(t, models.ROLE_VIEWER, *query.Result[0].Role)
require.False(t, query.Result[0].Inherited)
require.Equal(t, defaultPermissionsId, query.Result[1].DashboardId)
require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role)
require.False(t, query.Result[1].Inherited)
})
}
@@ -10,132 +10,130 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestDashboardProvisioningTest(t *testing.T) {
Convey("Testing Dashboard provisioning", t, func() {
sqlStore := InitTestDB(t)
sqlStore := InitTestDB(t)
folderCmd := models.SaveDashboardCommand{
OrgId: 1,
FolderId: 0,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dashboard",
}),
folderCmd := models.SaveDashboardCommand{
OrgId: 1,
FolderId: 0,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dashboard",
}),
}
dash, err := sqlStore.SaveDashboard(folderCmd)
require.Nil(t, err)
saveDashboardCmd := models.SaveDashboardCommand{
OrgId: 1,
IsFolder: false,
FolderId: dash.Id,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dashboard",
}),
}
t.Run("Saving dashboards with provisioning meta data", func(t *testing.T) {
now := time.Now()
provisioning := &models.DashboardProvisioning{
Name: "default",
ExternalId: "/var/grafana.json",
Updated: now.Unix(),
}
dash, err := sqlStore.SaveDashboard(folderCmd)
So(err, ShouldBeNil)
saveDashboardCmd := models.SaveDashboardCommand{
OrgId: 1,
IsFolder: false,
FolderId: dash.Id,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dashboard",
}),
}
Convey("Saving dashboards with provisioning meta data", func() {
now := time.Now()
dash, err := sqlStore.SaveProvisionedDashboard(saveDashboardCmd, provisioning)
require.Nil(t, err)
require.NotNil(t, dash)
require.NotEqual(t, 0, dash.Id)
dashId := dash.Id
t.Run("Deleting orphaned provisioned dashboards", func(t *testing.T) {
saveCmd := models.SaveDashboardCommand{
OrgId: 1,
IsFolder: false,
FolderId: dash.Id,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "another_dashboard",
}),
}
provisioning := &models.DashboardProvisioning{
Name: "default",
Name: "another_reader",
ExternalId: "/var/grafana.json",
Updated: now.Unix(),
}
dash, err := sqlStore.SaveProvisionedDashboard(saveDashboardCmd, provisioning)
So(err, ShouldBeNil)
So(dash, ShouldNotBeNil)
So(dash.Id, ShouldNotEqual, 0)
dashId := dash.Id
anotherDash, err := sqlStore.SaveProvisionedDashboard(saveCmd, provisioning)
require.Nil(t, err)
Convey("Deleting orphaned provisioned dashboards", func() {
saveCmd := models.SaveDashboardCommand{
OrgId: 1,
IsFolder: false,
FolderId: dash.Id,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "another_dashboard",
}),
}
provisioning := &models.DashboardProvisioning{
Name: "another_reader",
ExternalId: "/var/grafana.json",
Updated: now.Unix(),
}
query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherDash.Id}}
err = GetDashboards(context.Background(), query)
require.Nil(t, err)
require.NotNil(t, query.Result)
anotherDash, err := sqlStore.SaveProvisionedDashboard(saveCmd, provisioning)
So(err, ShouldBeNil)
deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}}
require.Nil(t, DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd))
query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherDash.Id}}
err = GetDashboards(context.Background(), query)
So(err, ShouldBeNil)
So(query.Result, ShouldNotBeNil)
query = &models.GetDashboardsQuery{DashboardIds: []int64{dash.Id, anotherDash.Id}}
err = GetDashboards(context.Background(), query)
require.Nil(t, err)
deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}}
So(DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd), ShouldBeNil)
require.Equal(t, 1, len(query.Result))
require.Equal(t, dashId, query.Result[0].Id)
})
query = &models.GetDashboardsQuery{DashboardIds: []int64{dash.Id, anotherDash.Id}}
err = GetDashboards(context.Background(), query)
So(err, ShouldBeNil)
t.Run("Can query for provisioned dashboards", func(t *testing.T) {
rslt, err := sqlStore.GetProvisionedDashboardData("default")
require.Nil(t, err)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Id, ShouldEqual, dashId)
})
require.Equal(t, 1, len(rslt))
require.Equal(t, dashId, rslt[0].DashboardId)
require.Equal(t, now.Unix(), rslt[0].Updated)
})
Convey("Can query for provisioned dashboards", func() {
rslt, err := sqlStore.GetProvisionedDashboardData("default")
So(err, ShouldBeNil)
t.Run("Can query for one provisioned dashboard", func(t *testing.T) {
data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id)
require.Nil(t, err)
require.NotNil(t, data)
})
So(len(rslt), ShouldEqual, 1)
So(rslt[0].DashboardId, ShouldEqual, dashId)
So(rslt[0].Updated, ShouldEqual, now.Unix())
})
t.Run("Can query for none provisioned dashboard", func(t *testing.T) {
data, err := sqlStore.GetProvisionedDataByDashboardID(3000)
require.Nil(t, err)
require.Nil(t, data)
})
Convey("Can query for one provisioned dashboard", func() {
data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id)
So(err, ShouldBeNil)
t.Run("Deleting folder should delete provision meta data", func(t *testing.T) {
deleteCmd := &models.DeleteDashboardCommand{
Id: dash.Id,
OrgId: 1,
}
So(data, ShouldNotBeNil)
})
require.Nil(t, DeleteDashboard(context.Background(), deleteCmd))
Convey("Can query for none provisioned dashboard", func() {
data, err := sqlStore.GetProvisionedDataByDashboardID(3000)
So(err, ShouldBeNil)
So(data, ShouldBeNil)
})
data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id)
require.Nil(t, err)
require.Nil(t, data)
})
Convey("Deleting folder should delete provision meta data", func() {
deleteCmd := &models.DeleteDashboardCommand{
Id: dash.Id,
OrgId: 1,
}
t.Run("UnprovisionDashboard should delete provisioning metadata", func(t *testing.T) {
unprovisionCmd := &models.UnprovisionDashboardCommand{
Id: dashId,
}
So(DeleteDashboard(context.Background(), deleteCmd), ShouldBeNil)
require.Nil(t, UnprovisionDashboard(context.Background(), unprovisionCmd))
data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id)
So(err, ShouldBeNil)
So(data, ShouldBeNil)
})
Convey("UnprovisionDashboard should delete provisioning metadata", func() {
unprovisionCmd := &models.UnprovisionDashboardCommand{
Id: dashId,
}
So(UnprovisionDashboard(context.Background(), unprovisionCmd), ShouldBeNil)
data, err := sqlStore.GetProvisionedDataByDashboardID(dashId)
So(err, ShouldBeNil)
So(data, ShouldBeNil)
})
data, err := sqlStore.GetProvisionedDataByDashboardID(dashId)
require.Nil(t, err)
require.Nil(t, data)
})
})
}
+107 -107
View File
@@ -8,11 +8,10 @@ import (
"reflect"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func updateTestDashboard(t *testing.T, sqlStore *SQLStore, dashboard *models.Dashboard, data map[string]interface{}) {
@@ -26,151 +25,152 @@ func updateTestDashboard(t *testing.T, sqlStore *SQLStore, dashboard *models.Das
Dashboard: simplejson.NewFromAny(data),
}
_, err := sqlStore.SaveDashboard(saveCmd)
So(err, ShouldBeNil)
require.Nil(t, err)
}
func TestGetDashboardVersion(t *testing.T) {
Convey("Testing dashboard version retrieval", t, func() {
sqlStore := InitTestDB(t)
sqlStore := InitTestDB(t)
Convey("Get a Dashboard ID and version ID", func() {
savedDash := insertTestDashboard(t, sqlStore, "test dash 26", 1, 0, false, "diff")
t.Run("Get a Dashboard ID and version ID", func(t *testing.T) {
savedDash := insertTestDashboard(t, sqlStore, "test dash 26", 1, 0, false, "diff")
query := models.GetDashboardVersionQuery{
DashboardId: savedDash.Id,
Version: savedDash.Version,
OrgId: 1,
}
query := models.GetDashboardVersionQuery{
DashboardId: savedDash.Id,
Version: savedDash.Version,
OrgId: 1,
}
err := GetDashboardVersion(&query)
So(err, ShouldBeNil)
So(savedDash.Id, ShouldEqual, query.DashboardId)
So(savedDash.Version, ShouldEqual, query.Version)
err := GetDashboardVersion(&query)
require.Nil(t, err)
require.Equal(t, query.DashboardId, savedDash.Id)
require.Equal(t, query.Version, savedDash.Version)
dashCmd := models.GetDashboardQuery{
OrgId: savedDash.OrgId,
Uid: savedDash.Uid,
}
dashCmd := models.GetDashboardQuery{
OrgId: savedDash.OrgId,
Uid: savedDash.Uid,
}
err = GetDashboard(context.Background(), &dashCmd)
So(err, ShouldBeNil)
eq := reflect.DeepEqual(dashCmd.Result.Data, query.Result.Data)
So(eq, ShouldEqual, true)
})
err = GetDashboard(context.Background(), &dashCmd)
require.Nil(t, err)
eq := reflect.DeepEqual(dashCmd.Result.Data, query.Result.Data)
require.Equal(t, true, eq)
})
Convey("Attempt to get a version that doesn't exist", func() {
query := models.GetDashboardVersionQuery{
DashboardId: int64(999),
Version: 123,
OrgId: 1,
}
t.Run("Attempt to get a version that doesn't exist", func(t *testing.T) {
query := models.GetDashboardVersionQuery{
DashboardId: int64(999),
Version: 123,
OrgId: 1,
}
err := GetDashboardVersion(&query)
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardVersionNotFound)
})
err := GetDashboardVersion(&query)
require.Error(t, err)
require.Equal(t, models.ErrDashboardVersionNotFound, err)
})
}
func TestGetDashboardVersions(t *testing.T) {
Convey("Testing dashboard versions retrieval", t, func() {
sqlStore := InitTestDB(t)
savedDash := insertTestDashboard(t, sqlStore, "test dash 43", 1, 0, false, "diff-all")
sqlStore := InitTestDB(t)
savedDash := insertTestDashboard(t, sqlStore, "test dash 43", 1, 0, false, "diff-all")
Convey("Get all versions for a given Dashboard ID", func() {
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
t.Run("Get all versions for a given Dashboard ID", func(t *testing.T) {
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
err := GetDashboardVersions(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
err := GetDashboardVersions(&query)
require.Nil(t, err)
require.Equal(t, 1, len(query.Result))
})
t.Run("Attempt to get the versions for a non-existent Dashboard ID", func(t *testing.T) {
query := models.GetDashboardVersionsQuery{DashboardId: int64(999), OrgId: 1}
err := GetDashboardVersions(&query)
require.Error(t, err)
require.Equal(t, models.ErrNoVersionsForDashboardId, err)
require.Equal(t, 0, len(query.Result))
})
t.Run("Get all versions for an updated dashboard", func(t *testing.T) {
updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{
"tags": "different-tag",
})
Convey("Attempt to get the versions for a non-existent Dashboard ID", func() {
query := models.GetDashboardVersionsQuery{DashboardId: int64(999), OrgId: 1}
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
err := GetDashboardVersions(&query)
err := GetDashboardVersions(&query)
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrNoVersionsForDashboardId)
So(len(query.Result), ShouldEqual, 0)
})
Convey("Get all versions for an updated dashboard", func() {
updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{
"tags": "different-tag",
})
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
err := GetDashboardVersions(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
})
require.Nil(t, err)
require.Equal(t, 2, len(query.Result))
})
}
func TestDeleteExpiredVersions(t *testing.T) {
Convey("Testing dashboard versions clean up", t, func() {
sqlStore := InitTestDB(t)
versionsToKeep := 5
versionsToWrite := 10
setting.DashboardVersionsToKeep = versionsToKeep
versionsToKeep := 5
versionsToWrite := 10
setting.DashboardVersionsToKeep = versionsToKeep
savedDash := insertTestDashboard(t, sqlStore, "test dash 53", 1, 0, false, "diff-all")
var sqlStore *SQLStore
var savedDash *models.Dashboard
setup := func(t *testing.T) {
sqlStore = InitTestDB(t)
savedDash = insertTestDashboard(t, sqlStore, "test dash 53", 1, 0, false, "diff-all")
for i := 0; i < versionsToWrite-1; i++ {
updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{
"tags": "different-tag",
})
}
}
Convey("Clean up old dashboard versions", func() {
err := DeleteExpiredVersions(&models.DeleteExpiredVersionsCommand{})
So(err, ShouldBeNil)
t.Run("Clean up old dashboard versions", func(t *testing.T) {
setup(t)
err := DeleteExpiredVersions(&models.DeleteExpiredVersionsCommand{})
require.Nil(t, err)
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
err = GetDashboardVersions(&query)
So(err, ShouldBeNil)
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
err = GetDashboardVersions(&query)
require.Nil(t, err)
So(len(query.Result), ShouldEqual, versionsToKeep)
// Ensure latest versions were kept
So(query.Result[versionsToKeep-1].Version, ShouldEqual, versionsToWrite-versionsToKeep+1)
So(query.Result[0].Version, ShouldEqual, versionsToWrite)
})
require.Equal(t, versionsToKeep, len(query.Result))
// Ensure latest versions were kept
require.Equal(t, versionsToWrite-versionsToKeep+1, query.Result[versionsToKeep-1].Version)
require.Equal(t, versionsToWrite, query.Result[0].Version)
})
Convey("Don't delete anything if there are no expired versions", func() {
setting.DashboardVersionsToKeep = versionsToWrite
t.Run("Don't delete anything if there are no expired versions", func(t *testing.T) {
setup(t)
setting.DashboardVersionsToKeep = versionsToWrite
err := DeleteExpiredVersions(&models.DeleteExpiredVersionsCommand{})
So(err, ShouldBeNil)
err := DeleteExpiredVersions(&models.DeleteExpiredVersionsCommand{})
require.Nil(t, err)
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWrite}
err = GetDashboardVersions(&query)
So(err, ShouldBeNil)
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWrite}
err = GetDashboardVersions(&query)
require.Nil(t, err)
So(len(query.Result), ShouldEqual, versionsToWrite)
})
require.Equal(t, versionsToWrite, len(query.Result))
})
Convey("Don't delete more than MAX_VERSIONS_TO_DELETE_PER_BATCH * MAX_VERSION_DELETION_BATCHES per iteration", func() {
perBatch := 10
maxBatches := 10
t.Run("Don't delete more than MAX_VERSIONS_TO_DELETE_PER_BATCH * MAX_VERSION_DELETION_BATCHES per iteration", func(t *testing.T) {
setup(t)
perBatch := 10
maxBatches := 10
versionsToWriteBigNumber := perBatch*maxBatches + versionsToWrite
for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ {
updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{
"tags": "different-tag",
})
}
versionsToWriteBigNumber := perBatch*maxBatches + versionsToWrite
for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ {
updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{
"tags": "different-tag",
})
}
err := deleteExpiredVersions(&models.DeleteExpiredVersionsCommand{}, perBatch, maxBatches)
So(err, ShouldBeNil)
err := deleteExpiredVersions(&models.DeleteExpiredVersionsCommand{}, perBatch, maxBatches)
require.Nil(t, err)
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWriteBigNumber}
err = GetDashboardVersions(&query)
So(err, ShouldBeNil)
query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWriteBigNumber}
err = GetDashboardVersions(&query)
require.Nil(t, err)
// Ensure we have at least versionsToKeep versions
So(len(query.Result), ShouldBeGreaterThanOrEqualTo, versionsToKeep)
// Ensure we haven't deleted more than perBatch * maxBatches rows
So(versionsToWriteBigNumber-len(query.Result), ShouldBeLessThanOrEqualTo, perBatch*maxBatches)
})
// Ensure we have at least versionsToKeep versions
require.GreaterOrEqual(t, len(query.Result), versionsToKeep)
// Ensure we haven't deleted more than perBatch * maxBatches rows
require.LessOrEqual(t, versionsToWriteBigNumber-len(query.Result), perBatch*maxBatches)
})
}
+88 -83
View File
@@ -8,7 +8,8 @@ import (
"time"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func mockTime(mock time.Time) time.Time {
@@ -17,112 +18,116 @@ func mockTime(mock time.Time) time.Time {
}
func TestLoginAttempts(t *testing.T) {
Convey("Testing Login Attempts DB Access", t, func() {
var beginningOfTime, timePlusOneMinute, timePlusTwoMinutes time.Time
user := "user"
setup := func(t *testing.T) {
InitTestDB(t)
user := "user"
beginningOfTime := mockTime(time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local))
beginningOfTime = mockTime(time.Date(2017, 10, 22, 8, 0, 0, 0, time.Local))
err := CreateLoginAttempt(&models.CreateLoginAttemptCommand{
Username: user,
IpAddress: "192.168.0.1",
})
So(err, ShouldBeNil)
timePlusOneMinute := mockTime(beginningOfTime.Add(time.Minute * 1))
require.Nil(t, err)
timePlusOneMinute = mockTime(beginningOfTime.Add(time.Minute * 1))
err = CreateLoginAttempt(&models.CreateLoginAttemptCommand{
Username: user,
IpAddress: "192.168.0.1",
})
So(err, ShouldBeNil)
timePlusTwoMinutes := mockTime(beginningOfTime.Add(time.Minute * 2))
require.Nil(t, err)
timePlusTwoMinutes = mockTime(beginningOfTime.Add(time.Minute * 2))
err = CreateLoginAttempt(&models.CreateLoginAttemptCommand{
Username: user,
IpAddress: "192.168.0.1",
})
So(err, ShouldBeNil)
require.Nil(t, err)
}
Convey("Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s", func() {
query := models.GetUserLoginAttemptCountQuery{
Username: user,
Since: timePlusTwoMinutes.Add(time.Second * 1),
}
err := GetUserLoginAttemptCount(&query)
So(err, ShouldBeNil)
So(query.Result, ShouldEqual, 0)
})
t.Run("Should return a total count of zero login attempts when comparing since beginning of time + 2min and 1s", func(t *testing.T) {
setup(t)
query := models.GetUserLoginAttemptCountQuery{
Username: user,
Since: timePlusTwoMinutes.Add(time.Second * 1),
}
err := GetUserLoginAttemptCount(&query)
require.Nil(t, err)
require.Equal(t, int64(0), query.Result)
})
Convey("Should return the total count of login attempts since beginning of time", func() {
query := models.GetUserLoginAttemptCountQuery{
Username: user,
Since: beginningOfTime,
}
err := GetUserLoginAttemptCount(&query)
So(err, ShouldBeNil)
So(query.Result, ShouldEqual, 3)
})
t.Run("Should return the total count of login attempts since beginning of time", func(t *testing.T) {
setup(t)
query := models.GetUserLoginAttemptCountQuery{
Username: user,
Since: beginningOfTime,
}
err := GetUserLoginAttemptCount(&query)
require.Nil(t, err)
require.Equal(t, int64(3), query.Result)
})
Convey("Should return the total count of login attempts since beginning of time + 1min", func() {
query := models.GetUserLoginAttemptCountQuery{
Username: user,
Since: timePlusOneMinute,
}
err := GetUserLoginAttemptCount(&query)
So(err, ShouldBeNil)
So(query.Result, ShouldEqual, 2)
})
t.Run("Should return the total count of login attempts since beginning of time + 1min", func(t *testing.T) {
setup(t)
query := models.GetUserLoginAttemptCountQuery{
Username: user,
Since: timePlusOneMinute,
}
err := GetUserLoginAttemptCount(&query)
require.Nil(t, err)
require.Equal(t, int64(2), query.Result)
})
Convey("Should return the total count of login attempts since beginning of time + 2min", func() {
query := models.GetUserLoginAttemptCountQuery{
Username: user,
Since: timePlusTwoMinutes,
}
err := GetUserLoginAttemptCount(&query)
So(err, ShouldBeNil)
So(query.Result, ShouldEqual, 1)
})
t.Run("Should return the total count of login attempts since beginning of time + 2min", func(t *testing.T) {
setup(t)
query := models.GetUserLoginAttemptCountQuery{
Username: user,
Since: timePlusTwoMinutes,
}
err := GetUserLoginAttemptCount(&query)
require.Nil(t, err)
require.Equal(t, int64(1), query.Result)
})
Convey("Should return deleted rows older than beginning of time", func() {
cmd := models.DeleteOldLoginAttemptsCommand{
OlderThan: beginningOfTime,
}
err := DeleteOldLoginAttempts(&cmd)
t.Run("Should return deleted rows older than beginning of time", func(t *testing.T) {
setup(t)
cmd := models.DeleteOldLoginAttemptsCommand{
OlderThan: beginningOfTime,
}
err := DeleteOldLoginAttempts(&cmd)
So(err, ShouldBeNil)
So(cmd.DeletedRows, ShouldEqual, 0)
})
require.Nil(t, err)
require.Equal(t, int64(0), cmd.DeletedRows)
})
Convey("Should return deleted rows older than beginning of time + 1min", func() {
cmd := models.DeleteOldLoginAttemptsCommand{
OlderThan: timePlusOneMinute,
}
err := DeleteOldLoginAttempts(&cmd)
t.Run("Should return deleted rows older than beginning of time + 1min", func(t *testing.T) {
setup(t)
cmd := models.DeleteOldLoginAttemptsCommand{
OlderThan: timePlusOneMinute,
}
err := DeleteOldLoginAttempts(&cmd)
So(err, ShouldBeNil)
So(cmd.DeletedRows, ShouldEqual, 1)
})
require.Nil(t, err)
require.Equal(t, int64(1), cmd.DeletedRows)
})
Convey("Should return deleted rows older than beginning of time + 2min", func() {
cmd := models.DeleteOldLoginAttemptsCommand{
OlderThan: timePlusTwoMinutes,
}
err := DeleteOldLoginAttempts(&cmd)
t.Run("Should return deleted rows older than beginning of time + 2min", func(t *testing.T) {
setup(t)
cmd := models.DeleteOldLoginAttemptsCommand{
OlderThan: timePlusTwoMinutes,
}
err := DeleteOldLoginAttempts(&cmd)
So(err, ShouldBeNil)
So(cmd.DeletedRows, ShouldEqual, 2)
})
require.Nil(t, err)
require.Equal(t, int64(2), cmd.DeletedRows)
})
Convey("Should return deleted rows older than beginning of time + 2min and 1s", func() {
cmd := models.DeleteOldLoginAttemptsCommand{
OlderThan: timePlusTwoMinutes.Add(time.Second * 1),
}
err := DeleteOldLoginAttempts(&cmd)
t.Run("Should return deleted rows older than beginning of time + 2min and 1s", func(t *testing.T) {
setup(t)
cmd := models.DeleteOldLoginAttemptsCommand{
OlderThan: timePlusTwoMinutes.Add(time.Second * 1),
}
err := DeleteOldLoginAttempts(&cmd)
So(err, ShouldBeNil)
So(cmd.DeletedRows, ShouldEqual, 3)
})
require.Nil(t, err)
require.Equal(t, int64(3), cmd.DeletedRows)
})
}
@@ -82,4 +82,8 @@ func addApiKeyMigrations(mg *Migrator) {
mg.AddMigration("Add expires to api_key table", NewAddColumnMigration(apiKeyV2, &Column{
Name: "expires", Type: DB_BigInt, Nullable: true,
}))
mg.AddMigration("Add service account foreign key", NewAddColumnMigration(apiKeyV2, &Column{
Name: "service_account_id", Type: DB_BigInt, Nullable: true,
}))
}
-1
View File
@@ -320,7 +320,6 @@ func TestAccountDataAccess(t *testing.T) {
query := models.GetOrgUsersQuery{OrgId: ac1.OrgId}
err = sqlStore.GetOrgUsers(context.Background(), &query)
require.NoError(t, err)
fmt.Println(query.Result)
// require.Equal(t, len(query.Result), 3)
dash1 := insertTestDashboard(t, sqlStore, "1 test dash", ac1.OrgId, 0, false, "prod", "webapp")
+12 -13
View File
@@ -8,22 +8,21 @@ import (
"testing"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestSavingTags(t *testing.T) {
Convey("Testing tags saving", t, func() {
InitTestDB(t)
InitTestDB(t)
tagPairs := []*models.Tag{
{Key: "outage"},
{Key: "type", Value: "outage"},
{Key: "server", Value: "server-1"},
{Key: "error"},
}
tags, err := EnsureTagsExist(newSession(context.Background()), tagPairs)
tagPairs := []*models.Tag{
{Key: "outage"},
{Key: "type", Value: "outage"},
{Key: "server", Value: "server-1"},
{Key: "error"},
}
tags, err := EnsureTagsExist(newSession(context.Background()), tagPairs)
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 4)
})
require.Nil(t, err)
require.Equal(t, 4, len(tags))
}
+64 -60
View File
@@ -8,82 +8,86 @@ import (
"time"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestTempUserCommandsAndQueries(t *testing.T) {
Convey("Testing Temp User commands & queries", t, func() {
cmd := models.CreateTempUserCommand{
OrgId: 2256,
Name: "hello",
Code: "asd",
Email: "e@as.co",
Status: models.TmpUserInvitePending,
}
setup := func(t *testing.T) {
InitTestDB(t)
err := CreateTempUser(&cmd)
require.Nil(t, err)
}
Convey("Given saved api key", func() {
cmd := models.CreateTempUserCommand{
OrgId: 2256,
Name: "hello",
Code: "asd",
Email: "e@as.co",
Status: models.TmpUserInvitePending,
}
err := CreateTempUser(&cmd)
So(err, ShouldBeNil)
t.Run("Should be able to get temp users by org id", func(t *testing.T) {
setup(t)
query := models.GetTempUsersQuery{OrgId: 2256, Status: models.TmpUserInvitePending}
err := GetTempUsersQuery(&query)
Convey("Should be able to get temp users by org id", func() {
query := models.GetTempUsersQuery{OrgId: 2256, Status: models.TmpUserInvitePending}
err = GetTempUsersQuery(&query)
require.Nil(t, err)
require.Equal(t, 1, len(query.Result))
})
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
})
t.Run("Should be able to get temp users by email", func(t *testing.T) {
setup(t)
query := models.GetTempUsersQuery{Email: "e@as.co", Status: models.TmpUserInvitePending}
err := GetTempUsersQuery(&query)
Convey("Should be able to get temp users by email", func() {
query := models.GetTempUsersQuery{Email: "e@as.co", Status: models.TmpUserInvitePending}
err = GetTempUsersQuery(&query)
require.Nil(t, err)
require.Equal(t, 1, len(query.Result))
})
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
})
t.Run("Should be able to get temp users by code", func(t *testing.T) {
setup(t)
query := models.GetTempUserByCodeQuery{Code: "asd"}
err := GetTempUserByCode(&query)
Convey("Should be able to get temp users by code", func() {
query := models.GetTempUserByCodeQuery{Code: "asd"}
err = GetTempUserByCode(&query)
require.Nil(t, err)
require.Equal(t, "hello", query.Result.Name)
})
So(err, ShouldBeNil)
So(query.Result.Name, ShouldEqual, "hello")
})
t.Run("Should be able update status", func(t *testing.T) {
setup(t)
cmd2 := models.UpdateTempUserStatusCommand{Code: "asd", Status: models.TmpUserRevoked}
err := UpdateTempUserStatus(&cmd2)
require.Nil(t, err)
})
Convey("Should be able update status", func() {
cmd2 := models.UpdateTempUserStatusCommand{Code: "asd", Status: models.TmpUserRevoked}
err := UpdateTempUserStatus(&cmd2)
So(err, ShouldBeNil)
})
t.Run("Should be able update email sent and email sent on", func(t *testing.T) {
setup(t)
cmd2 := models.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
err := UpdateTempUserWithEmailSent(&cmd2)
require.Nil(t, err)
Convey("Should be able update email sent and email sent on", func() {
cmd2 := models.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
err := UpdateTempUserWithEmailSent(&cmd2)
So(err, ShouldBeNil)
query := models.GetTempUsersQuery{OrgId: 2256, Status: models.TmpUserInvitePending}
err = GetTempUsersQuery(&query)
query := models.GetTempUsersQuery{OrgId: 2256, Status: models.TmpUserInvitePending}
err = GetTempUsersQuery(&query)
require.Nil(t, err)
require.True(t, query.Result[0].EmailSent)
require.False(t, query.Result[0].EmailSentOn.UTC().Before(query.Result[0].Created.UTC()))
})
So(err, ShouldBeNil)
So(query.Result[0].EmailSent, ShouldBeTrue)
So(query.Result[0].EmailSentOn.UTC(), ShouldHappenOnOrAfter, query.Result[0].Created.UTC())
})
t.Run("Should be able expire temp user", func(t *testing.T) {
setup(t)
createdAt := time.Unix(cmd.Result.Created, 0)
cmd2 := models.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)}
err := ExpireOldUserInvites(&cmd2)
require.Nil(t, err)
require.Equal(t, int64(1), cmd2.NumExpired)
Convey("Should be able expire temp user", func() {
createdAt := time.Unix(cmd.Result.Created, 0)
cmd2 := models.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)}
err := ExpireOldUserInvites(&cmd2)
So(err, ShouldBeNil)
So(cmd2.NumExpired, ShouldEqual, int64(1))
Convey("Should do nothing when no temp users to expire", func() {
createdAt := time.Unix(cmd.Result.Created, 0)
cmd2 := models.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)}
err := ExpireOldUserInvites(&cmd2)
So(err, ShouldBeNil)
So(cmd2.NumExpired, ShouldEqual, int64(0))
})
})
t.Run("Should do nothing when no temp users to expire", func(t *testing.T) {
createdAt := time.Unix(cmd.Result.Created, 0)
cmd2 := models.ExpireTempUsersCommand{OlderThan: createdAt.Add(1 * time.Second)}
err := ExpireOldUserInvites(&cmd2)
require.Nil(t, err)
require.Equal(t, int64(0), cmd2.NumExpired)
})
})
}
+28 -29
View File
@@ -8,9 +8,8 @@ import (
"errors"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/require"
)
var ErrProvokedError = errors.New("testing error")
@@ -18,40 +17,40 @@ var ErrProvokedError = errors.New("testing error")
func TestTransaction(t *testing.T) {
ss := InitTestDB(t)
Convey("InTransaction", t, func() {
cmd := &models.AddApiKeyCommand{Key: "secret-key", Name: "key", OrgId: 1}
cmd := &models.AddApiKeyCommand{Key: "secret-key", Name: "key", OrgId: 1}
t.Run("can update key", func(t *testing.T) {
err := AddAPIKey(context.Background(), cmd)
So(err, ShouldBeNil)
require.Nil(t, err)
Convey("can update key", func() {
err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
return deleteAPIKey(sess, cmd.Result.Id, 1)
})
So(err, ShouldBeNil)
query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id}
err = GetApiKeyById(query)
So(err, ShouldEqual, models.ErrInvalidApiKey)
err = ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
return deleteAPIKey(sess, cmd.Result.Id, 1)
})
Convey("won't update if one handler fails", func() {
err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
err := deleteAPIKey(sess, cmd.Result.Id, 1)
if err != nil {
return err
}
require.Nil(t, err)
return ErrProvokedError
})
query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id}
err = GetApiKeyById(query)
require.Equal(t, models.ErrInvalidApiKey, err)
})
So(err, ShouldEqual, ErrProvokedError)
t.Run("won't update if one handler fails", func(t *testing.T) {
err := AddAPIKey(context.Background(), cmd)
require.Nil(t, err)
query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id}
err = GetApiKeyById(query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, cmd.Result.Id)
err = ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
err := deleteAPIKey(sess, cmd.Result.Id, 1)
if err != nil {
return err
}
return ErrProvokedError
})
require.Equal(t, ErrProvokedError, err)
query := &models.GetApiKeyByIdQuery{ApiKeyId: cmd.Result.Id}
err = GetApiKeyById(query)
require.Nil(t, err)
require.Equal(t, cmd.Result.Id, query.Result.Id)
})
}
+9 -4
View File
@@ -159,7 +159,9 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
if query.RangeQuery {
rangeResponse, _, err := client.QueryRange(ctx, query.Expr, timeRange)
if err != nil {
return &result, fmt.Errorf("query: %s failed with: %v", query.Expr, err)
plog.Error("Range query", query.Expr, "failed with", err)
result.Responses[query.RefId] = backend.DataResponse{Error: err}
continue
}
response[RangeQueryType] = rangeResponse
}
@@ -167,16 +169,19 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
if query.InstantQuery {
instantResponse, _, err := client.Query(ctx, query.Expr, query.End)
if err != nil {
return &result, fmt.Errorf("query: %s failed with: %v", query.Expr, err)
plog.Error("Instant query", query.Expr, "failed with", err)
result.Responses[query.RefId] = backend.DataResponse{Error: err}
continue
}
response[InstantQueryType] = instantResponse
}
// For now, we ignore exemplar errors and continue with processing of other results
if query.ExemplarQuery {
exemplarResponse, err := client.QueryExemplars(ctx, query.Expr, timeRange.Start, timeRange.End)
if err != nil {
exemplarResponse = nil
plog.Error("Exemplar query", query.Expr, "failed with", err)
result.Responses[query.RefId] = backend.DataResponse{Error: err}
continue
}
response[ExemplarQueryType] = exemplarResponse
}
+2 -1
View File
@@ -4,8 +4,9 @@ import { PanelEvents } from '@grafana/data';
import { PanelModel } from '../../features/dashboard/state';
import { PanelCtrl } from './panel_ctrl';
import { Subscription } from 'rxjs';
import { PanelDirectiveReadyEvent, RefreshEvent, RenderEvent } from 'app/types/events';
import { PanelDirectiveReadyEvent, RenderEvent } from 'app/types/events';
import { coreModule } from 'app/core/core_module';
import { RefreshEvent } from '@grafana/runtime';
const panelTemplate = `
<ng-transclude class="panel-height-helper"></ng-transclude>
@@ -100,11 +100,12 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
{() => (
<HorizontalGroup>
<Select
menuShouldPortal
aria-label="Role to add new permission to"
isSearchable={false}
value={this.state.type}
options={dashboardAclTargets}
onChange={this.onTypeChanged}
menuShouldPortal
/>
{newItem.type === AclTarget.User ? (
@@ -118,12 +119,13 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
<span className={styles.label}>Can</span>
<Select
menuShouldPortal
aria-label="Permission level"
isSearchable={false}
value={this.state.permission}
options={dashboardPermissionLevels}
onChange={this.onPermissionChanged}
width={25}
menuShouldPortal
/>
<Button data-save-permission type="submit" disabled={!isValid}>
Save
@@ -25,16 +25,17 @@ export default class DisabledPermissionListItem extends Component<Props, any> {
<td>
<div className="gf-form">
<Select
menuShouldPortal
aria-label={`Permission level for "${item.name}"`}
options={dashboardPermissionLevels}
onChange={() => {}}
disabled={true}
value={currentPermissionLevel}
menuShouldPortal
/>
</div>
</td>
<td>
<Button size="sm" disabled icon="lock" />
<Button aria-label={`Remove permission for "${item.name}"`} size="sm" icon="lock" disabled />
</td>
</tr>
);
@@ -75,20 +75,27 @@ export default class PermissionsListItem extends PureComponent<Props> {
<td className="query-keyword">Can</td>
<td>
<Select
menuShouldPortal
aria-label={`Permission level for "${item.name}"`}
isSearchable={false}
options={dashboardPermissionLevels}
onChange={this.onPermissionChanged}
disabled={item.inherited}
value={currentPermissionLevel}
width={25}
menuShouldPortal
/>
</td>
<td>
{!item.inherited ? (
<Button size="sm" variant="destructive" icon="times" onClick={this.onRemoveItem} />
<Button
aria-label={`Remove permission for "${item.name}"`}
size="sm"
variant="destructive"
icon="times"
onClick={this.onRemoveItem}
/>
) : (
<Button size="sm" disabled icon="times" />
<Button aria-label={`Remove permission for "${item.name}" (Disabled)`} size="sm" disabled icon="times" />
)}
</td>
</tr>
@@ -65,6 +65,7 @@ export class TeamPicker extends Component<Props, State> {
className={className}
placeholder="Select a team"
noOptionsMessage="No teams found"
aria-label="Team picker"
/>
</div>
);
@@ -76,6 +76,7 @@ export class UserPicker extends Component<Props, State> {
onChange={onSelected}
placeholder="Start typing to search for user"
noOptionsMessage="No users found"
aria-label="User picker"
/>
</div>
);
@@ -236,11 +236,8 @@ export class KeybindingSrv {
if (contextSrv.hasAccessToExplore()) {
this.bindWithPanelId('x', async (panelId) => {
const panel = dashboard.getPanelById(panelId)!;
const datasource = await getDatasourceSrv().get(panel.datasource);
const url = await getExploreUrl({
panel,
panelTargets: panel.targets,
panelDatasource: datasource,
datasourceSrv: getDatasourceSrv(),
timeSrv: getTimeSrv(),
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { ThemeChangedEvent } from 'app/types/events';
import { ThemeChangedEvent } from '@grafana/runtime';
import appEvents from '../app_events';
import { config } from '../config';
import { PreferencesService } from './PreferencesService';
+3
View File
@@ -16,6 +16,8 @@ import {
} from '../../types/events';
import { ConfirmModal, ConfirmModalProps } from '@grafana/ui';
import { deprecationWarning, textUtil } from '@grafana/data';
import { CopyPanelEvent } from '@grafana/runtime';
import { copyPanel } from 'app/features/dashboard/utils/panel';
export class UtilSrv {
modalScope: any;
@@ -32,6 +34,7 @@ export class UtilSrv {
appEvents.subscribe(HideModalEvent, this.hideModal.bind(this));
appEvents.subscribe(ShowConfirmModalEvent, (e) => this.showConfirmModal(e.payload));
appEvents.subscribe(ShowModalReactEvent, (e) => this.showModalReact(e.payload));
appEvents.subscribe(CopyPanelEvent, (e) => copyPanel(e.payload));
}
showModalReact(options: any) {
+1 -2
View File
@@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react';
import { config, GrafanaBootConfig } from '@grafana/runtime';
import { config, GrafanaBootConfig, ThemeChangedEvent } from '@grafana/runtime';
import { ThemeContext } from '@grafana/ui';
import { appEvents } from '../core';
import { ThemeChangedEvent } from 'app/types/events';
import { createTheme } from '@grafana/data';
export const ConfigContext = React.createContext<GrafanaBootConfig>(config);
+4 -8
View File
@@ -194,16 +194,12 @@ describe('getExploreUrl', () => {
const args = ({
panel: {
getSavedId: () => 1,
},
panelTargets: [{ refId: 'A', expr: 'query1', legendFormat: 'legendFormat1' }],
panelDatasource: {
name: 'testDataSource',
meta: {
id: '1',
},
targets: [{ refId: 'A', expr: 'query1', legendFormat: 'legendFormat1' }],
},
datasourceSrv: {
get: jest.fn(),
get() {
return {};
},
getDataSourceById: jest.fn(),
},
timeSrv: {
+10 -15
View File
@@ -49,40 +49,35 @@ const MAX_HISTORY_ITEMS = 100;
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
export const lastUsedDatasourceKeyForOrgId = (orgId: number) => `${LAST_USED_DATASOURCE_KEY}.${orgId}`;
/**
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
*
* @param panelTargets The origin panel's query targets
* @param panelDatasource The origin panel's datasource
* @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
* @param timeSrv Time service to get the current dashboard range from
*/
export interface GetExploreUrlArguments {
panel: PanelModel;
panelTargets: DataQuery[];
panelDatasource: DataSourceApi;
/** Datasource service to query other datasources in case the panel datasource is mixed */
datasourceSrv: DataSourceSrv;
/** Time service to get the current dashboard range from */
timeSrv: TimeSrv;
}
/**
* Returns an Explore-URL that contains a panel's queries and the dashboard time range.
*/
export async function getExploreUrl(args: GetExploreUrlArguments): Promise<string | undefined> {
const { panel, panelTargets, panelDatasource, datasourceSrv, timeSrv } = args;
let exploreDatasource = panelDatasource;
const { panel, datasourceSrv, timeSrv } = args;
let exploreDatasource = await datasourceSrv.get(panel.datasource);
/** In Explore, we don't have legend formatter and we don't want to keep
* legend formatting as we can't change it
*/
let exploreTargets: DataQuery[] = panelTargets.map((t) => omit(t, 'legendFormat'));
let exploreTargets: DataQuery[] = panel.targets.map((t) => omit(t, 'legendFormat'));
let url: string | undefined;
// Mixed datasources need to choose only one datasource
if (panelDatasource.meta?.id === 'mixed' && exploreTargets) {
if (exploreDatasource.meta?.id === 'mixed' && exploreTargets) {
// Find first explore datasource among targets
for (const t of exploreTargets) {
const datasource = await datasourceSrv.get(t.datasource || undefined);
if (datasource) {
exploreDatasource = datasource;
exploreTargets = panelTargets.filter((t) => t.datasource === datasource.name);
exploreTargets = panel.targets.filter((t) => t.datasource === datasource.name);
break;
}
}
+32
View File
@@ -1,5 +1,6 @@
import {
addToRichHistory,
getRichHistory,
updateStarredInRichHistory,
updateCommentInRichHistory,
mapNumbertoTimeInSlider,
@@ -274,4 +275,35 @@ describe('richHistory', () => {
expect(heading).toEqual(mock.storedHistory[0].datasourceName);
});
});
describe('getRichHistory', () => {
afterEach(() => {
deleteAllFromRichHistory();
expect(store.exists(key)).toBeFalsy();
});
it('should load from localStorage data in old format', () => {
const oldHistoryItem = { ...mock.storedHistory[0], queries: ['test query 1', 'test query 2', 'test query 3'] };
store.setObject(key, [oldHistoryItem]);
const expectedHistoryItem = {
...mock.storedHistory[0],
queries: [
{
expr: 'test query 1',
refId: 'A',
},
{
expr: 'test query 2',
refId: 'B',
},
{
expr: 'test query 3',
refId: 'C',
},
],
};
const result = getRichHistory();
expect(result).toStrictEqual([expectedHistoryItem]);
});
});
});
-1
View File
@@ -394,7 +394,6 @@ export function filterAndSortQueries(
return sortQueries(filteredQueriesToBeSorted, sortOrder);
}
/* These functions are created to migrate string queries (from 6.7 release) to DataQueries. They can be removed after 7.1 release. */
function migrateRichHistory(richHistory: RichHistoryQuery[]) {
const transformedRichHistory = richHistory.map((query) => {
const transformedQueries: DataQuery[] = query.queries.map((q, index) => createDataQuery(query, q, index));
@@ -4,7 +4,7 @@ import { PanelModel } from 'app/features/dashboard/state';
export const hiddenReducerTypes = ['percent_diff', 'percent_diff_abs'];
export class ThresholdMapper {
static alertToGraphThresholds(panel: PanelModel) {
if (!panel.alert || config.featureToggles.ngalert) {
if (!panel.alert || config.unifiedAlertingEnabled) {
return false; // no update when no alerts
}
+4 -8
View File
@@ -46,11 +46,11 @@ export class Scene {
selecto?: Selecto;
div?: HTMLDivElement;
constructor(cfg: CanvasGroupOptions, public onSave: (cfg: CanvasGroupOptions) => void) {
this.root = this.load(cfg);
constructor(cfg: CanvasGroupOptions, enableEditing: boolean, public onSave: (cfg: CanvasGroupOptions) => void) {
this.root = this.load(cfg, enableEditing);
}
load(cfg: CanvasGroupOptions) {
load(cfg: CanvasGroupOptions, enableEditing: boolean) {
this.root = new RootElement(
cfg ?? {
type: 'group',
@@ -66,7 +66,7 @@ export class Scene {
});
setTimeout(() => {
if (this.div) {
if (this.div && enableEditing) {
this.initMoveable();
}
}, 100);
@@ -158,10 +158,6 @@ export class Scene {
};
initMoveable = () => {
if (this.selecto) {
this.selecto.destroy();
}
const targetElements: HTMLDivElement[] = [];
this.root.elements.forEach((element: ElementState) => {
targetElements.push(element.div!);
@@ -1,6 +1,7 @@
// Libraries
import React, { Component } from 'react';
import { dateMath, TimeRange, TimeZone } from '@grafana/data';
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
// Types
import { DashboardModel } from '../../state';
@@ -12,7 +13,7 @@ import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePicker
// Utils & Services
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { appEvents } from 'app/core/core';
import { ShiftTimeEvent, ShiftTimeEventPayload, TimeRangeUpdatedEvent, ZoomOutEvent } from '../../../../types/events';
import { ShiftTimeEvent, ShiftTimeEventPayload, ZoomOutEvent } from '../../../../types/events';
import { Unsubscribable } from 'rxjs';
export interface Props {
@@ -5,8 +5,8 @@ import { PanelModel } from '../../state/PanelModel';
import { DashboardModel } from '../../state/DashboardModel';
import appEvents from 'app/core/app_events';
import { RowOptionsButton } from '../RowOptions/RowOptionsButton';
import { getTemplateSrv } from '@grafana/runtime';
import { RefreshEvent, ShowConfirmModalEvent } from '../../../../types/events';
import { getTemplateSrv, RefreshEvent } from '@grafana/runtime';
import { ShowConfirmModalEvent } from '../../../../types/events';
import { Unsubscribable } from 'rxjs';
export interface DashboardRowProps {
@@ -42,7 +42,7 @@ export const DeleteDashboardModal: React.FC<DeleteDashboardModalProps> = ({ hide
const getModalBody = (panels: PanelModel[], title: string) => {
const totalAlerts = sumBy(panels, (panel) => (panel.alert ? 1 : 0));
return totalAlerts > 0 && !config.featureToggles.ngalert ? (
return totalAlerts > 0 && !config.unifiedAlertingEnabled ? (
<>
<p>Do you want to delete this dashboard?</p>
<p>
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { PanelModel, DashboardModel } from '../../state';
import { usePanelLatestData } from './usePanelLatestData';
import { PanelOptions } from 'app/plugins/panel/table/models.gen';
import { RefreshEvent } from 'app/types/events';
import { RefreshEvent } from '@grafana/runtime';
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { getTimeSrv, TimeSrv } from '../../services/TimeSrv';
interface Props {
@@ -9,7 +9,7 @@ import { DashboardLink } from '../../state/DashboardModel';
import { linkIconMap } from '../LinksSettings/LinkSettingsEdit';
import { useEffectOnce } from 'react-use';
import { selectors } from '@grafana/e2e-selectors';
import { TimeRangeUpdatedEvent } from 'app/types/events';
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
export interface Props {
dashboard: DashboardModel;
@@ -41,6 +41,12 @@ export const DiffViewer: React.FC<ReactDiffViewerProps> = ({ oldValue, newValue
codeFold: {
fontSize: theme.typography.size.sm,
},
gutter: `
pre {
color: ${tinycolor(theme.colors.textFaint).setAlpha(1).toString()};
opacity: 0.61;
}
`,
};
return (
@@ -25,6 +25,7 @@ export const VersionHistoryTable: React.FC<VersionsTableProps> = ({ versions, on
<tr key={version.id}>
<td>
<Checkbox
aria-label={`Toggle selection of version ${version.version}`}
className={css`
display: inline;
`}
@@ -12,6 +12,7 @@ import { notifyApp } from 'app/core/actions';
import { selectors } from '@grafana/e2e-selectors';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { createTheme } from '@grafana/data';
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => {
class GeneralSettings extends React.Component<{}, {}> {
@@ -37,6 +38,12 @@ jest.mock('app/core/core', () => ({
},
}));
jest.mock('react-virtualized-auto-sizer', () => {
// The size of the children need to be small enough to be outside the view.
// So it does not trigger the query to be run by the PanelQueryRunner.
return ({ children }: AutoSizerProps) => children({ height: 1, width: 1 });
});
interface ScenarioContext {
dashboard?: DashboardModel | null;
container?: HTMLElement;
@@ -214,49 +214,57 @@ export class DashboardGrid extends PureComponent<Props, State> {
render() {
const { dashboard } = this.props;
const autoSizerStyle: CSSProperties = {
width: '100%',
height: '100%',
};
return (
<AutoSizer style={autoSizerStyle} disableHeight>
{({ width }) => {
if (width === 0) {
return null;
}
/**
* We have a parent with "flex: 1 1 0" we need to reset it to "flex: 1 1 auto" to have the AutoSizer
* properly working. For more information go here:
* https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#can-i-use-autosizer-within-a-flex-container
*/
<div style={{ flex: '1 1 auto' }}>
<AutoSizer disableHeight>
{({ width }) => {
if (width === 0) {
return null;
}
const draggable = width <= 769 ? false : dashboard.meta.canEdit;
const draggable = width <= 769 ? false : dashboard.meta.canEdit;
/*
/*
Disable draggable if mobile device, solving an issue with unintentionally
moving panels. https://github.com/grafana/grafana/issues/18497
theme.breakpoints.md = 769
*/
return (
<ReactGridLayout
width={width}
isDraggable={draggable}
isResizable={dashboard.meta.canEdit}
containerPadding={[0, 0]}
useCSSTransforms={false}
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
cols={GRID_COLUMN_COUNT}
rowHeight={GRID_CELL_HEIGHT}
draggableHandle=".grid-drag-handle"
layout={this.buildLayout()}
onDragStop={this.onDragStop}
onResize={this.onResize}
onResizeStop={this.onResizeStop}
onLayoutChange={this.onLayoutChange}
>
{this.renderPanels(width)}
</ReactGridLayout>
);
}}
</AutoSizer>
return (
/**
* The children is using a width of 100% so we need to guarantee that it is wrapped
* in an element that has the calculated size given by the AutoSizer. The AutoSizer
* has a width of 0 and will let its content overflow its div.
*/
<div style={{ width: `${width}px`, height: '100%' }}>
<ReactGridLayout
width={width}
isDraggable={draggable}
isResizable={dashboard.meta.canEdit}
containerPadding={[0, 0]}
useCSSTransforms={false}
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
cols={GRID_COLUMN_COUNT}
rowHeight={GRID_CELL_HEIGHT}
draggableHandle=".grid-drag-handle"
layout={this.buildLayout()}
onDragStop={this.onDragStop}
onResize={this.onResize}
onResizeStop={this.onResizeStop}
onLayoutChange={this.onLayoutChange}
>
{this.renderPanels(width)}
</ReactGridLayout>
</div>
);
}}
</AutoSizer>
</div>
);
}
}
@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { Subscription } from 'rxjs';
import { locationService } from '@grafana/runtime';
import { locationService, RefreshEvent } from '@grafana/runtime';
import {
AbsoluteTimeRange,
AnnotationChangeEvent,
@@ -30,7 +30,7 @@ import config from 'app/core/config';
import { DashboardModel, PanelModel } from '../state';
import { PANEL_BORDER } from 'app/core/constants';
import { loadSnapshotData } from '../utils/loadSnapshotData';
import { RefreshEvent, RenderEvent } from 'app/types/events';
import { RenderEvent } from 'app/types/events';
import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/overrides/colorSeriesConfigFactory';
import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory';
import { deleteAnnotation, saveAnnotation, updateAnnotation } from '../../annotations/api';
@@ -1,17 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
<AutoSizer
disableHeight={true}
disableWidth={false}
onResize={[Function]}
<div
style={
Object {
"height": "100%",
"width": "100%",
"flex": "1 1 auto",
}
}
>
<Component />
</AutoSizer>
<AutoSizer
disableHeight={true}
disableWidth={false}
onResize={[Function]}
style={Object {}}
>
<Component />
</AutoSizer>
</div>
`;
@@ -42,10 +42,11 @@ import { variableAdapters } from 'app/features/variables/adapters';
import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
import { dispatch } from '../../../store/store';
import { isAllVariable } from '../../variables/utils';
import { DashboardPanelsChangedEvent, RefreshEvent, RenderEvent, TimeRangeUpdatedEvent } from 'app/types/events';
import { DashboardPanelsChangedEvent, RenderEvent } from 'app/types/events';
import { getTimeSrv } from '../services/TimeSrv';
import { mergePanels, PanelMergeInfo } from '../utils/panelMerge';
import { isOnTheSameGridRow } from './utils';
import { RefreshEvent, TimeRangeUpdatedEvent } from '@grafana/runtime';
export interface CloneOptions {
saveVariables?: boolean;
@@ -2,7 +2,7 @@
import { cloneDeep, defaultsDeep, isArray, isEqual, keys } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
// Utils
import { getTemplateSrv } from '@grafana/runtime';
import { getTemplateSrv, RefreshEvent } from '@grafana/runtime';
import { getNextRefIdChar } from 'app/core/utils/query';
// Types
import {
@@ -27,7 +27,6 @@ import {
PanelOptionsChangedEvent,
PanelQueriesChangedEvent,
PanelTransformationsChangedEvent,
RefreshEvent,
RenderEvent,
} from 'app/types/events';
import { getTimeSrv } from '../services/TimeSrv';
+3 -3
View File
@@ -4,7 +4,7 @@ import store from 'app/core/store';
// Models
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { TimeRange, AppEvents, rangeUtil, dateMath } from '@grafana/data';
import { TimeRange, AppEvents, rangeUtil, dateMath, PanelModel as IPanelModel } from '@grafana/data';
// Utils
import { isString as _isString } from 'lodash';
@@ -26,7 +26,7 @@ export const removePanel = (dashboard: DashboardModel, panel: PanelModel, ask: b
// confirm deletion
if (ask !== false) {
const text2 =
panel.alert && !config.featureToggles.ngalert
panel.alert && !config.unifiedAlertingEnabled
? 'Panel includes an alert rule. removing the panel will also remove the alert rule'
: undefined;
const confirmText = panel.alert ? 'YES' : undefined;
@@ -52,7 +52,7 @@ export const duplicatePanel = (dashboard: DashboardModel, panel: PanelModel) =>
dashboard.duplicatePanel(panel);
};
export const copyPanel = (panel: PanelModel) => {
export const copyPanel = (panel: IPanelModel) => {
let saveModel = panel;
if (panel instanceof PanelModel) {
saveModel = panel.getSaveModel();
@@ -84,7 +84,6 @@ const dummyProps: Props = {
showNodeGraph: true,
splitOpen: (() => {}) as any,
logsVolumeData: undefined,
logsVolumeDataProvider: undefined,
loadLogsVolumeData: () => {},
};
+1 -15
View File
@@ -6,15 +6,7 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import memoizeOne from 'memoize-one';
import { selectors } from '@grafana/e2e-selectors';
import { Collapse, CustomScrollbar, ErrorBoundaryAlert, Themeable2, withTheme2 } from '@grafana/ui';
import {
AbsoluteTimeRange,
DataFrame,
DataQuery,
GrafanaTheme2,
hasLogsVolumeSupport,
LoadingState,
RawTimeRange,
} from '@grafana/data';
import { AbsoluteTimeRange, DataFrame, DataQuery, GrafanaTheme2, LoadingState, RawTimeRange } from '@grafana/data';
import LogsContainer from './LogsContainer';
import { QueryRows } from './QueryRows';
@@ -302,8 +294,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
showLogs,
showTrace,
showNodeGraph,
logsVolumeDataProvider,
loadLogsVolumeData,
} = this.props;
const { openDrawer } = this.state;
const styles = getStyles(theme);
@@ -326,11 +316,9 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
addQueryRowButtonHidden={false}
richHistoryButtonActive={showRichHistory}
queryInspectorButtonActive={showQueryInspector}
loadingLogsVolumeAvailable={hasLogsVolumeSupport(datasourceInstance) && !!logsVolumeDataProvider}
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
onClickRichHistoryButton={this.toggleShowRichHistory}
onClickQueryInspectorButton={this.toggleShowQueryInspector}
onClickLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
/>
<ResponseErrorContainer exploreId={exploreId} />
</div>
@@ -392,7 +380,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
queryKeys,
isLive,
graphResult,
logsVolumeDataProvider,
logsVolumeData,
logsResult,
showLogs,
@@ -411,7 +398,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
queryKeys,
isLive,
graphResult,
logsVolumeDataProvider,
logsVolumeData,
logsResult: logsResult ?? undefined,
absoluteRange,
+29 -11
View File
@@ -69,6 +69,8 @@ interface Props extends Themeable2 {
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
addResultsToCache: () => void;
clearCache: () => void;
loadingLogsVolumeAvailable: boolean;
onClickLoadLogsVolume: () => void;
}
interface State {
@@ -268,6 +270,8 @@ export class UnthemedLogs extends PureComponent<Props, State> {
logsQueries,
clearCache,
addResultsToCache,
onClickLoadLogsVolume,
loadingLogsVolumeAvailable,
} = this.props;
const {
@@ -340,16 +344,30 @@ export class UnthemedLogs extends PureComponent<Props, State> {
/>
</InlineField>
</InlineFieldRow>
<Button
variant="secondary"
disabled={isFlipping}
title={logsSortOrder === LogsSortOrder.Ascending ? 'Change to newest first' : 'Change to oldest first'}
aria-label="Flip results order"
className={styles.flipButton}
onClick={this.onChangeLogsSortOrder}
>
{isFlipping ? 'Flipping...' : 'Flip results order'}
</Button>
<div>
{loadingLogsVolumeAvailable && (
<Button
variant="secondary"
aria-label="Load volume button"
title="Execute a query to show full range logs volume"
onClick={onClickLoadLogsVolume}
icon="graph-bar"
className={styles.headerButton}
>
Load volume
</Button>
)}
<Button
variant="secondary"
disabled={isFlipping}
title={logsSortOrder === LogsSortOrder.Ascending ? 'Change to newest first' : 'Change to oldest first'}
aria-label="Flip results order"
className={styles.headerButton}
onClick={this.onChangeLogsSortOrder}
>
{isFlipping ? 'Flipping...' : 'Flip results order'}
</Button>
</div>
</div>
<LogsMetaRow
logRows={logRows}
@@ -441,7 +459,7 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean) => {
margin: ${theme.spacing(2, 0, 1)};
border: 1px solid ${theme.colors.border.medium};
`,
flipButton: css`
headerButton: css`
margin: ${theme.spacing(0.5, 0, 0, 1)};
`,
radioButtons: css`
+22 -2
View File
@@ -6,6 +6,7 @@ import {
AbsoluteTimeRange,
Field,
hasLogsContextSupport,
hasLogsVolumeSupport,
LoadingState,
LogRowModel,
RawTimeRange,
@@ -13,7 +14,7 @@ import {
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types';
import { splitOpen } from './state/main';
import { addResultsToCache, clearCache } from './state/query';
import { addResultsToCache, clearCache, loadLogsVolumeData } from './state/query';
import { updateTimeRange } from './state/time';
import { getTimeZone } from '../profile/state/selectors';
import { LiveLogsWithTheme } from './LiveLogs';
@@ -67,6 +68,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
render() {
const {
datasourceInstance,
loading,
loadingState,
logRows,
@@ -87,6 +89,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
exploreId,
addResultsToCache,
clearCache,
logsVolumeDataProvider,
loadLogsVolumeData,
} = this.props;
if (!logRows) {
@@ -146,6 +150,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
getFieldLinks={this.getFieldLinks}
addResultsToCache={() => addResultsToCache(exploreId)}
clearCache={() => clearCache(exploreId)}
loadingLogsVolumeAvailable={hasLogsVolumeSupport(datasourceInstance) && !!logsVolumeDataProvider}
onClickLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
/>
</Collapse>
</LogsCrossFadeTransition>
@@ -158,7 +164,18 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
const explore = state.explore;
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
const { logsResult, loading, scanning, datasourceInstance, isLive, isPaused, range, absoluteRange } = item;
const {
logsResult,
loading,
scanning,
datasourceInstance,
isLive,
isPaused,
range,
absoluteRange,
logsVolumeDataProvider,
logsVolumeData,
} = item;
const timeZone = getTimeZone(state.user);
return {
@@ -175,6 +192,8 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
isPaused,
range,
absoluteRange,
logsVolumeDataProvider,
logsVolumeData,
};
}
@@ -183,6 +202,7 @@ const mapDispatchToProps = {
splitOpen,
addResultsToCache,
clearCache,
loadLogsVolumeData,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
@@ -6,7 +6,6 @@ import { SecondaryActions } from './SecondaryActions';
const addQueryRowButtonSelector = '[aria-label="Add row button"]';
const richHistoryButtonSelector = '[aria-label="Rich history button"]';
const queryInspectorButtonSelector = '[aria-label="Query inspector button"]';
const onClickLoadLogsVolumeSelector = '[aria-label="Load logs volume button"]';
describe('SecondaryActions', () => {
it('should render component two buttons', () => {
@@ -15,7 +14,6 @@ describe('SecondaryActions', () => {
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
onClickQueryInspectorButton={noop}
onClickLoadLogsVolume={noop}
/>
);
expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(1);
@@ -29,7 +27,6 @@ describe('SecondaryActions', () => {
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
onClickQueryInspectorButton={noop}
onClickLoadLogsVolume={noop}
/>
);
expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(0);
@@ -43,7 +40,6 @@ describe('SecondaryActions', () => {
onClickAddQueryRowButton={noop}
onClickRichHistoryButton={noop}
onClickQueryInspectorButton={noop}
onClickLoadLogsVolume={noop}
/>
);
expect(wrapper.find(addQueryRowButtonSelector).props().disabled).toBe(true);
@@ -53,14 +49,11 @@ describe('SecondaryActions', () => {
const onClickAddRow = jest.fn();
const onClickHistory = jest.fn();
const onClickQueryInspector = jest.fn();
const onClickLoadLogsVolumeInspector = jest.fn();
const wrapper = shallow(
<SecondaryActions
onClickAddQueryRowButton={onClickAddRow}
onClickRichHistoryButton={onClickHistory}
onClickQueryInspectorButton={onClickQueryInspector}
loadingLogsVolumeAvailable={true}
onClickLoadLogsVolume={onClickLoadLogsVolumeInspector}
/>
);
@@ -72,8 +65,5 @@ describe('SecondaryActions', () => {
wrapper.find(queryInspectorButtonSelector).simulate('click');
expect(onClickQueryInspector).toBeCalled();
wrapper.find(onClickLoadLogsVolumeSelector).simulate('click');
expect(onClickQueryInspector).toBeCalled();
});
});
@@ -8,12 +8,10 @@ type Props = {
addQueryRowButtonHidden?: boolean;
richHistoryButtonActive?: boolean;
queryInspectorButtonActive?: boolean;
loadingLogsVolumeAvailable?: boolean;
onClickAddQueryRowButton: () => void;
onClickRichHistoryButton: () => void;
onClickQueryInspectorButton: () => void;
onClickLoadLogsVolume: () => void;
};
const getStyles = (theme: GrafanaTheme2) => {
@@ -58,16 +56,6 @@ export function SecondaryActions(props: Props) {
>
Inspector
</Button>
{props.loadingLogsVolumeAvailable && (
<Button
variant="secondary"
aria-label="Load logs volume button"
onClick={props.onClickLoadLogsVolume}
icon="graph-bar"
>
Load logs volume
</Button>
)}
</HorizontalGroup>
</div>
);
@@ -20,9 +20,7 @@ exports[`Explore should render component 1`] = `
<SecondaryActions
addQueryRowButtonDisabled={false}
addQueryRowButtonHidden={false}
loadingLogsVolumeAvailable={false}
onClickAddQueryRowButton={[Function]}
onClickLoadLogsVolume={[Function]}
onClickQueryInspectorButton={[Function]}
onClickRichHistoryButton={[Function]}
queryInspectorButtonActive={false}
+2 -21
View File
@@ -27,7 +27,6 @@ const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => vo
return {
url,
panel,
datasource,
get,
getDataSourceSrv,
getTimeSrv,
@@ -51,13 +50,6 @@ describe('navigateToExplore', () => {
expect(getDataSourceSrv).toHaveBeenCalledTimes(1);
});
it('then getDataSourceSrv.get should have been called with correct arguments', async () => {
const { get, panel } = await getNavigateToExploreContext(openInNewWindow);
expect(get).toHaveBeenCalledTimes(1);
expect(get).toHaveBeenCalledWith(panel.datasource);
});
it('then getTimeSrv should have been called once', async () => {
const { getTimeSrv } = await getNavigateToExploreContext(openInNewWindow);
@@ -65,15 +57,13 @@ describe('navigateToExplore', () => {
});
it('then getExploreUrl should have been called with correct arguments', async () => {
const { getExploreUrl, panel, datasource, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(
const { getExploreUrl, panel, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(
openInNewWindow
);
expect(getExploreUrl).toHaveBeenCalledTimes(1);
expect(getExploreUrl).toHaveBeenCalledWith({
panel,
panelTargets: panel.targets,
panelDatasource: datasource,
datasourceSrv: getDataSourceSrv(),
timeSrv: getTimeSrv(),
});
@@ -94,13 +84,6 @@ describe('navigateToExplore', () => {
expect(getDataSourceSrv).toHaveBeenCalledTimes(1);
});
it('then getDataSourceSrv.get should have been called with correct arguments', async () => {
const { get, panel } = await getNavigateToExploreContext(openInNewWindow);
expect(get).toHaveBeenCalledTimes(1);
expect(get).toHaveBeenCalledWith(panel.datasource);
});
it('then getTimeSrv should have been called once', async () => {
const { getTimeSrv } = await getNavigateToExploreContext(openInNewWindow);
@@ -108,15 +91,13 @@ describe('navigateToExplore', () => {
});
it('then getExploreUrl should have been called with correct arguments', async () => {
const { getExploreUrl, panel, datasource, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(
const { getExploreUrl, panel, getDataSourceSrv, getTimeSrv } = await getNavigateToExploreContext(
openInNewWindow
);
expect(getExploreUrl).toHaveBeenCalledTimes(1);
expect(getExploreUrl).toHaveBeenCalledWith({
panel,
panelTargets: panel.targets,
panelDatasource: datasource,
datasourceSrv: getDataSourceSrv(),
timeSrv: getTimeSrv(),
});
@@ -138,11 +138,8 @@ export const navigateToExplore = (
return async (dispatch) => {
const { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow } = dependencies;
const datasourceSrv = getDataSourceSrv();
const datasource = await datasourceSrv.get(panel.datasource);
const path = await getExploreUrl({
panel,
panelTargets: panel.targets,
panelDatasource: datasource,
datasourceSrv,
timeSrv: getTimeSrv(),
});
@@ -8,11 +8,10 @@ import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipb
import { PanelModel } from 'app/features/dashboard/state';
import { getPanelInspectorStyles } from './styles';
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
import { config } from '@grafana/runtime';
import { config, RefreshEvent } from '@grafana/runtime';
import { css } from '@emotion/css';
import { Subscription } from 'rxjs';
import { backendSrv } from 'app/core/services/backend_srv';
import { RefreshEvent } from 'app/types/events';
interface DsQuery {
isLoading: boolean;
@@ -4,7 +4,32 @@ import { PluginErrorCode, PluginSignatureStatus, PluginType } from '@grafana/dat
import { PluginListItem } from './PluginListItem';
import { CatalogPlugin, PluginListDisplayMode } from '../types';
/**
* The whole Icon component needs to be mock
* currently is using react-inlinesvg that does not render the icon svg in the test.
*
* There is solution to mock the library on __mocks__
* https://github.com/gilbarbara/react-inlinesvg/issues/145
* But unfortunately that causes conflict with DashboardSearch.test.tsx
*/
jest.mock('@grafana/ui', () => {
const IconMock = ({ title }: { title: string }) => {
return (
<svg>
<title> {title} </title>
</svg>
);
};
IconMock.displayName = 'Icon';
return Object.assign({}, jest.requireActual('@grafana/ui'), { Icon: IconMock });
});
describe('PluginListItem', () => {
afterEach(() => {
jest.clearAllMocks();
});
const plugin: CatalogPlugin = {
description: 'The test plugin',
downloads: 5,
@@ -49,21 +74,21 @@ describe('PluginListItem', () => {
const datasourcePlugin = { ...plugin, type: PluginType.datasource };
render(<PluginListItem plugin={datasourcePlugin} pathName="" />);
expect(screen.getByLabelText(/datasource plugin icon/i)).toBeVisible();
expect(screen.getByTitle(/datasource plugin/i)).toBeInTheDocument();
});
it('renders a panel plugin with correct icon', () => {
const panelPlugin = { ...plugin, type: PluginType.panel };
render(<PluginListItem plugin={panelPlugin} pathName="" />);
expect(screen.getByLabelText(/panel plugin icon/i)).toBeVisible();
expect(screen.getByTitle(/panel plugin/i)).toBeInTheDocument();
});
it('renders an app plugin with correct icon', () => {
const appPlugin = { ...plugin, type: PluginType.app };
render(<PluginListItem plugin={appPlugin} pathName="" />);
expect(screen.getByLabelText(/app plugin icon/i)).toBeVisible();
expect(screen.getByTitle(/app plugin/i)).toBeInTheDocument();
});
it('renders a disabled plugin with a badge to indicate its error', () => {
@@ -92,21 +117,21 @@ describe('PluginListItem', () => {
const datasourcePlugin = { ...plugin, type: PluginType.datasource };
render(<PluginListItem plugin={datasourcePlugin} pathName="" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByLabelText(/datasource plugin icon/i)).toBeVisible();
expect(screen.getByTitle(/datasource plugin/i)).toBeInTheDocument();
});
it('renders a panel plugin with correct icon', () => {
const panelPlugin = { ...plugin, type: PluginType.panel };
render(<PluginListItem plugin={panelPlugin} pathName="" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByLabelText(/panel plugin icon/i)).toBeVisible();
expect(screen.getByTitle(/panel plugin/i)).toBeInTheDocument();
});
it('renders an app plugin with correct icon', () => {
const appPlugin = { ...plugin, type: PluginType.app };
render(<PluginListItem plugin={appPlugin} pathName="" displayMode={PluginListDisplayMode.List} />);
expect(screen.getByLabelText(/app plugin icon/i)).toBeVisible();
expect(screen.getByTitle(/app plugin/i)).toBeInTheDocument();
});
it('renders a disabled plugin with a badge to indicate its error', () => {
@@ -30,7 +30,7 @@ export function PluginListItem({ plugin, pathName, displayMode = PluginListDispl
<PluginListItemBadges plugin={plugin} />
</div>
<div className={styles.pluginType}>
{plugin.type && <Icon name={PluginIconName[plugin.type]} aria-label={`${plugin.type} plugin icon`} />}
{plugin.type && <Icon name={PluginIconName[plugin.type]} title={`${plugin.type} plugin`} />}
</div>
</a>
);
@@ -16,7 +16,7 @@ import { AnnotationsWorker } from './AnnotationsWorker';
import { getAnnotationsByPanelId } from './utils';
import { DashboardModel } from '../../../dashboard/state';
import { getTimeSrv, TimeSrv } from '../../../dashboard/services/TimeSrv';
import { RefreshEvent } from '../../../../types/events';
import { RefreshEvent } from '@grafana/runtime';
import { config } from 'app/core/config';
import { UnifiedAlertStatesWorker } from './UnifiedAlertStatesWorker';
@@ -31,7 +31,7 @@ class DashboardQueryRunnerImpl implements DashboardQueryRunner {
private readonly dashboard: DashboardModel,
private readonly timeSrv: TimeSrv = getTimeSrv(),
private readonly workers: DashboardQueryRunnerWorker[] = [
config.featureToggles.ngalert ? new UnifiedAlertStatesWorker() : new AlertStatesWorker(),
config.unifiedAlertingEnabled ? new UnifiedAlertStatesWorker() : new AlertStatesWorker(),
new SnapshotWorker(),
new AnnotationsWorker(),
]
@@ -75,57 +75,78 @@ export const LabelFilter: FunctionComponent<Props> = ({
noFillEnd={filters.length > 1}
>
<VerticalGroup spacing="xs" width="auto">
{filters.map(({ key, operator, value, condition }, index) => (
<HorizontalGroup key={index} spacing="xs" width="auto">
<Select
menuShouldPortal
width={SELECT_WIDTH}
allowCustomValue
formatCreateLabel={(v) => `Use label key: ${v}`}
value={key}
options={options}
onChange={({ value: key = '' }) => {
onChange(
filtersToStringArray(
filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f))
)
);
}}
/>
<Select
menuShouldPortal
value={operator}
options={operators.map(toOption)}
onChange={({ value: operator = '=' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
menuPlacement="bottom"
renderControl={OperatorButton}
/>
<Select
menuShouldPortal
width={SELECT_WIDTH}
formatCreateLabel={(v) => `Use label value: ${v}`}
allowCustomValue
value={value}
placeholder="add filter value"
options={
labels.hasOwnProperty(key) ? [variableOptionGroup, ...labels[key].map(toOption)] : [variableOptionGroup]
}
onChange={({ value = '' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
<Button
variant="secondary"
size="md"
icon="trash-alt"
aria-label="Remove"
onClick={() => onChange(filtersToStringArray(filters.filter((_, i) => i !== index)))}
></Button>
{index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && <AddFilter />}
</HorizontalGroup>
))}
{filters.map(({ key, operator, value, condition }, index) => {
// Add the current key and value as options if they are manually entered
const keyPresent = options.some((op) => {
if (op.options) {
return options.some((opp) => opp.label === key);
}
return op.label === key;
});
if (!keyPresent) {
options.push({ label: key, value: key });
}
const valueOptions = labels.hasOwnProperty(key)
? [variableOptionGroup, ...labels[key].map(toOption)]
: [variableOptionGroup];
const valuePresent = valueOptions.some((op) => {
return op.label === value;
});
if (!valuePresent) {
valueOptions.push({ label: value, value });
}
return (
<HorizontalGroup key={index} spacing="xs" width="auto">
<Select
menuShouldPortal
width={SELECT_WIDTH}
allowCustomValue
formatCreateLabel={(v) => `Use label key: ${v}`}
value={key}
options={options}
onChange={({ value: key = '' }) => {
onChange(
filtersToStringArray(
filters.map((f, i) => (i === index ? { key, operator, condition, value: '' } : f))
)
);
}}
/>
<Select
menuShouldPortal
value={operator}
options={operators.map(toOption)}
onChange={({ value: operator = '=' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, operator } : f))))
}
menuPlacement="bottom"
renderControl={OperatorButton}
/>
<Select
menuShouldPortal
width={SELECT_WIDTH}
formatCreateLabel={(v) => `Use label value: ${v}`}
allowCustomValue
value={value}
placeholder="add filter value"
options={valueOptions}
onChange={({ value = '' }) =>
onChange(filtersToStringArray(filters.map((f, i) => (i === index ? { ...f, value } : f))))
}
/>
<Button
variant="secondary"
size="md"
icon="trash-alt"
aria-label="Remove"
onClick={() => onChange(filtersToStringArray(filters.filter((_, i) => i !== index)))}
></Button>
{index + 1 === filters.length && Object.values(filters).every(({ value }) => value) && <AddFilter />}
</HorizontalGroup>
);
})}
{!filters.length && <AddFilter />}
</VerticalGroup>
</QueryEditorRow>

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