Merge remote-tracking branch 'origin/main' into guicaulada/unified-secrets

This commit is contained in:
Guilherme Caulada
2022-04-04 15:35:51 -03:00
340 changed files with 10388 additions and 4005 deletions
+2 -2
View File
@@ -92,7 +92,7 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.js:3669774385": [
[15, 26, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js:1298620967": [
"packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js:2095199604": [
[15, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js:793239444": [
@@ -305,7 +305,7 @@ exports[`no enzyme tests`] = {
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:2974837543": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:4272466995": [
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:132770839": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:1089831034": [
+4 -4
View File
@@ -1,8 +1,8 @@
[run]
init_cmds = [
["make", "gen-go"],
["go", "run", "build.go", "-dev", "build-cli"],
["go", "run", "build.go", "-dev", "build-server"],
["GO_BUILD_DEV=1", "make", "build-cli"],
["GO_BUILD_DEV=1", "make", "build-server"],
["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
]
watch_all = true
@@ -13,10 +13,10 @@ watch_dirs = [
"$WORKDIR/conf",
]
watch_exts = [".go", ".ini", ".toml", ".template.html"]
ignore_files = ["wire_gen.go"]
ignore_files = ["wire_gen.go"]
build_delay = 1500
cmds = [
["make", "gen-go"],
["go", "run", "build.go", "-dev", "build-server"],
["GO_BUILD_DEV=1", "make", "build-server"],
["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
]
+58 -68
View File
@@ -11,7 +11,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -114,7 +114,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -130,7 +130,7 @@ steps:
name: initialize
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--variants linux-amd64,linux-amd64-musl,darwin-amd64,windows-amd64,armv6 --no-pull-enterprise
--variants linux-amd64,linux-amd64-musl,darwin-amd64,windows-amd64,armv6
depends_on:
- initialize
environment: {}
@@ -138,7 +138,6 @@ steps:
name: build-backend
- commands:
- ./bin/grabpl build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -147,7 +146,6 @@ steps:
name: build-frontend
- commands:
- ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -182,7 +180,7 @@ steps:
name: ensure-cuetsified
- commands:
- . scripts/build/gpg-test-vars.sh && ./bin/grabpl package --jobs 8 --edition oss
--build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise --variants linux-amd64,linux-amd64-musl,darwin-amd64,windows-amd64,armv6
--build-id ${DRONE_BUILD_NUMBER} --variants linux-amd64,linux-amd64-musl,darwin-amd64,windows-amd64,armv6
depends_on:
- build-plugins
- build-backend
@@ -358,7 +356,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -422,7 +420,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -460,7 +458,6 @@ steps:
name: lint-docs
- commands:
- ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -506,7 +503,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -544,7 +541,6 @@ steps:
name: lint-docs
- commands:
- ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -589,7 +585,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -692,7 +688,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -744,7 +740,6 @@ steps:
from_secret: drone_token
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment: {}
@@ -752,7 +747,6 @@ steps:
name: build-backend
- commands:
- ./bin/grabpl build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -761,7 +755,6 @@ steps:
name: build-frontend
- commands:
- ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -797,8 +790,7 @@ steps:
image: grafana/build-container:1.5.3
name: ensure-cuetsified
- commands:
- ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise
--sign
- ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --sign
depends_on:
- build-plugins
- build-backend
@@ -1102,7 +1094,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1172,7 +1164,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -1239,6 +1231,8 @@ trigger:
- exclude
include:
- .drone.yml
repo:
- grafana/grafana
type: docker
---
depends_on:
@@ -1257,7 +1251,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1343,7 +1337,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1360,7 +1354,7 @@ steps:
name: initialize
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --github-token $${GITHUB_TOKEN}
--no-pull-enterprise ${DRONE_TAG}
${DRONE_TAG}
depends_on:
- initialize
environment:
@@ -1370,7 +1364,7 @@ steps:
name: build-backend
- commands:
- ./bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} --edition
oss --no-pull-enterprise ${DRONE_TAG}
oss ${DRONE_TAG}
depends_on:
- initialize
environment:
@@ -1379,7 +1373,7 @@ steps:
name: build-frontend
- commands:
- ./bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN}
--edition oss --no-pull-enterprise ${DRONE_TAG}
--edition oss ${DRONE_TAG}
depends_on:
- initialize
environment:
@@ -1415,8 +1409,8 @@ steps:
image: grafana/build-container:1.5.3
name: ensure-cuetsified
- commands:
- ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --no-pull-enterprise
--sign ${DRONE_TAG}
- ./bin/grabpl package --jobs 8 --edition oss --github-token $${GITHUB_TOKEN} --sign
${DRONE_TAG}
depends_on:
- build-plugins
- build-backend
@@ -1651,7 +1645,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1772,7 +1766,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1856,7 +1850,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -1915,7 +1909,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1954,7 +1948,7 @@ steps:
name: initialize
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN}
--no-pull-enterprise ${DRONE_TAG}
${DRONE_TAG}
depends_on:
- initialize
environment:
@@ -1964,7 +1958,7 @@ steps:
name: build-backend
- commands:
- ./bin/grabpl build-frontend --jobs 8 --github-token $${GITHUB_TOKEN} --edition
enterprise --no-pull-enterprise ${DRONE_TAG}
enterprise ${DRONE_TAG}
depends_on:
- initialize
environment:
@@ -1973,7 +1967,7 @@ steps:
name: build-frontend
- commands:
- ./bin/grabpl build-frontend-packages --jobs 8 --github-token $${GITHUB_TOKEN}
--edition enterprise --no-pull-enterprise ${DRONE_TAG}
--edition enterprise ${DRONE_TAG}
depends_on:
- initialize
environment:
@@ -2010,7 +2004,7 @@ steps:
name: ensure-cuetsified
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN}
--no-pull-enterprise ${DRONE_TAG}
${DRONE_TAG}
depends_on:
- initialize
environment:
@@ -2020,7 +2014,7 @@ steps:
name: build-backend-enterprise2
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN}
--no-pull-enterprise --sign ${DRONE_TAG}
--sign ${DRONE_TAG}
depends_on:
- build-plugins
- build-backend
@@ -2178,7 +2172,7 @@ steps:
name: upload-packages
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise2 --github-token $${GITHUB_TOKEN}
--no-pull-enterprise --sign ${DRONE_TAG}
--sign ${DRONE_TAG}
depends_on:
- build-plugins
- build-backend
@@ -2258,7 +2252,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2431,7 +2425,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2559,7 +2553,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -2634,7 +2628,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2712,7 +2706,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2773,7 +2767,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2852,7 +2846,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2914,7 +2908,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2950,7 +2944,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2997,7 +2991,7 @@ steps:
name: initialize
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3034,6 +3028,8 @@ volumes:
---
depends_on:
- publish-artifacts-public
- publish-docker-oss-public
- publish-docker-enterprise-public
kind: pipeline
name: publish-packages
node:
@@ -3045,7 +3041,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3108,7 +3104,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3124,7 +3120,6 @@ steps:
name: initialize
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment: {}
@@ -3132,7 +3127,6 @@ steps:
name: build-backend
- commands:
- ./bin/grabpl build-frontend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -3141,7 +3135,6 @@ steps:
name: build-frontend
- commands:
- ./bin/grabpl build-frontend-packages --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -3177,8 +3170,7 @@ steps:
image: grafana/build-container:1.5.3
name: ensure-cuetsified
- commands:
- ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --no-pull-enterprise
--sign
- ./bin/grabpl package --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER} --sign
depends_on:
- build-plugins
- build-backend
@@ -3373,7 +3365,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3489,7 +3481,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3568,7 +3560,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: initialize
@@ -3618,7 +3610,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3653,7 +3645,6 @@ steps:
name: initialize
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment: {}
@@ -3661,7 +3652,6 @@ steps:
name: build-backend
- commands:
- ./bin/grabpl build-frontend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
depends_on:
- initialize
environment:
@@ -3670,7 +3660,7 @@ steps:
name: build-frontend
- commands:
- ./bin/grabpl build-frontend-packages --jobs 8 --edition enterprise --build-id
${DRONE_BUILD_NUMBER} --no-pull-enterprise
${DRONE_BUILD_NUMBER}
depends_on:
- initialize
environment:
@@ -3707,7 +3697,7 @@ steps:
name: ensure-cuetsified
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER}
--variants linux-amd64 --no-pull-enterprise
--variants linux-amd64
depends_on:
- initialize
environment: {}
@@ -3715,7 +3705,7 @@ steps:
name: build-backend-enterprise2
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise --sign
--sign
depends_on:
- build-plugins
- build-backend
@@ -3882,7 +3872,7 @@ steps:
name: upload-packages
- commands:
- ./bin/grabpl package --jobs 8 --edition enterprise2 --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise --variants linux-amd64 --sign
--variants linux-amd64 --sign
depends_on:
- build-plugins
- build-backend
@@ -3957,7 +3947,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -4122,7 +4112,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -4242,7 +4232,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.27/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.30/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -4441,6 +4431,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: b5191e597c2ea7a1152ab18ca2c4aece939024ceb8fd5c76581c087a64ae6ad3
hmac: 430843c058dc4f40a3f881e13fc61ab12f5b6dc823f99067e56ab49fc1bdd459
...
+1
View File
@@ -63,6 +63,7 @@ go.sum @grafana/backend-platform
/pkg/services/ngalert @grafana/alerting-squad-backend
/pkg/services/sqlstore/migrations/ualert @grafana/alerting-squad-backend
/pkg/services/alerting @grafana/alerting-squad-backend
/pkg/tests/api/alerting @grafana/alerting-squad-backend
/public/app/features/alerting @grafana/alerting-squad-frontend
# Library Services
+1 -1
View File
@@ -181,7 +181,7 @@
},
{
"type": "changedfiles",
"matches": ["/pkg/services/ngalert/**/*", "/pkg/services/sqlstore/migrations/ualert/**/*", "/pkg/services/alerting/**/*", "/public/app/features/alerting/**/*"],
"matches": ["/pkg/services/ngalert/**/*", "/pkg/services/sqlstore/migrations/ualert/**/*", "/pkg/services/alerting/**/*", "/public/app/features/alerting/**/*", "/pkg/tests/api/alerting/**/*"],
"action": "updateLabel",
"addLabel": "area/alerting"
},
-6
View File
@@ -60,12 +60,6 @@ jobs:
echo "::set-output name=branch_name::v${{steps.regex-match.outputs.group1}}"
echo "::set-output name=branch_exist::$(git ls-remote --heads https://github.com/grafana/grafana.git v${{ steps.regex-match.outputs.group1 }}.x | wc -l)"
- name: Check input version is aligned with branch(not main)
if: ${{ github.event.inputs.version != '' && steps.intermedia.outputs.branch_exist != '0' && !contains(steps.intermedia.outputs.short_ref, steps.intermedia.outputs.branch_name) }}
run: |
echo " You need to run the workflow on branch v${{steps.regex-match.outputs.group1}}.x
exit 1
- name: Check input version is aligned with branch(main)
if: ${{ github.event.inputs.version != '' && steps.intermedia.outputs.branch_exist == '0' && !contains(steps.intermedia.outputs.short_ref, 'main') }}
run: |
+1 -19
View File
@@ -8,10 +8,8 @@ name: "CodeQL"
on:
push:
branches: [main, v1.8.x, v2.0.x, v2.1.x, v2.6.x, v3.0.x, v3.1.x, v4.0.x, v4.1.x, v4.2.x, v4.3.x, v4.4.x, v4.5.x, v4.6.x, v4.7.x, v5.0.x, v5.1.x, v5.2.x, v5.3.x, v5.4.x, v6.0.x, v6.1.x, v6.2.x, v6.3.x, v6.4.x, v6.5.x, v6.6.x, v6.7.x, v7.0.x, v7.1.x, v7.2.x]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
paths-ignore:
- '**/*.cue'
- '**/*.json'
- '**/*.md'
- '**/*.txt'
@@ -51,21 +49,5 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
@@ -0,0 +1,29 @@
name: "CodeQL for PR / go"
on:
pull_request:
branches: [main]
paths:
- '**/*.go'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: "go"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
@@ -0,0 +1,31 @@
name: "CodeQL for PR / javascript"
on:
pull_request:
branches: [main]
paths:
- '**/*.js'
- '**/*.ts'
- '**/*.tsx'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: "javascript"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
@@ -0,0 +1,29 @@
name: "CodeQL for PR / python"
on:
pull_request:
branches: [main]
paths:
- '**/*.py'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: "python"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
Vendored Executable → Regular
View File
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eslint",
"version": "8.10.0-sdk",
"version": "8.11.0-sdk",
"main": "./lib/api.js",
"type": "commonjs"
}
+17
View File
@@ -1,3 +1,20 @@
<!-- 8.4.5 START -->
# 8.4.5 (2022-03-31)
### Features and enhancements
- **Instrumentation:** Make backend plugin metrics endpoints available with optional authentication. [#46467](https://github.com/grafana/grafana/pull/46467), [@marefr](https://github.com/marefr)
- **Table panel:** Show datalinks for cell display modes JSON View and Gauge derivates. [#46020](https://github.com/grafana/grafana/pull/46020), [@mdvictor](https://github.com/mdvictor)
### Bug fixes
- **Azure Monitor:** Small bug fixes for Resource Picker. [#46665](https://github.com/grafana/grafana/pull/46665), [@sarahzinger](https://github.com/sarahzinger)
- **Logger:** Use specified format for file logger. [#46970](https://github.com/grafana/grafana/pull/46970), [@sakjur](https://github.com/sakjur)
- **Logs:** Handle missing fields in dataframes better. [#46963](https://github.com/grafana/grafana/pull/46963), [@gabor](https://github.com/gabor)
- **ManageDashboards:** Fix error when deleting all dashboards from folder view. [#46877](https://github.com/grafana/grafana/pull/46877), [@joshhunt](https://github.com/joshhunt)
<!-- 8.4.5 END -->
<!-- 8.4.4 START -->
# 8.4.4 (2022-03-16)
+5 -3
View File
@@ -14,6 +14,8 @@ GO_FILES ?= ./pkg/...
SH_FILES ?= $(shell find ./scripts -name *.sh)
API_DEFINITION_FILES = $(shell find ./pkg/api/docs/definitions -name '*.go' -print)
SWAGGER_TAG ?= latest
GO_BUILD_FLAGS += $(if $(GO_BUILD_DEV),-dev)
GO_BUILD_FLAGS += $(if $(GO_BUILD_TAGS),-build-tags=$(GO_BUILD_TAGS))
all: deps build
@@ -87,15 +89,15 @@ gen-go: $(WIRE)
build-go: $(MERGED_SPEC_TARGET) gen-go ## Build all Go binaries.
@echo "build go files"
$(GO) run build.go build
$(GO) run build.go $(GO_BUILD_FLAGS) build
build-server: ## Build Grafana server.
@echo "build server"
$(GO) run build.go build-server
$(GO) run build.go $(GO_BUILD_FLAGS) build-server
build-cli: ## Build Grafana CLI application.
@echo "build grafana-cli"
$(GO) run build.go build-cli
$(GO) run build.go $(GO_BUILD_FLAGS) build-cli
build-js: ## Build frontend assets.
@echo "build frontend"
@@ -69,8 +69,8 @@ Configure alerting behavior in the absence of data using information in the foll
| Alerting | Set alert rule state to `Alerting`. |
| Ok | Set alert rule state to `Normal`. |
| Error or timeout option | Description |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Alerting | Set alert rule state to `Alerting` |
| OK | Set alert rule state to `Normal` |
| Error | Create a new alert `DatasourceError` with the name and UID of the alert rule, and UID of the datasource that returned no data as labels. |
| Error or timeout option | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Alerting | Set alert rule state to `Alerting`. From Grafana 8.5, the alert rule waits for the entire duration for which the condition is true before firing. |
| OK | Set alert rule state to `Normal` |
| Error | Create a new alert `DatasourceError` with the name and UID of the alert rule, and UID of the datasource that returned no data as labels. |
@@ -323,7 +323,16 @@ If you're not currently logged in to the CloudWatch console, the link will forwa
## Alerting
Since CloudWatch Logs queries can return numeric data, for example through the use of the `stats` command, alerts are supported.
Alerting require queries that return numeric data, which CloudWatch Logs support. For example through the use of the `stats` command, alerts are supported. For example, this is a valid query for alerting on messages that include the text "Exception":
```
filter @message like /Exception/
| stats count(*) as exceptionCount by bin(1h)
| sort exceptionCount desc
```
**NOTE**: When trying to alert on a query, if an error like `input data must be a wide series but got ...` is received, make sure that your query returns valid numeric data that can be printed in a Time series panel.
For more information on Grafana alerts, refer to [Alerting]({{< relref "../../alerting/_index.md" >}}) documentation.
## Configure CloudWatch with grafana.ini
@@ -53,6 +53,7 @@ Improve an existing plugin with one of our guides:
- [Build a streaming data source plugin]({{< relref "build-a-streaming-data-source-plugin.md" >}})
- [Error handling]({{< relref "error-handling.md" >}})
- [Working with data frames]({{< relref "working-with-data-frames.md" >}})
- [Development with local Grafana]({{< relref "development-with-local-grafana.md" >}})
### Concepts
@@ -0,0 +1,83 @@
+++
title = "Development with local Grafana"
+++
# Development with local Grafana
This guide allows you to setup a development environment where you run Grafana and your plugin locally. With this, you will be able to see your changes as you add them.
## Run Grafana in your host
If you have git, Go and the required version of NodeJS in your system, you can clone and run Grafana locally:
1. Download and set up Grafana. You can find instructions on how to do it in the [developer-guide](https://github.com/grafana/grafana/blob/HEAD/contribute/developer-guide.md).
2. Grafana will look for plugins, by default, on its `data/plugins` directory. You can create a symbolic link to your plugin repository to detect new changes:
```bash
ln -s <plugin-path>/dist data/plugins/<plugin-name>
```
3. (Optional) If the step above doesn't work for you (e.g. you are running on Windows), you can also modify the default path in the Grafana configuration (that can be found at `conf/custom.ini`) and point to the directory with your plugin:
```ini
[paths]
plugins = <path-to-your-plugin-parent-directory>
```
## Run Grafana with docker-compose
Another possibility is to run Grafana with docker-compose so it runs in a container. For doing so, create the docker-compose file in your plugin directory:
**NOTE**: Some plugins already include a docker-compose file so you can skip this step.
```yaml
version: '3.7'
services:
grafana:
# Change latest with your target version, if needed
image: grafana/grafana:latest
ports:
- 3000:3000/tcp
volumes:
# Use your plugin folder (e.g. redshift-datasource)
- ./dist:/var/lib/grafana/plugins/<plugin-folder>
- ./provisioning:/etc/grafana/provisioning
environment:
- TERM=linux
- GF_LOG_LEVEL=debug
- GF_DATAPROXY_LOGGING=true
- GF_DEFAULT_APP_MODE=development
```
## Run your plugin
Finally start your plugin in development mode. Go to your plugin root directory and follow these steps:
1. Build your plugin backend and start the frontend in watch mode:
```bash
mage -v
yarn watch
```
2. Start Grafana backend and frontend:
2.1 For a local copy of Grafana, go to the directory with Grafana source code and run:
```bash
make run
```
```bash
yarn start
```
2.2 Or with docker-compose, in your plugin directory, run:
```bash
docker-compose up
```
After this, you should be able to see your plugin listed in Grafana and test your changes. Note that any change in the fronted will require you to refresh your browser while changes in the backend may require to rebuild your plugin binaries and reload the plugin (`mage && mage reloadPlugin` for local development or `docker-compose up` again if you are using docker-compose).
@@ -50,6 +50,9 @@ The reference information that follows complements conceptual information about
| `fixed:folders:writer` | All permissions from `fixed:dashboards:writer` and <br>`folders:read`<br>`folders:write`<br>`folders:create`<br>`folders:delete`<br>`folders.permissions:read`<br>`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. |
| `fixed:folders.permissions:reader` | `folders.permissions:read` | Read all folder permissions. |
| `fixed:folders.permissions:writer` | All permissions from `fixed:folders.permissions:reader` and <br>`folders.permissions:write` | Read and update all folder permissions. |
| `fixed:annotations:reader` | `annotations:read` | Read all annotations and annotation tags. |
| `fixed:annotations.dashboard:writer` | `annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. |
| `fixed:annotations:writer` | `annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:*` | Create, update and delete all annotations and annotation tags. |
## Default built-in role assignments
@@ -57,5 +60,5 @@ The reference information that follows complements conceptual information about
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Grafana Admin | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer` | Default [Grafana server administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#grafana-server-administrators" >}}) assignments. |
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixes:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer` | Default [Grafana organization administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled | Default [Editor]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader` | Default [Viewer]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled | Default [Editor]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer` | Default [Viewer]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
@@ -113,23 +113,28 @@ The following list contains fine-grained access control actions.
| `folders:delete` | `folders:*`<br>`folders:id:*` | Delete one or more folders. |
| `folers.permissions:read` | `folders:*`<br>`folders:id:*` | Read permissions for one or more folders. |
| `folders.permissions:write` | `folders:*`<br>`folders:id:*` | Update permissions for one or more folders. |
| `annotations.read` | `annotations:*`<br>`annotations:type:*` | Read annotations and annotation tags. |
| `annotations.create` | `annotations:*`<br>`annotations:type:*` | Create annotations. |
| `annotations.write` | `annotations:*`<br>`annotations:type:*` | Update annotations. |
| `annotations.delete` | `annotations:*`<br>`annotations:type:*` | Delete annotations. |
## Scope definitions
The following list contains fine-grained access control scopes.
| Scopes | Descriptions |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `permissions:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. |
| `roles:*` <br> `roles:uid:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role and `roles:uid:randomuid` matches only the role whose UID is `randomuid`. |
| `reports:*` <br> `reports:id:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:id:1` matches the report whose ID is `1`. |
| `services:accesscontrol` | Restrict an action to target only the fine-grained access control service. You can use this in conjunction with the `status:accesscontrol` actions. |
| `global.users:*` <br> `global.users:id:*` | Restrict an action to a set of global users. For example, `global.users:*` matches any user and `global.users:id:1` matches the user whose ID is `1`. |
| `teams:*` <br> `teams:id:*` | Restrict an action to a set of teams from an organization. For example, `teams:*` matches any team and `teams:id:1` matches the team whose ID is `1`. |
| `users:*` <br> `users:id:*` | Restrict an action to a set of users from an organization. For example, `users:*` matches any user and `users:id:1` matches the user whose ID is `1`. |
| `orgs:*` <br> `orgs:id:*` | Restrict an action to a set of organizations. For example, `orgs:*` matches any organization and `orgs:id:1` matches the organization whose ID is `1`. |
| `settings:*` | Restrict an action to a subset of settings. For example, `settings:*` matches all settings, `settings:auth.saml:*` matches all SAML settings, and `settings:auth.saml:enabled` matches the enable property on the SAML settings. |
| `provisioners:*` | Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the fine-grained access control [provisioner]({{< relref "./provisioning.md" >}}). |
| `datasources:*`<br>`datasources:id:*`<br>`datasources:uid:*`<br>`datasources:name:*` | Restrict an action to a set of data sources. For example, `datasources:*` matches any data source, and `datasources:name:postgres` matches the data source named `postgres`. |
| `folders:*`<br>`folders:id:*` | Restrict an action to a set of folders. For example, `folders:*` matches any folder, and `folders:id:1` matches the folder whose ID is `1`. |
| `dashboards:*`<br>`dashboards:id:*` | Restrict an action to a set of dashboards. For example, `dashboards:*` matches any dashboard, and `dashboards:id:1` matches the dashboard whose ID is `1`. |
| Scopes | Descriptions |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `permissions:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. |
| `roles:*` <br> `roles:uid:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role and `roles:uid:randomuid` matches only the role whose UID is `randomuid`. |
| `reports:*` <br> `reports:id:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:id:1` matches the report whose ID is `1`. |
| `services:accesscontrol` | Restrict an action to target only the fine-grained access control service. You can use this in conjunction with the `status:accesscontrol` actions. |
| `global.users:*` <br> `global.users:id:*` | Restrict an action to a set of global users. For example, `global.users:*` matches any user and `global.users:id:1` matches the user whose ID is `1`. |
| `teams:*` <br> `teams:id:*` | Restrict an action to a set of teams from an organization. For example, `teams:*` matches any team and `teams:id:1` matches the team whose ID is `1`. |
| `users:*` <br> `users:id:*` | Restrict an action to a set of users from an organization. For example, `users:*` matches any user and `users:id:1` matches the user whose ID is `1`. |
| `orgs:*` <br> `orgs:id:*` | Restrict an action to a set of organizations. For example, `orgs:*` matches any organization and `orgs:id:1` matches the organization whose ID is `1`. |
| `settings:*` | Restrict an action to a subset of settings. For example, `settings:*` matches all settings, `settings:auth.saml:*` matches all SAML settings, and `settings:auth.saml:enabled` matches the enable property on the SAML settings. |
| `provisioners:*` | Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the fine-grained access control [provisioner]({{< relref "./provisioning.md" >}}). |
| `datasources:*`<br>`datasources:id:*`<br>`datasources:uid:*`<br>`datasources:name:*` | Restrict an action to a set of data sources. For example, `datasources:*` matches any data source, and `datasources:name:postgres` matches the data source named `postgres`. |
| `folders:*`<br>`folders:id:*` | Restrict an action to a set of folders. For example, `folders:*` matches any folder, and `folders:id:1` matches the folder whose ID is `1`. |
| `dashboards:*`<br>`dashboards:id:*` | Restrict an action to a set of dashboards. For example, `dashboards:*` matches any dashboard, and `dashboards:id:1` matches the dashboard whose ID is `1`. |
| `annotations:*`<br>`annotations:type:*` | Restrict an action to a set of annotations. For example, `annotations:*` matches any annotation, `annotations:type:dashboard` matches annotations associated with dashboards and `annotations:type:organization` matches organization annotations. |
+63 -4
View File
@@ -5,14 +5,25 @@ keywords = ["grafana", "http", "documentation", "api", "annotation", "annotation
aliases = ["/docs/grafana/latest/http_api/annotations/"]
+++
# Annotations resources / actions
# Annotations API
This is the API documentation for the new Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be global annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel.
This is the API documentation for the new Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be organization annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel.
> If you are running Grafana Enterprise and have [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, access to endpoints will be controlled by Fine-grained access control permissions.
> Refer to specific endpoints to understand what permissions are required.
## Find Annotations
`GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100`
#### Required permissions
See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation.
| Action | Scope |
| ---------------- | -------------- |
| annotations:read | annotations:\* |
**Example Request**:
```http
@@ -32,7 +43,7 @@ Query Parameters:
- `panelId`: number. Optional. Find annotations that are scoped to a specific panel
- `userId`: number. Optional. Find annotations created by a specific user
- `type`: string. Optional. `alert`|`annotation` Return alerts or user created annotations
- `tags`: string. Optional. Use this to filter global annotations. Global annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. `tags=tag1&tags=tag2`.
- `tags`: string. Optional. Use this to filter organization annotations. Organization annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. To do an "AND" filtering with multiple tags, specify the tags parameter multiple times e.g. `tags=tag1&tags=tag2`.
**Example Response**:
@@ -85,13 +96,21 @@ Content-Type: application/json
## Create Annotation
Creates an annotation in the Grafana database. The `dashboardId` and `panelId` fields are optional.
If they are not specified then a global annotation is created and can be queried in any dashboard that adds
If they are not specified then an organization annotation is created and can be queried in any dashboard that adds
the Grafana annotations data source. When creating a region annotation include the timeEnd property.
The format for `time` and `timeEnd` should be epoch numbers in millisecond resolution.
`POST /api/annotations`
#### Required permissions
See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation.
| Action | Scope |
| ------------------ | ----------------------- |
| annotations:create | annotations:type:<type> |
**Example Request**:
```http
@@ -132,6 +151,14 @@ format (string with multiple tags being separated by a space).
`POST /api/annotations/graphite`
#### Required permissions
See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation.
| Action | Scope |
| ------------------ | ----------------------------- |
| annotations:create | annotations:type:organization |
**Example Request**:
```http
@@ -165,6 +192,14 @@ Content-Type: application/json
Updates all properties of an annotation that matches the specified id. To only update certain property, consider using the [Patch Annotation](#patch-annotation) operation.
#### Required permissions
See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation.
| Action | Scope |
| ----------------- | ----------------------- |
| annotations:write | annotations:type:<type> |
**Example Request**:
```http
@@ -202,6 +237,14 @@ Updates one or more properties of an annotation that matches the specified id.
This operation currently supports updating of the `text`, `tags`, `time` and `timeEnd` properties.
#### Required permissions
See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation.
| Action | Scope |
| ----------------- | ----------------------- |
| annotations:write | annotations:type:<type> |
**Example Request**:
```http
@@ -233,6 +276,14 @@ Content-Type: application/json
Deletes the annotation that matches the specified id.
#### Required permissions
See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation.
| Action | Scope |
| ------------------ | ----------------------- |
| annotations:delete | annotations:type:<type> |
**Example Request**:
```http
@@ -259,6 +310,14 @@ Content-Type: application/json
Find all the event tags created in the annotations.
#### Required permissions
See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation.
| Action | Scope |
| ---------------- | ----- |
| annotations:read | N/A |
**Example Request**:
```http
@@ -6,6 +6,6 @@ aliases = ["/docs/sources/panels/library-panels/about-library-panels/"]
# About Grafana panel libraries
A library panel is a reusable panel that you can use in any dashboard. When you make a change to a library panel, that change propegates to all instances of where the panel is used. Library panels streamline reuse of panels across multiple dashboards.
A library panel is a reusable panel that you can use in any dashboard. When you make a change to a library panel, that change propagates to all instances of where the panel is used. Library panels streamline reuse of panels across multiple dashboards.
You can save a library panel in a folder alongside saved dashboards.
+1
View File
@@ -8,6 +8,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 8.4.5]({{< relref "release-notes-8-4-5" >}})
- [Release notes for 8.4.4]({{< relref "release-notes-8-4-4" >}})
- [Release notes for 8.4.3]({{< relref "release-notes-8-4-3" >}})
- [Release notes for 8.4.2]({{< relref "release-notes-8-4-2" >}})
@@ -0,0 +1,20 @@
+++
title = "Release notes for Grafana 8.4.5"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 8.4.5
### Features and enhancements
- **Instrumentation:** Make backend plugin metrics endpoints available with optional authentication. [#46467](https://github.com/grafana/grafana/pull/46467), [@marefr](https://github.com/marefr)
- **Table panel:** Show datalinks for cell display modes JSON View and Gauge derivates. [#46020](https://github.com/grafana/grafana/pull/46020), [@mdvictor](https://github.com/mdvictor)
### Bug fixes
- **Azure Monitor:** Small bug fixes for Resource Picker. [#46665](https://github.com/grafana/grafana/pull/46665), [@sarahzinger](https://github.com/sarahzinger)
- **Logger:** Use specified format for file logger. [#46970](https://github.com/grafana/grafana/pull/46970), [@sakjur](https://github.com/sakjur)
- **Logs:** Handle missing fields in dataframes better. [#46963](https://github.com/grafana/grafana/pull/46963), [@gabor](https://github.com/gabor)
- **ManageDashboards:** Fix error when deleting all dashboards from folder view. [#46877](https://github.com/grafana/grafana/pull/46877), [@joshhunt](https://github.com/joshhunt)
@@ -105,3 +105,7 @@ Enables value inspection from table cell. The raw value is presented in a modal
## Column filter
You can temporarily change how column data is displayed. For example, you can order values from highest to lowest or hide specific values. For more information, refer to [Filter table columns]({{< relref "./filter-table-columns.md" >}}).
## Pagination
Use this option to enable or disable pagination. It is a front-end option that does not affect queries. When enabled, the page size automatically adjusts to the height of the table.
+1 -1
View File
@@ -102,7 +102,7 @@ e2e.scenario({
e2e.components.PanelEditor.DataPane.content().should('be.visible');
// Field & Overrides tabs (need to switch to React based vis, i.e. Table)
e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show header').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Header and footer Show header').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible');
},
});
+7 -6
View File
@@ -17,8 +17,6 @@ require (
cloud.google.com/go/storage v1.18.2
cuelang.org/go v0.4.0
github.com/Azure/azure-sdk-for-go v59.3.0+incompatible
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.10.0
github.com/Azure/go-autorest/autorest v0.11.22
github.com/BurntSushi/toml v0.3.1
github.com/Masterminds/semver v1.5.0
@@ -54,6 +52,7 @@ require (
github.com/gosimple/slug v1.9.0
github.com/grafana/cuetsy v0.0.0-20211119211437-8c25464cc9bf
github.com/grafana/grafana-aws-sdk v0.10.1
github.com/grafana/grafana-azure-sdk-go v1.0.0
github.com/grafana/grafana-plugin-sdk-go v0.129.0
github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
@@ -183,7 +182,7 @@ require (
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/flatbuffers v2.0.0+incompatible // indirect
github.com/googleapis/gax-go/v2 v2.1.1
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
@@ -247,19 +246,20 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
)
require (
cloud.google.com/go/kms v1.1.0
github.com/Azure/go-autorest/autorest/adal v0.9.17
github.com/golang-migrate/migrate/v4 v4.7.0
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f
gocloud.dev v0.24.0
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.10.0 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.17 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
@@ -270,6 +270,7 @@ require (
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/getkin/kin-openapi v0.91.0 // indirect
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
+2 -2
View File
@@ -1335,6 +1335,8 @@ github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5J
github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/grafana/grafana-aws-sdk v0.10.1 h1:Ksguhjx6EuGLN/5Oc7oZoxuDReJ5RxIH99yqSMpLGUs=
github.com/grafana/grafana-aws-sdk v0.10.1/go.mod h1:vFIOHEnY1u5nY0/tge1IHQjPuG6DRKr2ISf/HikUdjE=
github.com/grafana/grafana-azure-sdk-go v1.0.0 h1:RIVQyVb89/y/BnOVsVDcxiMtmWF8NmAX8ql0OJvzwNc=
github.com/grafana/grafana-azure-sdk-go v1.0.0/go.mod h1:xbzMaG74BN4rOP1NYEsCMNWkPbK7GfSU09PGYfQYm+g=
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2ud7NNM7LrGPO4x0NFR8qLq68CqI4SmB7I2yRN2w9oE=
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M=
@@ -2462,8 +2464,6 @@ github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBn
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/vectordotdev/go-datemath v0.1.1-0.20220110192739-f9ce83ec349f h1:2upw/ZfjkCKpc4k6DXg7lMfCSLkfw/8epV5/y2ZUQ8U=
github.com/vectordotdev/go-datemath v0.1.1-0.20220110192739-f9ce83ec349f/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae h1:oyiy3uBj1F4O3AaFh7hUGBrJjAssJhKyAbwxtkslxqo=
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
+2 -2
View File
@@ -1,4 +1,4 @@
{
"stable": "8.4.4",
"testing": "8.4.4"
"stable": "8.4.5",
"testing": "8.4.5"
}
+7 -2
View File
@@ -54,7 +54,8 @@
"i18n:extract": "lingui extract",
"i18n:compile": "lingui compile",
"postinstall": "husky install",
"betterer": "betterer"
"betterer": "betterer",
"betterer:stats": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/reportBettererStats.ts"
},
"grafana": {
"whatsNewUrl": "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v8-4/",
@@ -86,6 +87,7 @@
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@betterer/betterer": "5.1.7",
"@betterer/cli": "5.1.7",
"@betterer/regexp": "5.1.7",
"@emotion/eslint-plugin": "11.7.0",
@@ -128,9 +130,10 @@
"@types/hoist-non-react-statics": "3.3.1",
"@types/jest": "27.4.1",
"@types/jquery": "3.5.14",
"@types/js-yaml": "^4.0.5",
"@types/jsurl": "^1.2.28",
"@types/lingui__macro": "^3",
"@types/lodash": "4.14.149",
"@types/lodash": "4.14.181",
"@types/logfmt": "^1.2.1",
"@types/mousetrap": "1.6.9",
"@types/node": "16.11.26",
@@ -246,6 +249,7 @@
"@grafana/e2e-selectors": "workspace:*",
"@grafana/experimental": "0.0.2-canary.22",
"@grafana/google-sdk": "0.0.3",
"@grafana/lezer-logql": "^0.0.11",
"@grafana/runtime": "workspace:*",
"@grafana/schema": "workspace:*",
"@grafana/slate-react": "0.22.10-grafana",
@@ -310,6 +314,7 @@
"immer": "9.0.12",
"immutable": "4.0.0",
"jquery": "3.6.0",
"js-yaml": "^4.1.0",
"json-source-map": "0.6.1",
"jsurl": "^0.1.5",
"lezer-promql": "0.22.0",
+1 -1
View File
@@ -55,7 +55,7 @@
"@types/history": "4.7.11",
"@types/jest": "27.4.1",
"@types/jquery": "3.5.14",
"@types/lodash": "4.14.149",
"@types/lodash": "4.14.181",
"@types/marked": "4.0.3",
"@types/node": "16.11.26",
"@types/papaparse": "5.3.2",
@@ -202,7 +202,8 @@ export function dataFrameToJSON(frame: DataFrame): DataFrameJSON {
meta: frame.meta,
name: frame.name,
fields: frame.fields.map((f) => {
const { values, ...sfield } = f;
const { values, state, display, ...sfield } = f;
delete (sfield as any).entities;
data.values.push(values.toArray());
return sfield;
}),
@@ -84,7 +84,7 @@ export const getTimeZoneGroups = memoize(
return groups;
}
const group = zone.substr(0, delimiter);
const group = zone.slice(0, delimiter);
groups[group] = groups[group] ?? [];
groups[group].push(zone);
@@ -414,6 +414,46 @@ describe('Date display options', () => {
expect(processor('2020-08-01T08:48:43.783337Z').text).toEqual('2020-08-01 08:48:43');
});
it('should handle ISO string dates when in other timezones than UTC', () => {
const processor = getDisplayProcessor({
timeZone: 'CET',
field: {
type: FieldType.time,
config: {},
},
theme: createTheme(),
});
expect(processor('2020-08-01T08:48:43.783337Z').text).toEqual('2020-08-01 10:48:43'); //DST
expect(processor('2020-12-01T08:48:43.783337Z').text).toEqual('2020-12-01 09:48:43'); //STD
});
it('should handle ISO string dates with timezone offset', () => {
const processor = getDisplayProcessor({
timeZone: 'utc',
field: {
type: FieldType.time,
config: {},
},
theme: createTheme(),
});
expect(processor('2020-12-01T08:48:43.783337+02:00').text).toEqual('2020-12-01 06:48:43');
});
it('should handle ISO string dates without timezone qualifier by assuming UTC', () => {
const processor = getDisplayProcessor({
timeZone: 'CET',
field: {
type: FieldType.time,
config: {},
},
theme: createTheme(),
});
expect(processor('2020-12-01T08:48:43.783337').text).toEqual('2020-12-01 09:48:43');
});
describe('number formatting for string values', () => {
it('should preserve string unchanged if unit is strings', () => {
const processor = getDisplayProcessor({
@@ -6,7 +6,7 @@ import { Field, FieldType } from '../types/dataFrame';
import { DisplayProcessor, DisplayValue } from '../types/displayValue';
import { getValueFormat, isBooleanUnit } from '../valueFormats/valueFormats';
import { getValueMappingResult } from '../utils/valueMappings';
import { dateTime, dateTimeParse } from '../datetime';
import { toUtc, dateTimeParse } from '../datetime';
import { KeyValue, TimeZone } from '../types';
import { getScaleCalculator } from './scale';
import { GrafanaTheme2 } from '../themes/types';
@@ -77,7 +77,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
const isStringUnit = unit === 'string';
if (hasDateUnit && typeof value === 'string') {
value = dateTime(value).valueOf();
value = toUtc(value).valueOf();
}
let numeric = isStringUnit ? NaN : anyToNumber(value);
+25 -5
View File
@@ -57,12 +57,32 @@ describe('stringToMs', () => {
});
});
describe('[un]escapeStringForRegex', () => {
it.each([
'[]',
'\\',
'[(abc])',
'onetwothree',
'<namedgroup}(this is not a regex>',
'string\\with\\backslash',
'everyspecialchar([{])}.,/?&*-^&<>#',
])('should be symmetric', (input) => {
const output = unEscapeStringFromRegex(escapeStringForRegex(input));
expect(output).toEqual(input);
});
});
describe('escapeStringForRegex', () => {
describe('when using a string with special chars', () => {
it('then all special chars should be escaped', () => {
const result = escapeStringForRegex('([{}])|*+-.?<>#&^$');
expect(result).toBe('\\(\\[\\{\\}\\]\\)\\|\\*\\+\\-\\.\\?\\<\\>\\#\\&\\^\\$');
});
it.each([
'[[[',
'[]\\',
'[(abc])',
'onetwothree',
'<namedgroup}(this is not a regex>',
'string\\with\\backslash',
'everyspecialchar([{])}.,/?&*-^&<>#',
])('should always produce output that compiles', (value) => {
expect(() => new RegExp(escapeStringForRegex(value))).not.toThrowError();
});
describe('when using a string without special chars', () => {
+8 -4
View File
@@ -1,12 +1,16 @@
import { camelCase } from 'lodash';
const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$'];
const specialChars = ['(', '[', '{', '}', ']', ')', '\\', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$'];
const specialMatcher = '([\\' + specialChars.join('\\') + '])';
const specialCharEscape = new RegExp(specialMatcher, 'g');
const specialCharUnescape = new RegExp('(\\\\)' + specialMatcher, 'g');
export const escapeStringForRegex = (value: string) => {
if (!value) {
return value;
}
return specialChars.reduce((escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar), value);
return value.replace(specialCharEscape, '\\$1');
};
export const unEscapeStringFromRegex = (value: string) => {
@@ -14,7 +18,7 @@ export const unEscapeStringFromRegex = (value: string) => {
return value;
}
return specialChars.reduce((escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar), value);
return value.replace(specialCharUnescape, '$2');
};
export function stringStartsAsRegEx(str: string): boolean {
@@ -45,7 +49,7 @@ export function stringToMs(str: string): number {
}
const nr = parseInt(str, 10);
const unit = str.substr(String(nr).length);
const unit = str.slice(String(nr).length);
const s = 1000;
const m = s * 60;
const h = m * 60;
@@ -29,7 +29,7 @@ function clamp(value: number, min = 0, max = 1) {
* @beta
*/
export function hexToRgb(color: string) {
color = color.substr(1);
color = color.slice(1);
const re = new RegExp(`.{1,${color.length >= 6 ? 2 : 1}}`, 'g');
let colors = color.match(re);
@@ -141,7 +141,7 @@ export function decomposeColor(color: string | DecomposeColor): DecomposeColor {
values = values.split(' ');
colorSpace = values.shift();
if (values.length === 4 && values[3].charAt(0) === '/') {
values[3] = values[3].substr(1);
values[3] = values[3].slice(1);
}
if (['srgb', 'display-p3', 'a98-rgb', 'prophoto-rgb', 'rec-2020'].indexOf(colorSpace) === -1) {
throw new Error(
@@ -34,6 +34,7 @@ export interface FeatureToggles {
tempoServiceGraph?: boolean;
lokiBackendMode?: boolean;
accesscontrol?: boolean;
['accesscontrol-builtins']?: boolean;
prometheus_azure_auth?: boolean;
influxdbBackendMigration?: boolean;
newNavigation?: boolean;
@@ -46,7 +47,9 @@ export interface FeatureToggles {
dashboardComments?: boolean;
annotationComments?: boolean;
migrationLocking?: boolean;
saveDashboardDrawer?: boolean;
storage?: boolean;
alertProvisioning?: boolean;
storageLocalUpload?: boolean;
azureMonitorResourcePickerForMetrics?: boolean;
}
+2 -2
View File
@@ -87,7 +87,7 @@ export class CSVReader {
// #{columkey}#a,b,c
const idx = first.indexOf('#', 2);
if (idx > 0) {
const k = first.substr(1, idx - 1);
const k = first.slice(1, idx);
const isName = 'name' === k;
// Simple object used to check if headers match
@@ -103,7 +103,7 @@ export class CSVReader {
this.data.push(this.current);
}
const v = first.substr(idx + 1);
const v = first.slice(idx + 1);
if (isName) {
this.current.addFieldFor(undefined, v);
for (let j = 1; j < line.length; j++) {
@@ -66,7 +66,7 @@ export function toFixed(value: number, decimals?: DecimalCount): string {
const decimalPos = formatted.indexOf('.');
const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
if (precision < decimals) {
return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision);
return (precision ? formatted : formatted + '.') + String(factor).slice(1, decimals - precision + 1);
}
return formatted;
+1 -1
View File
@@ -34,7 +34,7 @@
"@rollup/plugin-commonjs": "21.0.2",
"@rollup/plugin-node-resolve": "13.1.3",
"@types/chrome-remote-interface": "0.31.4",
"@types/lodash": "4.14.180",
"@types/lodash": "4.14.181",
"@types/node": "16.11.26",
"@types/uuid": "8.3.4",
"rollup": "2.70.1",
+1 -1
View File
@@ -44,7 +44,7 @@
"@types/angular": "1.8.4",
"@types/history": "4.7.11",
"@types/jest": "27.4.1",
"@types/lodash": "4.14.149",
"@types/lodash": "4.14.181",
"@types/react": "17.0.42",
"@types/react-dom": "17.0.14",
"@types/systemjs": "^0.20.6",
@@ -105,7 +105,7 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
if (ds) {
return {
label: ds.name.substr(0, 37),
label: ds.name.slice(0, 37),
value: ds.uid,
imgUrl: ds.meta.info.logos.small,
hideText: hideTextValue,
+6 -1
View File
@@ -9,5 +9,10 @@
},
"exclude": ["dist", "node_modules"],
"extends": "@grafana/tsconfig",
"include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/*.d.ts"]
"include": [
"src/**/*.ts*",
"../../public/app/types/jquery/*.ts",
"../../public/app/types/*.d.ts",
"../grafana-ui/src/types/*.d.ts"
]
}
@@ -11,7 +11,7 @@ var fs = require('fs');
var githubClient_1 = tslib_1.__importDefault(require('./githubClient'));
var resolveContentType = function (extension) {
if (extension.startsWith('.')) {
extension = extension.substr(1);
extension = extension.slice(1);
}
switch (extension) {
case 'zip':
@@ -8,7 +8,7 @@ import { AxiosResponse } from 'axios';
const resolveContentType = (extension: string): string => {
if (extension.startsWith('.')) {
extension = extension.substr(1);
extension = extension.slice(1);
}
switch (extension) {
case 'zip':
@@ -197,7 +197,7 @@ const getBaseWebpackConfig: WebpackConfigurationGetter = async (options) => {
(context, request, callback) => {
const prefix = 'grafana/';
if (request.indexOf(prefix) === 0) {
return callback(null, request.substr(prefix.length));
return callback(null, request.slice(prefix.length));
}
// @ts-ignore
+1 -1
View File
@@ -130,7 +130,7 @@
"@types/is-hotkey": "0.1.7",
"@types/jest": "27.4.1",
"@types/jquery": "3.5.14",
"@types/lodash": "4.14.149",
"@types/lodash": "4.14.181",
"@types/mock-raf": "1.0.3",
"@types/node": "16.11.26",
"@types/prismjs": "1.26.0",
@@ -1,9 +1,10 @@
import React, { FunctionComponent } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { css, cx } from '@emotion/css';
import { Tooltip, PopoverContent } from '../Tooltip';
import { Icon } from '../Icon/Icon';
import { GrafanaTheme } from '@grafana/data';
import React, { FunctionComponent } from 'react';
import { useTheme } from '../../themes';
import { Icon } from '../Icon/Icon';
import { PopoverContent, Tooltip } from '../Tooltip';
import { LabelProps } from './Label';
export interface Props extends Omit<LabelProps, 'css' | 'description' | 'category'> {
@@ -41,7 +42,7 @@ export const InlineLabel: FunctionComponent<Props> = ({
{children}
{tooltip && (
<Tooltip placement="top" content={tooltip} theme="info">
<Icon name="info-circle" size="sm" className={styles.icon} />
<Icon tabIndex={0} name="info-circle" size="sm" className={styles.icon} />
</Tooltip>
)}
</Component>
@@ -216,7 +216,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
const propsChanged = !sameProps(prevProps, this.props, propsToDiff);
if (frames !== prevProps.frames || propsChanged) {
if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) {
let newState = this.prepState(this.props, false);
if (newState) {
@@ -112,6 +112,22 @@ describe('nullInsertThreshold Transformer', () => {
expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 10, 11]);
expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8, null]);
expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c', null]);
// should work for frames with 1 datapoint
const df2 = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] },
{ name: 'One', type: FieldType.number, values: [1] },
{ name: 'Two', type: FieldType.string, values: ['a'] },
],
});
const result2 = applyNullInsertThreshold(df2, null, 13);
expect(result2.fields[0].values.toArray()).toStrictEqual([1, 2]);
expect(result2.fields[1].values.toArray()).toStrictEqual([1, null]);
expect(result2.fields[2].values.toArray()).toStrictEqual(['a', null]);
});
// TODO: make this work
@@ -132,12 +148,12 @@ describe('nullInsertThreshold Transformer', () => {
expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, 'c']);
});
test('should noop on fewer than two values', () => {
test('should noop on 0 datapoints', () => {
const df = new MutableDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] },
{ name: 'Value', type: FieldType.number, values: [1] },
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [] },
{ name: 'Value', type: FieldType.number, values: [] },
],
});
@@ -15,7 +15,7 @@ export function applyNullInsertThreshold(
refFieldPseudoMax: number | null = null,
insertMode: InsertMode = INSERT_MODES.threshold
): DataFrame {
if (frame.length < 2) {
if (frame.length === 0) {
return frame;
}
@@ -0,0 +1,14 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Pagination } from './Pagination';
describe('Pagination component', () => {
it('should render only 10 buttons when number of pages is higher than 8', () => {
render(<Pagination currentPage={1} numberOfPages={90} onNavigate={() => {}} />);
expect(screen.getAllByRole('button')).toHaveLength(10);
});
it('should only show 3 buttons when showSmallVersion is true', () => {
render(<Pagination currentPage={1} numberOfPages={90} onNavigate={() => {}} showSmallVersion />);
expect(screen.getAllByRole('button')).toHaveLength(4);
});
});
@@ -1,77 +1,94 @@
import React from 'react';
import React, { useMemo } from 'react';
import { css } from '@emotion/css';
import { stylesFactory } from '../../themes';
import { useStyles2 } from '../../themes';
import { Button, ButtonVariant } from '../Button';
import { Icon } from '../Icon/Icon';
const PAGE_LENGTH_TO_CONDENSE = 8;
export interface Props {
/** The current page index being shown. */
/** The current page index being shown. */
currentPage: number;
/** Number of total pages. */
/** Number of total pages. */
numberOfPages: number;
/** Callback function for fetching the selected page */
/** Callback function for fetching the selected page. */
onNavigate: (toPage: number) => void;
/** When set to true and the pagination result is only one page it will not render the pagination at all */
/** When set to true and the pagination result is only one page it will not render the pagination at all. */
hideWhenSinglePage?: boolean;
/** Small version only shows the current page and the navigation buttons. */
showSmallVersion?: boolean;
}
export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavigate, hideWhenSinglePage }) => {
const styles = getStyles();
const pages = [...new Array(numberOfPages).keys()];
export const Pagination: React.FC<Props> = ({
currentPage,
numberOfPages,
onNavigate,
hideWhenSinglePage,
showSmallVersion,
}) => {
const styles = useStyles2(getStyles);
const pageLengthToCondense = showSmallVersion ? 1 : 8;
const condensePages = numberOfPages > PAGE_LENGTH_TO_CONDENSE;
const getListItem = (page: number, variant: 'primary' | 'secondary') => (
<li key={page} className={styles.item}>
<Button size="sm" variant={variant} onClick={() => onNavigate(page)}>
{page}
</Button>
</li>
);
const pageButtons = useMemo(() => {
const pages = [...new Array(numberOfPages).keys()];
const pageButtons = pages.reduce<JSX.Element[]>((pagesToRender, pageIndex) => {
const page = pageIndex + 1;
const variant: ButtonVariant = page === currentPage ? 'primary' : 'secondary';
const condensePages = numberOfPages > pageLengthToCondense;
const getListItem = (page: number, variant: 'primary' | 'secondary') => (
<li key={page} className={styles.item}>
<Button size="sm" variant={variant} onClick={() => onNavigate(page)}>
{page}
</Button>
</li>
);
// The indexes at which to start and stop condensing pages
const lowerBoundIndex = PAGE_LENGTH_TO_CONDENSE;
const upperBoundIndex = numberOfPages - PAGE_LENGTH_TO_CONDENSE + 1;
// When the indexes overlap one another this number is negative
const differenceOfBounds = upperBoundIndex - lowerBoundIndex;
return pages.reduce<JSX.Element[]>((pagesToRender, pageIndex) => {
const page = pageIndex + 1;
const variant: ButtonVariant = page === currentPage ? 'primary' : 'secondary';
const isFirstOrLastPage = page === 1 || page === numberOfPages;
// This handles when the lowerBoundIndex < currentPage < upperBoundIndex
const currentPageIsBetweenBounds =
differenceOfBounds > -1 && currentPage >= lowerBoundIndex && currentPage <= upperBoundIndex;
// The indexes at which to start and stop condensing pages
const lowerBoundIndex = pageLengthToCondense;
const upperBoundIndex = numberOfPages - pageLengthToCondense + 1;
// When the indexes overlap one another this number is negative
const differenceOfBounds = upperBoundIndex - lowerBoundIndex;
if (condensePages) {
if (
isFirstOrLastPage ||
(currentPage < lowerBoundIndex && page < lowerBoundIndex) ||
(differenceOfBounds >= 0 && currentPage > upperBoundIndex && page > upperBoundIndex) ||
(differenceOfBounds < 0 && currentPage >= lowerBoundIndex && page > upperBoundIndex) ||
(currentPageIsBetweenBounds && page >= currentPage - 2 && page <= currentPage + 2)
) {
// Renders a button for the page
const isFirstOrLastPage = page === 1 || page === numberOfPages;
// This handles when the lowerBoundIndex < currentPage < upperBoundIndex
const currentPageIsBetweenBounds =
differenceOfBounds > -1 && currentPage >= lowerBoundIndex && currentPage <= upperBoundIndex;
// Show ellipsis after that many pages
const ellipsisOffset = showSmallVersion ? 1 : 3;
// The offset to show more pages when currentPageIsBetweenBounds
const pageOffset = showSmallVersion ? 0 : 2;
if (condensePages) {
if (
isFirstOrLastPage ||
(currentPage < lowerBoundIndex && page < lowerBoundIndex) ||
(differenceOfBounds >= 0 && currentPage > upperBoundIndex && page > upperBoundIndex) ||
(differenceOfBounds < 0 && currentPage >= lowerBoundIndex && page > upperBoundIndex) ||
(currentPageIsBetweenBounds && page >= currentPage - pageOffset && page <= currentPage + pageOffset)
) {
// Renders a button for the page
pagesToRender.push(getListItem(page, variant));
} else if (
(page === lowerBoundIndex && currentPage < lowerBoundIndex) ||
(page === upperBoundIndex && currentPage > upperBoundIndex) ||
(currentPageIsBetweenBounds &&
(page === currentPage - ellipsisOffset || page === currentPage + ellipsisOffset))
) {
// Renders and ellipsis to represent condensed pages
pagesToRender.push(
<li key={page} className={styles.item}>
<Icon className={styles.ellipsis} name="ellipsis-v" />
</li>
);
}
} else {
pagesToRender.push(getListItem(page, variant));
} else if (
(page === lowerBoundIndex && currentPage < lowerBoundIndex) ||
(page === upperBoundIndex && currentPage > upperBoundIndex) ||
(currentPageIsBetweenBounds && (page === currentPage - 3 || page === currentPage + 3))
) {
// Renders and ellipsis to represent condensed pages
pagesToRender.push(
<li key={page} className={styles.item}>
<Icon className={styles.ellipsis} name="ellipsis-v" />
</li>
);
}
} else {
pagesToRender.push(getListItem(page, variant));
}
return pagesToRender;
}, []);
return pagesToRender;
}, []);
}, [currentPage, numberOfPages, onNavigate, pageLengthToCondense, showSmallVersion, styles.ellipsis, styles.item]);
if (hideWhenSinglePage && numberOfPages <= 1) {
return null;
@@ -108,7 +125,7 @@ export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavi
);
};
const getStyles = stylesFactory(() => {
const getStyles = () => {
return {
container: css`
float: right;
@@ -122,4 +139,4 @@ const getStyles = stylesFactory(() => {
transform: rotate(90deg);
`,
};
});
};
@@ -30,6 +30,8 @@ export interface PanelContext {
onToggleSeriesVisibility?: (label: string, mode: SeriesVisibilityChangeMode) => void;
canAddAnnotations?: () => boolean;
canEditAnnotations?: (dashboardId: number) => boolean;
canDeleteAnnotations?: (dashboardId: number) => boolean;
onAnnotationCreate?: (annotation: AnnotationEventUIModel) => void;
onAnnotationUpdate?: (annotation: AnnotationEventUIModel) => void;
onAnnotationDelete?: (id: string) => void;
@@ -10,18 +10,19 @@ export interface FooterRowProps {
totalColumnsWidth: number;
footerGroups: HeaderGroup[];
footerValues: FooterItem[];
isPaginationVisible: boolean;
height: number;
}
export const FooterRow = (props: FooterRowProps) => {
const { totalColumnsWidth, footerGroups, height } = props;
const { totalColumnsWidth, footerGroups, height, isPaginationVisible } = props;
const e2eSelectorsTable = selectors.components.Panels.Visualization.Table;
const tableStyles = useStyles2(getTableStyles);
return (
<table
style={{
position: 'absolute',
position: isPaginationVisible ? 'relative' : 'absolute',
width: totalColumnsWidth ? `${totalColumnsWidth}px` : '100%',
bottom: '0px',
}}
@@ -29,7 +29,7 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
return (
<div {...cellProps} className={inspectEnabled ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer}>
<div className={cx(tableStyles.cellText, txt)}>
{!link && <div className={tableStyles.cellText}>{value}</div>}
{!link && <div className={tableStyles.cellText}>{displayValue}</div>}
{link && (
<a
href={link.href}
@@ -186,3 +186,8 @@ export const Footer: Story = (args) => {
</div>
);
};
export const Pagination: Story = (args) => <Basic {...args} />;
Pagination.args = {
pageSize: 10,
};
@@ -1,15 +1,14 @@
import React, { FC, memo, useCallback, useMemo } from 'react';
import React, { FC, memo, useCallback, useEffect, useMemo } from 'react';
import { DataFrame, getFieldDisplayName } from '@grafana/data';
import {
Cell,
Column,
TableState,
useAbsoluteLayout,
useFilters,
UseFiltersState,
usePagination,
useResizeColumns,
UseResizeColumnsState,
useSortBy,
UseSortByState,
useTable,
} from 'react-table';
import { FixedSizeList } from 'react-window';
@@ -27,6 +26,7 @@ import { TableCell } from './TableCell';
import { useStyles2 } from '../../themes';
import { FooterRow } from './FooterRow';
import { HeaderRow } from './HeaderRow';
import { Pagination } from '../Pagination/Pagination';
const COLUMN_MIN_WIDTH = 150;
@@ -45,13 +45,12 @@ export interface Props {
onSortByChange?: TableSortByActionCallback;
onCellFilterAdded?: TableFilterActionCallback;
footerValues?: FooterItem[];
enablePagination?: boolean;
}
interface ReactTableInternalState extends UseResizeColumnsState<{}>, UseSortByState<{}>, UseFiltersState<{}> {}
function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
return useCallback(
(newState: ReactTableInternalState, action: any) => {
(newState: TableState, action: any) => {
switch (action.type) {
case 'columnDoneResizing':
if (onColumnResize) {
@@ -95,8 +94,8 @@ function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
);
}
function getInitialState(initialSortBy: Props['initialSortBy'], columns: Column[]): Partial<ReactTableInternalState> {
const state: Partial<ReactTableInternalState> = {};
function getInitialState(initialSortBy: Props['initialSortBy'], columns: Column[]): Partial<TableState> {
const state: Partial<TableState> = {};
if (initialSortBy) {
state.sortBy = [];
@@ -126,6 +125,7 @@ export const Table: FC<Props> = memo((props: Props) => {
initialSortBy,
footerValues,
showTypeIcons,
enablePagination,
} = props;
const tableStyles = useStyles2(getTableStyles);
@@ -188,17 +188,40 @@ export const Table: FC<Props> = memo((props: Props) => {
[initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer]
);
const { getTableProps, headerGroups, rows, prepareRow, totalColumnsWidth, footerGroups } = useTable(
options,
useFilters,
useSortBy,
useAbsoluteLayout,
useResizeColumns
);
const {
getTableProps,
headerGroups,
rows,
prepareRow,
totalColumnsWidth,
footerGroups,
page,
state,
gotoPage,
setPageSize,
pageOptions,
} = useTable(options, useFilters, useSortBy, usePagination, useAbsoluteLayout, useResizeColumns);
let listHeight = height - (headerHeight + footerHeight);
if (enablePagination) {
listHeight -= tableStyles.cellHeight;
}
const pageSize = Math.round(listHeight / tableStyles.cellHeight) - 1;
useEffect(() => {
// Don't update the page size if it is less than 1
if (pageSize <= 0) {
return;
}
setPageSize(pageSize);
}, [pageSize, setPageSize]);
const RenderRow = React.useCallback(
({ index: rowIndex, style }) => {
const row = rows[rowIndex];
let row = rows[rowIndex];
if (enablePagination) {
row = page[rowIndex];
}
prepareRow(row);
return (
<div {...row.getRowProps({ style })} className={tableStyles.row}>
@@ -215,20 +238,52 @@ export const Table: FC<Props> = memo((props: Props) => {
</div>
);
},
[onCellFilterAdded, prepareRow, rows, tableStyles]
[onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles]
);
const listHeight = height - (headerHeight + footerHeight);
const onNavigate = useCallback(
(toPage: number) => {
gotoPage(toPage - 1);
},
[gotoPage]
);
const itemCount = enablePagination ? page.length : data.length;
let paginationEl = null;
if (enablePagination) {
const itemsRangeStart = state.pageIndex * state.pageSize + 1;
let itemsRangeEnd = itemsRangeStart + state.pageSize - 1;
const isSmall = width < 500;
if (itemsRangeEnd > data.length) {
itemsRangeEnd = data.length;
}
paginationEl = (
<div className={tableStyles.paginationWrapper}>
<div>
<Pagination
currentPage={state.pageIndex + 1}
numberOfPages={pageOptions.length}
showSmallVersion={isSmall}
onNavigate={onNavigate}
/>
</div>
{isSmall ? null : (
<div className={tableStyles.paginationSummary}>
{itemsRangeStart} - {itemsRangeEnd} of {data.length} rows
</div>
)}
</div>
);
}
return (
<div {...getTableProps()} className={tableStyles.table} aria-label={ariaLabel} role="table">
<CustomScrollbar hideVerticalTrack={true}>
<div style={{ width: totalColumnsWidth ? `${totalColumnsWidth}px` : '100%' }}>
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
{!noHeader && <HeaderRow data={data} headerGroups={headerGroups} showTypeIcons={showTypeIcons} />}
{rows.length > 0 ? (
{itemCount > 0 ? (
<FixedSizeList
height={listHeight}
itemCount={rows.length}
itemCount={itemCount}
itemSize={tableStyles.rowHeight}
width={'100%'}
style={{ overflow: 'hidden auto' }}
@@ -243,11 +298,13 @@ export const Table: FC<Props> = memo((props: Props) => {
{footerValues && (
<FooterRow
height={footerHeight}
isPaginationVisible={Boolean(enablePagination)}
footerValues={footerValues}
footerGroups={footerGroups}
totalColumnsWidth={totalColumnsWidth}
/>
)}
{paginationEl}
</div>
</CustomScrollbar>
</div>
@@ -170,6 +170,32 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
label: headerFilter;
cursor: pointer;
`,
paginationWrapper: css`
display: flex;
background: ${headerBg};
height: ${cellHeight}px;
justify-content: center;
align-items: center;
width: 100%;
border-top: 1px solid ${theme.colors.border.weak};
li {
margin-bottom: 0;
}
div:not(:only-child):first-child {
flex-grow: 0.6;
}
`,
paginationSummary: css`
color: ${theme.colors.text.secondary};
font-size: ${theme.typography.bodySmall.fontSize};
margin-left: auto;
`,
tableContentWrapper: (totalColumnsWidth: number) => css`
width: ${totalColumnsWidth ?? '100%'};
display: flex;
flex-direction: column;
`,
row: css`
label: row;
border-bottom: 1px solid ${borderColor};
@@ -21,7 +21,7 @@ const getStyles = (theme: GrafanaTheme, height: number, visible: boolean) => {
box-shadow: 0 0 20px ${theme.colors.dropdownShadow};
visibility: ${visible === true ? 'visible' : 'hidden'};
width: 250px;
height: ${height + parseInt(theme.spacing.xxs, 10)}px;
min-height: ${height + parseInt(theme.spacing.xxs, 10)}px;
position: relative;
word-break: break-word;
`,
@@ -214,7 +214,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
let series: SeriesTableRowProps[] = [];
const frame = otherProps.data;
const fields = frame.fields;
const sortIdx: Array<[number, number]> = [];
const sortIdx: any[] = [];
for (let i = 0; i < fields.length; i++) {
const field = frame.fields[i];
@@ -232,7 +232,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
const v = otherProps.data.fields[i].values.get(focusedPointIdxs[i]!);
const display = field.display!(v);
sortIdx.push([series.length, v]);
sortIdx.push(v);
series.push({
color: display.color || FALLBACK_COLOR,
label: getFieldDisplayName(field, frame),
@@ -242,7 +242,16 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
}
if (sortOrder !== SortOrder.None) {
series.sort((a, b) => arrayUtils.sortValues(sortOrder)(a.value, b.value));
// create sort reference series array, as Array.sort() mutates the original array
const sortRef = [...series];
const sortFn = arrayUtils.sortValues(sortOrder);
series.sort((a, b) => {
// get compared values indices to retrieve raw values from sortIdx
const aIdx = sortRef.indexOf(a);
const bIdx = sortRef.indexOf(b);
return sortFn(sortIdx[aIdx], sortIdx[bIdx]);
});
}
tooltip = <SeriesTable series={series} timestamp={xVal} />;
+111
View File
@@ -0,0 +1,111 @@
import type {
UseColumnOrderInstanceProps,
UseColumnOrderState,
UseExpandedHooks,
UseExpandedInstanceProps,
UseExpandedOptions,
UseExpandedRowProps,
UseExpandedState,
UseFiltersColumnOptions,
UseFiltersColumnProps,
UseFiltersInstanceProps,
UseFiltersOptions,
UseFiltersState,
UseGlobalFiltersColumnOptions,
UseGlobalFiltersInstanceProps,
UseGlobalFiltersOptions,
UseGlobalFiltersState,
UseGroupByCellProps,
UseGroupByColumnOptions,
UseGroupByColumnProps,
UseGroupByHooks,
UseGroupByInstanceProps,
UseGroupByOptions,
UseGroupByRowProps,
UseGroupByState,
UsePaginationInstanceProps,
UsePaginationOptions,
UsePaginationState,
UseResizeColumnsColumnOptions,
UseResizeColumnsColumnProps,
UseResizeColumnsOptions,
UseResizeColumnsState,
UseRowSelectHooks,
UseRowSelectInstanceProps,
UseRowSelectOptions,
UseRowSelectRowProps,
UseRowSelectState,
UseRowStateCellProps,
UseRowStateInstanceProps,
UseRowStateOptions,
UseRowStateRowProps,
UseRowStateState,
UseSortByColumnOptions,
UseSortByColumnProps,
UseSortByHooks,
UseSortByInstanceProps,
UseSortByOptions,
UseSortByState,
} from 'react-table';
declare module 'react-table' {
export interface TableOptions<D extends Record<string, unknown>>
extends UseExpandedOptions<D>,
UseFiltersOptions<D>,
UseGlobalFiltersOptions<D>,
UseGroupByOptions<D>,
UsePaginationOptions<D>,
UseResizeColumnsOptions<D>,
UseRowSelectOptions<D>,
UseRowStateOptions<D>,
UseSortByOptions<D>,
// note that having Record here allows you to add anything to the options, this matches the spirit of the
// underlying js library, but might be cleaner if it's replaced by a more specific type that matches your
// feature set, this is a safe default.
Record<string, any> {}
export interface Hooks<D extends Record<string, unknown> = Record<string, unknown>>
extends UseExpandedHooks<D>,
UseGroupByHooks<D>,
UseRowSelectHooks<D>,
UseSortByHooks<D> {}
export interface TableInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderInstanceProps<D>,
UseExpandedInstanceProps<D>,
UseFiltersInstanceProps<D>,
UseGlobalFiltersInstanceProps<D>,
UseGroupByInstanceProps<D>,
UsePaginationInstanceProps<D>,
UseRowSelectInstanceProps<D>,
UseRowStateInstanceProps<D>,
UseSortByInstanceProps<D> {}
export interface TableState<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderState<D>,
UseExpandedState<D>,
UseFiltersState<D>,
UseGlobalFiltersState<D>,
UseGroupByState<D>,
UsePaginationState<D>,
UseResizeColumnsState<D>,
UseRowSelectState<D>,
UseRowStateState<D>,
UseSortByState<D> {}
export interface ColumnInterface<D extends Record<string, unknown> = Record<string, unknown>>
extends UseGlobalFiltersColumnOptions<D>,
UseGroupByColumnOptions<D>,
UseResizeColumnsColumnOptions<D>,
UseSortByColumnOptions<D> {}
export interface ColumnInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnProps<D>,
UseGroupByColumnProps<D>,
UseResizeColumnsColumnProps<D>,
UseSortByColumnProps<D> {}
export interface Cell<D extends Record<string, unknown> = Record<string, unknown>, V = any>
extends UseGroupByCellProps<D>,
UseRowStateCellProps<D> {}
}
+1 -1
View File
@@ -37,5 +37,5 @@ export function getNextCharacter(global?: any) {
const range = selection.getRangeAt(0);
const text = selection.anchorNode.textContent;
const offset = range.startOffset;
return text!.substr(offset, 1);
return text!.slice(offset, offset + 1);
}
+1 -1
View File
@@ -14,7 +14,7 @@
"@types/deep-freeze": "^0.1.1",
"@types/grafana__slate-react": "npm:@types/slate-react@0.22.5",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/lodash": "4.14.149",
"@types/lodash": "4.14.181",
"@types/prop-types": "^15",
"@types/react": "17.0.42",
"@types/react-icons": "2.2.7",
@@ -21,7 +21,6 @@ import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import SpanGraph from './SpanGraph';
import TracePageSearchBar from './TracePageSearchBar';
import { autoColor, TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '..';
import LabeledList from '../common/LabeledList';
import TraceName from '../common/TraceName';
@@ -138,22 +137,15 @@ const getStyles = (theme: GrafanaTheme2) => {
type TracePageHeaderEmbedProps = {
canCollapse: boolean;
clearSearch: () => void;
focusUiFindMatches: () => void;
hideMap: boolean;
hideSummary: boolean;
nextResult: () => void;
onSlimViewClicked: () => void;
onTraceGraphViewClicked: () => void;
prevResult: () => void;
resultCount: number;
slimView: boolean;
trace: Trace;
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
updateViewRangeTime: TUpdateViewRangeTimeFunction;
viewRange: ViewRange;
searchValue: string;
onSearchValueChange: (value: string) => void;
timeZone: TimeZone;
};
@@ -200,21 +192,14 @@ export const HEADER_ITEMS = [
export default function TracePageHeader(props: TracePageHeaderEmbedProps) {
const {
canCollapse,
clearSearch,
focusUiFindMatches,
hideMap,
hideSummary,
nextResult,
onSlimViewClicked,
prevResult,
resultCount,
slimView,
trace,
updateNextViewRangeTime,
updateViewRangeTime,
viewRange,
searchValue,
onSearchValueChange,
timeZone,
} = props;
@@ -267,17 +252,6 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) {
) : (
title
)}
<TracePageSearchBar
clearSearch={clearSearch}
focusUiFindMatches={focusUiFindMatches}
nextResult={nextResult}
prevResult={prevResult}
resultCount={resultCount}
// TODO: we can change this when we have scroll to span functionality
navigable={false}
searchValue={searchValue}
onSearchValueChange={onSearchValueChange}
/>
</div>
{summaryItems && <LabeledList className={styles.TracePageHeaderOverviewItems} items={summaryItems} />}
{!hideMap && !slimView && (
@@ -14,6 +14,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { createTheme } from '@grafana/data';
import * as markers from './TracePageSearchBar.markers';
import TracePageSearchBar, { getStyles } from './TracePageSearchBar';
@@ -24,7 +25,7 @@ const defaultProps = {
navigable: true,
nextResult: () => {},
prevResult: () => {},
resultCount: 0,
suffix: '',
searchValue: 'something',
};
@@ -45,26 +46,28 @@ describe('<TracePageSearchBar>', () => {
name: 'search',
})
);
expect(suffix.hasClass(getStyles().TracePageSearchBarCount)).toBe(true);
expect(suffix.text()).toBe(String(defaultProps.resultCount));
const theme = createTheme();
expect(suffix.hasClass(getStyles(theme).TracePageSearchBarSuffix)).toBe(true);
expect(suffix.text()).toBe(String(defaultProps.suffix));
});
it('renders buttons', () => {
const buttons = wrapper.find('Button');
expect(buttons.length).toBe(4);
expect(buttons.length).toBe(2);
buttons.forEach((button) => {
expect(button.prop('disabled')).toBe(false);
});
expect(wrapper.find('Button[icon="arrow-up"]').prop('onClick')).toBe(defaultProps.prevResult);
expect(wrapper.find('Button[icon="arrow-down"]').prop('onClick')).toBe(defaultProps.nextResult);
expect(wrapper.find('Button[icon="times"]').prop('onClick')).toBe(defaultProps.clearSearch);
});
it('hides navigation buttons when not navigable', () => {
it('only shows navigable buttons when navigable is true', () => {
wrapper.setProps({ navigable: false });
const button = wrapper.find('Button');
expect(button.length).toBe(1);
expect(button.prop('icon')).toBe('times');
var buttons = wrapper.find('Button');
expect(buttons.length).toBe(0);
wrapper.setProps({ navigable: true });
buttons = wrapper.find('Button');
expect(buttons.length).toBe(2);
});
});
@@ -79,7 +82,7 @@ describe('<TracePageSearchBar>', () => {
it('renders buttons', () => {
const buttons = wrapper.find('Button');
expect(buttons.length).toBe(4);
expect(buttons.length).toBe(2);
buttons.forEach((button) => {
expect(button.prop('disabled')).toBe(true);
});
@@ -14,7 +14,6 @@
import * as React from 'react';
import cx from 'classnames';
import IoAndroidLocate from 'react-icons/lib/io/android-locate';
import { css } from '@emotion/css';
import { Button, useStyles2 } from '@grafana/ui';
@@ -24,11 +23,24 @@ import UiFindInput from '../common/UiFindInput';
import { ubFlexAuto, ubJustifyEnd } from '../uberUtilityStyles';
// eslint-disable-next-line no-duplicate-imports
import { memo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
export const getStyles = () => {
export const getStyles = (theme: GrafanaTheme2) => {
return {
TracePageSearchBar: css`
label: TracePageSearchBar;
float: right;
position: sticky;
top: 8px;
right: 0;
z-index: ${theme.zIndex.navbarFixed};
background: ${theme.colors.background.primary};
margin-top: 8px;
margin-bottom: -48px;
padding: 8px;
margin-right: 2px;
border-radius: 4px;
box-shadow: ${theme.shadows.z2};
`,
TracePageSearchBarBar: css`
label: TracePageSearchBarBar;
@@ -38,14 +50,14 @@ export const getStyles = () => {
max-width: 100%;
}
`,
TracePageSearchBarCount: css`
label: TracePageSearchBarCount;
TracePageSearchBarSuffix: css`
label: TracePageSearchBarSuffix;
opacity: 0.6;
`,
TracePageSearchBarBtn: css`
label: TracePageSearchBarBtn;
border-left: none;
transition: 0.2s;
margin-left: 8px;
`,
TracePageSearchBarBtnDisabled: css`
label: TracePageSearchBarBtnDisabled;
@@ -61,72 +73,62 @@ export const getStyles = () => {
type TracePageSearchBarProps = {
prevResult: () => void;
nextResult: () => void;
clearSearch: () => void;
focusUiFindMatches: () => void;
resultCount: number;
navigable: boolean;
searchValue: string;
onSearchValueChange: (value: string) => void;
searchBarSuffix: string;
};
export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) {
const {
clearSearch,
focusUiFindMatches,
navigable,
nextResult,
prevResult,
resultCount,
onSearchValueChange,
searchValue,
} = props;
const { navigable, nextResult, prevResult, onSearchValueChange, searchValue, searchBarSuffix } = props;
const styles = useStyles2(getStyles);
const count = searchValue ? <span className={styles.TracePageSearchBarCount}>{resultCount}</span> : null;
const suffix = searchValue ? (
<span className={styles.TracePageSearchBarSuffix} data-testid="trace-page-search-bar-suffix">
{searchBarSuffix}
</span>
) : null;
const btnClass = cx(styles.TracePageSearchBarBtn, { [styles.TracePageSearchBarBtnDisabled]: !searchValue });
const uiFindInputInputProps = {
'data-test': markers.IN_TRACE_SEARCH,
className: cx(styles.TracePageSearchBarBar, ubFlexAuto),
name: 'search',
suffix: count,
suffix,
};
return (
<div className={styles.TracePageSearchBar}>
<span className={ubJustifyEnd} style={{ display: 'flex' }}>
<UiFindInput onChange={onSearchValueChange} value={searchValue} inputProps={uiFindInputInputProps} />
<UiFindInput
onChange={onSearchValueChange}
value={searchValue}
inputProps={uiFindInputInputProps}
allowClear={true}
/>
<>
{navigable && (
<>
<Button
className={cx(btnClass, styles.TracePageSearchBarLocateBtn)}
disabled={!searchValue}
type="button"
onClick={focusUiFindMatches}
>
<IoAndroidLocate />
</Button>
<Button className={btnClass} disabled={!searchValue} type="button" icon="arrow-up" onClick={prevResult} />
<Button
className={btnClass}
variant="secondary"
disabled={!searchValue}
type="button"
icon="arrow-down"
data-testid="trace-page-search-bar-next-result-button"
onClick={nextResult}
/>
<Button
className={btnClass}
variant="secondary"
disabled={!searchValue}
type="button"
icon="arrow-up"
data-testid="trace-page-search-bar-prev-result-button"
onClick={prevResult}
/>
</>
)}
<Button
variant={'secondary'}
fill={'text'}
// className={btnClass}
disabled={!searchValue}
type="button"
icon="times"
onClick={clearSearch}
title={'Clear search'}
/>
</>
</span>
</div>
@@ -166,17 +166,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
`,
rowMatchingFilter: css`
label: rowMatchingFilter;
background-color: ${autoColor(theme, '#fffce4')};
background-color: ${autoColor(theme, '#fffbde')};
&:hover .${nameWrapperClassName} {
background: linear-gradient(
90deg,
${autoColor(theme, '#fff5e1')},
${autoColor(theme, '#fff5e1')} 75%,
${autoColor(theme, '#ffe6c9')}
${autoColor(theme, '#fffbde')},
${autoColor(theme, '#fffbde')} 75%,
${autoColor(theme, '#f7f1c6')}
);
}
&:hover .${viewClassName} {
background-color: ${autoColor(theme, '#fff3d7')};
background-color: ${autoColor(theme, '#f7f1c6')};
outline: 1px solid ${autoColor(theme, '#ddd')};
}
`,
@@ -13,7 +13,7 @@
// limitations under the License.
import * as React from 'react';
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { Icon, useStyles2 } from '@grafana/ui';
import AccordianKeyValues from './AccordianKeyValues';
@@ -98,6 +98,9 @@ const getStyles = (theme: GrafanaTheme2) => {
content: attr(data-label);
}
`,
serviceName: css`
margin-right: 8px;
`,
};
};
@@ -134,7 +137,9 @@ export function References(props: ReferenceItemProps) {
<span className={styles.itemContent}>
{reference.span ? (
<span>
<span className="span-svc-name">{reference.span.process.serviceName}</span>
<span className={cx('span-svc-name', styles.serviceName)}>
{reference.span.process.serviceName}
</span>
<small className="endpoint-name">{reference.span.operationName}</small>
</span>
) : (
@@ -101,6 +101,7 @@ type TVirtualizedTraceViewOwnProps = {
createSpanLink?: SpanLinkFunc;
scrollElement?: Element;
focusedSpanId?: string;
focusedSpanIdForSearch: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
};
@@ -223,6 +224,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
trace: nextTrace,
uiFind,
focusedSpanId,
focusedSpanIdForSearch,
} = this.props;
if (trace !== nextTrace) {
@@ -241,6 +243,10 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
if (focusedSpanId !== prevProps.focusedSpanId) {
this.scrollToSpan(focusedSpanId);
}
if (focusedSpanIdForSearch !== prevProps.focusedSpanIdForSearch) {
this.scrollToSpan(focusedSpanIdForSearch);
}
}
getRowStates(): RowState[] {
@@ -378,6 +384,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
theme,
createSpanLink,
focusedSpanId,
focusedSpanIdForSearch,
} = this.props;
// to avert flow error
if (!trace) {
@@ -387,7 +394,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
const isCollapsed = childrenHiddenIDs.has(spanID);
const isDetailExpanded = detailStates.has(spanID);
const isMatchingFilter = findMatchesIDs ? findMatchesIDs.has(spanID) : false;
const isFocused = spanID === focusedSpanId;
const isFocused = spanID === focusedSpanId || spanID === focusedSpanIdForSearch;
const showErrorIcon = isErrorSpan(span) || (isCollapsed && spanContainsErredSpan(trace.spans, spanIndex));
// Check for direct child "server" span if the span is a "client" span.
@@ -105,6 +105,7 @@ type TProps = TExtractUiFindFromStateReturn & {
createSpanLink?: SpanLinkFunc;
scrollElement?: Element;
focusedSpanId?: string;
focusedSpanIdForSearch: string;
createFocusSpanLink: (traceId: string, spanId: string) => LinkModel;
topOfExploreViewRef?: RefObject<HTMLDivElement>;
};
@@ -163,6 +164,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
traceTimeline,
theme,
topOfExploreViewRef,
focusedSpanIdForSearch,
...rest
} = this.props;
const { trace } = rest;
@@ -194,6 +196,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, Sta
setSpanNameColumnWidth={setSpanNameColumnWidth}
currentViewRangeTime={viewRange.time.current}
topOfExploreViewRef={topOfExploreViewRef}
focusedSpanIdForSearch={focusedSpanIdForSearch}
/>
</div>
</ExternalLinkContext.Provider>
@@ -32,7 +32,7 @@ export default function filterSpans(textFilter: string, spans: TraceSpan[] | TNi
.filter(Boolean)
.forEach((w) => {
if (w[0] === '-') {
excludeKeys.push(w.substr(1).toLowerCase());
excludeKeys.push(w.slice(1).toLowerCase());
} else {
includeFilters.push(w.toLowerCase());
}
+7 -1
View File
@@ -6,5 +6,11 @@
},
"exclude": ["dist", "node_modules"],
"extends": "@grafana/tsconfig",
"include": ["src/**/*.ts*", "typings", "../../public/app/types/jquery/*.ts", "../../public/app/types/*.d.ts"]
"include": [
"src/**/*.ts*",
"typings",
"../../public/app/types/jquery/*.ts",
"../../public/app/types/*.d.ts",
"../grafana-ui/src/types/*.d.ts"
]
}
+1 -2
View File
@@ -265,10 +265,9 @@ func (hs *HTTPServer) declareFixedRoles() error {
DisplayName: "Annotation reader",
Description: "Read annotations and tags",
Group: "Annotations",
Version: 1,
Version: 2,
Permissions: []ac.Permission{
{Action: ac.ActionAnnotationsRead, Scope: ac.ScopeAnnotationsAll},
{Action: ac.ActionAnnotationsTagsRead, Scope: ac.ScopeAnnotationsTagsAll},
},
},
Grants: []string{string(models.ROLE_VIEWER)},
+2 -2
View File
@@ -495,7 +495,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
{
name: "AccessControl getting tags for annotations with correct permissions is allowed",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsTagsRead, Scope: accesscontrol.ScopeAnnotationsTagsAll}},
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead}},
url: "/api/annotations/tags",
method: http.MethodGet,
},
@@ -504,7 +504,7 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
{
name: "AccessControl getting tags for annotations without correct permissions is forbidden",
args: args{
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsTagsRead}},
permissions: []*accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite}},
url: "/api/annotations/tags",
method: http.MethodGet,
},
+1 -1
View File
@@ -448,7 +448,7 @@ func (hs *HTTPServer) registerRoutes() {
annotationsRoute.Put("/:annotationId", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsWrite, ac.ScopeAnnotationsID)), routing.Wrap(hs.UpdateAnnotation))
annotationsRoute.Patch("/:annotationId", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsWrite, ac.ScopeAnnotationsID)), routing.Wrap(hs.PatchAnnotation))
annotationsRoute.Post("/graphite", authorize(reqEditorRole, ac.EvalPermission(ac.ActionAnnotationsCreate, ac.ScopeAnnotationsTypeOrganization)), routing.Wrap(hs.PostGraphiteAnnotation))
annotationsRoute.Get("/tags", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsTagsRead, ac.ScopeAnnotationsTagsAll)), routing.Wrap(hs.GetAnnotationTags))
annotationsRoute.Get("/tags", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead)), routing.Wrap(hs.GetAnnotationTags))
})
apiRoute.Post("/frontend-metrics", routing.Wrap(hs.PostFrontendMetrics))
+43 -18
View File
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
@@ -115,25 +116,33 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
creator = hs.getUserLogin(c.Req.Context(), dash.CreatedBy)
}
annotationPermissions := &dtos.AnnotationPermission{}
if !hs.AccessControl.IsDisabled() {
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard)
hs.getAnnotationPermissionsByScope(c, &annotationPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization)
}
meta := dtos.DashboardMeta{
IsStarred: isStarred,
Slug: dash.Slug,
Type: models.DashTypeDB,
CanStar: c.IsSignedIn,
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CanDelete: canDelete,
Created: dash.Created,
Updated: dash.Updated,
UpdatedBy: updater,
CreatedBy: creator,
Version: dash.Version,
HasAcl: dash.HasAcl,
IsFolder: dash.IsFolder,
FolderId: dash.FolderId,
Url: dash.GetUrl(),
FolderTitle: "General",
IsStarred: isStarred,
Slug: dash.Slug,
Type: models.DashTypeDB,
CanStar: c.IsSignedIn,
CanSave: canSave,
CanEdit: canEdit,
CanAdmin: canAdmin,
CanDelete: canDelete,
Created: dash.Created,
Updated: dash.Updated,
UpdatedBy: updater,
CreatedBy: creator,
Version: dash.Version,
HasAcl: dash.HasAcl,
IsFolder: dash.IsFolder,
FolderId: dash.FolderId,
Url: dash.GetUrl(),
FolderTitle: "General",
AnnotationsPermissions: annotationPermissions,
}
// lookup folder title
@@ -190,6 +199,22 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response {
return response.JSON(200, dto)
}
func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, actions *dtos.AnnotationActions, scope string) {
var err error
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, scope)
actions.CanDelete, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
if err != nil {
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsDelete, "scope", scope)
}
evaluate = accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsWrite, scope)
actions.CanEdit, err = hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluate)
if err != nil {
hs.log.Warn("Failed to evaluate permission", "err", err, "action", accesscontrol.ActionAnnotationsWrite, "scope", scope)
}
}
func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string {
query := models.GetUserByIdQuery{Id: userID}
err := hs.SQLStore.GetUserById(ctx, &query)
+8 -4
View File
@@ -118,10 +118,11 @@ func TestDashboardAPIEndpoint(t *testing.T) {
mockSQLStore.ExpectedDashboard = fakeDash
hs := &HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: &fakePluginStore{},
SQLStore: mockSQLStore,
Features: featuremgmt.WithFeatures(),
Cfg: setting.NewCfg(),
pluginStore: &fakePluginStore{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
Features: featuremgmt.WithFeatures(),
}
hs.SQLStore = mockSQLStore
@@ -224,6 +225,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
dashboardService: service.ProvideDashboardService(
cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(),
),
@@ -890,6 +892,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
LibraryElementService: &mockLibraryElementService{},
dashboardProvisioningService: mockDashboardProvisioningService{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
}
hs.callGetDashboard(sc)
@@ -927,6 +930,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
LibraryElementService: &libraryElementsService,
SQLStore: sc.sqlStore,
ProvisioningService: provisioningService,
AccessControl: accesscontrolmock.New(),
dashboardProvisioningService: service.ProvideDashboardService(
cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(),
),
+2 -2
View File
@@ -29,7 +29,7 @@ import (
//
// Create Annotation.
//
// Creates an annotation in the Grafana database. The dashboardId and panelId fields are optional. If they are not specified then a global annotation is created and can be queried in any dashboard that adds the Grafana annotations data source. When creating a region annotation include the timeEnd property.
// Creates an annotation in the Grafana database. The dashboardId and panelId fields are optional. If they are not specified then an organization annotation is created and can be queried in any dashboard that adds the Grafana annotations data source. When creating a region annotation include the timeEnd property.
// The format for `time` and `timeEnd` should be epoch numbers in millisecond resolution.
// The response for this HTTP request is slightly different in versions prior to v6.4. In prior versions you would also get an endId if you where creating a region. But in 6.4 regions are represented using a single event with time and timeEnd properties.
//
@@ -141,7 +141,7 @@ type GetAnnotationsParams struct {
// in:query
// required:false
Limit int64 `json:"limit"`
// Use this to filter global annotations. Organization annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. You can filter by multiple tags.
// Use this to filter organization annotations. Organization annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. You can filter by multiple tags.
// in:query
// required:false
// type: array
+2 -2
View File
@@ -26,7 +26,7 @@
},
{
"name": "annotations",
"description": "Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be global annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel."
"description": "Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be organization annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel."
},
{
"name": "library_elements",
@@ -81,4 +81,4 @@
"description": "Grafana Alerting Prometheus-compatible endpoints"
}
]
}
}
+35 -25
View File
@@ -7,31 +7,41 @@ import (
)
type DashboardMeta struct {
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"`
CanDelete bool `json:"canDelete"`
Slug string `json:"slug"`
Url string `json:"url"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"`
FolderUid string `json:"folderUid"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
Provisioned bool `json:"provisioned"`
ProvisionedExternalId string `json:"provisionedExternalId"`
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Type string `json:"type,omitempty"`
CanSave bool `json:"canSave"`
CanEdit bool `json:"canEdit"`
CanAdmin bool `json:"canAdmin"`
CanStar bool `json:"canStar"`
CanDelete bool `json:"canDelete"`
Slug string `json:"slug"`
Url string `json:"url"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
HasAcl bool `json:"hasAcl"`
IsFolder bool `json:"isFolder"`
FolderId int64 `json:"folderId"`
FolderUid string `json:"folderUid"`
FolderTitle string `json:"folderTitle"`
FolderUrl string `json:"folderUrl"`
Provisioned bool `json:"provisioned"`
ProvisionedExternalId string `json:"provisionedExternalId"`
AnnotationsPermissions *AnnotationPermission `json:"annotationsPermissions"`
}
type AnnotationPermission struct {
Dashboard AnnotationActions `json:"dashboard"`
Organization AnnotationActions `json:"organization"`
}
type AnnotationActions struct {
CanEdit bool `json:"canEdit"`
CanDelete bool `json:"canDelete"`
}
type DashboardFullWithMeta struct {
+39 -40
View File
@@ -35,6 +35,7 @@ import (
)
func TestDataSourceProxy_routeRule(t *testing.T) {
cfg := &setting.Cfg{}
httpClientProvider := httpclient.NewProvider()
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
@@ -128,11 +129,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
return ctx, req
}
cfg := &setting.Cfg{}
t.Run("When matching route path", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider,
&oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -145,7 +144,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path and has dynamic url", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
proxy.matchedRoute = routes[3]
@@ -157,7 +156,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path with no url", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
proxy.matchedRoute = routes[4]
@@ -168,7 +167,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path and has dynamic body", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
proxy.matchedRoute = routes[5]
@@ -182,7 +181,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("Validating request", func(t *testing.T) {
t.Run("plugin route with valid role", func(t *testing.T) {
ctx, _ := setUp()
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
err = proxy.validateRequest()
@@ -191,7 +190,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is editor", func(t *testing.T) {
ctx, _ := setUp()
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
err = proxy.validateRequest()
@@ -201,7 +200,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is admin", func(t *testing.T) {
ctx, _ := setUp()
ctx.SignedInUser.OrgRole = models.ROLE_ADMIN
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
err = proxy.validateRequest()
@@ -280,8 +279,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
client = newFakeHTTPClient(t, json)
defer func() { client = originalClient }()
cfg := &setting.Cfg{}
jd, err := ds.JsonData.Map()
require.NoError(t, err)
dsInfo := DSInfo{
@@ -293,7 +290,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
},
}
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
@@ -309,7 +306,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
require.NoError(t, err)
client = newFakeHTTPClient(t, json2)
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg)
@@ -326,7 +323,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
require.NoError(t, err)
client = newFakeHTTPClient(t, []byte{})
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
@@ -349,7 +346,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -376,7 +373,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -401,7 +398,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -430,7 +427,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
var pluginRoutes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -454,7 +451,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -519,7 +516,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer, secretsService)
require.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -588,13 +585,14 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
createAuthTest(t, secretsService, models.DS_ES, "http://localhost:9200", authTypeBasic, authCheckHeader, true),
}
for _, test := range tests {
runDatasourceAuthTest(t, secretsService, test)
runDatasourceAuthTest(t, secretsService, cfg, test)
}
})
}
// test DataSourceProxy request handling.
func TestDataSourceProxy_requestHandling(t *testing.T) {
cfg := &setting.Cfg{}
httpClientProvider := httpclient.NewProvider()
var writeErr error
@@ -653,7 +651,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -672,7 +670,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -687,7 +685,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -710,7 +708,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -736,7 +734,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -761,7 +759,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -781,14 +779,14 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) {
Type: "test",
Url: "://host/root",
}
cfg := setting.Cfg{}
cfg := &setting.Cfg{}
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`))
}
@@ -802,15 +800,15 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
Type: "test",
Url: "127.0.01:5432",
}
cfg := setting.Cfg{}
cfg := &setting.Cfg{}
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
}
@@ -844,7 +842,7 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
}
for _, tc := range tcs {
t.Run(tc.description, func(t *testing.T) {
cfg := setting.Cfg{}
cfg := &setting.Cfg{}
ds := models.DataSource{
Type: "mssql",
Url: tc.url,
@@ -853,8 +851,8 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
if tc.err == nil {
require.NoError(t, err)
assert.Equal(t, &url.URL{
@@ -881,7 +879,7 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *sett
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@@ -993,14 +991,14 @@ func createAuthTest(t *testing.T, secretsService secrets.Service, dsType string,
return test
}
func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, test *testCase) {
func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, cfg *setting.Cfg, test *testCase) {
ctx := &models.ReqContext{}
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
var routes []*plugins.Route
secretsStore := kvstore.SetupTestService(t)
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
@@ -1013,6 +1011,7 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, test *t
}
func Test_PathCheck(t *testing.T) {
cfg := &setting.Cfg{}
// Ensure that we test routes appropriately. This test reproduces a historical bug where two routes were defined with different role requirements but the same method and the more privileged route was tested first. Here we ensure auth checks are applied based on the correct route, not just the method.
routes := []*plugins.Route{
{
@@ -1043,7 +1042,7 @@ func Test_PathCheck(t *testing.T) {
ctx, _ := setUp()
secretsStore := kvstore.SetupTestService(t)
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
dsService := datasourceservice.ProvideService(bus.New(), nil, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock())
proxy, err := NewDataSourceProxy(&models.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer, secretsService)
require.NoError(t, err)
+8 -6
View File
@@ -4,10 +4,12 @@ import (
"context"
"strings"
"github.com/grafana/grafana-azure-sdk-go/azcredentials"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-azure-sdk-go/aztokenprovider"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/aztokenprovider"
)
type azureAccessTokenProvider struct {
@@ -17,8 +19,8 @@ type azureAccessTokenProvider struct {
}
func newAzureAccessTokenProvider(ctx context.Context, cfg *setting.Cfg, authParams *plugins.JWTTokenAuth) (*azureAccessTokenProvider, error) {
credentials := getAzureCredentials(cfg, authParams)
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, credentials)
credentials := getAzureCredentials(cfg.Azure, authParams)
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg.Azure, credentials)
if err != nil {
return nil, err
}
@@ -33,7 +35,7 @@ func (provider *azureAccessTokenProvider) GetAccessToken() (string, error) {
return provider.tokenProvider.GetAccessToken(provider.ctx, provider.scopes)
}
func getAzureCredentials(cfg *setting.Cfg, authParams *plugins.JWTTokenAuth) azcredentials.AzureCredentials {
func getAzureCredentials(settings *azsettings.AzureSettings, authParams *plugins.JWTTokenAuth) azcredentials.AzureCredentials {
authType := strings.ToLower(authParams.Params["azure_auth_type"])
clientId := authParams.Params["client_id"]
@@ -43,7 +45,7 @@ func getAzureCredentials(cfg *setting.Cfg, authParams *plugins.JWTTokenAuth) azc
// before managed identities where introduced, therefore use client secret authentication
// * If authType and other fields aren't set then it means the datasource never been configured
// and managed identity is the default authentication choice as long as managed identities are enabled
isManagedIdentity := authType == "msi" || (authType == "" && clientId == "" && cfg.Azure.ManagedIdentityEnabled)
isManagedIdentity := authType == "msi" || (authType == "" && clientId == "" && settings.ManagedIdentityEnabled)
if isManagedIdentity {
return &azcredentials.AzureManagedIdentityCredentials{}
@@ -1,86 +0,0 @@
package httpclientprovider
import (
"fmt"
"net/http"
"net/url"
"path"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/azcredentials"
"github.com/grafana/grafana/pkg/tsdb/azuremonitor/aztokenprovider"
)
const azureMiddlewareName = "AzureAuthentication.Provider"
func AzureMiddleware(cfg *setting.Cfg) httpclient.Middleware {
return httpclient.NamedMiddlewareFunc(azureMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
credentials, err := getAzureCredentials(opts.CustomOptions)
if err != nil {
return errorResponse(err)
} else if credentials == nil {
return next
}
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, credentials)
if err != nil {
return errorResponse(err)
}
scopes, err := getAzureEndpointScopes(opts.CustomOptions)
if err != nil {
return errorResponse(err)
}
return aztokenprovider.ApplyAuth(tokenProvider, scopes, next)
})
}
func errorResponse(err error) http.RoundTripper {
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("invalid Azure configuration: %s", err)
})
}
func getAzureCredentials(customOptions map[string]interface{}) (azcredentials.AzureCredentials, error) {
if untypedValue, ok := customOptions["_azureCredentials"]; !ok {
return nil, nil
} else if value, ok := untypedValue.(azcredentials.AzureCredentials); !ok {
err := fmt.Errorf("the field 'azureCredentials' should be a valid credentials object")
return nil, err
} else {
return value, nil
}
}
func getAzureEndpointResourceId(customOptions map[string]interface{}) (*url.URL, error) {
var value string
if untypedValue, ok := customOptions["azureEndpointResourceId"]; !ok {
err := fmt.Errorf("the field 'azureEndpointResourceId' should be set")
return nil, err
} else if value, ok = untypedValue.(string); !ok {
err := fmt.Errorf("the field 'azureEndpointResourceId' should be a string")
return nil, err
}
resourceId, err := url.Parse(value)
if err != nil || resourceId.Scheme == "" || resourceId.Host == "" {
err := fmt.Errorf("invalid endpoint Resource ID URL '%s'", value)
return nil, err
}
return resourceId, nil
}
func getAzureEndpointScopes(customOptions map[string]interface{}) ([]string, error) {
resourceId, err := getAzureEndpointResourceId(customOptions)
if err != nil {
return nil, err
}
resourceId.Path = path.Join(resourceId.Path, ".default")
scopes := []string{resourceId.String()}
return scopes, nil
}
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/mwitkow/go-conntrack"
)
@@ -17,7 +16,7 @@ import (
var newProviderFunc = sdkhttpclient.NewProvider
// New creates a new HTTP client provider with pre-configured middlewares.
func New(cfg *setting.Cfg, tracer tracing.Tracer, features featuremgmt.FeatureToggles) *sdkhttpclient.Provider {
func New(cfg *setting.Cfg, tracer tracing.Tracer) *sdkhttpclient.Provider {
logger := log.New("httpclient")
userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion)
@@ -36,10 +35,6 @@ func New(cfg *setting.Cfg, tracer tracing.Tracer, features featuremgmt.FeatureTo
setDefaultTimeoutOptions(cfg)
if features.IsEnabled(featuremgmt.FlagHttpclientproviderAzureAuth) {
middlewares = append(middlewares, AzureMiddleware(cfg))
}
return newProviderFunc(sdkhttpclient.ProviderOptions{
Middlewares: middlewares,
ConfigureTransport: func(opts sdkhttpclient.Options, transport *http.Transport) {
@@ -5,7 +5,6 @@ import (
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
@@ -23,7 +22,7 @@ func TestHTTPClientProvider(t *testing.T) {
})
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer, featuremgmt.WithFeatures())
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, tracer)
require.Len(t, providerOpts, 1)
o := providerOpts[0]
require.Len(t, o.Middlewares, 6)
@@ -47,7 +46,7 @@ func TestHTTPClientProvider(t *testing.T) {
})
tracer, err := tracing.InitializeTracerForTest()
require.NoError(t, err)
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer, featuremgmt.WithFeatures())
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, tracer)
require.Len(t, providerOpts, 1)
o := providerOpts[0]
require.Len(t, o.Middlewares, 7)
+1 -1
View File
@@ -11,7 +11,7 @@ const (
)
type Logger interface {
// New returns a new Logger that has this logger's context plus the given context
// New returns a new contextual Logger that has this logger's context plus the given context.
New(ctx ...interface{}) *ConcreteLogger
Log(keyvals ...interface{}) error
+52 -1
View File
@@ -1,6 +1,9 @@
package level
import "github.com/go-kit/log"
import (
"github.com/go-kit/log"
gokitlevel "github.com/go-kit/log/level"
)
// Error returns a logger that includes a Key/ErrorValue pair.
func Error(logger log.Logger) log.Logger {
@@ -53,6 +56,12 @@ func (l *logger) Log(keyvals ...interface{}) error {
levelAllowed = l.allowed&v.level != 0
break
}
if v, ok := keyvals[i].(gokitlevel.Value); ok {
hasLevel = true
levelAllowed = l.allowed&levelFromGokitLevel(v) != 0
break
}
}
if !hasLevel && l.squelchNoLevel {
return l.errNoLevel
@@ -196,6 +205,35 @@ const (
levelError
)
func IsKey(v interface{}) bool {
return v != nil && (v == Key() || v == gokitlevel.Key())
}
func GetValue(v interface{}) Value {
if v == nil {
return nil
}
if val, ok := v.(Value); ok {
return val
}
if val, ok := v.(gokitlevel.Value); ok {
switch val {
case gokitlevel.InfoValue():
return InfoValue()
case gokitlevel.WarnValue():
return WarnValue()
case gokitlevel.ErrorValue():
return ErrorValue()
case gokitlevel.DebugValue():
return DebugValue()
}
}
return nil
}
type levelValue struct {
name string
level
@@ -203,3 +241,16 @@ type levelValue struct {
func (v *levelValue) String() string { return v.name }
func (v *levelValue) levelVal() {}
func levelFromGokitLevel(l gokitlevel.Value) level {
switch l.String() {
case gokitlevel.ErrorValue().String():
return levelError
case gokitlevel.WarnValue().String():
return levelWarn
case gokitlevel.DebugValue().String():
return levelDebug
}
return levelInfo
}
+154
View File
@@ -0,0 +1,154 @@
package level_test
import (
"testing"
gokitlog "github.com/go-kit/log"
gokitlevel "github.com/go-kit/log/level"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/log/level"
"github.com/stretchr/testify/require"
)
func TestNewFilter(t *testing.T) {
newFilteredLoggerScenario(t, "Given all levels is allowed should log all messages", level.AllowAll(), func(t *testing.T, ctx *scenarioContext) {
logTestMessages(t, ctx)
require.Len(t, ctx.loggedArgs, 8)
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string))
require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string))
require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[3][2].(string))
require.Equal(t, "dbug", ctx.loggedArgs[3][3].(level.Value).String())
require.Equal(t, "level", ctx.loggedArgs[4][0].(string))
require.Equal(t, "info", ctx.loggedArgs[4][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[5][0].(string))
require.Equal(t, "warn", ctx.loggedArgs[5][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[6][0].(string))
require.Equal(t, "error", ctx.loggedArgs[6][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[7][0].(string))
require.Equal(t, "debug", ctx.loggedArgs[7][1].(gokitlevel.Value).String())
})
newFilteredLoggerScenario(t, "Given error, warnings, info, debug is allowed should log all messages", level.AllowDebug(), func(t *testing.T, ctx *scenarioContext) {
logTestMessages(t, ctx)
require.Len(t, ctx.loggedArgs, 8)
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string))
require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string))
require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[3][2].(string))
require.Equal(t, "dbug", ctx.loggedArgs[3][3].(level.Value).String())
require.Equal(t, "level", ctx.loggedArgs[4][0].(string))
require.Equal(t, "info", ctx.loggedArgs[4][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[5][0].(string))
require.Equal(t, "warn", ctx.loggedArgs[5][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[6][0].(string))
require.Equal(t, "error", ctx.loggedArgs[6][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[7][0].(string))
require.Equal(t, "debug", ctx.loggedArgs[7][1].(gokitlevel.Value).String())
})
newFilteredLoggerScenario(t, "Given error, warnings is allowed should log error and warning messages", level.AllowWarn(), func(t *testing.T, ctx *scenarioContext) {
logTestMessages(t, ctx)
require.Len(t, ctx.loggedArgs, 4)
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
require.Equal(t, "warn", ctx.loggedArgs[0][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string))
require.Equal(t, "eror", ctx.loggedArgs[1][3].(level.Value).String())
require.Equal(t, "level", ctx.loggedArgs[2][0].(string))
require.Equal(t, "warn", ctx.loggedArgs[2][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[3][0].(string))
require.Equal(t, "error", ctx.loggedArgs[3][1].(gokitlevel.Value).String())
})
newFilteredLoggerScenario(t, "Given error allowed should log error messages", level.AllowError(), func(t *testing.T, ctx *scenarioContext) {
logTestMessages(t, ctx)
require.Len(t, ctx.loggedArgs, 2)
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
require.Equal(t, "eror", ctx.loggedArgs[0][3].(level.Value).String())
require.Equal(t, "level", ctx.loggedArgs[1][0].(string))
require.Equal(t, "error", ctx.loggedArgs[1][1].(gokitlevel.Value).String())
})
newFilteredLoggerScenario(t, "Given error, warnings, info is allowed should log error, warning and info messages", level.AllowInfo(), func(t *testing.T, ctx *scenarioContext) {
logTestMessages(t, ctx)
require.Len(t, ctx.loggedArgs, 6)
require.Equal(t, "lvl", ctx.loggedArgs[0][2].(string))
require.Equal(t, "info", ctx.loggedArgs[0][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[1][2].(string))
require.Equal(t, "warn", ctx.loggedArgs[1][3].(level.Value).String())
require.Equal(t, "lvl", ctx.loggedArgs[2][2].(string))
require.Equal(t, "eror", ctx.loggedArgs[2][3].(level.Value).String())
require.Equal(t, "level", ctx.loggedArgs[3][0].(string))
require.Equal(t, "info", ctx.loggedArgs[3][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[4][0].(string))
require.Equal(t, "warn", ctx.loggedArgs[4][1].(gokitlevel.Value).String())
require.Equal(t, "level", ctx.loggedArgs[5][0].(string))
require.Equal(t, "error", ctx.loggedArgs[5][1].(gokitlevel.Value).String())
})
newFilteredLoggerScenario(t, "Given no levels is allowed should not log any messages", level.AllowNone(), func(t *testing.T, ctx *scenarioContext) {
logTestMessages(t, ctx)
require.Len(t, ctx.loggedArgs, 0)
})
}
func logTestMessages(t *testing.T, ctx *scenarioContext) {
t.Helper()
ctx.logger.Info("info msg")
ctx.logger.Warn("warn msg")
ctx.logger.Error("error msg")
ctx.logger.Debug("debug msg")
err := gokitlevel.Info(ctx.logger).Log("msg", "gokit info msg")
require.NoError(t, err)
err = gokitlevel.Warn(ctx.logger).Log("msg", "gokit warn msg")
require.NoError(t, err)
err = gokitlevel.Error(ctx.logger).Log("msg", "gokit error msg")
require.NoError(t, err)
err = gokitlevel.Debug(ctx.logger).Log("msg", "gokit debug msg")
require.NoError(t, err)
}
type scenarioContext struct {
loggedArgs [][]interface{}
logger log.Logger
}
func newFilteredLoggerScenario(t *testing.T, desc string, option level.Option, fn func(t *testing.T, ctx *scenarioContext)) {
t.Helper()
ctx := &scenarioContext{
loggedArgs: [][]interface{}{},
}
l := gokitlog.LoggerFunc(func(i ...interface{}) error {
ctx.loggedArgs = append(ctx.loggedArgs, i)
return nil
})
filteredLogger := level.NewFilter(l, option)
testLogger := log.New("test")
testLogger.Swap(filteredLogger)
ctx.logger = testLogger
t.Run(desc, func(t *testing.T) {
fn(t, ctx)
})
}
+9
View File
@@ -192,6 +192,15 @@ func (cl *ConcreteLogger) New(ctx ...interface{}) *ConcreteLogger {
return newConcreteLogger(gokitlog.With(&cl.SwapLogger), ctx...)
}
// New creates a new logger.
// First ctx argument is expected to be the name of the logger.
// Note: For a contextual logger, i.e. a logger with a shared
// name plus additional contextual information, you must use the
// Logger interface New method for it to work as expected.
// Example creating a shared logger:
// requestLogger := log.New("request-logger")
// Example creating a contextual logger:
// contextualLogger := requestLogger.New("username", "user123")
func New(ctx ...interface{}) *ConcreteLogger {
if len(ctx) == 0 {
return root.New()
+13 -2
View File
@@ -30,8 +30,9 @@ func TestLogger(t *testing.T) {
log4 := log3.New("key", "value")
err = log4.Log("msg", "hello 4")
require.NoError(t, err)
log3.Error("hello 3 again")
require.Len(t, ctx.loggedArgs, 4)
require.Len(t, ctx.loggedArgs, 5)
require.Len(t, ctx.loggedArgs[0], 4)
require.Equal(t, "logger", ctx.loggedArgs[0][0].(string))
require.Equal(t, "one", ctx.loggedArgs[0][1].(string))
@@ -62,6 +63,16 @@ func TestLogger(t *testing.T) {
require.Equal(t, "msg", ctx.loggedArgs[3][4].(string))
require.Equal(t, "hello 4", ctx.loggedArgs[3][5].(string))
require.Len(t, ctx.loggedArgs[4], 8)
require.Equal(t, "logger", ctx.loggedArgs[4][0].(string))
require.Equal(t, "three", ctx.loggedArgs[4][1].(string))
require.Equal(t, "t", ctx.loggedArgs[4][2].(string))
require.Equal(t, ctx.mockedTime.Format("2006-01-02T15:04:05.99-0700"), ctx.loggedArgs[4][3].(fmt.Stringer).String())
require.Equal(t, "lvl", ctx.loggedArgs[4][4].(string))
require.Equal(t, level.ErrorValue(), ctx.loggedArgs[4][5].(level.Value))
require.Equal(t, "msg", ctx.loggedArgs[4][6].(string))
require.Equal(t, "hello 3 again", ctx.loggedArgs[4][7].(string))
t.Run("When initializing root logger should swap loggers as expected", func(t *testing.T) {
swappedLoggedArgs := [][]interface{}{}
swapLogger := gokitlog.LoggerFunc(func(i ...interface{}) error {
@@ -87,7 +98,7 @@ func TestLogger(t *testing.T) {
log3.Error("hello 3")
log3.Debug("debug")
require.Len(t, ctx.loggedArgs, 4)
require.Len(t, ctx.loggedArgs, 5)
require.Len(t, swappedLoggedArgs, 7, "expected 4 messages for AllowAll logger and 3 messages for AllowInfo logger")
})
})
+14 -19
View File
@@ -8,8 +8,8 @@ import (
"os"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
gokitsyslog "github.com/go-kit/log/syslog"
"github.com/grafana/grafana/pkg/infra/log/level"
"gopkg.in/ini.v1"
)
@@ -25,31 +25,26 @@ type SysLogHandler struct {
var selector = func(keyvals ...interface{}) syslog.Priority {
for i := 0; i < len(keyvals); i += 2 {
if keyvals[i] == level.Key() {
if v, ok := keyvals[i+1].(string); ok {
switch v {
case "emergency":
return syslog.LOG_EMERG
case "alert":
return syslog.LOG_ALERT
case "critical":
return syslog.LOG_CRIT
case "error":
if level.IsKey(keyvals[i]) {
val := level.GetValue(keyvals[i+1])
if val != nil {
switch val {
case level.ErrorValue():
return syslog.LOG_ERR
case "warning":
case level.WarnValue():
return syslog.LOG_WARNING
case "notice":
return syslog.LOG_NOTICE
case "info":
case level.InfoValue():
return syslog.LOG_INFO
case "debug":
case level.DebugValue():
return syslog.LOG_DEBUG
}
return syslog.LOG_LOCAL0
}
break
}
}
return syslog.LOG_LOCAL0
return syslog.LOG_INFO
}
func NewSyslog(sec *ini.Section, format Formatedlogger) *SysLogHandler {
@@ -62,7 +57,7 @@ func NewSyslog(sec *ini.Section, format Formatedlogger) *SysLogHandler {
handler.Tag = sec.Key("tag").MustString("")
if err := handler.Init(); err != nil {
_ = level.Error(root).Log("Failed to init syslog log handler", "error", err)
root.Error("Failed to init syslog log handler", "error", err)
os.Exit(1)
}
handler.logger = gokitsyslog.NewSyslogLogger(handler.syslog, format, gokitsyslog.PrioritySelectorOption(selector))
+3 -1
View File
@@ -1,6 +1,8 @@
package plugins
import (
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana/pkg/setting"
)
@@ -19,7 +21,7 @@ type Cfg struct {
AWSAssumeRoleEnabled bool
// Azure Cloud settings
Azure setting.AzureSettings
Azure *azsettings.AzureSettings
BuildVersion string // TODO Remove
}
@@ -84,14 +84,17 @@ func (i *Initializer) awsEnvVars() []string {
func (i *Initializer) azureEnvVars() []string {
var variables []string
if i.cfg.Azure.Cloud != "" {
variables = append(variables, "AZURE_CLOUD="+i.cfg.Azure.Cloud)
}
if i.cfg.Azure.ManagedIdentityClientId != "" {
variables = append(variables, "AZURE_MANAGED_IDENTITY_CLIENT_ID="+i.cfg.Azure.ManagedIdentityClientId)
}
if i.cfg.Azure.ManagedIdentityEnabled {
variables = append(variables, "AZURE_MANAGED_IDENTITY_ENABLED=true")
if i.cfg.Azure != nil {
if i.cfg.Azure.Cloud != "" {
variables = append(variables, "AZURE_CLOUD="+i.cfg.Azure.Cloud)
}
if i.cfg.Azure.ManagedIdentityClientId != "" {
variables = append(variables, "AZURE_MANAGED_IDENTITY_CLIENT_ID="+i.cfg.Azure.ManagedIdentityClientId)
}
if i.cfg.Azure.ManagedIdentityEnabled {
variables = append(variables, "AZURE_MANAGED_IDENTITY_ENABLED=true")
}
}
return variables
@@ -80,7 +80,7 @@ func TestPluginManager_int_init(t *testing.T) {
idb := influxdb.ProvideService(hcp)
lk := loki.ProvideService(hcp, tracer)
otsdb := opentsdb.ProvideService(hcp)
pr := prometheus.ProvideService(hcp, tracer)
pr := prometheus.ProvideService(hcp, cfg, features, tracer)
tmpo := tempo.ProvideService(hcp)
td := testdatasource.ProvideService(cfg, features)
pg := postgres.ProvideService(cfg)
+6 -3
View File
@@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/infra/log"
@@ -574,9 +575,11 @@ func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerS
cfg.AWSAllowedAuthProviders = []string{"keys", "credentials"}
cfg.AWSAssumeRoleEnabled = true
cfg.Azure.ManagedIdentityEnabled = true
cfg.Azure.Cloud = "AzureCloud"
cfg.Azure.ManagedIdentityClientId = "client-id"
cfg.Azure = &azsettings.AzureSettings{
ManagedIdentityEnabled: true,
Cloud: "AzureCloud",
ManagedIdentityClientId: "client-id",
}
loader := &fakeLoader{}
manager := New(cfg, nil, loader)
+4 -8
View File
@@ -320,11 +320,10 @@ const (
ScopeTeamsAll = "teams:*"
// Annotations related actions
ActionAnnotationsCreate = "annotations:create"
ActionAnnotationsDelete = "annotations:delete"
ActionAnnotationsRead = "annotations:read"
ActionAnnotationsWrite = "annotations:write"
ActionAnnotationsTagsRead = "annotations.tags:read"
ActionAnnotationsCreate = "annotations:create"
ActionAnnotationsDelete = "annotations:delete"
ActionAnnotationsRead = "annotations:read"
ActionAnnotationsWrite = "annotations:write"
// Dashboard actions
ActionDashboardsCreate = "dashboards:create"
@@ -382,9 +381,6 @@ var (
ScopeAnnotationsID = Scope(ScopeAnnotationsRoot, "id", Parameter(":annotationId"))
ScopeAnnotationsTypeDashboard = ScopeAnnotationsProvider.GetResourceScopeType("dashboard")
ScopeAnnotationsTypeOrganization = ScopeAnnotationsProvider.GetResourceScopeType("organization")
// Annotation tag scopes
ScopeAnnotationsTagsAll = "annotations:tags:*"
)
const RoleGrafanaAdmin = "Grafana Admin"
@@ -137,7 +137,7 @@ func (h *ContextHandler) Middleware(mContext *web.Context) {
case h.initContextWithAnonymousUser(reqContext):
}
reqContext.Logger = log.New("context", "userId", reqContext.UserId, "orgId", reqContext.OrgId, "uname", reqContext.Login)
reqContext.Logger = reqContext.Logger.New("userId", reqContext.UserId, "orgId", reqContext.OrgId, "uname", reqContext.Login)
span.AddEvents(
[]string{"uname", "orgId", "userId"},
[]tracing.EventValue{

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