Merge branch 'main' into encryption/use-secrets-service
This commit is contained in:
+72
-116
@@ -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
|
||||
|
||||
...
|
||||
|
||||
@@ -63,6 +63,7 @@ public/css/*.min.css
|
||||
!.vscode/launch.json
|
||||
.vs/
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
/data/*
|
||||
/bin/*
|
||||
|
||||
@@ -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 |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -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**:
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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: () => {},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user