Merge remote-tracking branch 'origin/main' into secret-service/feature-branch

This commit is contained in:
Matheus Macabu
2025-06-12 10:15:55 +02:00
110 changed files with 1888 additions and 362 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
module bra
go 1.24.3
go 1.24.4
tool github.com/unknwon/bra
+1 -1
View File
@@ -1,6 +1,6 @@
module cog
go 1.24.3
go 1.24.4
tool github.com/grafana/cog/cmd/cli
+1 -1
View File
@@ -1,6 +1,6 @@
module cue
go 1.24.3
go 1.24.4
tool cuelang.org/go/cmd/cue
+1 -1
View File
@@ -1,6 +1,6 @@
module golangci-lint
go 1.24.3
go 1.24.4
tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint
+1 -1
View File
@@ -1,6 +1,6 @@
module jb
go 1.24.3
go 1.24.4
tool github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb
+1 -1
View File
@@ -1,6 +1,6 @@
module lefthook
go 1.24.3
go 1.24.4
tool github.com/evilmartians/lefthook
+1 -1
View File
@@ -1,6 +1,6 @@
module swagger
go 1.24.3
go 1.24.4
tool github.com/go-swagger/go-swagger/cmd/swagger
+102 -102
View File
@@ -25,7 +25,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- ./bin/build verify-drone
@@ -75,7 +75,7 @@ steps:
- go install github.com/bazelbuild/buildtools/buildifier@latest
- buildifier --lint=warn -mode=check -r .
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: lint-starlark
trigger:
event:
@@ -219,7 +219,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -228,21 +228,21 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- apk add --update build-base shared-mime-info shared-mime-info-lang
- go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: test-backend
- commands:
- apk add --update build-base
@@ -251,7 +251,7 @@ steps:
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: test-backend-integration
trigger:
event:
@@ -306,7 +306,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- echo $(/usr/bin/github-app-external-token) > /github-app/token
@@ -351,16 +351,16 @@ steps:
- apk add --update make
- make gen-go
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- go run scripts/modowners/modowners.go check go.mod
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: validate-modfile
- commands:
- apk add --update make
- make swagger-validate
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: validate-openapi-spec
trigger:
event:
@@ -437,7 +437,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -447,7 +447,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -456,7 +456,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- yarn install --immutable || yarn install --immutable
@@ -507,7 +507,7 @@ steps:
environment:
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
from_secret: dagger_token
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: rgm-package
pull: always
volumes:
@@ -892,7 +892,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- echo $DRONE_RUNNER_NAME
@@ -906,7 +906,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -915,14 +915,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- dockerize -wait tcp://postgres:5432 -timeout 120s
@@ -943,7 +943,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
@@ -964,7 +964,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: mysql-8.0-integration-tests
- commands:
- dockerize -wait tcp://redis:6379 -timeout 120s
@@ -980,7 +980,7 @@ steps:
- wait-for-redis
environment:
REDIS_URL: redis://redis:6379/0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: redis-integration-tests
- commands:
- dockerize -wait tcp://memcached:11211 -timeout 120s
@@ -996,7 +996,7 @@ steps:
- wait-for-memcached
environment:
MEMCACHED_HOSTS: memcached:11211
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: memcached-integration-tests
- commands:
- dockerize -wait tcp://mimir_backend:8080 -timeout 120s
@@ -1012,7 +1012,7 @@ steps:
environment:
AM_TENANT_ID: test
AM_URL: http://mimir_backend:8080
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: remote-alertmanager-integration-tests
trigger:
event:
@@ -1094,7 +1094,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
trigger:
event:
@@ -1215,7 +1215,7 @@ steps:
&& return 1; fi
depends_on:
- clone-enterprise
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: swagger-gen
trigger:
event:
@@ -1320,7 +1320,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -1331,7 +1331,7 @@ steps:
- CODEGEN_VERIFY=1 make gen-cue
depends_on:
- clone-enterprise
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -1341,14 +1341,14 @@ steps:
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on:
- clone-enterprise
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- apk add --update build-base
@@ -1356,7 +1356,7 @@ steps:
- go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES}
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: sqlite-benchmark-integration-tests
- commands:
- apk add --update build-base
@@ -1368,7 +1368,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: postgres-benchmark-integration-tests
- commands:
- apk add --update build-base
@@ -1379,7 +1379,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: mysql-8.0-benchmark-integration-tests
trigger:
event:
@@ -1451,7 +1451,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
trigger:
branch: main
@@ -1500,7 +1500,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -1509,21 +1509,21 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- apk add --update build-base shared-mime-info shared-mime-info-lang
- go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: test-backend
- commands:
- apk add --update build-base
@@ -1532,7 +1532,7 @@ steps:
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: test-backend-integration
trigger:
branch: main
@@ -1577,22 +1577,22 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- apk add --update make
- make gen-go
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- go run scripts/modowners/modowners.go check go.mod
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: validate-modfile
- commands:
- apk add --update make
- make swagger-validate
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: validate-openapi-spec
- commands:
- ./bin/build verify-drone
@@ -1724,7 +1724,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -1734,7 +1734,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -1743,7 +1743,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- yarn install --immutable || yarn install --immutable
@@ -1793,7 +1793,7 @@ steps:
environment:
_EXPERIMENTAL_DAGGER_CLOUD_TOKEN:
from_secret: dagger_token
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: rgm-package
pull: always
volumes:
@@ -2253,7 +2253,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- echo $DRONE_RUNNER_NAME
@@ -2267,7 +2267,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -2276,14 +2276,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- dockerize -wait tcp://postgres:5432 -timeout 120s
@@ -2304,7 +2304,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
@@ -2325,7 +2325,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: mysql-8.0-integration-tests
- commands:
- dockerize -wait tcp://redis:6379 -timeout 120s
@@ -2341,7 +2341,7 @@ steps:
- wait-for-redis
environment:
REDIS_URL: redis://redis:6379/0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: redis-integration-tests
- commands:
- dockerize -wait tcp://memcached:11211 -timeout 120s
@@ -2357,7 +2357,7 @@ steps:
- wait-for-memcached
environment:
MEMCACHED_HOSTS: memcached:11211
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: memcached-integration-tests
- commands:
- dockerize -wait tcp://mimir_backend:8080 -timeout 120s
@@ -2373,7 +2373,7 @@ steps:
environment:
AM_TENANT_ID: test
AM_URL: http://mimir_backend:8080
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: remote-alertmanager-integration-tests
trigger:
branch: main
@@ -2641,7 +2641,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -2650,21 +2650,21 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- apk add --update build-base shared-mime-info shared-mime-info-lang
- go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: test-backend
- commands:
- apk add --update build-base
@@ -2673,7 +2673,7 @@ steps:
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: test-backend-integration
trigger:
branch:
@@ -2716,22 +2716,22 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- apk add --update make
- make gen-go
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- go run scripts/modowners/modowners.go check go.mod
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: validate-modfile
- commands:
- apk add --update make
- make swagger-validate
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: validate-openapi-spec
trigger:
branch:
@@ -2810,7 +2810,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- echo $DRONE_RUNNER_NAME
@@ -2824,7 +2824,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -2833,14 +2833,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- dockerize -wait tcp://postgres:5432 -timeout 120s
@@ -2861,7 +2861,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
@@ -2882,7 +2882,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: mysql-8.0-integration-tests
- commands:
- dockerize -wait tcp://redis:6379 -timeout 120s
@@ -2898,7 +2898,7 @@ steps:
- wait-for-redis
environment:
REDIS_URL: redis://redis:6379/0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: redis-integration-tests
- commands:
- dockerize -wait tcp://memcached:11211 -timeout 120s
@@ -2914,7 +2914,7 @@ steps:
- wait-for-memcached
environment:
MEMCACHED_HOSTS: memcached:11211
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: memcached-integration-tests
- commands:
- dockerize -wait tcp://mimir_backend:8080 -timeout 120s
@@ -2930,7 +2930,7 @@ steps:
environment:
AM_TENANT_ID: test
AM_URL: http://mimir_backend:8080
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: remote-alertmanager-integration-tests
trigger:
branch:
@@ -3030,7 +3030,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- ./bin/build artifacts docker fetch --edition oss
@@ -3160,7 +3160,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- ./bin/build artifacts docker fetch --edition oss
@@ -3301,7 +3301,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- ./bin/build artifacts packages --artifacts-editions=oss --tag $${DRONE_TAG} --src-bucket
@@ -3393,7 +3393,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- yarn install --immutable || yarn install --immutable
@@ -3493,7 +3493,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- depends_on:
- compile-build-cmd
@@ -3590,7 +3590,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: compile-build-cmd
- commands:
- ./bin/build publish grafana-com --edition oss ${DRONE_TAG}
@@ -3665,7 +3665,7 @@ steps:
STORYBOOK_DESTINATION:
from_secret: rgm_storybook_destination
UBUNTU_BASE: ubuntu:22.04
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: rgm-build
pull: always
volumes:
@@ -3742,7 +3742,7 @@ steps:
STORYBOOK_DESTINATION:
from_secret: rgm_storybook_destination
UBUNTU_BASE: ubuntu:22.04
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: rgm-build
pull: always
volumes:
@@ -3861,7 +3861,7 @@ steps:
STORYBOOK_DESTINATION:
from_secret: rgm_storybook_destination
UBUNTU_BASE: ubuntu:22.04
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: rgm-build
pull: always
volumes:
@@ -4000,7 +4000,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -4009,21 +4009,21 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- apk add --update build-base shared-mime-info shared-mime-info-lang
- go list -f '{{.Dir}}/...' -m | xargs go test -short -covermode=atomic -timeout=5m
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: test-backend
- commands:
- apk add --update build-base
@@ -4032,7 +4032,7 @@ steps:
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: test-backend-integration
trigger:
cron:
@@ -4100,7 +4100,7 @@ steps:
STORYBOOK_DESTINATION:
from_secret: rgm_storybook_destination
UBUNTU_BASE: ubuntu:22.04
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: rgm-build
pull: always
volumes:
@@ -4246,7 +4246,7 @@ steps:
STORYBOOK_DESTINATION:
from_secret: rgm_storybook_destination
UBUNTU_BASE: ubuntu:22.04
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: rgm-publish
pull: always
volumes:
@@ -4376,7 +4376,7 @@ steps:
STORYBOOK_DESTINATION:
from_secret: rgm_storybook_destination
UBUNTU_BASE: ubuntu:22.04
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: rgm-build
pull: always
volumes:
@@ -4502,7 +4502,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -4511,14 +4511,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: wire-install
- commands:
- dockerize -wait tcp://postgres:5432 -timeout 120s
@@ -4539,7 +4539,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
@@ -4560,7 +4560,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: mysql-8.0-integration-tests
- commands:
- dockerize -wait tcp://redis:6379 -timeout 120s
@@ -4576,7 +4576,7 @@ steps:
- wait-for-redis
environment:
REDIS_URL: redis://redis:6379/0
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: redis-integration-tests
- commands:
- dockerize -wait tcp://memcached:11211 -timeout 120s
@@ -4592,7 +4592,7 @@ steps:
- wait-for-memcached
environment:
MEMCACHED_HOSTS: memcached:11211
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: memcached-integration-tests
- commands:
- dockerize -wait tcp://mimir_backend:8080 -timeout 120s
@@ -4608,7 +4608,7 @@ steps:
environment:
AM_TENANT_ID: test
AM_URL: http://mimir_backend:8080
image: golang:1.24.3-alpine
image: golang:1.24.4-alpine
name: remote-alertmanager-integration-tests
trigger:
event:
@@ -4911,7 +4911,7 @@ steps:
- commands:
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM docker:27-cli
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine/git:2.40.1
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.24.3-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.24.4-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22.11.0-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22-bookworm
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0
@@ -4949,7 +4949,7 @@ steps:
- commands:
- trivy --exit-code 1 --severity HIGH,CRITICAL docker:27-cli
- trivy --exit-code 1 --severity HIGH,CRITICAL alpine/git:2.40.1
- trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.24.3-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.24.4-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL node:22.11.0-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL node:22-bookworm
- trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0
@@ -5206,6 +5206,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: e430c51190ba46dbd426d13f47c13c275f4d05b6b1a26d1d9f67b5a8f09ecb41
hmac: 7f318d682f431d61add0d60afcb9360f390217961709adeb88c34f11feec35b0
...
+18 -18
View File
@@ -499,28 +499,28 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/canvas/ @grafana/dataviz-squad
/public/app/features/geo/ @grafana/dataviz-squad
/public/app/features/visualization/data-hover/ @grafana/dataviz-squad
/public/app/features/commandPalette/ @grafana/grafana-frontend-platform
/public/app/features/commandPalette/ @grafana/grafana-search-navigate-organise
/public/app/features/connections/ @grafana/plugins-platform-frontend
/public/app/features/correlations/ @grafana/datapro
/public/app/features/dashboard/ @grafana/dashboards-squad
/public/app/features/dashboard/components/TransformationsEditor/ @grafana/datapro
/public/app/features/dashboard-scene/ @grafana/dashboards-squad
/public/app/features/scopes/ @grafana/dashboards-squad
/public/app/features/scopes/ @grafana/grafana-operator-experience-squad
/public/app/features/datasources/ @grafana/plugins-platform-frontend
/public/app/features/dimensions/ @grafana/dataviz-squad
/public/app/features/dataframe-import/ @grafana/dataviz-squad
/public/app/features/explore/ @grafana/observability-traces-and-profiling
/public/app/features/expressions/ @grafana/grafana-datasources-core-services
/public/app/features/folders/ @grafana/grafana-frontend-platform
/public/app/features/folders/ @grafana/grafana-search-navigate-organise
/public/app/features/inspector/ @grafana/dashboards-squad
/public/app/features/invites/ @grafana/grafana-frontend-platform
/public/app/features/invites/ @grafana/sharing-squad
/public/app/features/library-panels/ @grafana/dashboards-squad
/public/app/features/logs/ @grafana/observability-logs
/public/app/features/live/ @grafana/dashboards-squad
/public/app/features/apiserver/ @grafana/grafana-app-platform-squad
/public/app/features/manage-dashboards/ @grafana/dashboards-squad
/public/app/features/notifications/ @grafana/grafana-frontend-platform
/public/app/features/org/ @grafana/grafana-frontend-platform
/public/app/features/notifications/ @grafana/grafana-search-navigate-organise
/public/app/features/org/ @grafana/grafana-search-navigate-organise
/public/app/features/panel/ @grafana/dashboards-squad
/public/app/features/playlist/ @grafana/dashboards-squad
/public/app/features/plugins/ @grafana/plugins-platform-frontend
@@ -528,8 +528,8 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/runtime/ @ryantxu
/public/app/features/query/ @grafana/dashboards-squad
/public/app/features/sandbox/ @grafana/grafana-frontend-platform
/public/app/features/browse-dashboards/ @grafana/grafana-frontend-platform
/public/app/features/search/ @grafana/grafana-frontend-platform
/public/app/features/browse-dashboards/ @grafana/grafana-search-navigate-organise
/public/app/features/search/ @grafana/grafana-search-navigate-organise
/public/app/features/serviceaccounts/ @grafana/identity-squad
/public/app/features/teams/ @grafana/access-squad
/public/app/features/templating/ @grafana/dashboards-squad
@@ -539,16 +539,16 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/users/ @grafana/access-squad
/public/app/features/variables/ @grafana/dashboards-squad
/public/app/features/preferences/ @grafana/grafana-frontend-platform
/public/app/features/bookmarks/ @grafana/grafana-frontend-platform
/public/app/features/bookmarks/ @grafana/grafana-search-navigate-organise
/public/app/plugins/panel/alertlist/ @grafana/alerting-frontend
/public/app/plugins/panel/annolist/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/annolist/ @grafana/dashboards-squad
/public/app/plugins/panel/barchart/ @grafana/dataviz-squad
/public/app/plugins/panel/bargauge/ @grafana/dataviz-squad
/public/app/plugins/panel/dashlist/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/dashlist/ @grafana/grafana-search-navigate-organise
/public/app/plugins/panel/debug/ @ryantxu
/public/app/plugins/panel/datagrid/ @grafana/dataviz-squad
/public/app/plugins/panel/gauge/ @grafana/dataviz-squad
/public/app/plugins/panel/gettingstarted/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/gettingstarted/ @grafana/grafana-search-navigate-organise
/public/app/plugins/panel/heatmap/ @grafana/dataviz-squad
/public/app/plugins/panel/histogram/ @grafana/dataviz-squad
/public/app/plugins/panel/logs/ @grafana/observability-logs
@@ -567,12 +567,12 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/plugins/panel/canvas/ @grafana/dataviz-squad
/public/app/plugins/panel/candlestick/ @grafana/dataviz-squad
/public/app/plugins/panel/live/ @grafana/dashboards-squad
/public/app/plugins/panel/news/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/news/ @grafana/dataviz-squad
/public/app/plugins/panel/stat/ @grafana/dataviz-squad
/public/app/plugins/panel/text/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/welcome/ @grafana/grafana-frontend-platform
/public/app/plugins/panel/text/ @grafana/dataviz-squad
/public/app/plugins/panel/welcome/ @grafana/grafana-search-navigate-organise
/public/app/plugins/panel/xychart/ @grafana/dataviz-squad
/public/app/routes/ @grafana/grafana-frontend-platform
/public/app/routes/ @grafana/grafana-search-navigate-organise
/public/app/store/ @grafana/grafana-frontend-platform
/public/app/types/ @grafana/grafana-frontend-platform
/public/app/types/alerting.ts @grafana/alerting-frontend
@@ -600,7 +600,6 @@ playwright.config.ts @grafana/plugins-platform-frontend
/public/app/features/explore/NodeGraph/ @grafana/observability-traces-and-profiling
/public/app/features/explore/FlameGraph/ @grafana/observability-traces-and-profiling
/public/app/features/explore/TraceView/ @grafana/observability-traces-and-profiling
/public/app/features/explore/QueryLibrary/ @grafana/grafana-frontend-platform
/public/api-merged.json @grafana/grafana-backend-group
/public/api-enterprise-spec.json @grafana/grafana-backend-group
@@ -642,7 +641,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/scripts/levitate-parse-json-report.js @grafana/plugins-platform-frontend
/scripts/levitate-show-affected-plugins.js @grafana/plugins-platform-frontend
/scripts/codemods/explicit-barrel-imports.cjs @grafana/frontend-ops
/scripts/rtk-client-generator/ @grafana/grafana-frontend-platform
/scripts/rtk-client-generator/ @grafana/grafana-search-navigate-organise
/scripts/**/generate-transformations* @grafana/datapro
/scripts/webpack/ @grafana/frontend-ops
@@ -689,6 +688,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
/pkg/services/dashboardsnapshots/ @grafana/sharing-squad
/pkg/services/publicdashboards/ @grafana/sharing-squad
/pkg/services/rendering/ @grafana/sharing-squad
/public/app/features/explore/QueryLibrary/ @grafana/sharing-squad
# SSE - Server Side Expressions
/pkg/expr/ @grafana/grafana-datasources-core-services
+1 -1
View File
@@ -6,7 +6,7 @@
ARG BASE_IMAGE=alpine:3.21
ARG JS_IMAGE=node:22-alpine
ARG JS_PLATFORM=linux/amd64
ARG GO_IMAGE=golang:1.24.3-alpine
ARG GO_IMAGE=golang:1.24.4-alpine
# Default to building locally
ARG GO_SRC=go-builder
+1 -1
View File
@@ -9,7 +9,7 @@ include .bingo/Variables.mk
include .citools/Variables.mk
GO = go
GO_VERSION = 1.24.3
GO_VERSION = 1.24.4
GO_LINT_FILES ?= $(shell ./scripts/go-workspace/golangci-lint-includes.sh)
GO_TEST_FILES ?= $(shell ./scripts/go-workspace/test-includes.sh)
SH_FILES ?= $(shell find ./scripts -name *.sh)
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/apps/advisor
go 1.24.3
go 1.24.4
require (
github.com/grafana/grafana-app-sdk v0.38.2
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/authchecks"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/configchecks"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/instancechecks"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks/plugincheck"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/repo"
@@ -81,6 +82,7 @@ func (s *Service) Checks() []checks.Check {
),
authchecks.New(s.ssoSettingsSvc),
configchecks.New(s.cfg),
instancechecks.New(s.cfg),
}
}
@@ -0,0 +1,72 @@
package instancechecks
import (
"context"
"time"
"github.com/google/go-github/v70/github"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/pkg/setting"
)
var _ checks.Check = (*check)(nil)
type check struct {
cfg *setting.Cfg
isCloudInstance bool
}
func New(cfg *setting.Cfg) checks.Check {
return &check{
cfg: cfg,
isCloudInstance: cfg.StackID != "",
}
}
func (c *check) ID() string {
return "instance"
}
func (c *check) Name() string {
return "instance attribute"
}
func (c *check) Items(ctx context.Context) ([]any, error) {
if c.isCloudInstance {
return []any{
pinnedVersion, // pinned version check
}, nil
}
return []any{
outOfSupportVersion, // out of support version check
}, nil
}
func (c *check) Item(ctx context.Context, id string) (any, error) {
return id, nil
}
func (c *check) Init(ctx context.Context) error {
return nil
}
func (c *check) Steps() []checks.Step {
// If running in cloud, we need to check if the version is pinned
if c.isCloudInstance {
return []checks.Step{
&pinnedVersionStep{
BuildBranch: c.cfg.BuildBranch,
},
}
}
// If running in self-managed, we need to check if the version is out of support
return []checks.Step{
&outOfSupportVersionStep{
GrafanaVersion: c.cfg.BuildVersion,
BuildDate: time.Unix(c.cfg.BuildStamp, 0).UTC(),
ghClient: github.NewClient(nil).Repositories,
},
}
}
@@ -0,0 +1,63 @@
package instancechecks
import (
"context"
"testing"
"time"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheck_IsCloudInstance_Logic(t *testing.T) {
tests := []struct {
name string
stackID string
expectedCloud bool
expectedStepID string
expectedItemID string
}{
{
name: "empty stackID should run out of support check",
stackID: "",
expectedCloud: false,
expectedStepID: outOfSupportVersion,
expectedItemID: outOfSupportVersion,
},
{
name: "non-empty stackID should run pinned version check",
stackID: "12345",
expectedCloud: true,
expectedStepID: pinnedVersion,
expectedItemID: pinnedVersion,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &setting.Cfg{
StackID: tt.stackID,
BuildBranch: "v10.0.0",
BuildVersion: "10.0.0",
BuildStamp: time.Now().Unix(),
}
check := New(cfg).(*check)
// Verify isCloudInstance field is set correctly
assert.Equal(t, tt.expectedCloud, check.isCloudInstance)
// Verify Steps() returns the correct step type
steps := check.Steps()
require.Len(t, steps, 1)
assert.Equal(t, tt.expectedStepID, steps[0].ID())
// Verify Items() returns the correct item type
items, err := check.Items(context.Background())
require.NoError(t, err)
require.Len(t, items, 1)
assert.Equal(t, tt.expectedItemID, items[0])
})
}
}
@@ -0,0 +1,122 @@
package instancechecks
import (
"context"
"fmt"
"time"
"github.com/Masterminds/semver/v3"
"github.com/google/go-github/v70/github"
"github.com/grafana/grafana-app-sdk/logging"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
)
var _ checks.Step = &outOfSupportVersionStep{}
const (
outOfSupportVersion = "out_of_support_version"
)
type outOfSupportVersionStep struct {
GrafanaVersion string
BuildDate time.Time
ghClient gitHubClient
}
func (s *outOfSupportVersionStep) Title() string {
return "Grafana version check"
}
func (s *outOfSupportVersionStep) Description() string {
return "Check if the current Grafana version is out of support."
}
func (s *outOfSupportVersionStep) Resolution() string {
return "Out of support versions will not receive security updates or bug fixes. " +
"Upgrade to a more recent version. " +
"<a href='https://grafana.com/docs/grafana/latest/upgrade-guide/when-to-upgrade/#what-to-know-about-version-support' target='_blank'>" +
"Learn more about version support</a>."
}
func (s *outOfSupportVersionStep) ID() string {
return outOfSupportVersion
}
func (s *outOfSupportVersionStep) Run(ctx context.Context, log logging.Logger, _ *advisor.CheckSpec, it any) ([]advisor.CheckReportFailure, error) {
item, ok := it.(string)
if !ok {
return nil, fmt.Errorf("invalid item type %T", it)
}
if item != outOfSupportVersion {
// Not interested in this item
return nil, nil
}
// If the build date is less than 9 months old, it's supported
if s.BuildDate.After(time.Now().AddDate(0, -9, 0)) {
return nil, nil
}
// If the build date is older than 15 months, it's not supported
isOutOfSupport := false
if s.BuildDate.Before(time.Now().AddDate(0, -15, 0)) {
isOutOfSupport = true
} else {
// In other cases, we need to check if the version is out of support.
// Minor versions are generally supported for 9 months but the last
// minor version for a major version is supported for 15 months.
// This is the case when we keep doing releases for old minor versions (e.g. 11.x when 12.x is out).
// https://grafana.com/docs/grafana/latest/upgrade-guide/when-to-upgrade/#what-to-know-about-version-support
// Parse the current version using semver
version, err := semver.NewVersion(s.GrafanaVersion)
if err != nil {
// Unable to parse the version so unable to check if it's out of support
log.Error("Unable to parse the version", "version", s.GrafanaVersion, "error", err)
return nil, nil
}
// To verify if the current version is the latest minor version for this major version,
// we try to get the version vX.Y+1.0 from GitHub
nextMinorVersion := fmt.Sprintf("v%d.%d.0", version.Major(), version.Minor()+1)
_, res, err := s.ghClient.GetReleaseByTag(ctx, "grafana", "grafana", nextMinorVersion)
if err != nil && res.StatusCode != 404 {
// Unable to get the release info so unable to check if it's out of support
log.Error("Unable to get the release info", "version", s.GrafanaVersion, "error", err.Error())
return nil, nil
}
latestMinor := false
if res.StatusCode == 404 {
// No next minor version found, so the current version is the latest minor version
latestMinor = true
}
// Calculate support end date
supportMonths := 9
if latestMinor {
supportMonths = 15 // Extended support for last minor version
}
supportEndDate := s.BuildDate.AddDate(0, supportMonths, 0)
if time.Now().After(supportEndDate) {
isOutOfSupport = true
}
}
if isOutOfSupport {
return []advisor.CheckReportFailure{checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityHigh,
s.ID(),
fmt.Sprintf("Grafana version %s is out of support", s.GrafanaVersion),
outOfSupportVersion,
[]advisor.CheckErrorLink{},
)}, nil
}
return nil, nil
}
// gitHubClient is a mockable interface for the GitHub client
type gitHubClient interface {
GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error)
}
@@ -0,0 +1,157 @@
package instancechecks
import (
"context"
"net/http"
"testing"
"time"
"github.com/google/go-github/v70/github"
"github.com/grafana/grafana-app-sdk/logging"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOutOfSupportVersionStep(t *testing.T) {
now := time.Now()
oldDate := now.AddDate(0, -16, 0) // 16 months ago
recentDate := now.AddDate(0, -8, 0) // 8 months ago
oldDateButSupportedIfLatestMinor := now.AddDate(0, -10, 0) // 10 months ago
tests := []struct {
name string
step *outOfSupportVersionStep
input any
wantFailure bool
wantErr bool
}{
{
name: "should return error for invalid input type",
step: &outOfSupportVersionStep{
GrafanaVersion: "9.5.0",
BuildDate: now,
ghClient: &mockGitHubClient{},
},
input: 123, // invalid type
wantErr: true,
},
{
name: "should return nil for non-out-of-support item",
step: &outOfSupportVersionStep{
GrafanaVersion: "9.5.0",
BuildDate: now,
ghClient: &mockGitHubClient{},
},
input: "other_item",
wantFailure: false,
wantErr: false,
},
{
name: "should return nil for recent build date",
step: &outOfSupportVersionStep{
GrafanaVersion: "11.5.0",
BuildDate: recentDate,
ghClient: &mockGitHubClient{},
},
input: outOfSupportVersion,
wantFailure: false,
wantErr: false,
},
{
name: "should return failure for old build date",
step: &outOfSupportVersionStep{
GrafanaVersion: "9.5.0",
BuildDate: oldDate,
ghClient: &mockGitHubClient{},
},
input: outOfSupportVersion,
wantFailure: true,
wantErr: false,
},
{
name: "should return nil for invalid version format",
step: &outOfSupportVersionStep{
GrafanaVersion: "invalid-version",
BuildDate: oldDateButSupportedIfLatestMinor,
ghClient: &mockGitHubClient{},
},
input: outOfSupportVersion,
wantFailure: false,
wantErr: false,
},
{
name: "should return nil for GitHub API error",
step: &outOfSupportVersionStep{
GrafanaVersion: "9.5.0",
BuildDate: oldDateButSupportedIfLatestMinor,
ghClient: &mockGitHubClient{
errCode: http.StatusTooManyRequests,
},
},
input: outOfSupportVersion,
wantFailure: false,
wantErr: false,
},
{
name: "should return failure for non-latest minor version",
step: &outOfSupportVersionStep{
GrafanaVersion: "9.5.0",
BuildDate: oldDateButSupportedIfLatestMinor,
ghClient: &mockGitHubClient{
release: &github.RepositoryRelease{},
},
},
input: outOfSupportVersion,
wantFailure: true,
wantErr: false,
},
{
name: "should return nil for latest minor version",
step: &outOfSupportVersionStep{
GrafanaVersion: "9.5.0",
BuildDate: oldDateButSupportedIfLatestMinor,
ghClient: &mockGitHubClient{
errCode: http.StatusNotFound,
},
},
input: outOfSupportVersion,
wantFailure: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
failures, err := tt.step.Run(context.Background(), logging.DefaultLogger, &advisor.CheckSpec{}, tt.input)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
if tt.wantFailure {
require.Len(t, failures, 1)
assert.Equal(t, advisor.CheckReportFailureSeverityHigh, failures[0].Severity)
assert.Equal(t, outOfSupportVersion, failures[0].ItemID)
assert.Equal(t, "Grafana version "+tt.step.GrafanaVersion+" is out of support", failures[0].Item)
} else {
assert.Empty(t, failures)
}
})
}
}
type mockGitHubClient struct {
release *github.RepositoryRelease
errCode int
}
func (m *mockGitHubClient) GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*github.RepositoryRelease, *github.Response, error) {
if m.errCode != 0 {
return nil, &github.Response{Response: &http.Response{StatusCode: m.errCode}}, assert.AnError
}
return m.release, &github.Response{Response: &http.Response{StatusCode: http.StatusOK}}, nil
}
@@ -0,0 +1,62 @@
package instancechecks
import (
"context"
"fmt"
"github.com/grafana/grafana-app-sdk/logging"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
)
var _ checks.Step = &pinnedVersionStep{}
const (
pinnedVersion = "pinned_version"
)
type pinnedVersionStep struct {
BuildBranch string
}
func (s *pinnedVersionStep) Title() string {
return "Grafana Cloud version check"
}
func (s *pinnedVersionStep) Description() string {
return "Check if the Grafana version is pinned."
}
func (s *pinnedVersionStep) Resolution() string {
return "You may miss out on security updates and bug fixes if you use a pinned version. " +
"Contact your Grafana administrator and open a " +
"<a href='https://grafana.com/profile/org#support' target=_blank>support ticket</a> to help you get unpinned."
}
func (s *pinnedVersionStep) ID() string {
return pinnedVersion
}
func (s *pinnedVersionStep) Run(ctx context.Context, log logging.Logger, _ *advisor.CheckSpec, it any) ([]advisor.CheckReportFailure, error) {
item, ok := it.(string)
if !ok {
return nil, fmt.Errorf("invalid item type %T", it)
}
if item != pinnedVersion {
// Not interested in this item
return nil, nil
}
if s.BuildBranch == "HEAD" {
// Not a pinned version
return nil, nil
}
return []advisor.CheckReportFailure{checks.NewCheckReportFailure(
advisor.CheckReportFailureSeverityLow,
s.ID(),
"Grafana version is pinned",
"pinned_version",
[]advisor.CheckErrorLink{},
)}, nil
}
@@ -0,0 +1,79 @@
package instancechecks
import (
"context"
"testing"
"github.com/grafana/grafana-app-sdk/logging"
advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPinnedVersionStep(t *testing.T) {
tests := []struct {
name string
step *pinnedVersionStep
input any
wantFailure bool
wantErr bool
}{
{
name: "should return error for invalid input type",
step: &pinnedVersionStep{
BuildBranch: "test-branch",
},
input: 123, // invalid type
wantErr: true,
},
{
name: "should return nil for non-pinned version item",
step: &pinnedVersionStep{
BuildBranch: "test-branch",
},
input: "other_item",
wantFailure: false,
wantErr: false,
},
{
name: "should return nil for HEAD build branch",
step: &pinnedVersionStep{
BuildBranch: "HEAD",
},
input: pinnedVersion,
wantFailure: false,
wantErr: false,
},
{
name: "should return failure for pinned version",
step: &pinnedVersionStep{
BuildBranch: "v9.5.0",
},
input: pinnedVersion,
wantFailure: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
failures, err := tt.step.Run(context.Background(), logging.DefaultLogger, &advisor.CheckSpec{}, tt.input)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
if tt.wantFailure {
require.Len(t, failures, 1)
assert.Equal(t, advisor.CheckReportFailureSeverityLow, failures[0].Severity)
assert.Equal(t, "pinned_version", failures[0].ItemID)
assert.Equal(t, "Grafana version is pinned", failures[0].Item)
} else {
assert.Empty(t, failures)
}
})
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/apps/alerting/notifications
go 1.24.3
go 1.24.4
require (
github.com/grafana/grafana-app-sdk v0.38.2
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/apps/dashboard
go 1.24.3
go 1.24.4
require (
cuelang.org/go v0.11.1
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/apps/folder
go 1.24.3
go 1.24.4
require (
github.com/grafana/grafana-app-sdk v0.38.2
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/apps/iam
go 1.24.3
go 1.24.4
require (
github.com/grafana/grafana-app-sdk v0.38.2
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/apps/investigations
go 1.24.3
go 1.24.4
require (
github.com/grafana/grafana-app-sdk v0.38.2
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/apps/playlist
go 1.24.3
go 1.24.4
require (
github.com/grafana/grafana-app-sdk v0.38.2
@@ -1,6 +1,6 @@
module high-card
go 1.24.3
go 1.24.4
require github.com/prometheus/client_golang v1.22.0
+1 -1
View File
@@ -1,6 +1,6 @@
module utf8-support
go 1.24.3
go 1.24.4
require (
github.com/prometheus/client_golang v1.22.0
@@ -1,4 +1,4 @@
FROM golang:1.24.3
FROM golang:1.24.4
ADD main.go /go/src/webhook/main.go
@@ -59,7 +59,7 @@ The following tables list permissions associated with basic and fixed roles. Thi
| Grafana Admin | `basic_grafana_admin` | fixed:roles:writer`<br>`fixed:users:writer`<br>`fixed:org.users:writer`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:maintainer`<br>`fixed:licensing:writer`<br>`fixed:plugins:maintainer`<br>`fixed:authentication.config:writer`<br>`fixed:migrationassistant:migrator` | Default [Grafana server administrator](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators) assignments. |
| Admin | `basic_admin` | All roles assigned to Editor and `fixed:reports:writer` <br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:writer`<br>`fixed:dashboards.public:writer`<br>`fixed:folders:writer`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:writer`<br>`fixed:apikeys:writer`<br>`fixed:alerting.provisioning.secrets:reader`<br>`fixed:alerting.provisioning:writer`<br>`fixed:datasources.caching:writer`<br>`fixed:plugins:writer`<br>`fixed:library.panels:writer` | Default [Grafana organization administrator](ref:rbac-basic-roles) assignments. |
| Editor | `basic_editor` | All roles assigned to Viewer and `fixed:datasources:explorer` <br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:alerting:writer`<br>`fixed:library.panels:creator`<br>`fixed:library.panels:general.writer`<br>`fixed:alerting.provisioning.status:writer` | Default [Editor](ref:rbac-basic-roles) assignments. |
| Viewer | `basic_viewer` | `fixed:datasources.id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader`<br>`fixed:plugins.app:reader`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:library.panels:general.reader`<br>`fixed:folders.general:reader` | Default [Viewer](ref:rbac-basic-roles) assignments. |
| Viewer | `basic_viewer` | `fixed:datasources.id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader`<br>`fixed:plugins.app:reader`<br>`fixed:dashboards.insights:reader`<br>`fixed:datasources.insights:reader`<br>`fixed:library.panels:general.reader`<br>`fixed:folders.general:reader`<br>`fixed:datasources.builtin:reader` | Default [Viewer](ref:rbac-basic-roles) assignments. |
| No Basic Role | n/a | | Default [No Basic Role](ref:rbac-basic-roles) |
## Fixed role definitions
@@ -103,6 +103,7 @@ To learn how to use the roles API to determine the role UUIDs, refer to [Manage
| `fixed:datasources:explorer` | `fixed_qDzW9mzx9yM91T5Bi8dHUM2muTw` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. |
| `fixed:datasources:reader` | `fixed_C2x8IxkiBc1KZVjyYH775T9jNMQ` | `datasources:read`<br>`datasources:query` | Read and query data sources. |
| `fixed:datasources:writer` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | All permissions from `fixed:datasources:reader` and <br>`datasources:create`<br>`datasources:write`<br>`datasources:delete` | Read, query, create, delete, or update a data source. |
| `fixed:datasources.builtin:reader` | `fixed_q8HXq8kjjA5IlHHgBJlKlUyaNik` | `datasources:read` and `datasources:query` scoped to `datasources:uid:grafana` | An internal role used to grant Viewers access to the builtin example data source in Grafana. |
| `fixed:datasources.caching:reader` | `fixed_D2ddpGxJYlw0mbsTS1ek9fj0kj4` | `datasources.caching:read` | Read data source query caching settings. |
| `fixed:datasources.caching:writer` | `fixed_JtFjHr7jd7hSqUYcktKvRvIOGRE` | `datasources.caching:read`<br>`datasources.caching:write` | Enable, disable, or update query caching settings. |
| `fixed:datasources.id:reader` | `fixed_entg--fHmDqWY2-69N0ocawK0Os` | `datasources.id:read` | Read the ID of a data source based on its name. |
@@ -112,6 +113,7 @@ To learn how to use the roles API to determine the role UUIDs, refer to [Manage
| `fixed:folders:creator` | `fixed_gGLRbZGAGB6n9uECqSh_W382RlQ` | `folders:create` | Create folders in the root level. |
| `fixed:folders:reader` | `fixed_yeW-5QPeo-i5PZUIUXMlAA97GnQ` | `folders:read`<br>`dashboards:read` | Read all folders and dashboards. |
| `fixed:folders:writer` | `fixed_wJXLoTzgE7jVuz90dryYoiogL0o` | 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, update, and delete all folders and dashboards. Create folders and subfolders. |
| `fixed:folders.general:reader` | `fixed_rSASbkg8DvpG_gTX5s41d7uxRvI` | `folders:read` scoped to `folders:uid:general` | An internal role used to correctly display access to the folder tree for Viewer role. |
| `fixed:folders.permissions:reader` | `fixed_E06l4cx0JFm47EeLBE4nmv3pnSo` | `folders.permissions:read` | Read all folder permissions. |
| `fixed:folders.permissions:writer` | `fixed_3GAgpQ_hWG8o7-lwNb86_VB37eI` | All permissions from `fixed:folders.permissions:reader` and <br>`folders.permissions:write` | Read and update all folder permissions. |
| `fixed:ldap:reader` | `fixed_lMcOPwSkxKY-qCK8NMJc5k6izLE` | `ldap.user:read`<br>`ldap.status:read` | Read the LDAP configuration and LDAP status information. |
+14 -1
View File
@@ -229,7 +229,7 @@ To correct this, use the `--homepath` global option to specify the Grafana defau
grafana cli --homepath "/usr/share/grafana" admin reset-admin-password <new password>
```
If you have not lost the admin password, we recommend that you change the user password either in the User Preferences or in the Server Admin > User tab.
If you have not lost the admin password, we recommend that you change the user password either in the User Preferences or in the **Server Admin > User** tab.
If you need to set the password in a script, then you can use the [Grafana User API](../developers/http_api/user/#change-password).
@@ -241,6 +241,19 @@ If you installed Grafana using Homebrew, you can reset the admin password using
/opt/homebrew/opt/grafana/bin/grafana cli --config /opt/homebrew/etc/grafana/grafana.ini --homepath /opt/homebrew/opt/grafana/share/grafana --configOverrides cfg:default.paths.data=/opt/homebrew/var/lib/grafana admin reset-admin-password <new password>
```
#### Reset admin password for Grafana deployed with Grafana Operator and using an external database
If you deploy Grafana with Grafana Operator and configure Grafana to use an external PostgreSQL or MySQL database, specify both the homepath and configuration file in your command:
```bash
grafana cli --homepath /usr/share/grafana --config /etc/grafana/grafana.ini admin reset-admin-password <new password>
```
If you don't do this, the CLI:
- Won't see the database connection information since it's in a directory outside of the Grafana homepath
- Will configure the default SQLite database in `/var/lib/grafana` and reset that password instead of for your external database
### Migrate data and encrypt passwords
`data-migration` runs a script that migrates or cleans up data in your database.
+1 -1
View File
@@ -84,7 +84,7 @@ In addition to Grafana, Grafana Labs also provides the following open source pro
**Grafana Beyla:** Grafana Beyla is an eBPF-based application auto-instrumentation tool for application observability. eBPF is used to automatically inspect application executables and the OS networking layer as well as capture basic trace spans related to web transactions and Rate-Errors-Duration (RED) metrics for Linux HTTP/S and gRPC services. All data capture occurs without any modifications to application code or configuration. For more information about Grafana Beyla, refer to [Grafana Beyla documentation](/docs/beyla/latest/).
**Grafana Alloy:** Grafana Alloy is a flexible, high performance, vendor-neutral distribution of the [OpenTelemetry][] (OTel) Collector.
**Grafana Alloy:** Grafana Alloy is a flexible, high performance, vendor-neutral distribution of the [OpenTelemetry](https://opentelemetry.io/) (OTel) Collector.
It's fully compatible with the most popular open source observability standards such as OpenTelemetry (OTel) and Prometheus.
For more information about Grafana Alloy, refer to the [Grafana Alloy documentation](https://grafana.com/docs/alloy/latest/).
@@ -164,10 +164,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Go 1.24.3
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.24.3
go-version: 1.24.4
- name: Verify Go version
run: go version
@@ -208,7 +208,7 @@ This GitHub Action automates the deployment of a Grafana dashboard using the Fou
The first few steps:
- Check out the repository to access the project code.
- Install Go 1.24.3 using the `actions/setup-go` action.
- Install Go 1.24.4 using the `actions/setup-go` action.
- Verify Go is properly installed.
### 2. Download and install `grafanactl`
@@ -5,10 +5,6 @@ describe('Grouping panels', () => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
after(() => {
e2e.flows.revertAllChanges();
});
/*
* Rows
*/
@@ -0,0 +1,26 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
describe('Dashboard Outline', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('can use dashboard outline', () => {
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1` });
e2e.flows.scenes.toggleEditMode();
e2e.components.PanelEditor.Outline.section().click();
// Should be able to click Variables item in outline to see add variable button
e2e.components.PanelEditor.Outline.item('Variables').click();
e2e.components.PanelEditor.ElementEditPane.addVariableButton().should('exist');
// Clicking a panel should scroll that panel in view
cy.contains('Dashboard panel 48').should('not.exist');
e2e.components.PanelEditor.Outline.item('Panel #48').click();
cy.contains('Dashboard panel 48').should('exist');
});
});
@@ -0,0 +1,309 @@
import { e2e } from '../utils';
describe('Dashboard', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('can switch to auto grid layout', () => {
e2e.flows.scenes.importV2Dashboard({ title: 'Switch to auto grid' });
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
e2e.components.OptionsGroup.toggle('grid-layout-category').click();
e2e.flows.scenes.selectAutoGridLayout();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
const checkInputs = () => {
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('be.visible');
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns().should('be.visible');
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('be.visible');
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen().should('exist');
};
checkInputs();
e2e.flows.scenes.saveDashboard();
cy.reload();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
e2e.components.NavToolbar.editDashboard.editButton().click();
checkInputs();
});
it('can change min column width in auto grid layout', () => {
e2e.flows.scenes.importV2Dashboard({ title: 'Set min column width' });
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
e2e.components.OptionsGroup.toggle('grid-layout-category').click();
e2e.flows.scenes.selectAutoGridLayout();
let firstStandardPanelTopOffset = 0;
// standard min column width will have 1 panel on a second row in edit mode
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
firstStandardPanelTopOffset = el.offset().top;
});
e2e.components.Panels.Panel.title('New panel')
.last()
.then((el) => {
expect(el.offset().top).to.be.greaterThan(firstStandardPanelTopOffset);
});
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('be.visible').click();
cy.get('[id=combobox-option-narrow]').click();
const checkOffset = () => {
// narrow min column width will have all panels on the same row
let narrowPanelTopOffset = 0;
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
narrowPanelTopOffset = el.offset().top;
});
e2e.components.Panels.Panel.title('New panel')
.last()
.then((el) => {
expect(el.offset().top).to.eq(narrowPanelTopOffset);
});
};
checkOffset();
e2e.flows.scenes.saveDashboard();
cy.reload();
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('have.value', 'Narrow');
checkOffset();
});
it('can change to custom min column width in auto grid layout', () => {
e2e.flows.scenes.importV2Dashboard({ title: 'Set custom min column width' });
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
e2e.components.OptionsGroup.toggle('grid-layout-category').click();
e2e.flows.scenes.selectAutoGridLayout();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('be.visible').click();
cy.get('[id=combobox-option-custom]').click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth()
.should('be.visible')
.clear()
.type('900')
.blur();
cy.wait(100); // cy too fast and executes next command before resizing is done
// // changing to 900 custom width to have each panel span the whole row to verify offset
e2e.flows.scenes.verifyPanelsStackedVertically();
e2e.flows.scenes.saveDashboard();
cy.reload();
e2e.flows.scenes.verifyPanelsStackedVertically();
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth().should('have.value', '900');
e2e.flows.scenes.verifyPanelsStackedVertically();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomMinColumnWidth().should('be.visible').click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('have.value', 'Standard');
});
it('can change max columns in auto grid layout', () => {
e2e.flows.scenes.importV2Dashboard({ title: 'Set max columns' });
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
e2e.components.OptionsGroup.toggle('grid-layout-category').click();
e2e.flows.scenes.selectAutoGridLayout();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns().should('be.visible').click();
cy.get('[id=combobox-option-1]').click();
// changing to 1 max column to have each panel span the whole row to verify offset
e2e.flows.scenes.verifyPanelsStackedVertically();
e2e.flows.scenes.saveDashboard();
cy.reload();
e2e.flows.scenes.verifyPanelsStackedVertically();
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns().should('have.value', '1');
e2e.flows.scenes.verifyPanelsStackedVertically();
});
it('can change row height in auto grid layout', () => {
e2e.flows.scenes.importV2Dashboard({ title: 'Set row height' });
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
e2e.components.OptionsGroup.toggle('grid-layout-category').click();
e2e.flows.scenes.selectAutoGridLayout();
let regularRowHeight = 0;
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
regularRowHeight = el.height();
});
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('be.visible').click();
cy.get('[id=combobox-option-short]').click();
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
expect(el.height()).to.be.lessThan(regularRowHeight);
});
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('be.visible').click();
cy.get('[id=combobox-option-tall]').click();
const checkHeight = () => {
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
expect(el.height()).to.be.greaterThan(regularRowHeight);
});
};
checkHeight();
e2e.flows.scenes.saveDashboard();
cy.reload();
checkHeight();
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('have.value', 'Tall');
checkHeight();
});
it('can change to custom row height in auto grid layout', () => {
e2e.flows.scenes.importV2Dashboard({ title: 'Set custom row height' });
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
e2e.components.OptionsGroup.toggle('grid-layout-category').click();
e2e.flows.scenes.selectAutoGridLayout();
let regularRowHeight = 0;
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
regularRowHeight = el.height();
});
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('be.visible').click();
cy.get('[id=combobox-option-custom]').click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight().clear().type('800').blur();
cy.wait(100); // cy too fast and executes next command before resizing is done
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
const elHeight = el.height();
expect(elHeight).be.closeTo(800, 5); // some flakyness and get 798 sometimes
expect(elHeight).to.be.greaterThan(regularRowHeight);
});
e2e.flows.scenes.saveDashboard();
cy.reload();
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
expect(el.height()).be.closeTo(800, 5); // some flakyness and get 798 sometimes
});
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight().should('have.value', '800');
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomRowHeight().should('be.visible').click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight().should('have.value', 'Standard');
});
it('can change fill screen in auto grid layout', () => {
e2e.flows.scenes.importV2Dashboard({ title: 'Set fill screen' });
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.Panels.Panel.title('New panel').should('have.length', 3);
e2e.components.OptionsGroup.toggle('grid-layout-category').click();
e2e.flows.scenes.selectAutoGridLayout();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth().should('be.visible').click();
cy.get('[id=combobox-option-narrow]').click();
let initialHeight = 0;
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
initialHeight = el.height();
});
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen().click({ force: true });
const checkHeight = () => {
e2e.components.Panels.Panel.title('New panel')
.first()
.then((el) => {
expect(el.height()).to.be.greaterThan(initialHeight);
});
};
checkHeight();
e2e.flows.scenes.saveDashboard();
cy.reload();
checkHeight();
e2e.components.NavToolbar.editDashboard.editButton().click();
e2e.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen().should('be.checked');
checkHeight();
});
});
@@ -12,6 +12,14 @@ export const selectRowsLayout = () => {
clickGroupLayoutButton('Rows');
};
export const selectCustomGridLayout = () => {
clickGroupLayoutButton('Custom');
};
export const selectAutoGridLayout = () => {
clickGroupLayoutButton('Auto grid');
};
const editPaneCopyOrDuplicate = (buttonLabel: string) => {
e2e.components.EditPaneHeader.copyDropdown().click();
cy.get('[role="menu"]').within(() => {
+1
View File
@@ -8,3 +8,4 @@ export * from './saveDashboard';
export * from './importDashboard';
export * from './editPaneActions';
export * from './selectPanel';
export * from './panelPosition';
+14
View File
@@ -0,0 +1,14 @@
import { e2e } from '../..';
export const verifyPanelsStackedVertically = () => {
let initialOffset = 0;
e2e.components.Panels.Panel.title('New panel').each((el) => {
if (!initialOffset) {
initialOffset = el.offset().top;
} else {
const elOffset = el.offset().top;
expect(elOffset).to.be.greaterThan(initialOffset);
initialOffset = elOffset;
}
});
};
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana
go 1.24.3
go 1.24.4
require (
buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1 // @grafana/observability-traces-and-profiling
+1 -1
View File
@@ -1,4 +1,4 @@
go 1.24.3
go 1.24.4
// The `skip:golangci-lint` comment tag is used to exclude the package from the `golangci-lint` GitHub Action.
// The module at the root of the repo (`.`) is excluded because ./pkg/... is included manually in the `golangci-lint` configuration.
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/hack
go 1.24.3
go 1.24.4
require k8s.io/code-generator v0.33.1
-1
View File
@@ -235,7 +235,6 @@
"react-select-event": "5.5.1",
"redux-mock-store": "1.5.5",
"rimraf": "6.0.1",
"rudder-sdk-js": "2.48.44",
"sass": "1.83.4",
"sass-loader": "16.0.4",
"smtp-tester": "^2.1.0",
@@ -606,6 +606,32 @@ export const versionedComponents = {
variableLabelInput: {
'12.0.0': 'data-testid variable label input',
},
AutoGridLayout: {
minColumnWidth: {
'12.1.0': 'data-testid min column width selector',
},
customMinColumnWidth: {
'12.1.0': 'data-testid custom min column width input',
},
clearCustomMinColumnWidth: {
'12.1.0': 'data-testid clear custom min column width input',
},
maxColumns: {
'12.1.0': 'data-testid max columns selector',
},
rowHeight: {
'12.1.0': 'data-testid row height selector',
},
customRowHeight: {
'12.1.0': 'data-testid custom row height input',
},
clearCustomRowHeight: {
'12.1.0': 'data-testid clear custom row height input',
},
fillScreen: {
'12.1.0': 'data-testid fill screen switch',
},
},
},
},
PanelInspector: {
+8 -2
View File
@@ -30,17 +30,20 @@ export async function loadPluginResources(id: string, language: string, loaders?
}
// exported for testing
export async function initDefaultI18nInstance() {
export function initDefaultI18nInstance() {
// If the resources are not an object, we need to initialize the plugin translations
if (getI18nInstance().options?.resources && typeof getI18nInstance().options.resources === 'object') {
return;
}
await getI18nInstance().use(initReactI18next).init({
const initPromise = getI18nInstance().use(initReactI18next).init({
resources: {},
returnEmptyString: false,
lng: DEFAULT_LANGUAGE, // this should be the locale of the phrases in our source JSX
});
tFunc = getI18nInstance().t;
transComponent = (props: TransProps) => <I18NextTrans shouldUnescape {...props} />;
return initPromise;
}
// exported for testing
@@ -171,6 +174,7 @@ export function addResourceBundle(language: string, namespace: string, resources
}
export const t: TFunction = (id: string, defaultMessage: string, values?: Record<string, unknown>) => {
initDefaultI18nInstance();
if (!tFunc) {
if (process.env.NODE_ENV !== 'test') {
console.warn(
@@ -189,10 +193,12 @@ export const t: TFunction = (id: string, defaultMessage: string, values?: Record
};
export function useTranslate() {
initDefaultI18nInstance();
return { t };
}
export function Trans(props: TransProps) {
initDefaultI18nInstance();
const Component = transComponent ?? I18NextTrans;
return <Component shouldUnescape {...props} />;
}
@@ -2,6 +2,7 @@ import { SelectableValue } from '@grafana/data';
import { QueryWithDefaults } from '../../defaults';
import { DB, SQLQuery } from '../../types';
import { getColumnsWithIndices } from '../../utils/getColumnsWithIndices';
import { useSqlChange } from '../../utils/useSqlChange';
import { GroupByRow } from './GroupByRow';
@@ -15,6 +16,6 @@ interface SQLGroupByRowProps {
export function SQLGroupByRow({ fields, query, onQueryChange, db }: SQLGroupByRowProps) {
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
return <GroupByRow columns={fields} sql={query.sql!} onSqlChange={onSqlChange} />;
let columnsWithIndices: SelectableValue[] = getColumnsWithIndices(query, fields);
return <GroupByRow columns={columnsWithIndices} sql={query.sql!} onSqlChange={onSqlChange} />;
}
@@ -2,6 +2,7 @@ import { SelectableValue } from '@grafana/data';
import { QueryWithDefaults } from '../../defaults';
import { DB, SQLQuery } from '../../types';
import { getColumnsWithIndices } from '../../utils/getColumnsWithIndices';
import { useSqlChange } from '../../utils/useSqlChange';
import { OrderByRow } from './OrderByRow';
@@ -15,26 +16,6 @@ type SQLOrderByRowProps = {
export function SQLOrderByRow({ fields, query, onQueryChange, db }: SQLOrderByRowProps) {
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
let columnsWithIndices: SelectableValue[] = [];
if (fields) {
const options = query.sql?.columns?.map((c, i) => {
const value = c.name ? `${c.name}(${c.parameters?.map((p) => p.name)})` : c.parameters?.map((p) => p.name);
return {
value,
label: `${i + 1} - ${value}`,
};
});
columnsWithIndices = [
{
value: '',
label: 'Selected columns',
options,
expanded: true,
},
...fields,
];
}
let columnsWithIndices: SelectableValue[] = getColumnsWithIndices(query, fields);
return <OrderByRow sql={query.sql!} onSqlChange={onSqlChange} columns={columnsWithIndices} />;
}
@@ -0,0 +1,29 @@
import { SelectableValue } from '@grafana/data';
import { SQLQuery } from '../types';
export function getColumnsWithIndices(query: SQLQuery, fields: SelectableValue[]): SelectableValue[] {
if (!fields || !query.sql?.columns) {
return fields;
}
const options = query.sql.columns.map((c, i) => {
const value = c.name
? `${c.name}(${c.parameters?.map((p) => p.name).join(', ')})`
: c.parameters?.map((p) => p.name).join(', ');
return {
value,
label: `${i + 1} - ${value}`,
};
});
return [
{
value: '',
label: 'Selected columns',
options,
expanded: true,
},
...fields,
];
}
@@ -105,6 +105,7 @@ export const getBodyStyles = (theme: GrafanaTheme2) => {
'&:disabled': {
color: theme.colors.action.disabledText,
cursor: 'not-allowed',
},
}),
body: css({
@@ -153,13 +154,18 @@ export const getBodyStyles = (theme: GrafanaTheme2) => {
outline: 0,
},
[`${hasActiveSelector}, .react-calendar__tile--active, .react-calendar__tile--active:hover`]: {
[`${hasActiveSelector}, .react-calendar__tile--active`]: {
color: theme.colors.primary.contrastText,
fontWeight: theme.typography.fontWeightMedium,
background: theme.colors.primary.main,
border: '0px',
},
'.react-calendar__tile:hover:not(.react-calendar__tile--active):not(.react-calendar__tile--rangeEnd):not(.react-calendar__tile--rangeStart)':
{
backgroundColor: theme.colors.action.hover,
},
'.react-calendar__tile--rangeEnd, .react-calendar__tile--rangeStart': {
padding: 0,
border: '0px',
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/aggregator
go 1.24.3
go 1.24.4
require (
github.com/emicklei/go-restful/v3 v3.11.0
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/apimachinery
go 1.24.3
go 1.24.4
require (
github.com/grafana/authlib v0.0.0-20250515162837-2f4a8263eabb // @grafana/identity-access-team
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/apis/secret
go 1.24.3
go 1.24.4
require (
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/apiserver
go 1.24.3
go 1.24.4
require (
github.com/google/go-cmp v0.7.0
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/build
go 1.24.3
go 1.24.4
// Override docker/docker to avoid:
// go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/build/wire
go 1.24.3
go 1.24.4
require (
github.com/google/go-cmp v0.7.0
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/codegen
go 1.24.3
go 1.24.4
require (
cuelang.org/go v0.11.1
-8
View File
@@ -328,14 +328,6 @@ func buildGraphEdges(dp *simple.DirectedGraph, registry map[string]Node) error {
for _, neededVar := range cmdNode.Command.NeedsVars() {
neededNode, ok := registry[neededVar]
if !ok {
_, ok := cmdNode.Command.(*SQLCommand)
// If the SSE is a SQL expression, and the node can't be found, it might be a CTE table name
// CTEs are calculated during the evaluation of the SQL, so we won't have a node for them
// So we `continue` in order to support CTE functionality
// TODO: remove CTE table names from the list of table names during parsing of the SQL
if ok {
continue
}
return fmt.Errorf("unable to find dependent node '%v'", neededVar)
}
+19 -3
View File
@@ -3,6 +3,7 @@ package sql
import (
"fmt"
"sort"
"strings"
"github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/grafana/grafana/pkg/infra/log"
@@ -10,7 +11,8 @@ import (
var logger = log.New("sql_expr")
// TablesList returns a list of tables for the sql statement
// TablesList returns a list of tables for the sql statement excluding
// CTEs and the 'dual' table. The list is sorted alphabetically.
func TablesList(rawSQL string) ([]string, error) {
stmt, err := sqlparser.Parse(rawSQL)
if err != nil {
@@ -19,10 +21,18 @@ func TablesList(rawSQL string) ([]string, error) {
}
tables := make(map[string]struct{})
cteNames := make(map[string]struct{})
walkSubtree := func(node sqlparser.SQLNode) error {
err = sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
switch v := node.(type) {
case *sqlparser.CommonTableExpr:
// Track CTE name from the As field
cteName := v.As.String()
if cteName != "" {
cteNames[strings.ToLower(cteName)] = struct{}{}
}
case *sqlparser.AliasedTableExpr:
if tableName, ok := v.Expr.(sqlparser.TableName); ok {
tables[tableName.Name.String()] = struct{}{}
@@ -49,9 +59,15 @@ func TablesList(rawSQL string) ([]string, error) {
// Remove 'dual' table if it exists
// This is a special table in MySQL that always returns a single row with a single column
// See: https://dev.mysql.com/doc/refman/5.7/en/select.html#:~:text=You%20are%20permitted%20to%20specify%20DUAL%20as%20a%20dummy%20table%20name%20in%20situations%20where%20no%20tables%20are%20referenced
if table != "dual" {
result = append(result, table)
if table == "dual" {
continue
}
// Skip CTEs
if _, ok := cteNames[strings.ToLower(table)]; ok {
continue
}
result = append(result, table)
}
sort.Strings(result)
+1 -1
View File
@@ -105,7 +105,7 @@ func TestTablesList(t *testing.T) {
)
SELECT name, price
FROM top_products;`,
expected: []string{"products", "top_products"},
expected: []string{"products"},
},
{
name: "with quote",
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/plugins/codegen
go 1.24.3
go 1.24.4
replace github.com/grafana/grafana/pkg/codegen => ../../codegen
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/promlib
go 1.24.3
go 1.24.4
require (
github.com/grafana/dskit v0.0.0-20250317084829-9cdd36a91f10
+2 -8
View File
@@ -167,14 +167,8 @@ func (p *queryParser) parseRequest(ctx context.Context, input *query.QueryDataRe
q, ok := queryRefIDs[refId]
if !ok {
_, isSQLCMD := target.Command.(*expr.SQLCommand)
if isSQLCMD {
continue
} else {
target, ok = expressions[refId]
if !ok {
return rsp, makeDependencyError(exp.RefID, refId)
}
if target, ok = expressions[refId]; !ok {
return rsp, makeDependencyError(exp.RefID, refId)
}
}
+40
View File
@@ -201,6 +201,46 @@ func TestSqlInputs(t *testing.T) {
require.Equal(t, parsedRequestInfo.SqlInputs["B"], struct{}{})
}
func TestSqlCTE(t *testing.T) {
parser := newQueryParser(
expr.NewExpressionQueryReader(featuremgmt.WithFeatures(featuremgmt.FlagSqlExpressions)),
nil,
tracing.InitializeTracerForTest(),
log.NewNopLogger(),
)
parsedRequestInfo, err := parser.parseRequest(context.Background(), &query.QueryDataRequest{
QueryDataRequest: data.QueryDataRequest{
Queries: []data.DataQuery{
data.NewDataQuery(map[string]any{
"refId": "A",
"datasource": &data.DataSourceRef{
Type: "prometheus",
UID: "local-prom",
},
}),
data.NewDataQuery(map[string]any{
"refId": "B",
"datasource": &data.DataSourceRef{
Type: "__expr__",
UID: "__expr__",
},
"type": "sql",
"expression": `WITH CTE AS (
SELECT
Month
FROM A
)
SELECT * FROM CTE`,
}),
},
},
})
require.NoError(t, err)
require.Equal(t, parsedRequestInfo.SqlInputs["B"], struct{}{})
}
func TestGrafanaDS(t *testing.T) {
ctx := context.Background()
parser := newQueryParser(expr.NewExpressionQueryReader(featuremgmt.WithFeatures()),
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana/pkg/semconv
go 1.24.3
go 1.24.4
require go.opentelemetry.io/otel v1.35.0
+1
View File
@@ -9,6 +9,7 @@ const (
grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad"
grafanaDatavizSquad codeowner = "@grafana/dataviz-squad"
grafanaFrontendPlatformSquad codeowner = "@grafana/grafana-frontend-platform"
grafanaFrontendSearchNavOrganise codeowner = "@grafana/grafana-search-navigate-organise"
grafanaBackendServicesSquad codeowner = "@grafana/grafana-backend-services-squad"
grafanaSearchAndStorageSquad codeowner = "@grafana/search-and-storage"
grafanaPluginsPlatformSquad codeowner = "@grafana/plugins-platform-backend"
+8 -1
View File
@@ -780,7 +780,14 @@ func (b *backend) getHistory(ctx context.Context, req *resourcepb.ListRequest, c
listReq.MinRV = latestDeletedRV + 1
}
rows, err := dbutil.QueryRows(ctx, tx, sqlResourceHistoryGet, listReq)
var rows db.Rows
if listReq.Trash {
// unlike history, trash will not return an object if an object of the same name is live
// (i.e. in the resource table)
rows, err = dbutil.QueryRows(ctx, tx, sqlResourceTrash, listReq)
} else {
rows, err = dbutil.QueryRows(ctx, tx, sqlResourceHistoryGet, listReq)
}
if rows != nil {
defer func() {
if err := rows.Close(); err != nil {
@@ -15,9 +15,6 @@ WHERE 1 = 1
{{ if .Key.Name }}
AND {{ .Ident "name" }} = {{ .Arg .Key.Name }}
{{ end }}
{{ if .Trash }}
AND {{ .Ident "action" }} = 3
{{ end }}
{{ if (gt .StartRV 0) }}
{{ if .SortAscending }}
AND {{ .Ident "resource_version" }} > {{ .Arg .StartRV }}
@@ -0,0 +1,54 @@
SELECT
h.{{ .Ident "guid" }},
h.{{ .Ident "resource_version" }},
h.{{ .Ident "namespace" }},
h.{{ .Ident "group" }},
h.{{ .Ident "resource" }},
h.{{ .Ident "name" }},
h.{{ .Ident "folder" }},
h.{{ .Ident "value" }}
FROM {{ .Ident "resource_history" }} h
INNER JOIN (
SELECT {{ .Ident "name" }}, MAX({{ .Ident "resource_version" }}) as max_rv
FROM {{ .Ident "resource_history" }}
WHERE 1 = 1
AND {{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }}
AND {{ .Ident "group" }} = {{ .Arg .Key.Group }}
AND {{ .Ident "resource" }} = {{ .Arg .Key.Resource }}
{{ if .Key.Name }}
AND {{ .Ident "name" }} = {{ .Arg .Key.Name }}
{{ end }}
AND {{ .Ident "action" }} = 3
{{ if (gt .StartRV 0) }}
{{ if .SortAscending }}
AND {{ .Ident "resource_version" }} > {{ .Arg .StartRV }}
{{ else }}
AND {{ .Ident "resource_version" }} < {{ .Arg .StartRV }}
{{ end }}
{{ end }}
{{ if (gt .MinRV 0) }}
AND {{ .Ident "resource_version" }} >= {{ .Arg .MinRV }}
{{ end }}
{{ if (gt .ExactRV 0) }}
AND {{ .Ident "resource_version" }} = {{ .Arg .ExactRV }}
{{ end }}
GROUP BY {{ .Ident "name" }}
) max_versions ON h.{{ .Ident "name" }} = max_versions.{{ .Ident "name" }}
AND h.{{ .Ident "resource_version" }} = max_versions.max_rv
WHERE 1 = 1
AND h.{{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }}
AND h.{{ .Ident "group" }} = {{ .Arg .Key.Group }}
AND h.{{ .Ident "resource" }} = {{ .Arg .Key.Resource }}
AND h.{{ .Ident "action" }} = 3
AND NOT EXISTS (
SELECT 1 FROM {{ .Ident "resource" }} r
WHERE r.{{ .Ident "namespace" }} = h.{{ .Ident "namespace" }}
AND r.{{ .Ident "group" }} = h.{{ .Ident "group" }}
AND r.{{ .Ident "resource" }} = h.{{ .Ident "resource" }}
AND r.{{ .Ident "name" }} = h.{{ .Ident "name" }}
)
{{ if .SortAscending }}
ORDER BY h.{{ .Ident "resource_version" }} ASC
{{ else }}
ORDER BY h.{{ .Ident "resource_version" }} DESC
{{ end }}
+1
View File
@@ -46,6 +46,7 @@ var (
sqlResourceHistoryGet = mustTemplate("resource_history_get.sql")
sqlResourceHistoryDelete = mustTemplate("resource_history_delete.sql")
sqlResourceHistoryPrune = mustTemplate("resource_history_prune.sql")
sqlResourceTrash = mustTemplate("resource_trash.sql")
sqlResourceInsertFromHistory = mustTemplate("resource_insert_from_history.sql")
// sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql")
+3
View File
@@ -245,6 +245,9 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
},
},
sqlResourceTrash: {
{
Name: "read trash",
Data: &sqlGetHistoryRequest{
@@ -0,0 +1,35 @@
SELECT
h.`guid`,
h.`resource_version`,
h.`namespace`,
h.`group`,
h.`resource`,
h.`name`,
h.`folder`,
h.`value`
FROM `resource_history` h
INNER JOIN (
SELECT `name`, MAX(`resource_version`) as max_rv
FROM `resource_history`
WHERE 1 = 1
AND `namespace` = 'nn'
AND `group` = 'gg'
AND `resource` = 'rr'
AND `action` = 3
AND `resource_version` < 123456
GROUP BY `name`
) max_versions ON h.`name` = max_versions.`name`
AND h.`resource_version` = max_versions.max_rv
WHERE 1 = 1
AND h.`namespace` = 'nn'
AND h.`group` = 'gg'
AND h.`resource` = 'rr'
AND h.`action` = 3
AND NOT EXISTS (
SELECT 1 FROM `resource` r
WHERE r.`namespace` = h.`namespace`
AND r.`group` = h.`group`
AND r.`resource` = h.`resource`
AND r.`name` = h.`name`
)
ORDER BY h.`resource_version` DESC
+34
View File
@@ -0,0 +1,34 @@
SELECT
h.`guid`,
h.`resource_version`,
h.`namespace`,
h.`group`,
h.`resource`,
h.`name`,
h.`folder`,
h.`value`
FROM `resource_history` h
INNER JOIN (
SELECT `name`, MAX(`resource_version`) as max_rv
FROM `resource_history`
WHERE 1 = 1
AND `namespace` = 'nn'
AND `group` = 'gg'
AND `resource` = 'rr'
AND `action` = 3
GROUP BY `name`
) max_versions ON h.`name` = max_versions.`name`
AND h.`resource_version` = max_versions.max_rv
WHERE 1 = 1
AND h.`namespace` = 'nn'
AND h.`group` = 'gg'
AND h.`resource` = 'rr'
AND h.`action` = 3
AND NOT EXISTS (
SELECT 1 FROM `resource` r
WHERE r.`namespace` = h.`namespace`
AND r.`group` = h.`group`
AND r.`resource` = h.`resource`
AND r.`name` = h.`name`
)
ORDER BY h.`resource_version` DESC
@@ -0,0 +1,35 @@
SELECT
h."guid",
h."resource_version",
h."namespace",
h."group",
h."resource",
h."name",
h."folder",
h."value"
FROM "resource_history" h
INNER JOIN (
SELECT "name", MAX("resource_version") as max_rv
FROM "resource_history"
WHERE 1 = 1
AND "namespace" = 'nn'
AND "group" = 'gg'
AND "resource" = 'rr'
AND "action" = 3
AND "resource_version" < 123456
GROUP BY "name"
) max_versions ON h."name" = max_versions."name"
AND h."resource_version" = max_versions.max_rv
WHERE 1 = 1
AND h."namespace" = 'nn'
AND h."group" = 'gg'
AND h."resource" = 'rr'
AND h."action" = 3
AND NOT EXISTS (
SELECT 1 FROM "resource" r
WHERE r."namespace" = h."namespace"
AND r."group" = h."group"
AND r."resource" = h."resource"
AND r."name" = h."name"
)
ORDER BY h."resource_version" DESC
@@ -0,0 +1,34 @@
SELECT
h."guid",
h."resource_version",
h."namespace",
h."group",
h."resource",
h."name",
h."folder",
h."value"
FROM "resource_history" h
INNER JOIN (
SELECT "name", MAX("resource_version") as max_rv
FROM "resource_history"
WHERE 1 = 1
AND "namespace" = 'nn'
AND "group" = 'gg'
AND "resource" = 'rr'
AND "action" = 3
GROUP BY "name"
) max_versions ON h."name" = max_versions."name"
AND h."resource_version" = max_versions.max_rv
WHERE 1 = 1
AND h."namespace" = 'nn'
AND h."group" = 'gg'
AND h."resource" = 'rr'
AND h."action" = 3
AND NOT EXISTS (
SELECT 1 FROM "resource" r
WHERE r."namespace" = h."namespace"
AND r."group" = h."group"
AND r."resource" = h."resource"
AND r."name" = h."name"
)
ORDER BY h."resource_version" DESC
@@ -0,0 +1,35 @@
SELECT
h."guid",
h."resource_version",
h."namespace",
h."group",
h."resource",
h."name",
h."folder",
h."value"
FROM "resource_history" h
INNER JOIN (
SELECT "name", MAX("resource_version") as max_rv
FROM "resource_history"
WHERE 1 = 1
AND "namespace" = 'nn'
AND "group" = 'gg'
AND "resource" = 'rr'
AND "action" = 3
AND "resource_version" < 123456
GROUP BY "name"
) max_versions ON h."name" = max_versions."name"
AND h."resource_version" = max_versions.max_rv
WHERE 1 = 1
AND h."namespace" = 'nn'
AND h."group" = 'gg'
AND h."resource" = 'rr'
AND h."action" = 3
AND NOT EXISTS (
SELECT 1 FROM "resource" r
WHERE r."namespace" = h."namespace"
AND r."group" = h."group"
AND r."resource" = h."resource"
AND r."name" = h."name"
)
ORDER BY h."resource_version" DESC
+34
View File
@@ -0,0 +1,34 @@
SELECT
h."guid",
h."resource_version",
h."namespace",
h."group",
h."resource",
h."name",
h."folder",
h."value"
FROM "resource_history" h
INNER JOIN (
SELECT "name", MAX("resource_version") as max_rv
FROM "resource_history"
WHERE 1 = 1
AND "namespace" = 'nn'
AND "group" = 'gg'
AND "resource" = 'rr'
AND "action" = 3
GROUP BY "name"
) max_versions ON h."name" = max_versions."name"
AND h."resource_version" = max_versions.max_rv
WHERE 1 = 1
AND h."namespace" = 'nn'
AND h."group" = 'gg'
AND h."resource" = 'rr'
AND h."action" = 3
AND NOT EXISTS (
SELECT 1 FROM "resource" r
WHERE r."namespace" = h."namespace"
AND r."group" = h."group"
AND r."resource" = h."resource"
AND r."name" = h."name"
)
ORDER BY h."resource_version" DESC
@@ -35,6 +35,7 @@ const (
TestGetResourceStats = "get resource stats"
TestListHistory = "list history"
TestListHistoryErrorReporting = "list history error reporting"
TestListTrash = "list trash"
TestCreateNewResource = "create new resource"
)
@@ -79,6 +80,7 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp
{TestGetResourceStats, runTestIntegrationBackendGetResourceStats},
{TestListHistory, runTestIntegrationBackendListHistory},
{TestListHistoryErrorReporting, runTestIntegrationBackendListHistoryErrorReporting},
{TestListTrash, runTestIntegrationBackendTrash},
{TestCreateNewResource, runTestIntegrationBackendCreateNewResource},
}
@@ -1128,3 +1130,95 @@ func newServer(t *testing.T, b resource.StorageBackend) resource.ResourceServer
return server
}
func runTestIntegrationBackendTrash(t *testing.T, backend resource.StorageBackend, nsPrefix string) {
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
server := newServer(t, backend)
ns := nsPrefix + "-ns-trash"
// item1 deleted with multiple history events
rv1, err := writeEvent(ctx, backend, "item1", resourcepb.WatchEvent_ADDED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rv1, int64(0))
rvDelete1, err := writeEvent(ctx, backend, "item1", resourcepb.WatchEvent_DELETED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rvDelete1, rv1)
rvDelete2, err := writeEvent(ctx, backend, "item1", resourcepb.WatchEvent_DELETED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rvDelete2, rvDelete1)
// item2 deleted and recreated, should not be returned in trash
rv2, err := writeEvent(ctx, backend, "item2", resourcepb.WatchEvent_ADDED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rv2, int64(0))
rvDelete3, err := writeEvent(ctx, backend, "item2", resourcepb.WatchEvent_DELETED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rvDelete3, rv2)
rv3, err := writeEvent(ctx, backend, "item2", resourcepb.WatchEvent_ADDED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rv3, int64(0))
tests := []struct {
name string
request *resourcepb.ListRequest
expectedVersions []int64
expectedValues []string
minExpectedHeadRV int64
expectedContinueRV int64
expectedSortAsc bool
}{
{
name: "returns the latest delete event",
request: &resourcepb.ListRequest{
Source: resourcepb.ListRequest_TRASH,
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Namespace: ns,
Group: "group",
Resource: "resource",
Name: "item1",
},
},
},
expectedVersions: []int64{rvDelete2},
expectedValues: []string{"item1 DELETED"},
minExpectedHeadRV: rvDelete2,
expectedContinueRV: rvDelete2,
expectedSortAsc: false,
},
{
name: "does not return a version in the resource table",
request: &resourcepb.ListRequest{
Source: resourcepb.ListRequest_TRASH,
Options: &resourcepb.ListOptions{
Key: &resourcepb.ResourceKey{
Namespace: ns,
Group: "group",
Resource: "resource",
Name: "item2",
},
},
},
expectedVersions: []int64{},
expectedValues: []string{},
minExpectedHeadRV: rv3,
expectedContinueRV: rv3,
expectedSortAsc: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
res, err := server.List(ctx, tc.request)
require.NoError(t, err)
require.Nil(t, res.Error)
expectedItemCount := len(tc.expectedVersions)
require.Len(t, res.Items, expectedItemCount)
for i := 0; i < expectedItemCount; i++ {
require.Equal(t, tc.expectedVersions[i], res.Items[i].ResourceVersion)
require.Contains(t, string(res.Items[i].Value), tc.expectedValues[i])
}
require.GreaterOrEqual(t, res.ResourceVersion, tc.minExpectedHeadRV)
})
}
}
@@ -29,12 +29,12 @@ var (
)
type ProfilingClient interface {
ProfileTypes(ctx context.Context, start int64, end int64) ([]*ProfileType, http.Header, error)
LabelNames(ctx context.Context, labelSelector string, start int64, end int64) ([]string, http.Header, error)
LabelValues(ctx context.Context, label string, labelSelector string, start int64, end int64) ([]string, http.Header, error)
GetSeries(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, groupBy []string, limit *int64, step float64) (*SeriesResponse, http.Header, error)
GetProfile(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, maxNodes *int64) (*ProfileResponse, http.Header, error)
GetSpanProfile(ctx context.Context, profileTypeID string, labelSelector string, spanSelector []string, start int64, end int64, maxNodes *int64) (*ProfileResponse, http.Header, error)
ProfileTypes(ctx context.Context, start int64, end int64) ([]*ProfileType, error)
LabelNames(ctx context.Context, labelSelector string, start int64, end int64) ([]string, error)
LabelValues(ctx context.Context, label string, labelSelector string, start int64, end int64) ([]string, error)
GetSeries(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, groupBy []string, limit *int64, step float64) (*SeriesResponse, error)
GetProfile(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, maxNodes *int64) (*ProfileResponse, error)
GetSpanProfile(ctx context.Context, profileTypeID string, labelSelector string, spanSelector []string, start int64, end int64, maxNodes *int64) (*ProfileResponse, error)
}
// PyroscopeDatasource is a datasource for querying application performance profiles.
@@ -109,7 +109,7 @@ func (d *PyroscopeDatasource) profileTypes(ctx context.Context, req *backend.Cal
}
}
types, respHeaders, err := d.client.ProfileTypes(ctx, start, end)
types, err := d.client.ProfileTypes(ctx, start, end)
if err != nil {
ctxLogger.Error("Received error from client", "error", err, "function", logEntrypoint())
return err
@@ -119,7 +119,7 @@ func (d *PyroscopeDatasource) profileTypes(ctx context.Context, req *backend.Cal
ctxLogger.Error("Failed to marshal response", "error", err, "function", logEntrypoint())
return err
}
err = sender.Send(&backend.CallResourceResponse{Body: bodyData, Headers: respHeaders, Status: 200})
err = sender.Send(&backend.CallResourceResponse{Body: bodyData, Status: 200})
if err != nil {
ctxLogger.Error("Failed to send response", "error", err, "function", logEntrypoint())
return err
@@ -146,7 +146,7 @@ func (d *PyroscopeDatasource) labelNames(ctx context.Context, req *backend.CallR
return fmt.Errorf("failed parsing label selector: %v", err)
}
labelNames, respHeaders, err := d.client.LabelNames(ctx, labelSelector, start, end)
labelNames, err := d.client.LabelNames(ctx, labelSelector, start, end)
if err != nil {
ctxLogger.Error("Received error from client", "error", err, "function", logEntrypoint())
return fmt.Errorf("error calling LabelNames: %v", err)
@@ -167,7 +167,7 @@ func (d *PyroscopeDatasource) labelNames(ctx context.Context, req *backend.CallR
ctxLogger.Error("Failed to marshal response", "error", err, "function", logEntrypoint())
return err
}
err = sender.Send(&backend.CallResourceResponse{Body: jsonResponse, Headers: respHeaders, Status: 200})
err = sender.Send(&backend.CallResourceResponse{Body: jsonResponse, Status: 200})
if err != nil {
ctxLogger.Error("Failed to send response", "error", err, "function", logEntrypoint())
return err
@@ -195,7 +195,7 @@ func (d *PyroscopeDatasource) labelValues(ctx context.Context, req *backend.Call
end, _ := strconv.ParseInt(query.Get("end"), 10, 64)
label := query.Get("label")
res, respHeaders, err := d.client.LabelValues(ctx, label, query.Get("query"), start, end)
res, err := d.client.LabelValues(ctx, label, query.Get("query"), start, end)
if err != nil {
ctxLogger.Error("Received error from client", "error", err, "function", logEntrypoint())
return fmt.Errorf("error calling LabelValues: %v", err)
@@ -207,7 +207,7 @@ func (d *PyroscopeDatasource) labelValues(ctx context.Context, req *backend.Call
return err
}
err = sender.Send(&backend.CallResourceResponse{Body: data, Headers: respHeaders, Status: 200})
err = sender.Send(&backend.CallResourceResponse{Body: data, Status: 200})
if err != nil {
ctxLogger.Error("Failed to send response", "error", err, "function", logEntrypoint())
return err
@@ -255,7 +255,7 @@ func (d *PyroscopeDatasource) CheckHealth(ctx context.Context, _ *backend.CheckH
// request succeeded or failed, we set the window to be small.
start := time.Unix(1, 0).UnixMilli()
end := time.Unix(4, 0).UnixMilli()
if _, _, err := d.client.ProfileTypes(ctx, start, end); err != nil {
if _, err := d.client.ProfileTypes(ctx, start, end); err != nil {
status = backend.HealthStatusError
message = err.Error()
}
@@ -71,7 +71,7 @@ func NewPyroscopeClient(httpClient *http.Client, url string) *PyroscopeClient {
}
}
func (c *PyroscopeClient) ProfileTypes(ctx context.Context, start int64, end int64) ([]*ProfileType, http.Header, error) {
func (c *PyroscopeClient) ProfileTypes(ctx context.Context, start int64, end int64) ([]*ProfileType, error) {
ctx, span := tracing.DefaultTracer().Start(ctx, "datasource.pyroscope.ProfileTypes")
defer span.End()
res, err := c.connectClient.ProfileTypes(ctx, connect.NewRequest(&querierv1.ProfileTypesRequest{
@@ -82,11 +82,11 @@ func (c *PyroscopeClient) ProfileTypes(ctx context.Context, start int64, end int
logger.Error("Received error from client", "error", err, "function", logEntrypoint())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
return nil, err
}
if res.Msg.ProfileTypes == nil {
// Let's make sure we send at least empty array if we don't have any types
return []*ProfileType{}, nil, nil
return []*ProfileType{}, nil
} else {
pTypes := make([]*ProfileType, len(res.Msg.ProfileTypes))
for i, pType := range res.Msg.ProfileTypes {
@@ -95,11 +95,11 @@ func (c *PyroscopeClient) ProfileTypes(ctx context.Context, start int64, end int
Label: pType.Name + " - " + pType.SampleType,
}
}
return pTypes, res.Header(), nil
return pTypes, nil
}
}
func (c *PyroscopeClient) GetSeries(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, groupBy []string, limit *int64, step float64) (*SeriesResponse, http.Header, error) {
func (c *PyroscopeClient) GetSeries(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, groupBy []string, limit *int64, step float64) (*SeriesResponse, error) {
ctx, span := tracing.DefaultTracer().Start(ctx, "datasource.pyroscope.GetSeries", trace.WithAttributes(attribute.String("profileTypeID", profileTypeID), attribute.String("labelSelector", labelSelector)))
defer span.End()
req := connect.NewRequest(&querierv1.SelectSeriesRequest{
@@ -117,7 +117,7 @@ func (c *PyroscopeClient) GetSeries(ctx context.Context, profileTypeID string, l
logger.Error("Received error from client", "error", err, "function", logEntrypoint())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
return nil, err
}
series := make([]*Series, len(resp.Msg.Series))
@@ -152,10 +152,10 @@ func (c *PyroscopeClient) GetSeries(ctx context.Context, profileTypeID string, l
Series: series,
Units: getUnits(profileTypeID),
Label: parts[1],
}, resp.Header(), nil
}, nil
}
func (c *PyroscopeClient) GetProfile(ctx context.Context, profileTypeID, labelSelector string, start, end int64, maxNodes *int64) (*ProfileResponse, http.Header, error) {
func (c *PyroscopeClient) GetProfile(ctx context.Context, profileTypeID, labelSelector string, start, end int64, maxNodes *int64) (*ProfileResponse, error) {
ctx, span := tracing.DefaultTracer().Start(ctx, "datasource.pyroscope.GetProfile", trace.WithAttributes(attribute.String("profileTypeID", profileTypeID), attribute.String("labelSelector", labelSelector)))
defer span.End()
req := &connect.Request[querierv1.SelectMergeStacktracesRequest]{
@@ -173,18 +173,18 @@ func (c *PyroscopeClient) GetProfile(ctx context.Context, profileTypeID, labelSe
logger.Error("Received error from client", "error", err, "function", logEntrypoint())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
return nil, err
}
if resp.Msg.Flamegraph == nil {
// Not an error, can happen when querying data oout of range.
return nil, nil, nil
return nil, nil
}
return profileQuery(resp.Msg.Flamegraph, profileTypeID), resp.Header(), nil
return profileQuery(ctx, err, span, resp.Msg.Flamegraph, profileTypeID)
}
func (c *PyroscopeClient) GetSpanProfile(ctx context.Context, profileTypeID, labelSelector string, spanSelector []string, start, end int64, maxNodes *int64) (*ProfileResponse, http.Header, error) {
func (c *PyroscopeClient) GetSpanProfile(ctx context.Context, profileTypeID, labelSelector string, spanSelector []string, start, end int64, maxNodes *int64) (*ProfileResponse, error) {
ctx, span := tracing.DefaultTracer().Start(ctx, "datasource.pyroscope.GetSpanProfile", trace.WithAttributes(attribute.String("profileTypeID", profileTypeID), attribute.String("labelSelector", labelSelector), attribute.String("spanSelector", strings.Join(spanSelector, ","))))
defer span.End()
req := &connect.Request[querierv1.SelectMergeSpanProfileRequest]{
@@ -202,18 +202,18 @@ func (c *PyroscopeClient) GetSpanProfile(ctx context.Context, profileTypeID, lab
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
return nil, err
}
if resp.Msg.Flamegraph == nil {
// Not an error, can happen when querying data oout of range.
return nil, nil, nil
return nil, nil
}
return profileQuery(resp.Msg.Flamegraph, profileTypeID), resp.Header(), nil
return profileQuery(ctx, err, span, resp.Msg.Flamegraph, profileTypeID)
}
func profileQuery(flamegraph *querierv1.FlameGraph, profileTypeID string) *ProfileResponse {
func profileQuery(ctx context.Context, err error, span trace.Span, flamegraph *querierv1.FlameGraph, profileTypeID string) (*ProfileResponse, error) {
levels := make([]*Level, len(flamegraph.Levels))
for i, level := range flamegraph.Levels {
levels[i] = &Level{
@@ -229,7 +229,7 @@ func profileQuery(flamegraph *querierv1.FlameGraph, profileTypeID string) *Profi
MaxSelf: flamegraph.MaxSelf,
},
Units: getUnits(profileTypeID),
}
}, nil
}
func getUnits(profileTypeID string) string {
@@ -244,7 +244,7 @@ func getUnits(profileTypeID string) string {
return unit
}
func (c *PyroscopeClient) LabelNames(ctx context.Context, labelSelector string, start int64, end int64) ([]string, http.Header, error) {
func (c *PyroscopeClient) LabelNames(ctx context.Context, labelSelector string, start int64, end int64) ([]string, error) {
ctx, span := tracing.DefaultTracer().Start(ctx, "datasource.pyroscope.LabelNames")
defer span.End()
resp, err := c.connectClient.LabelNames(ctx, connect.NewRequest(&typesv1.LabelNamesRequest{
@@ -256,11 +256,11 @@ func (c *PyroscopeClient) LabelNames(ctx context.Context, labelSelector string,
logger.Error("Received error from client", "error", err, "function", logEntrypoint())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, nil, fmt.Errorf("error sending LabelNames request %v", err)
return nil, fmt.Errorf("error sending LabelNames request %v", err)
}
if resp.Msg.Names == nil {
return []string{}, nil, nil
return []string{}, nil
}
var filtered []string
@@ -270,10 +270,10 @@ func (c *PyroscopeClient) LabelNames(ctx context.Context, labelSelector string,
}
}
return filtered, resp.Header(), nil
return filtered, nil
}
func (c *PyroscopeClient) LabelValues(ctx context.Context, label string, labelSelector string, start int64, end int64) ([]string, http.Header, error) {
func (c *PyroscopeClient) LabelValues(ctx context.Context, label string, labelSelector string, start int64, end int64) ([]string, error) {
ctx, span := tracing.DefaultTracer().Start(ctx, "datasource.pyroscope.LabelValues")
defer span.End()
resp, err := c.connectClient.LabelValues(ctx, connect.NewRequest(&typesv1.LabelValuesRequest{
@@ -286,12 +286,12 @@ func (c *PyroscopeClient) LabelValues(ctx context.Context, label string, labelSe
logger.Error("Received error from client", "error", err, "function", logEntrypoint())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, nil, err
return nil, err
}
if resp.Msg.Names == nil {
return []string{}, nil, nil
return []string{}, nil
}
return resp.Msg.Names, resp.Header(), nil
return resp.Msg.Names, nil
}
func isPrivateLabel(label string) bool {
@@ -19,7 +19,7 @@ func Test_PyroscopeClient(t *testing.T) {
t.Run("GetSeries", func(t *testing.T) {
limit := int64(42)
resp, _, err := client.GetSeries(context.Background(), "memory:alloc_objects:count:space:bytes", "{}", 0, 100, []string{}, &limit, 15)
resp, err := client.GetSeries(context.Background(), "memory:alloc_objects:count:space:bytes", "{}", 0, 100, []string{}, &limit, 15)
require.Nil(t, err)
series := &SeriesResponse{
@@ -34,7 +34,7 @@ func Test_PyroscopeClient(t *testing.T) {
t.Run("GetProfile", func(t *testing.T) {
maxNodes := int64(-1)
resp, _, err := client.GetProfile(context.Background(), "memory:alloc_objects:count:space:bytes", "{}", 0, 100, &maxNodes)
resp, err := client.GetProfile(context.Background(), "memory:alloc_objects:count:space:bytes", "{}", 0, 100, &maxNodes)
require.Nil(t, err)
series := &ProfileResponse{
@@ -56,7 +56,7 @@ func Test_PyroscopeClient(t *testing.T) {
t.Run("GetProfile with empty response", func(t *testing.T) {
connectClient.SendEmptyProfileResponse = true
maxNodes := int64(-1)
resp, _, err := client.GetProfile(context.Background(), "memory:alloc_objects:count:space:bytes", "{}", 0, 100, &maxNodes)
resp, err := client.GetProfile(context.Background(), "memory:alloc_objects:count:space:bytes", "{}", 0, 100, &maxNodes)
require.Nil(t, err)
// Mainly ensuring this does not panic like before
require.Nil(t, resp)
@@ -77,7 +77,7 @@ func (d *PyroscopeDatasource) query(ctx context.Context, pCtx backend.PluginCont
}
}
logger.Debug("Sending SelectSeriesRequest", "queryModel", qm, "function", logEntrypoint())
seriesResp, _, err := d.client.GetSeries(
seriesResp, err := d.client.GetSeries(
gCtx,
profileTypeId,
labelSelector,
@@ -114,7 +114,7 @@ func (d *PyroscopeDatasource) query(ctx context.Context, pCtx backend.PluginCont
var profileResp *ProfileResponse
if len(qm.SpanSelector) > 0 {
logger.Debug("Calling GetSpanProfile", "queryModel", qm, "function", logEntrypoint())
prof, _, err := d.client.GetSpanProfile(gCtx, profileTypeId, labelSelector, qm.SpanSelector, query.TimeRange.From.UnixMilli(), query.TimeRange.To.UnixMilli(), qm.MaxNodes)
prof, err := d.client.GetSpanProfile(gCtx, profileTypeId, labelSelector, qm.SpanSelector, query.TimeRange.From.UnixMilli(), query.TimeRange.To.UnixMilli(), qm.MaxNodes)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
@@ -124,7 +124,7 @@ func (d *PyroscopeDatasource) query(ctx context.Context, pCtx backend.PluginCont
profileResp = prof
} else {
logger.Debug("Calling GetProfile", "queryModel", qm, "function", logEntrypoint())
prof, _, err := d.client.GetProfile(gCtx, profileTypeId, labelSelector, query.TimeRange.From.UnixMilli(), query.TimeRange.To.UnixMilli(), qm.MaxNodes)
prof, err := d.client.GetProfile(gCtx, profileTypeId, labelSelector, query.TimeRange.From.UnixMilli(), query.TimeRange.To.UnixMilli(), qm.MaxNodes)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
@@ -2,7 +2,6 @@ package pyroscope
import (
"context"
"net/http"
"testing"
"time"
@@ -419,7 +418,7 @@ type FakeClient struct {
Args []any
}
func (f *FakeClient) ProfileTypes(ctx context.Context, start int64, end int64) ([]*ProfileType, http.Header, error) {
func (f *FakeClient) ProfileTypes(ctx context.Context, start int64, end int64) ([]*ProfileType, error) {
return []*ProfileType{
{
ID: "type:1",
@@ -429,18 +428,18 @@ func (f *FakeClient) ProfileTypes(ctx context.Context, start int64, end int64) (
ID: "type:2",
Label: "memory",
},
}, http.Header{}, nil
}, nil
}
func (f *FakeClient) LabelValues(ctx context.Context, label string, labelSelector string, start int64, end int64) ([]string, http.Header, error) {
func (f *FakeClient) LabelValues(ctx context.Context, label string, labelSelector string, start int64, end int64) ([]string, error) {
panic("implement me")
}
func (f *FakeClient) LabelNames(ctx context.Context, labelSelector string, start int64, end int64) ([]string, http.Header, error) {
func (f *FakeClient) LabelNames(ctx context.Context, labelSelector string, start int64, end int64) ([]string, error) {
panic("implement me")
}
func (f *FakeClient) GetProfile(ctx context.Context, profileTypeID, labelSelector string, start, end int64, maxNodes *int64) (*ProfileResponse, http.Header, error) {
func (f *FakeClient) GetProfile(ctx context.Context, profileTypeID, labelSelector string, start, end int64, maxNodes *int64) (*ProfileResponse, error) {
return &ProfileResponse{
Flamebearer: &Flamebearer{
Names: []string{"foo", "bar", "baz"},
@@ -453,10 +452,10 @@ func (f *FakeClient) GetProfile(ctx context.Context, profileTypeID, labelSelecto
MaxSelf: 56,
},
Units: "count",
}, http.Header{}, nil
}, nil
}
func (f *FakeClient) GetSpanProfile(ctx context.Context, profileTypeID, labelSelector string, spanSelector []string, start, end int64, maxNodes *int64) (*ProfileResponse, http.Header, error) {
func (f *FakeClient) GetSpanProfile(ctx context.Context, profileTypeID, labelSelector string, spanSelector []string, start, end int64, maxNodes *int64) (*ProfileResponse, error) {
return &ProfileResponse{
Flamebearer: &Flamebearer{
Names: []string{"foo", "bar", "baz"},
@@ -469,10 +468,10 @@ func (f *FakeClient) GetSpanProfile(ctx context.Context, profileTypeID, labelSel
MaxSelf: 56,
},
Units: "count",
}, http.Header{}, nil
}, nil
}
func (f *FakeClient) GetSeries(ctx context.Context, profileTypeID, labelSelector string, start, end int64, groupBy []string, limit *int64, step float64) (*SeriesResponse, http.Header, error) {
func (f *FakeClient) GetSeries(ctx context.Context, profileTypeID, labelSelector string, start, end int64, groupBy []string, limit *int64, step float64) (*SeriesResponse, error) {
f.Args = []any{profileTypeID, labelSelector, start, end, groupBy, step}
return &SeriesResponse{
Series: []*Series{
@@ -483,5 +482,5 @@ func (f *FakeClient) GetSeries(ctx context.Context, profileTypeID, labelSelector
},
Units: "count",
Label: "test",
}, http.Header{}, nil
}, nil
}
+3 -3
View File
@@ -65,7 +65,7 @@ func (d *ParcaDatasource) callProfileTypes(ctx context.Context, req *backend.Cal
span.SetStatus(codes.Error, err.Error())
return err
}
err = sender.Send(&backend.CallResourceResponse{Body: data, Headers: res.Header(), Status: 200})
err = sender.Send(&backend.CallResourceResponse{Body: data, Status: 200})
if err != nil {
ctxLogger.Error("Failed to send data to Parca", "error", err, "function", logEntrypoint())
span.RecordError(err)
@@ -98,7 +98,7 @@ func (d *ParcaDatasource) callLabelNames(ctx context.Context, req *backend.CallR
span.SetStatus(codes.Error, err.Error())
return err
}
err = sender.Send(&backend.CallResourceResponse{Body: data, Headers: res.Header(), Status: 200})
err = sender.Send(&backend.CallResourceResponse{Body: data, Status: 200})
if err != nil {
ctxLogger.Error("Failed to send data to Parca", "error", err, "function", logEntrypoint())
span.RecordError(err)
@@ -142,7 +142,7 @@ func (d *ParcaDatasource) callLabelValues(ctx context.Context, req *backend.Call
span.SetStatus(codes.Error, err.Error())
return err
}
err = sender.Send(&backend.CallResourceResponse{Body: data, Headers: res.Header(), Status: 200})
err = sender.Send(&backend.CallResourceResponse{Body: data, Status: 200})
if err != nil {
ctxLogger.Error("Failed to send data to Parca", "error", err, "function", logEntrypoint())
span.RecordError(err)
+43 -32
View File
@@ -347,48 +347,59 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
pointsFilter = (u, seriesIdx, show, gaps) => {
let filtered = [];
let series = u.series[seriesIdx];
if (!show && gaps && gaps.length) {
const [firstIdx, lastIdx] = series.idxs!;
const xData = u.data[0];
if (!show) {
const yData = u.data[seriesIdx];
const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true));
const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true));
if (gaps[0][0] === firstPos) {
filtered.push(firstIdx);
}
if (gaps && gaps.length) {
const firstIdx = u.posToIdx(gaps[0][0], true);
// show single points between consecutive gaps that share end/start
for (let i = 0; i < gaps.length; i++) {
let thisGap = gaps[i];
let nextGap = gaps[i + 1];
if (yData[firstIdx - 1] == null) {
filtered.push(firstIdx);
}
if (nextGap && thisGap[1] === nextGap[0]) {
// approx when data density is > 1pt/px, since gap start/end pixels are rounded
let approxIdx = u.posToIdx(thisGap[1], true);
// show single points between consecutive gaps that share end/start
for (let i = 0; i < gaps.length; i++) {
let thisGap = gaps[i];
let nextGap = gaps[i + 1];
if (yData[approxIdx] == null) {
// scan left/right alternating to find closest index with non-null value
for (let j = 1; j < 100; j++) {
if (yData[approxIdx + j] != null) {
approxIdx += j;
break;
}
if (yData[approxIdx - j] != null) {
approxIdx -= j;
break;
if (nextGap && thisGap[1] === nextGap[0]) {
// approx when data density is > 1pt/px, since gap start/end pixels are rounded
let approxIdx = u.posToIdx(thisGap[1], true);
if (yData[approxIdx] == null) {
// scan left/right alternating to find closest index with non-null value
for (let j = 1; j < 100; j++) {
if (yData[approxIdx + j] != null) {
approxIdx += j;
break;
}
if (yData[approxIdx - j] != null) {
approxIdx -= j;
break;
}
}
}
}
filtered.push(approxIdx);
filtered.push(approxIdx);
}
}
const lastIdx = u.posToIdx(gaps[gaps.length - 1][1], true);
if (yData[lastIdx + 1] == null) {
filtered.push(lastIdx);
}
}
if (gaps[gaps.length - 1][1] === lastPos) {
filtered.push(lastIdx);
// single point surrounded by nulls
else {
// meh heuristic
if (yData[0] == null && yData[yData.length - 1] == null) {
for (let i = 0; i < yData.length; i++) {
if (yData[i] != null) {
filtered.push(i);
}
}
}
}
}
@@ -1,5 +1,3 @@
import type { apiOptions, identify, load, page, track } from 'rudder-sdk-js'; // SDK is loaded dynamically from config, so we only import types from the SDK package
import { BuildInfo, CurrentUserDTO } from '@grafana/data';
import {
EchoBackend,
@@ -12,11 +10,19 @@ import {
import { loadScript } from '../../utils';
type Properties = Record<string, string | boolean | number>;
interface RudderstackAPIOptions {
Intercom?: {
user_hash: string;
};
}
interface Rudderstack {
identify: typeof identify;
load: typeof load;
page: typeof page;
track: typeof track;
identify: (identifier: string, traits: Properties, options?: RudderstackAPIOptions) => void;
load: (writeKey: string, dataPlaneURL: string, options: { configUrl?: string; destSDKBaseURL?: string }) => void;
page: () => void;
track: (eventName: string, properties?: Properties) => void;
}
declare global {
@@ -76,7 +82,7 @@ export class RudderstackBackend implements EchoBackend<PageviewEchoEvent, Rudder
if (options.user) {
const { identifier, intercomIdentifier } = options.user.analytics;
const apiOptions: apiOptions = {};
const apiOptions: RudderstackAPIOptions = {};
if (intercomIdentifier) {
apiOptions.Intercom = {
@@ -3,6 +3,7 @@ import { capitalize } from 'lodash';
import React, { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Button, Combobox, ComboboxOption, Field, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui';
@@ -109,6 +110,7 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan
options={minWidthOptions}
value={columnWidth}
onChange={onNamedMinWidthChanged}
data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth}
/>
) : (
<Input
@@ -120,6 +122,7 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan
min={50}
max={2000}
invalid={customMinWidthError}
data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth}
suffix={
<Button
size="sm"
@@ -127,6 +130,7 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan
icon="times"
tooltip={t('dashboard.auto-grid.options.min-width-custom-clear', 'Back to standard min column width')}
onClick={onClearCustomMinWidth}
data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomMinColumnWidth}
>
{t('dashboard.auto-grid.options.custom-min-width.clear', 'Clear')}
</Button>
@@ -141,6 +145,7 @@ function GridLayoutColumns({ layoutManager }: { layoutManager: AutoGridLayoutMan
value={String(maxColumnCount)}
onChange={({ value }) => layoutManager.onMaxColumnCountChanged(parseInt(value, 10))}
width={6.5}
data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns}
/>
</Field>
</Stack>
@@ -217,7 +222,13 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage
className={styles.wideSelector}
>
{isStandardHeight ? (
<Combobox id="min-height" options={minWidthOptions} value={rowHeight} onChange={onNamedMinHeightChanged} />
<Combobox
id="min-height"
options={minWidthOptions}
value={rowHeight}
onChange={onNamedMinHeightChanged}
data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight}
/>
) : (
<Input
id="min-height"
@@ -228,6 +239,7 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage
min={50}
max={2000}
invalid={customMinWidthError}
data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight}
suffix={
<Button
size="sm"
@@ -235,6 +247,7 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage
icon="times"
tooltip={t('dashboard.auto-grid.options.min-width-custom-clear', 'Back to standard min column width')}
onClick={onClearCustomRowHeight}
data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomRowHeight}
>
{t('dashboard.auto-grid.options.custom-min-height.clear', 'Clear')}
</Button>
@@ -247,6 +260,7 @@ function GridLayoutRows({ layoutManager }: { layoutManager: AutoGridLayoutManage
id="fill-screen-toggle"
value={fillScreen}
onChange={() => layoutManager.onFillScreenChanged(!fillScreen)}
data-testid={selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen}
/>
</Field>
</Stack>
@@ -273,6 +273,48 @@ describe('LokiQueryBuilderOptions', () => {
});
});
});
describe('Step validation', () => {
it('considers empty step as valid', async () => {
setup({ expr: 'rate({foo="bar"}[5m]' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument();
});
it('considers variable step that exists in the datasource as valid', async () => {
const datasource = createLokiDatasource();
datasource.getVariables = jest.fn().mockReturnValue(['$interval']);
setup({ expr: 'rate({foo="bar"}[5m]', step: '$interval' }, undefined, { datasource });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument();
});
it('considers variable step that does not exist in the datasource as invalid', async () => {
const datasource = createLokiDatasource();
datasource.getVariables = jest.fn().mockReturnValue(['$interval']);
setup({ expr: 'rate({foo="bar"}[5m]', step: '$custom' }, undefined, { datasource });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.getByText(/Invalid step/)).toBeInTheDocument();
});
it('considers valid duration step as valid', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: '1m' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument();
});
it('considers invalid step as invalid', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: 'invalid' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.getByText(/Invalid step/)).toBeInTheDocument();
});
it('considers non-duration number as invalid', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: '123' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.getByText(/Invalid step/)).toBeInTheDocument();
});
});
});
function setup(queryOverrides: Partial<LokiQuery> = {}, onChange = jest.fn(), propOverrides: Partial<Props> = {}) {
@@ -138,8 +138,18 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
if (!query.step) {
return true;
}
return typeof query.step === 'string' && isValidGrafanaDuration(query.step) && !isNaN(parseInt(query.step, 10));
}, [query.step]);
if (typeof query.step === 'string') {
// If we use a variable as step, we consider it valid
if (datasource.getVariables().includes(query.step)) {
return true;
}
// Check if the step is a valid Grafana duration
return isValidGrafanaDuration(query.step) && !isNaN(parseInt(query.step, 10));
}
return false;
}, [query.step, datasource]);
return (
<EditorRow>
@@ -2,9 +2,9 @@ import { css } from '@emotion/css';
import { defaults } from 'lodash';
import { useState } from 'react';
import { GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { CoreApp, GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, InlineLabel, useStyles2 } from '@grafana/ui';
import { Alert, Button, Icon, InlineLabel, useStyles2 } from '@grafana/ui';
import { TempoDatasource } from '../datasource';
import { defaultQuery, MyDataSourceOptions, TempoQuery } from '../types';
@@ -28,8 +28,26 @@ export function QueryEditor(props: Props) {
return genQuery === query.query || genQuery === '{}';
});
const alertingWarning = (
<Alert title="Tempo metrics is an experimental feature" severity="warning">
Please note that TraceQL metrics is an experimental feature and should not be used in production. Read more about
it in{' '}
<a
className={css({ textDecoration: 'underline' })}
href="https://grafana.com/docs/tempo/latest/operations/traceql-metrics/"
target="_blank"
>
documentation
<Icon name="external-link-alt" />
</a>
.
</Alert>
);
const inAlerting = props.app === CoreApp.UnifiedAlerting || props.app === CoreApp.CloudAlerting;
return (
<>
{inAlerting && alertingWarning}
<InlineLabel>
Build complex queries using TraceQL to select a list of traces.{' '}
<a rel="noreferrer" target="_blank" href="https://grafana.com/docs/tempo/latest/traceql/">
+5 -2
View File
@@ -4465,6 +4465,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Vybrat ",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5283,7 +5284,7 @@
},
"save-dashboard-form": {
"aria-label-message": "zpráva",
"body-dashboard-drastically-changed": "Protože používáte nové funkce nástěnky, které jsou podporované pouze v novém formátu schématu nástěnky Grafana, nástěnka bude uložena v novém formátu. Ujistěte se, jestli chcete provést tuto akci nebo chcete uložit nástěnku jako novou kopii.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Zrušit"
},
@@ -5299,7 +5300,7 @@
},
"schema-v2-library-panels-export": "Funkce dynamické nástěnky panelu je experimentální a nemá plnou funkčnost srovnatelnou se současným chováním nástěnek. Je založena na novém formátu schématu, který nepodporuje panely knihovny. To znamená, že při exportu nástěnky pro použití v jiné instanci nebudeme zahrnovat panely knihoven. Jejich podpora je naplánována v rámci <2>životního cyklu</2> funkce.",
"schema-v2-library-panels-export-title": "Schéma nástěnky V2 zatím nepodporuje export panelů knihoven pro použití v jiné instanci",
"title-dashboard-drastically-changed": "Nástěnka se podstatně změnila"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Nastaví aktuální obnovení jako nové výchozí",
@@ -7767,6 +7768,8 @@
},
"enable-highlighting": "Povolit zvýraznění",
"escape-newlines": "Opravit nesprávně uniklé sekvence nového řádku a záložek v řádcích protokolu",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Skrýt časová razítka",
"hide-unique-labels": "Skrýt jedinečné štítky",
"newest-first": "Seřazeno od nejnovějších protokolů – kliknutím zobrazíte nejstarší protokoly jako první",
+5 -2
View File
@@ -4427,6 +4427,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Auswählen",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5243,7 +5244,7 @@
},
"save-dashboard-form": {
"aria-label-message": "Nachricht",
"body-dashboard-drastically-changed": "Da Sie neue Dashboard-Funktionen nutzen, die nur im neuen Dashboard-Schema-Format von Grafana unterstützt werden, wird das Dashboard im neuen Format gespeichert. Bitte prüfen Sie, ob Sie diese Aktion wirklich ausführen möchten, oder ob Sie das Dashboard lieber als neue Kopie speichern möchten.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Abbrechen"
},
@@ -5259,7 +5260,7 @@
},
"schema-v2-library-panels-export": "Die dynamische Dashboard-Funktionalität ist experimentell und weist keine vollständige Funktionsparität mit dem aktuellen Dashboard-Verhalten auf. Sie basiert auf einem neuen Schemaformat, das keine Bibliotheks-Panels unterstützt. Das bedeutet, dass wir bei einem Export des Dashboards für die Nutzung in einer anderen Instanz keine Bibliotheks-Panels einbeziehen. Wir planen, sie im weiteren Verlauf des <2>Lebenszyklus</2> der Funktion zu unterstützen.",
"schema-v2-library-panels-export-title": "Das Dashboard-Schema V2 unterstützt noch keinen Export von Bibliotheks-Panels für die Verwendung in einer anderen Instanz",
"title-dashboard-drastically-changed": "Dashboard erheblich verändert"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Macht die aktuelle Aktualisierung zum neuen Standard",
@@ -7715,6 +7716,8 @@
},
"enable-highlighting": "Markierung aktivieren",
"escape-newlines": "Falsch dargestellte Zeilenumbrüche und Tab-Sequenzen in Log-Zeilen korrigieren",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Zeitstempel ausblenden",
"hide-unique-labels": "Eindeutige Labels ausblenden",
"newest-first": "Sortiert nach neuesten Logs zuerst – klicken Sie, um die ältesten zuerst anzuzeigen",
+5 -2
View File
@@ -4427,6 +4427,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Elegir",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5243,7 +5244,7 @@
},
"save-dashboard-form": {
"aria-label-message": "mensaje",
"body-dashboard-drastically-changed": "Como estás utilizando nuevas funciones de dashboards que solo son compatibles con el nuevo formato de esquema de dashboards de Grafana, el dashboard se guardará en el nuevo formato. Asegúrate de que quieres realizar esta acción o prefieres guardar el dashboard como una nueva copia.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Cancelar"
},
@@ -5259,7 +5260,7 @@
},
"schema-v2-library-panels-export": "La funcionalidad del dashboard dinámico es experimental y no tiene una paridad de características completa con el comportamiento actual de los dashboards. Se basa en un nuevo formato de esquema, que no admite paneles de librería. Esto significa que al exportar el dashboard para usarlo en otra instancia, no se incluirán paneles de librería. Tenemos la intención de admitirlos a medida que avanzamos en el <2>ciclo de vida</2> de la función.",
"schema-v2-library-panels-export-title": "El esquema de dashboard V2 aún no admite la exportación de paneles de librería para su uso en otra instancia",
"title-dashboard-drastically-changed": "El dashboard ha cambiado drásticamente"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Hará que la actualización actual sea la nueva predeterminada",
@@ -7715,6 +7716,8 @@
},
"enable-highlighting": "Activar resaltado",
"escape-newlines": "Corregir las secuencias de tabulación y de nueva línea que escaparon incorrectamente en las líneas de log",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Ocultar marcas de tiempo",
"hide-unique-labels": "Ocultar etiquetas únicas",
"newest-first": "Ordenado por los logs más nuevos primero: haga clic para mostrar los más antiguos primero",
+5 -2
View File
@@ -4427,6 +4427,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Choisir",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5243,7 +5244,7 @@
},
"save-dashboard-form": {
"aria-label-message": "message",
"body-dashboard-drastically-changed": "Étant donné que vous utilisez de nouvelles fonctionnalités de tableau de bord uniquement prises en charge par le nouveau format de schéma de tableau de bord Grafana, le tableau de bord sera enregistré dans le nouveau format. Veuillez vous assurer que vous souhaitez effectuer cette action ou que vous préférez enregistrer le tableau de bord en tant que nouvelle copie.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Annuler"
},
@@ -5259,7 +5260,7 @@
},
"schema-v2-library-panels-export": "La fonctionnalité de tableau de bord dynamique est expérimentale et n’a pas une parité complète de fonctionnalités avec le comportement actuel des tableaux de bord. Elle est basée sur un nouveau format de schéma, qui ne prend pas en charge les panneaux de bibliothèque. Cela signifie que lors de l’exportation du tableau de bord pour l’utiliser dans une autre instance, nous n’inclurons pas les panneaux de la bibliothèque. Nous avons l’intention de les prendre en charge au fur et à mesure de notre progression dans le <2>cycle de vie</2> de la fonctionnalité.",
"schema-v2-library-panels-export-title": "Le schéma de tableau de bord V2 ne prend pas encore en charge l’exportation de panneaux de bibliothèque à utiliser dans une autre instance",
"title-dashboard-drastically-changed": "Le tableau de bord a radicalement changé"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "L’actualisation actuelle deviendra la nouvelle valeur par défaut",
@@ -7715,6 +7716,8 @@
},
"enable-highlighting": "Activer la surbrillance",
"escape-newlines": "Correction des séquences de sauts de ligne et de tabulations incorrectement échappées dans les lignes du journal",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Masquer les horodatages",
"hide-unique-labels": "Masquer les étiquettes uniques",
"newest-first": "Trié par les journaux les plus récents en premier - Cliquez pour afficher les plus anciens en premier",
+5 -2
View File
@@ -4427,6 +4427,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Kiválasztás",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5243,7 +5244,7 @@
},
"save-dashboard-form": {
"aria-label-message": "üzenet",
"body-dashboard-drastically-changed": "Mivel olyan új irányítópult-funkciókat használ, amelyeket csak a Grafana új irányítópult-sémaformátuma támogat, az irányítópultot az új formátumban menti a rendszer. Győződjön meg róla, hogy végre kívánja hajtani ezt a műveletet, vagy inkább új példányként menti az irányítópultot.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Mégse"
},
@@ -5259,7 +5260,7 @@
},
"schema-v2-library-panels-export": "A dinamikus irányítópult funkcionalitása kísérleti jellegű, és nem rendelkezik teljes funkcionalitással a jelenlegi irányítópultok viselkedéséhez képest. Új sémaformátumon alapul, amely nem támogatja a könyvtárpaneleket. Ez azt jelenti, hogy amikor az irányítópultot egy másik példányban való használathoz exportálja, nem fogja tartalmazni a könyvtárpaneleket. Támogatni kívánjuk őket a funkció <2>életciklusának</2> előrehaladtával.",
"schema-v2-library-panels-export-title": "A Dashboard Schema V2 még nem támogatja a könyvtárpanelek exportálását egy másik példányban való használathoz",
"title-dashboard-drastically-changed": "Irányítópult nagymértékben megváltozott"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Az aktuális frissítés lesz az új alapértelmezés",
@@ -7715,6 +7716,8 @@
},
"enable-highlighting": "Kiemelés engedélyezése",
"escape-newlines": "Helytelenül értelmezett újsor- és tabulátorkarakteres szekvenciák javítása a naplósorokban",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Időbélyegek elrejtése",
"hide-unique-labels": "Egyedi címkék elrejtése",
"newest-first": "Rendezés a legújabb naplók szerint – kattintson, hogy a legrégebbi naplók jelenjenek meg elsőként",
+5 -2
View File
@@ -4408,6 +4408,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Pilih",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5223,7 +5224,7 @@
},
"save-dashboard-form": {
"aria-label-message": "pesan",
"body-dashboard-drastically-changed": "Anda menggunakan fitur dasbor baru yang hanya didukung pada format skema dasbor Grafana baru, jadi dasbor akan disimpan dalam format baru. Pastikan Anda ingin melakukan tindakan ini atau Anda lebih suka menyimpan dasbor sebagai salinan baru.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Batalkan"
},
@@ -5239,7 +5240,7 @@
},
"schema-v2-library-panels-export": "Fungsionalitas dasbor dinamis bersifat eksperimen, dan tidak memiliki paritas fitur penuh dengan perilaku dasbor saat ini. Ini didasarkan pada format skema baru, yang tidak mendukung panel pustaka. Ini berarti ketika mengekspor dasbor untuk menggunakannya di instans lain, kami tidak akan menyertakan panel pustaka. Kami bermaksud untuk mendukung seiring fitur <2>siklus pengembangan</2> fitur.",
"schema-v2-library-panels-export-title": "Skema Dasbor V2 belum mendukung ekspor panel pustaka untuk digunakan di instans lain",
"title-dashboard-drastically-changed": "Dasbor berubah drastis"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Akan menjadikan muat ulang saat ini sebagai default baru",
@@ -7689,6 +7690,8 @@
},
"enable-highlighting": "Aktifkan penyorotan",
"escape-newlines": "Perbaiki escape sequence baris baru dan tab yang salah di baris log",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Sembunyikan stempel waktu",
"hide-unique-labels": "Sembunyikan label unik",
"newest-first": "Diurutkan berdasarkan log terbaru lebih dulu - Klik untuk menampilkan yang paling lama lebih dulu",
+5 -2
View File
@@ -4427,6 +4427,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Scegli",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5243,7 +5244,7 @@
},
"save-dashboard-form": {
"aria-label-message": "messaggio",
"body-dashboard-drastically-changed": "Dato che stai utilizzando nuove funzionalità della dashboard supportate solo nel nuovo formato grafico della dashboard Grafana, la dashboard verrà salvata nel nuovo formato. Assicurati di voler eseguire questa azione o di voler salvare la dashboard come nuova copia.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Annulla"
},
@@ -5259,7 +5260,7 @@
},
"schema-v2-library-panels-export": "La funzionalità della dashboard dinamica è in fase sperimentale e non ha una parità di funzionalità completa con il comportamento attuale delle dashboard. Si basa su un nuovo formato di schema, che non supporta i pannelli della libreria. Ciò significa che quando si esporta la dashboard per utilizzarla in un'altra istanza, non includeremo i pannelli della libreria. Intendiamo supportarli man mano che progrediamo nel <2>ciclo di vita</2> della funzionalità.",
"schema-v2-library-panels-export-title": "Schema Dashboard V2 non supporta ancora l'esportazione di pannelli della libreria da utilizzare in un'altra istanza",
"title-dashboard-drastically-changed": "La dashboard è cambiata radicalmente"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Imposterà l'aggiornamento corrente come nuovo valore predefinito",
@@ -7715,6 +7716,8 @@
},
"enable-highlighting": "Abilita evidenziazione",
"escape-newlines": "Correggi le sequenze di nuove righe e tabulazioni non corrette nelle righe del registro",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Nascondi marca temporale",
"hide-unique-labels": "Nascondi etichette univoche",
"newest-first": "Ordine: prima i registri più recenti – Fai clic per mostrare prima i meno recenti",
+5 -2
View File
@@ -4408,6 +4408,7 @@
}
},
"repeat-row-select2": {
"placeholder": "選択する",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5223,7 +5224,7 @@
},
"save-dashboard-form": {
"aria-label-message": "メッセージ",
"body-dashboard-drastically-changed": "新しいGrafanaダッシュボードスキーマ形式でのみサポートされている新たなダッシュボード機能を使用しているため、ダッシュボードは新形式で保存されます。この操作を実行するか、ダッシュボードを新しいコピーとして保存するかを選択してください。",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "キャンセル"
},
@@ -5239,7 +5240,7 @@
},
"schema-v2-library-panels-export": "動的ダッシュボード機能は実験的なもので、現在のダッシュボード動作と完全な機能互換性はありません。この機能は、ライブラリパネルをサポートしていない新しいスキーマ形式に基づいています。そのため、別のインスタンスで使用する目的でダッシュボードをエクスポートした場合、ライブラリパネルは含まれません。機能の<2>ライフサイクル</2>が進むにつれ、これらをサポートする予定です。",
"schema-v2-library-panels-export-title": "現在ダッシュボードスキーマV2では、別のインスタンスでの使用目的でのライブラリパネルのエクスポートをサポートしていません",
"title-dashboard-drastically-changed": "ダッシュボードが大幅に変更されました"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "現在の更新を新しいデフォルトに設定",
@@ -7689,6 +7690,8 @@
},
"enable-highlighting": "ハイライトを有効にする",
"escape-newlines": "ログ行で誤ってエスケープされた改行とタブシーケンスを修正",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "タイムスタンプを非表示",
"hide-unique-labels": "一意のラベルを非表示",
"newest-first": "最新のログ順に並び替え - クリックして最も古いログを最初に表示",
+5 -2
View File
@@ -4408,6 +4408,7 @@
}
},
"repeat-row-select2": {
"placeholder": "선택",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5223,7 +5224,7 @@
},
"save-dashboard-form": {
"aria-label-message": "메시지",
"body-dashboard-drastically-changed": "새로운 Grafana 대시보드 스키마 형식에서만 지원되는 새 대시보드 기능을 사용하고 있기 때문에 대시보드가 새 형식으로 저장됩니다. 이 동작을 수행할지 아니면 대시보드를 새 복사본으로 저장할지 확인하세요.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "취소"
},
@@ -5239,7 +5240,7 @@
},
"schema-v2-library-panels-export": "동적 대시보드 기능은 실험 단계이며 현재 대시보드 동작과 모든 기능이 동일하지는 않습니다. 이 기능은 라이브러리 패널을 지원하지 않는 새로운 스키마 형식을 기반으로 합니다. 즉, 다른 인스턴스에서 사용하기 위해 대시보드를 내보낼 때 라이브러리 패널이 포함되지 않는다는 것을 의미합니다. Grafana는 기능 <2>수명 주기</2>가 진행됨에 따라 이를 지원할 계획입니다.",
"schema-v2-library-panels-export-title": "대시보드 스키마 V2는 아직 다른 인스턴스에서 사용할 라이브러리 패널 내보내기를 지원하지 않습니다",
"title-dashboard-drastically-changed": "대시보드가 대폭 변경됨"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "현재 새로 고침을 새로운 기본값으로 설정합니다",
@@ -7689,6 +7690,8 @@
},
"enable-highlighting": "강조 표시 활성화",
"escape-newlines": "로그 줄에서 잘못된 줄 바꿈 및 탭 시퀀스 수정",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "타임스탬프 숨기기",
"hide-unique-labels": "고유 라벨 숨기기",
"newest-first": "최신 로그순으로 정렬 - 클릭하여 오래된 로그순으로 표시",
+5 -2
View File
@@ -4427,6 +4427,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Kies",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5243,7 +5244,7 @@
},
"save-dashboard-form": {
"aria-label-message": "bericht",
"body-dashboard-drastically-changed": "Omdat je nieuwe dashboardfuncties gebruikt die alleen worden ondersteund in de nieuwe indeling van het Grafana-dashboardschema, wordt het dashboard opgeslagen in de nieuwe indeling. Beslis of je deze actie wilt uitvoeren of dat je het dashboard liever als een nieuwe kopie opslaat.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Annuleren"
},
@@ -5259,7 +5260,7 @@
},
"schema-v2-library-panels-export": "De dynamische dashboardfunctionaliteit is experimenteel en heeft geen volledige functiepariteit met het huidige gedrag van dashboards. Het is gebaseerd op een nieuw schemaformaat, dat geen bibliotheekpanelen ondersteunt. Dit betekent dat we bij het exporteren van het dashboard om het in een andere instantie te gebruiken, geen bibliotheekpanelen opnemen. We zijn van plan om ze te ondersteunen naarmate we verder komen in de <2>levenscyclus</2> van de functie.",
"schema-v2-library-panels-export-title": "Dashboardschema V2 biedt nog geen ondersteuning voor het exporteren van bibliotheekpanelen die in een andere instantie kunnen worden gebruikt",
"title-dashboard-drastically-changed": "Dashboard is drastisch gewijzigd"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Maakt de huidige vernieuwing de nieuwe standaard",
@@ -7715,6 +7716,8 @@
},
"enable-highlighting": "Markeren inschakelen",
"escape-newlines": "Corrigeer onjuist escaped nieuwe lijn en tabbladsequenties in logregels",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Tijdstempels verbergen",
"hide-unique-labels": "Unieke labels verbergen",
"newest-first": "Gesorteerd op nieuwste logboeken eerst - klik om oudste eerst weer te geven",
+5 -2
View File
@@ -4465,6 +4465,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Wybierz",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5283,7 +5284,7 @@
},
"save-dashboard-form": {
"aria-label-message": "Wiadomość",
"body-dashboard-drastically-changed": "Ponieważ używasz nowych funkcji obsługiwanych tylko w nowym formacie schematu pulpitu Grafany, pulpit zostanie zapisany w nowym formacie. Zdecyduj, czy chcesz wykonać tę czynność, czy wolisz zapisać pulpit jako nową kopię.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Anuluj"
},
@@ -5299,7 +5300,7 @@
},
"schema-v2-library-panels-export": "Funkcja pulpitu dynamicznego jest eksperymentalna i nie odpowiada w pełni obecnemu sposobowi działania pulpitów. Opiera się na nowym formacie schematu, który nie obsługuje paneli biblioteki. Oznacza to, że przy eksporcie pulpitu, aby użyć go w innej instancji, nie uwzględnimy paneli biblioteki. Zamierzamy dodać obsługę tych paneli w miarę <2>rozwoju tej funkcji</2>.",
"schema-v2-library-panels-export-title": "Schemat pulpitu w wersji 2 nie obsługuje jeszcze eksportowania paneli biblioteki do użytku w innej instancji",
"title-dashboard-drastically-changed": "Panel znacznie się zmienił"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Bieżące odświeżenie stanie się nową wartością domyślną",
@@ -7767,6 +7768,8 @@
},
"enable-highlighting": "Włącz wyróżnianie",
"escape-newlines": "Napraw nieprawidłowe sekwencje znaków nowego wiersza i tabulacji we wpisach logów",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Ukryj znaczniki czasu",
"hide-unique-labels": "Ukryj unikalne etykiety",
"newest-first": "Sortowanie od najnowszych wpisów dziennika – kliknij, aby wyświetlić najpierw najstarsze",
+5 -2
View File
@@ -4427,6 +4427,7 @@
}
},
"repeat-row-select2": {
"placeholder": "Escolher",
"variable-options": {
"label": {
"disable-repeating": "",
@@ -5243,7 +5244,7 @@
},
"save-dashboard-form": {
"aria-label-message": "mensagem",
"body-dashboard-drastically-changed": "Como você está usando novos recursos de painéis compatíveis apenas com o novo formato de esquema de painel da Grafana, o painel será salvo no novo formato. Certifique-se de que você deseja executar esta ação ou prefere salvar o painel como uma nova cópia.",
"body-dashboard-drastically-changed": "",
"cancel-button": {
"cancel": "Cancelar"
},
@@ -5259,7 +5260,7 @@
},
"schema-v2-library-panels-export": "A funcionalidade de painel dinâmico está em testes e não possui todos os recursos presentes no comportamento atual dos painéis. Ele se baseia em um novo formato de esquema, que não aceita painéis da biblioteca. Isso significa que, ao exportar o painel para usá-lo em outra instância, não incluiremos painéis da biblioteca. Pretendemos aceitá-los à medida que avançamos no <2>ciclo de vida</2> do recurso.",
"schema-v2-library-panels-export-title": "O esquema de painel V2 ainda não aceita a exportação de painéis da biblioteca para uso em outra instância",
"title-dashboard-drastically-changed": "O painel foi alterado drasticamente"
"title-dashboard-drastically-changed": ""
},
"save-dashboard-form-common-options": {
"save-refresh-description-current-refresh-default": "Fará com que a atualização atual seja o novo padrão",
@@ -7715,6 +7716,8 @@
},
"enable-highlighting": "Ativar destaque",
"escape-newlines": "Corrigir sequências de tabulação e quebra de linha adicionadas incorretamente por escape nas linhas de log",
"font-size-default": "",
"font-size-small": "",
"hide-timestamps": "Ocultar data e hora",
"hide-unique-labels": "Ocultar rótulos exclusivos",
"newest-first": "Organizado por logs mais recentes primeiro: clique para exibir os mais antigos primeiro",

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