Merge branch 'main' into leeoniya/state-timeline-fix-dual-time-multi-series

This commit is contained in:
Leon Sorokin
2025-06-11 11:08:14 -05:00
142 changed files with 3178 additions and 650 deletions
+3
View File
@@ -1400,6 +1400,9 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/hooks/useControlledFieldArray.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/alerting/unified/hooks/useFilteredRules.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/alerting/unified/insights/InsightsMenuButton.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
+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 -19
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
@@ -836,7 +836,6 @@ embed.go @grafana/grafana-as-code
/.github/workflows/frontend-lint.yml @grafana/grafana-frontend-platform
/.github/workflows/analytics-events-report.yml @grafana/grafana-frontend-platform
/.github/workflows/pr-e2e-tests.yml @grafana/grafana-developer-enablement-squad
/.github/workflows/run-e2e-suite.yml @grafana/grafana-developer-enablement-squad
/.github/workflows/skye-add-to-project.yml @grafana/grafana-frontend-platform
/.github/zizmor.yml @grafana/grafana-developer-enablement-squad
/.github/license_finder.yaml @bergquist
+11
View File
@@ -74,6 +74,7 @@ jobs:
contents: read
id-token: write
steps:
# Set up repository clone
- name: Checkout code
uses: actions/checkout@v4
with:
@@ -86,18 +87,28 @@ jobs:
uses: ./.github/actions/setup-enterprise
with:
github-app-name: 'grafana-ci-bot'
# Prepare what we need to upload test results
- run: echo "RESULTS_FILE=$(date --rfc-3339=seconds --utc | sed -s 's/ /-/g')_${SHARD/\//_}.xml" >> "$GITHUB_ENV"
env:
SHARD: ${{ matrix.shard }}
- run: go install github.com/jstemmer/go-junit-report/v2@85bf4716ac1f025f2925510a9f5e9f5bb347c009
# Run code
- name: Generate Go code
run: make gen-go
- name: Run unit tests
env:
SHARD: ${{ matrix.shard }}
run: |
set -euo pipefail
readarray -t PACKAGES <<< "$(./scripts/ci/backend-tests/shard.sh -N"$SHARD")"
# This tee requires pipefail to be set, otherwise `go test`'s exit code is thrown away.
# That means having no `-o pipefail` => failing tests => exit code 0, which is wrong.
go test -short -v -timeout=30m "${PACKAGES[@]}" | tee >(go-junit-report -set-exit-code > "$RESULTS_FILE")
# Upload results to GCS
- name: Log in to GCS
if: github.repository == 'grafana/grafana' && (success() || failure())
uses: grafana/shared-workflows/actions/login-to-gcs@login-to-gcs-v0.2.0
@@ -314,7 +314,7 @@ jobs:
"fields": [
{
"type": "mrkdwn",
"text": "*PR:* <${{ github.event.pull_request.html_url }}|#${{ github.event.pull_request.number }}>"
"text": "*PR:* <${{ github.event.pull_request.html_url }}|#${{ github.event.pull_request.number }}>\n\nAuthor: ${{ github.event.pull_request.user.login }}"
},
{
"type": "mrkdwn",
+90 -31
View File
@@ -39,39 +39,98 @@ jobs:
retention-days: 1
name: ${{ steps.artifact.outputs.artifact }}
path: grafana.tar.gz
e2e-matrix:
build-e2e-runner:
name: Build E2E test runner
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
artifact: ${{ steps.artifact.outputs.artifact }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: ${{ !github.event.pull_request.head.repo.fork }}
- name: Build E2E test runner
id: artifact
run: |
# We want a static binary, so we need to set CGO_ENABLED=0
CGO_ENABLED=0 go build -o ./e2e-runner ./e2e/
echo "artifact=e2e-runner-${{github.run_number}}" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
id: upload
with:
retention-days: 1
name: ${{ steps.artifact.outputs.artifact }}
path: e2e-runner
run-e2e-tests:
needs:
- build-grafana
- build-e2e-runner
strategy:
fail-fast: false
matrix:
include:
- suite: various-suite
path: e2e/various-suite
- suite: dashboards-suite
path: e2e/dashboards-suite
- suite: smoke-tests-suite
path: e2e/smoke-tests-suite
- suite: panels-suite
path: e2e/panels-suite
- suite: various-suite (old arch)
path: e2e/old-arch/various-suite
flags: --flags="--env dashboardScene=false"
- suite: dashboards-suite (old arch)
path: e2e/old-arch/dashboards-suite
flags: --flags="--env dashboardScene=false"
- suite: smoke-tests-suite (old arch)
path: e2e/old-arch/smoke-tests-suite
flags: --flags="--env dashboardScene=false"
- suite: panels-suite (old arch)
path: e2e/old-arch/panels-suite
flags: --flags="--env dashboardScene=false"
name: ${{ matrix.suite }}
strategy:
fail-fast: false
matrix:
suite:
- various-suite
- dashboards-suite
- smoke-tests-suite
- panels-suite
needs:
- build-grafana
uses: ./.github/workflows/run-e2e-suite.yml
with:
package: ${{ needs.build-grafana.outputs.artifact }}
suite: ${{ matrix.suite }}
e2e-matrix-old-arch:
runs-on: ubuntu-latest-8-cores
permissions:
contents: read
name: ${{ matrix.suite }} (old arch)
strategy:
fail-fast: false
matrix:
suite:
- old-arch/various-suite
- old-arch/dashboards-suite
- old-arch/smoke-tests-suite
- old-arch/panels-suite
needs:
- build-grafana
uses: ./.github/workflows/run-e2e-suite.yml
with:
package: ${{ needs.build-grafana.outputs.artifact }}
suite: ${{ matrix.suite }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build-grafana.outputs.artifact }}
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build-e2e-runner.outputs.artifact }}
- name: chmod +x
run: chmod +x ./e2e-runner
- name: Run E2E tests
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with:
verb: run
args: go run ./pkg/build/e2e --package=grafana.tar.gz
--suite=${{ matrix.path }}
${{ matrix.flags }}
- name: Set suite name
id: set-suite-name
if: success() || failure()
env:
SUITE: ${{ matrix.path }}
run: |
echo "suite=$(echo "$SUITE" | sed 's/\//-/g')" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: ${{ steps.set-suite-name.outputs.suite }}-${{ github.run_number }}
path: videos
retention-days: 1
-39
View File
@@ -1,39 +0,0 @@
name: e2e suite
on:
workflow_call:
inputs:
package:
type: string
required: true
suite:
type: string
required: true
jobs:
main:
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: ${{ inputs.package }}
- uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with:
verb: run
args: go run ./pkg/build/e2e --package=grafana.tar.gz --suite=${{ inputs.suite }}
- name: Set suite name
id: set-suite-name
if: success() || failure()
env:
SUITE: ${{ inputs.suite }}
run: |
echo "suite=$(echo "$SUITE" | sed 's/\//-/g')" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: e2e-${{ steps.set-suite-name.outputs.suite }}-${{github.run_number}}
path: videos
retention-days: 1
+1
View File
@@ -21,6 +21,7 @@ rules:
- pr-frontend-unit-tests.yml
- pr-test-integration.yml
- publish-kinds-release.yml
- pr-e2e-tests.yml
dangerous-triggers:
ignore:
- auto-milestone.yml
+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
+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.
@@ -77,7 +77,13 @@ refs:
# Create and edit dynamic dashboards
{{< docs/experimental product="Dynamic dashboards" featureFlag="dashboardNewLayouts" >}}
{{< admonition type="caution" >}}
Dynamic dashboards is an [experimental](https://grafana.com/docs/release-life-cycle/) feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog).
**Do not enable this feature in production environments as it may result in the irreversible loss of data.**
{{< /admonition >}}
Dashboards and panels allow you to show your data in visual form. Each panel needs at least one query to display a visualization.
+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`
@@ -18,7 +18,13 @@ weight: 200
# Dashboard JSON schema v2
{{< docs/experimental product="Dashboard JSON schema v2" featureFlag="`dashboardNewLayouts`" >}}
{{< admonition type="caution" >}}
Dashboard JSON schema v2 is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. To get early access to this feature, request it through [this form](https://docs.google.com/forms/d/e/1FAIpQLSd73nQzuhzcHJOrLFK4ef_uMxHAQiPQh1-rsQUT2MRqbeMLpg/viewform?usp=dialog).
**Do not enable this feature in production environments as it may result in the irreversible loss of data.**
{{< /admonition >}}
Grafana dashboards are represented as JSON objects that store metadata, panels, variables, and settings.
@@ -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();
});
});
+351
View File
@@ -0,0 +1,351 @@
package main
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/urfave/cli/v3"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := Run().Run(ctx, os.Args); err != nil {
cancel()
fmt.Println(err)
os.Exit(1)
}
}
func Run() *cli.Command {
var suitePath string
return &cli.Command{
Name: "e2e",
Usage: "Run the test suite",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "command",
Usage: "Cypress command to run. 'open' can be useful for development (enum: run, open)",
Value: "run",
Validator: func(s string) error {
if s != "run" && s != "open" {
return fmt.Errorf("invalid command: %s, must be 'run' or 'open'", s)
}
return nil
},
},
&cli.StringFlag{
Name: "browser",
Usage: "Browser to run tests with (e.g.: chrome, electron)",
Value: "chrome",
},
&cli.StringFlag{
Name: "grafana-base-url",
Usage: "Base URL for Grafana",
Value: "http://localhost:3001",
},
&cli.BoolFlag{
Name: "cypress-video",
Usage: "Enable Cypress video recordings",
Value: false,
},
&cli.BoolFlag{
Name: "smtp-plugin",
Usage: "Enable SMTP plugin",
Value: false,
},
&cli.BoolFlag{
Name: "benchmark-plugin",
Usage: "Enable Benchmark plugin",
Value: false,
},
&cli.BoolFlag{
Name: "slowmo",
Usage: "Slow down the test run",
Value: false,
},
&cli.StringSliceFlag{
Name: "env",
Usage: "Additional Cypress environment variables to set (format: KEY=VALUE)",
Validator: func(s []string) error {
pattern := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*`)
for _, v := range s {
if !pattern.MatchString(v) {
return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v)
}
}
return nil
},
},
&cli.StringSliceFlag{
Name: "parameters",
Usage: "Additional parameters to pass to the Cypress command (e.g. --headed)",
},
&cli.DurationFlag{
Name: "timeout",
Usage: "Timeout for the Cypress command (precision: milliseconds)",
Value: time.Second * 30,
Validator: func(d time.Duration) error {
if d < 0 {
return fmt.Errorf("timeout must be a positive duration")
}
if d.Round(time.Millisecond) != d {
return fmt.Errorf("timeout must be a whole number of milliseconds")
}
return nil
},
},
&cli.StringFlag{
Name: "timezone",
Usage: "Timezone to set for the Cypress run (e.g. 'America/New_York')",
Value: "Pacific/Honolulu",
},
&cli.BoolFlag{
Name: "start-grafana",
Usage: "Start and wait for Grafana before running the tests",
Value: true,
Category: "Grafana Server",
},
&cli.StringFlag{
Name: "license-path",
Usage: "Path to the Grafana Enterprise license file (optional; requires --start-grafana)",
Value: "",
TakesFile: true,
Category: "Grafana Server",
},
&cli.BoolFlag{
Name: "image-renderer",
Usage: "Install the image renderer plugin (requires --start-grafana)",
Category: "Grafana Server",
},
&cli.StringFlag{
Name: "suite",
Usage: "Path to the suite to run (e.g. './e2e/dashboards-suite')",
TakesFile: true,
Required: true,
Destination: &suitePath,
},
},
Action: runAction,
}
}
func runAction(ctx context.Context, c *cli.Command) error {
suitePath := c.String("suite")
suitePath, err := normalisePath(suitePath)
if err != nil {
return fmt.Errorf("failed to normalise suite path: %w", err)
}
repoRoot, err := gitRepoRoot(ctx, suitePath)
if err != nil {
return fmt.Errorf("failed to get git repo root: %w", err)
}
screenshotsFolder := path.Join(suitePath, "screenshots")
videosFolder := path.Join(suitePath, "videos")
fileServerFolder := path.Join(repoRoot, "e2e", "cypress")
fixturesFolder := path.Join(fileServerFolder, "fixtures")
downloadsFolder := path.Join(fileServerFolder, "downloads")
benchmarkPluginResultsFolder := path.Join(suitePath, "benchmark-results")
reporter := path.Join(repoRoot, "e2e", "log-reporter.js")
env := map[string]string{
"BENCHMARK_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("benchmark-plugin")),
"SMTP_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("smtp-plugin")),
"BENCHMARK_PLUGIN_RESULTS_FOLDER": benchmarkPluginResultsFolder,
"SLOWMO": "0",
"BASE_URL": c.String("grafana-base-url"),
}
for _, v := range c.StringSlice("env") {
parts := strings.SplitN(v, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v)
}
env[parts[0]] = parts[1]
}
cypressConfig := map[string]string{
"screenshotsFolder": screenshotsFolder,
"fixturesFolder": fixturesFolder,
"videosFolder": videosFolder,
"downloadsFolder": downloadsFolder,
"fileServerFolder": fileServerFolder,
"reporter": reporter,
"specPattern": path.Join(suitePath, "*.spec.ts"),
"defaultCommandTimeout": fmt.Sprintf("%d", c.Duration("timeout").Milliseconds()),
"viewportWidth": "1920",
"viewportHeight": "1080",
"trashAssetsBeforeRuns": "false",
"baseUrl": c.String("grafana-base-url"),
"video": fmt.Sprintf("%t", c.Bool("cypress-video")),
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if c.Bool("start-grafana") {
startServerPath := path.Join(repoRoot, "scripts", "grafana-server", "start-server")
waitForGrafanaPath := path.Join(repoRoot, "scripts", "grafana-server", "wait-for-grafana")
go func() {
var args []string
if c.String("license-path") != "" {
args = append(args, c.String("license-path"))
}
//nolint:gosec
cmd := exec.CommandContext(ctx, startServerPath, args...)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
if c.Bool("image-renderer") {
cmd.Env = append(cmd.Env, "INSTALL_IMAGE_RENDERER=true")
}
cmd.Stdout = prefixGrafana(os.Stdout)
cmd.Stderr = prefixGrafana(os.Stderr)
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
fmt.Println("Error running Grafana:", err)
}
}()
//nolint:gosec
cmd := exec.CommandContext(ctx, waitForGrafanaPath)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
cmd.Stdout = prefixGrafana(os.Stdout)
cmd.Stderr = prefixGrafana(os.Stderr)
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to wait for Grafana: %w", err)
}
}
args := []string{"run", "cypress", c.String("command"),
"--env", joinCypressCfg(env),
"--config", joinCypressCfg(cypressConfig),
"--browser", c.String("browser")}
args = append(args, c.StringSlice("parameters")...)
//nolint:gosec
cmd := exec.CommandContext(ctx, "yarn", args...)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
cmd.Stdout = prefixCypress(os.Stdout)
cmd.Stderr = prefixCypress(os.Stderr)
cmd.Stdin = os.Stdin
return cmd.Run()
}
func gitRepoRoot(ctx context.Context, dir string) (string, error) {
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get git repo root: %w", err)
}
p := strings.TrimSpace(string(out))
p, err = normalisePath(p)
if err != nil {
return "", fmt.Errorf("failed to normalise git repo root path: %w", err)
}
return p, nil
}
func normalisePath(p string) (string, error) {
absPath, err := filepath.Abs(p)
if err != nil {
return "", fmt.Errorf("failed to get absolute path: %w", err)
}
return path.Clean(filepath.ToSlash(absPath)), nil
}
func joinCypressCfg(cfg map[string]string) string {
config := make([]string, 0, len(cfg))
for k, v := range cfg {
config = append(config, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(config, ",")
}
const (
resetColor = "\033[0m"
yellowColor = "\033[0;33m"
cyanColor = "\033[0;36m"
)
func prefixCypress(w io.Writer) io.Writer {
if _, ok := os.LookupEnv("CI"); ok {
return w
}
return newWrappingOutput(cyanColor+"Cypress: ", resetColor, w)
}
func prefixGrafana(w io.Writer) io.Writer {
if _, ok := os.LookupEnv("CI"); ok {
return w
}
return newWrappingOutput(yellowColor+"Grafana: ", resetColor, w)
}
var _ io.Writer = (*wrappingOutput)(nil)
type wrappingOutput struct {
prefix string
suffix string
mu *sync.Mutex
inner io.Writer
writtenPrefix bool
}
func newWrappingOutput(prefix, suffix string, inner io.Writer) *wrappingOutput {
return &wrappingOutput{
prefix: prefix,
suffix: suffix,
mu: &sync.Mutex{},
inner: inner,
}
}
func (p *wrappingOutput) Write(b []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
for line := range bytes.Lines(b) {
if !p.writtenPrefix {
if _, err := p.inner.Write([]byte(p.prefix)); err != nil {
return 0, err
}
p.writtenPrefix = true
}
if _, err := p.inner.Write(line); err != nil {
return 0, err
}
if bytes.HasSuffix(line, []byte("\n")) {
p.writtenPrefix = false
if _, err := p.inner.Write([]byte(p.suffix)); err != nil {
return 0, err
}
}
}
return len(b), nil
}
+2 -4
View File
@@ -26,7 +26,7 @@ declare -A env=(
)
testFilesForSingleSuite="*.spec.ts"
rootForEnterpriseSuite="./e2e/extensions-suite"
rootForEnterpriseSuite="./e2e/extensions"
rootForOldArch="./e2e/old-arch"
rootForKubernetesDashboards="./e2e/dashboards-suite"
rootForSearchDashboards="./e2e/dashboards-search-suite"
@@ -45,6 +45,7 @@ declare -A cypressConfig=(
[trashAssetsBeforeRuns]=false
[reporter]=./e2e/log-reporter.js
[baseUrl]=${BASE_URL:-"http://$HOST:$PORT"}
[video]=${CYPRESS_VIDEO:-false}
)
case "$1" in
@@ -70,8 +71,6 @@ case "$1" in
"enterprise")
echo "Enterprise"
env[SMTP_PLUGIN_ENABLED]=true
CLEANUP="rm -rf ./e2e/extensions-suite"
SETUP="cp -Lr ./e2e/extensions ./e2e/extensions-suite"
enterpriseSuite=$(basename "${args[1]}")
case "$2" in
"debug")
@@ -87,7 +86,6 @@ case "$1" in
;;
esac
cypressConfig[specPattern]=$rootForEnterpriseSuite/$enterpriseSuite/*-suite/*.spec.ts
$CLEANUP && $SETUP
;;
"")
;;
@@ -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;
}
});
};
+2 -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
@@ -156,6 +156,7 @@ require (
github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 // @grafana/grafana-backend-group
github.com/urfave/cli v1.22.16 // indirect; @grafana/grafana-backend-group
github.com/urfave/cli/v2 v2.27.6 // @grafana/grafana-backend-group
github.com/urfave/cli/v3 v3.3.3 // @grafana/grafana-backend-group
github.com/wk8/go-ordered-map v1.0.0 // @grafana/grafana-backend-group
github.com/xlab/treeprint v1.2.0 // @grafana/observability-traces-and-profiling
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // @grafana/grafana-operator-experience-squad
+2
View File
@@ -2442,6 +2442,8 @@ github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
+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
+2 -1
View File
@@ -75,6 +75,7 @@
"releaseNotesUrl": "https://grafana.com/docs/grafana/next/release-notes/"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.2",
"@babel/core": "7.26.10",
"@babel/preset-env": "7.26.9",
"@babel/runtime": "7.27.0",
@@ -229,11 +230,11 @@
"postcss-reporter": "7.1.0",
"postcss-scss": "4.0.9",
"prettier": "3.4.2",
"publint": "^0.3.12",
"react-refresh": "0.14.0",
"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} />;
}
+1 -1
View File
@@ -8,5 +8,5 @@
},
"exclude": ["dist/**/*"],
"extends": "@grafana/tsconfig",
"include": ["src/**/*.ts*", "../../public/app/types/*.d.ts"]
"include": ["src/**/*.ts*"]
}
@@ -38,6 +38,7 @@ export const TableSelector = ({ db, dataset, table, className, onChange, inputId
isLoading={state.loading}
menuShouldPortal={true}
placeholder={state.loading ? 'Loading tables' : 'Select table'}
allowCustomValue={true}
/>
);
};
@@ -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
+3 -3
View File
@@ -217,12 +217,12 @@ func enterpriseDirectory(ctx context.Context, opts *pipeline.ArgumentOpts) (any,
return nil, fmt.Errorf("error initializing grafana directory: %w", err)
}
src, err := cloneOrMount(ctx, opts.Client, o.EnterpriseDir, o.EnterpriseRepo, o.EnterpriseRef, o.GitHubToken)
clone, err := cloneOrMount(ctx, opts.Client, o.EnterpriseDir, o.EnterpriseRepo, o.EnterpriseRef, o.GitHubToken)
if err != nil {
return nil, err
return nil, fmt.Errorf("error cloning or mounting Grafana Enterprise directory: %w", err)
}
return InitializeEnterprise(opts.Client, grafanaDir.(*dagger.Directory), src), nil
return InitializeEnterprise(opts.Client, grafanaDir.(*dagger.Directory), clone), nil
}
var GrafanaDirectoryFlags = []cli.Flag{
+130 -28
View File
@@ -2,71 +2,173 @@ package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"path"
"dagger.io/dagger"
"github.com/urfave/cli/v3"
)
func main() {
var (
ctx = context.Background()
grafanaPath = flag.String("grafana-dir", ".", "Path to cloned grafana repo")
targzPath = flag.String("package", "grafana.tar.gz", "Path to grafana tar.gz package")
suite = flag.String("suite", "", "e2e suite name (used in arg to run-suite script)")
)
flag.Parse()
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := NewApp().Run(ctx, os.Args); err != nil {
cancel()
fmt.Println(err)
os.Exit(1)
}
}
func NewApp() *cli.Command {
return &cli.Command{
Name: "e2e",
Usage: "Run the E2E tests for Grafana",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "suite",
Usage: "E2E test suite path (e.g. e2e/various-suite)",
Validator: mustBeDir("suite"),
TakesFile: true,
Required: true,
},
&cli.StringFlag{
Name: "grafana-dir",
Usage: "Path to the grafana/grafana clone directory",
Value: ".",
Validator: mustBeDir("grafana-dir"),
TakesFile: true,
},
&cli.StringFlag{
Name: "package",
Usage: "Path to the grafana tar.gz package",
Value: "grafana.tar.gz",
Validator: mustBeFile("package", false),
TakesFile: true,
},
&cli.StringFlag{
Name: "license",
Usage: "Path to the Grafana Enterprise license file (optional)",
Validator: mustBeFile("license", true),
TakesFile: true,
},
&cli.StringFlag{
Name: "flags",
Usage: "Flags to pass through to the e2e runner",
},
&cli.BoolFlag{
Name: "image-renderer",
Usage: "Install the image renderer plugin",
Value: false,
},
},
Action: run,
}
}
func run(ctx context.Context, cmd *cli.Command) error {
grafanaDir := cmd.String("grafana-dir")
suite := cmd.String("suite")
targzPath := cmd.String("package")
licensePath := cmd.String("license")
imageRenderer := cmd.Bool("image-renderer")
runnerFlags := cmd.String("flags")
d, err := dagger.Connect(ctx)
if err != nil {
panic(err)
return fmt.Errorf("failed to connect to Dagger: %w", err)
}
yarnCache := d.CacheVolume("yarn")
log.Println("grafana dir:", *grafanaPath)
log.Println("targz:", *targzPath)
log.Println("grafana dir:", grafanaDir)
log.Println("targz:", targzPath)
log.Println("license path:", licensePath)
grafana := d.Host().Directory(".", dagger.HostDirectoryOpts{
Exclude: []string{".git", "node_modules", "*.tar.gz"},
Exclude: []string{"node_modules", "*.tar.gz"},
})
targz := d.Host().File(targzPath)
targz := d.Host().File("grafana.tar.gz")
svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{
GrafanaDir: grafana,
GrafanaTarGz: targz,
YarnCache: yarnCache,
})
if err != nil {
panic(err)
var license *dagger.File
if licensePath != "" {
license = d.Host().File(licensePath)
}
videosDir := fmt.Sprintf("/src/e2e/%s/videos", *suite)
svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{
GrafanaDir: grafana,
GrafanaTarGz: targz,
YarnCache: yarnCache,
License: license,
InstallImageRenderer: imageRenderer,
})
if err != nil {
return fmt.Errorf("failed to create Grafana service: %w", err)
}
videosDir := path.Join("/src", suite, "videos")
// *spec.ts.mp4
c := RunSuite(d, svc, grafana, yarnCache, *suite)
c := RunSuite(d, svc, grafana, yarnCache, suite, runnerFlags)
c, err = c.Sync(ctx)
if err != nil {
log.Fatalf("error running dagger: %s", err)
return fmt.Errorf("failed to run e2e test suite: %w", err)
}
code, err := c.ExitCode(ctx)
if err != nil {
log.Fatalf("error getting exit code: %s", err)
return fmt.Errorf("failed to get exit code of e2e test suite: %w", err)
}
log.Println("exit code:", code)
// No sync error; export the videos dir
if _, err := c.Directory(videosDir).Export(ctx, "videos"); err != nil {
log.Fatalf("error getting videos: %s", err)
return fmt.Errorf("failed to export videos directory: %w", err)
}
if code != 0 {
log.Printf("tests failed: exit code %d", code)
return fmt.Errorf("e2e tests failed with exit code %d", code)
}
os.Exit(code)
log.Println("e2e tests completed successfully")
return nil
}
func mustBeFile(arg string, emptyOk bool) func(string) error {
return func(s string) error {
if s == "" {
if emptyOk {
return nil
}
return cli.Exit(arg+" cannot be empty", 1)
}
stat, err := os.Stat(s)
if err != nil {
return cli.Exit(arg+" does not exist or cannot be read: "+s, 1)
}
if stat.IsDir() {
return cli.Exit(arg+" must be a file, not a directory: "+s, 1)
}
return nil
}
}
func mustBeDir(arg string) func(string) error {
return func(s string) error {
if s == "" {
return cli.Exit(arg+" cannot be empty", 1)
}
stat, err := os.Stat(s)
if err != nil {
return cli.Exit(arg+" does not exist or cannot be read: "+s, 1)
}
if !stat.IsDir() {
return cli.Exit(arg+" must be a directory: "+s, 1)
}
return nil
}
}
+6 -9
View File
@@ -6,17 +6,14 @@ import (
"dagger.io/dagger"
)
func RunSuite(d *dagger.Client, svc *dagger.Service, src *dagger.Directory, cache *dagger.CacheVolume, suite string) *dagger.Container {
func RunSuite(d *dagger.Client, svc *dagger.Service, src *dagger.Directory, cache *dagger.CacheVolume, suite, runnerFlags string) *dagger.Container {
command := fmt.Sprintf(
"./e2e-runner --start-grafana=false --cypress-video"+
" --grafana-base-url http://grafana:3001 --suite %s %s", suite, runnerFlags)
return WithYarnCache(WithGrafanaFrontend(d.Container().From("cypress/included:13.1.0"), src), cache).
WithWorkdir("/src").
WithEnvVariable("HOST", "grafana").
WithEnvVariable("PORT", "3001").
WithServiceBinding("grafana", svc).
WithExec([]string{"yarn", "install", "--immutable"}).
WithExec([]string{
"/bin/bash", "-c",
fmt.Sprintf("./e2e/run-suite %s true", suite),
}, dagger.ContainerWithExecOpts{
Expect: dagger.ReturnTypeAny,
})
WithExec([]string{"/bin/bash", "-c", command}, dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny})
}
+33 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"os"
"strings"
"dagger.io/dagger"
@@ -22,9 +23,11 @@ func NodeImage(version string) string {
}
type GrafanaServiceOpts struct {
GrafanaDir *dagger.Directory
GrafanaTarGz *dagger.File
YarnCache *dagger.CacheVolume
GrafanaDir *dagger.Directory
GrafanaTarGz *dagger.File
YarnCache *dagger.CacheVolume
License *dagger.File
InstallImageRenderer bool
}
func Frontend(src *dagger.Directory) *dagger.Directory {
@@ -71,8 +74,9 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp
WithExec([]string{"yarn", "install", "--immutable"}).
WithExec([]string{"yarn", "e2e:plugin:build"})
svc := d.Container().From("alpine").
WithExec([]string{"apk", "add", "bash"}).
// _/ubuntu:latest sticks to latest LTS.
// We need ubuntu to support the image renderer plugin, which assumes glibc.
container := d.Container().From("ubuntu:latest").
WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz).
WithExec([]string{"mkdir", "-p", "/src/grafana"}).
WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}).
@@ -84,8 +88,30 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp
WithEnvVariable("GF_APP_MODE", "development").
WithEnvVariable("GF_SERVER_HTTP_PORT", "3001").
WithEnvVariable("GF_SERVER_ROUTER_LOGGING", "1").
WithExposedPort(3001).
AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "-x", "scripts/grafana-server/start-server"}})
WithExposedPort(3001)
var licenseArg string
if opts.License != nil {
container = container.WithMountedFile("/src/license.jwt", opts.License)
licenseArg = "/src/license.jwt"
}
if opts.InstallImageRenderer {
container = container.WithEnvVariable("INSTALL_IMAGE_RENDERER", "true").
WithExec([]string{"apt-get", "update"}).
WithExec([]string{"apt-get", "install", "-y", "ca-certificates"})
}
// We add all GF_ environment variables to allow for overriding Grafana configuration.
// It is unlikely the runner has any such otherwise.
for _, env := range os.Environ() {
if strings.HasPrefix(env, "GF_") {
parts := strings.SplitN(env, "=", 2)
container = container.WithEnvVariable(parts[0], parts[1])
}
}
svc := container.AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "-x", "scripts/grafana-server/start-server", licenseArg}})
return svc, nil
}
+2 -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
@@ -82,6 +82,7 @@ require (
github.com/Masterminds/semver v1.5.0
github.com/google/go-github/v70 v70.0.0
github.com/quasilyte/go-ruleguard/dsl v0.3.22
github.com/urfave/cli/v3 v3.3.3
)
require (
+2
View File
@@ -251,6 +251,8 @@ github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
+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"
@@ -393,6 +393,7 @@ func generateDocsMD() string {
buf := `---
aliases:
- /docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/
- ../../administration/feature-toggles/ # /docs/grafana/latest/administration/feature-toggles/
description: Learn about feature toggles, which you can enable or disable.
title: Configure feature toggles
weight: 150
@@ -513,8 +513,16 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(
return grafanaGroup, nil
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, config apimodels.AlertmanagerUserConfig) response.Response {
return response.Error(501, "Not implemented", nil)
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostAlertmanagerConfig(c *contextmodel.ReqContext, amCfg apimodels.AlertmanagerUserConfig) response.Response {
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
}
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteAlertmanagerConfig(c *contextmodel.ReqContext) response.Response {
return response.Error(http.StatusNotImplemented, "Not Implemented", nil)
}
// parseBooleanHeader parses a boolean header value, returning an error if the header
+4 -1
View File
@@ -147,8 +147,11 @@ func (api *API) authorize(method, path string) web.Handler {
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
)
case http.MethodPost + "/api/convert/api/v1/alerts":
case http.MethodPost + "/api/convert/api/v1/alerts",
http.MethodDelete + "/api/convert/api/v1/alerts":
eval = ac.EvalPermission(ac.ActionAlertingNotificationsWrite)
case http.MethodGet + "/api/convert/api/v1/alerts":
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
// Alert Instances and Silences
@@ -26,8 +26,10 @@ type ConvertPrometheusApi interface {
RouteConvertPrometheusCortexGetRules(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexPostRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusCortexPostRuleGroups(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteAlertmanagerConfig(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusDeleteRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetAlertmanagerConfig(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetNamespace(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetRuleGroup(*contextmodel.ReqContext) response.Response
RouteConvertPrometheusGetRules(*contextmodel.ReqContext) response.Response
@@ -69,6 +71,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroup(
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusCortexPostRuleGroups(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusCortexPostRuleGroups(ctx)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusDeleteAlertmanagerConfig(ctx)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
@@ -80,6 +85,9 @@ func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusDeleteRuleGroup(ctx
groupParam := web.Params(ctx.Req)[":Group"]
return f.handleRouteConvertPrometheusDeleteRuleGroup(ctx, namespaceTitleParam, groupParam)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
return f.handleRouteConvertPrometheusGetAlertmanagerConfig(ctx)
}
func (f *ConvertPrometheusApiHandler) RouteConvertPrometheusGetNamespace(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
namespaceTitleParam := web.Params(ctx.Req)[":NamespaceTitle"]
@@ -192,6 +200,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi,
m,
),
)
group.Delete(
toMacaronPath("/api/convert/api/v1/alerts"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodDelete, "/api/convert/api/v1/alerts"),
metrics.Instrument(
http.MethodDelete,
"/api/convert/api/v1/alerts",
api.Hooks.Wrap(srv.RouteConvertPrometheusDeleteAlertmanagerConfig),
m,
),
)
group.Delete(
toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
@@ -216,6 +236,18 @@ func (api *API) RegisterConvertPrometheusApiEndpoints(srv ConvertPrometheusApi,
m,
),
)
group.Get(
toMacaronPath("/api/convert/api/v1/alerts"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodGet, "/api/convert/api/v1/alerts"),
metrics.Instrument(
http.MethodGet,
"/api/convert/api/v1/alerts",
api.Hooks.Wrap(srv.RouteConvertPrometheusGetAlertmanagerConfig),
m,
),
)
group.Get(
toMacaronPath("/api/convert/prometheus/config/v1/rules/{NamespaceTitle}"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
@@ -134,3 +134,11 @@ func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusPostAlertmanag
return f.svc.RouteConvertPrometheusPostAlertmanagerConfig(ctx, config)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusGetAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RouteConvertPrometheusGetAlertmanagerConfig(ctx)
}
func (f *ConvertPrometheusApiHandler) handleRouteConvertPrometheusDeleteAlertmanagerConfig(ctx *contextmodel.ReqContext) response.Response {
return f.svc.RouteConvertPrometheusDeleteAlertmanagerConfig(ctx)
}
+32 -13
View File
@@ -580,6 +580,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
},
"template_files": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
},
"type": "object"
@@ -1408,6 +1414,20 @@
"title": "Frames is a slice of Frame pointers.",
"type": "array"
},
"GettableAlertmanagerUserConfig": {
"properties": {
"alertmanager_config": {
"type": "string"
},
"template_files": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
},
"type": "object"
},
"GettableAlertmanagers": {
"properties": {
"data": {
@@ -2038,7 +2058,11 @@
"description": "InhibitRule defines an inhibition rule that mutes alerts that match the\ntarget labels if an alert matching the source labels exists.\nBoth alerts have to have a set of labels being equal.",
"properties": {
"equal": {
"$ref": "#/definitions/LabelNames"
"description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
"items": {
"type": "string"
},
"type": "array"
},
"source_match": {
"additionalProperties": {
@@ -2162,17 +2186,6 @@
"title": "Label is a key/value pair of strings.",
"type": "object"
},
"LabelName": {
"description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
"type": "string"
},
"LabelNames": {
"items": {
"$ref": "#/definitions/LabelName"
},
"title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
"type": "array"
},
"LabelSet": {
"additionalProperties": {
"$ref": "#/definitions/LabelValue"
@@ -3674,6 +3687,7 @@
"type": "object"
},
"Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": {
"active_time_intervals": {
"items": {
@@ -3715,6 +3729,12 @@
},
"type": "array"
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"receiver": {
"type": "string"
},
@@ -3728,7 +3748,6 @@
"type": "array"
}
},
"title": "A Route is a node that contains definitions of how to handle alerts.",
"type": "object"
},
"RouteExport": {
@@ -3,6 +3,7 @@ package definitions
import (
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/common/model"
"gopkg.in/yaml.v3"
)
// Route for mimirtool
@@ -204,7 +205,10 @@ import (
// Route for `mimirtool alertmanager load`
// swagger:route POST /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusPostAlertmanagerConfig
//
// Load Alertmanager configuration to Grafana and merge it with the existing configuration.
// Load extra Alertmanager configuration to Grafana and merge it with the existing configuration.
// This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by
// a unique identifier and can include merge matchers to select which alerts should be handled by
// this specific configuration.
//
// Produces:
// - application/json
@@ -216,6 +220,32 @@ import (
// Extensions:
// x-raw-request: true
// Route for `mimirtool alertmanager get`
// swagger:route GET /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusGetAlertmanagerConfig
//
// Get extra Alertmanager configuration from Grafana.
// Returns a specific imported Alertmanager configuration by its identifier.
//
// Produces:
// - application/yaml
//
// Responses:
// 200: GettableAlertmanagerUserConfig
// 403: ForbiddenError
// Route for `mimirtool alertmanager delete`
// swagger:route DELETE /convert/api/v1/alerts convert_prometheus RouteConvertPrometheusDeleteAlertmanagerConfig
//
// Delete extra Alertmanager configuration from Grafana by its identifier.
// The main Grafana Alertmanager configuration remains unaffected.
//
// Produces:
// - application/json
//
// Responses:
// 202: ConvertPrometheusResponse
// 403: ForbiddenError
// swagger:parameters RouteConvertPrometheusPostRuleGroup RouteConvertPrometheusCortexPostRuleGroup
type RouteConvertPrometheusPostRuleGroupParams struct {
// in: path
@@ -286,11 +316,64 @@ type ConvertPrometheusResponse struct {
// swagger:parameters RouteConvertPrometheusPostAlertmanagerConfig
type RouteConvertPrometheusPostAlertmanagerConfigParams struct {
// Unique identifier for this Alertmanager configuration.
// This identifier is used to distinguish between different imported configurations.
// in: header
Identifier string `json:"x-grafana-alerting-config-identifier"`
// Comma-separated list of label matchers in 'key=value' format.
// These matchers determine which alerts this configuration should handle.
// For example: 'environment=production,team=backend' will only apply this
// configuration to alerts matching both environment=production AND team=backend.
// in: header
MergeMatchers string `json:"x-grafana-alerting-merge-matchers"`
// Alertmanager configuration including routing rules, receivers, and template files
// in:body
Body AlertmanagerUserConfig
}
// swagger:parameters RouteConvertPrometheusGetAlertmanagerConfig
type RouteConvertPrometheusGetAlertmanagerConfigParams struct {
// Unique identifier for the Alertmanager configuration to retrieve.
// in: header
Identifier string `json:"x-grafana-alerting-config-identifier"`
}
// swagger:parameters RouteConvertPrometheusDeleteAlertmanagerConfig
type RouteConvertPrometheusDeleteAlertmanagerConfigParams struct {
// Unique identifier for the Alertmanager configuration to delete.
// in: header
Identifier string `json:"x-grafana-alerting-config-identifier"`
}
// swagger:model
type AlertmanagerUserConfig struct {
AlertmanagerConfig config.Config `yaml:"alertmanager_config" json:"alertmanager_config"`
AlertmanagerConfig config.Config `yaml:"alertmanager_config" json:"alertmanager_config"`
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
}
func (c *AlertmanagerUserConfig) UnmarshalYAML(value *yaml.Node) error {
// mimirtool sends alertmanager_config as a string
type cortexAlertmanagerUserConfig struct {
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"`
}
var tmp cortexAlertmanagerUserConfig
if err := value.Decode(&tmp); err != nil {
return err
}
if err := yaml.Unmarshal([]byte(tmp.AlertmanagerConfig), &c.AlertmanagerConfig); err != nil {
return err
}
c.TemplateFiles = tmp.TemplateFiles
return nil
}
// swagger:model
type GettableAlertmanagerUserConfig struct {
AlertmanagerConfig string `yaml:"alertmanager_config" json:"alertmanager_config"`
TemplateFiles map[string]string `yaml:"template_files" json:"template_files"`
}
+114 -14
View File
@@ -580,6 +580,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
},
"template_files": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
},
"type": "object"
@@ -1408,6 +1414,20 @@
"title": "Frames is a slice of Frame pointers.",
"type": "array"
},
"GettableAlertmanagerUserConfig": {
"properties": {
"alertmanager_config": {
"type": "string"
},
"template_files": {
"additionalProperties": {
"type": "string"
},
"type": "object"
}
},
"type": "object"
},
"GettableAlertmanagers": {
"properties": {
"data": {
@@ -2038,7 +2058,11 @@
"description": "InhibitRule defines an inhibition rule that mutes alerts that match the\ntarget labels if an alert matching the source labels exists.\nBoth alerts have to have a set of labels being equal.",
"properties": {
"equal": {
"$ref": "#/definitions/LabelNames"
"description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
"items": {
"type": "string"
},
"type": "array"
},
"source_match": {
"additionalProperties": {
@@ -2162,17 +2186,6 @@
"title": "Label is a key/value pair of strings.",
"type": "object"
},
"LabelName": {
"description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
"type": "string"
},
"LabelNames": {
"items": {
"$ref": "#/definitions/LabelName"
},
"title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
"type": "array"
},
"LabelSet": {
"additionalProperties": {
"$ref": "#/definitions/LabelValue"
@@ -3674,6 +3687,7 @@
"type": "object"
},
"Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"properties": {
"active_time_intervals": {
"items": {
@@ -3715,6 +3729,12 @@
},
"type": "array"
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"receiver": {
"type": "string"
},
@@ -3728,7 +3748,6 @@
"type": "array"
}
},
"title": "A Route is a node that contains definitions of how to handle alerts.",
"type": "object"
},
"RouteExport": {
@@ -6823,10 +6842,91 @@
}
},
"/convert/api/v1/alerts": {
"delete": {
"description": "The main Grafana Alertmanager configuration remains unaffected.",
"operationId": "RouteConvertPrometheusDeleteAlertmanagerConfig",
"parameters": [
{
"description": "Unique identifier for the Alertmanager configuration to delete.",
"in": "header",
"name": "x-grafana-alerting-config-identifier",
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Delete extra Alertmanager configuration from Grafana by its identifier.",
"tags": [
"convert_prometheus"
]
},
"get": {
"description": "Returns a specific imported Alertmanager configuration by its identifier.",
"operationId": "RouteConvertPrometheusGetAlertmanagerConfig",
"parameters": [
{
"description": "Unique identifier for the Alertmanager configuration to retrieve.",
"in": "header",
"name": "x-grafana-alerting-config-identifier",
"type": "string"
}
],
"produces": [
"application/yaml"
],
"responses": {
"200": {
"description": "GettableAlertmanagerUserConfig",
"schema": {
"$ref": "#/definitions/GettableAlertmanagerUserConfig"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"summary": "Get extra Alertmanager configuration from Grafana.",
"tags": [
"convert_prometheus"
]
},
"post": {
"description": "This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by\na unique identifier and can include merge matchers to select which alerts should be handled by\nthis specific configuration.",
"operationId": "RouteConvertPrometheusPostAlertmanagerConfig",
"parameters": [
{
"description": "Unique identifier for this Alertmanager configuration.\nThis identifier is used to distinguish between different imported configurations.",
"in": "header",
"name": "x-grafana-alerting-config-identifier",
"type": "string"
},
{
"description": "Comma-separated list of label matchers in 'key=value' format.\nThese matchers determine which alerts this configuration should handle.",
"example": "'environment=production,team=backend' will only apply this",
"in": "header",
"name": "x-grafana-alerting-merge-matchers",
"type": "string"
},
{
"description": "Alertmanager configuration including routing rules, receivers, and template files",
"in": "body",
"name": "Body",
"schema": {
@@ -6851,7 +6951,7 @@
}
}
},
"summary": "Load Alertmanager configuration to Grafana and merge it with the existing configuration.",
"summary": "Load extra Alertmanager configuration to Grafana and merge it with the existing configuration.",
"tags": [
"convert_prometheus"
],
+114 -14
View File
@@ -1361,17 +1361,65 @@
}
},
"/convert/api/v1/alerts": {
"get": {
"description": "Returns a specific imported Alertmanager configuration by its identifier.",
"produces": [
"application/yaml"
],
"tags": [
"convert_prometheus"
],
"summary": "Get extra Alertmanager configuration from Grafana.",
"operationId": "RouteConvertPrometheusGetAlertmanagerConfig",
"parameters": [
{
"type": "string",
"description": "Unique identifier for the Alertmanager configuration to retrieve.",
"name": "x-grafana-alerting-config-identifier",
"in": "header"
}
],
"responses": {
"200": {
"description": "GettableAlertmanagerUserConfig",
"schema": {
"$ref": "#/definitions/GettableAlertmanagerUserConfig"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
},
"post": {
"description": "This endpoint allows importing Alertmanager configurations to Grafana. Each configuration is identified by\na unique identifier and can include merge matchers to select which alerts should be handled by\nthis specific configuration.",
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Load Alertmanager configuration to Grafana and merge it with the existing configuration.",
"summary": "Load extra Alertmanager configuration to Grafana and merge it with the existing configuration.",
"operationId": "RouteConvertPrometheusPostAlertmanagerConfig",
"parameters": [
{
"type": "string",
"description": "Unique identifier for this Alertmanager configuration.\nThis identifier is used to distinguish between different imported configurations.",
"name": "x-grafana-alerting-config-identifier",
"in": "header"
},
{
"type": "string",
"example": "'environment=production,team=backend' will only apply this",
"description": "Comma-separated list of label matchers in 'key=value' format.\nThese matchers determine which alerts this configuration should handle.",
"name": "x-grafana-alerting-merge-matchers",
"in": "header"
},
{
"description": "Alertmanager configuration including routing rules, receivers, and template files",
"name": "Body",
"in": "body",
"schema": {
@@ -1394,6 +1442,39 @@
}
},
"x-raw-request": "true"
},
"delete": {
"description": "The main Grafana Alertmanager configuration remains unaffected.",
"produces": [
"application/json"
],
"tags": [
"convert_prometheus"
],
"summary": "Delete extra Alertmanager configuration from Grafana by its identifier.",
"operationId": "RouteConvertPrometheusDeleteAlertmanagerConfig",
"parameters": [
{
"type": "string",
"description": "Unique identifier for the Alertmanager configuration to delete.",
"name": "x-grafana-alerting-config-identifier",
"in": "header"
}
],
"responses": {
"202": {
"description": "ConvertPrometheusResponse",
"schema": {
"$ref": "#/definitions/ConvertPrometheusResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
},
"/convert/prometheus/config/v1/rules": {
@@ -4788,6 +4869,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
},
"template_files": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
@@ -5616,6 +5703,20 @@
"$ref": "#/definitions/Frame"
}
},
"GettableAlertmanagerUserConfig": {
"type": "object",
"properties": {
"alertmanager_config": {
"type": "string"
},
"template_files": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"GettableAlertmanagers": {
"type": "object",
"properties": {
@@ -6247,7 +6348,11 @@
"type": "object",
"properties": {
"equal": {
"$ref": "#/definitions/LabelNames"
"description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
"type": "array",
"items": {
"type": "string"
}
},
"source_match": {
"description": "SourceMatch defines a set of labels that have to equal the given\nvalue for source alerts. Deprecated. Remove before v1.0 release.",
@@ -6370,17 +6475,6 @@
}
}
},
"LabelName": {
"description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
"type": "string"
},
"LabelNames": {
"type": "array",
"title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
"items": {
"$ref": "#/definitions/LabelName"
}
},
"LabelSet": {
"description": "A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet\nmay be fully-qualified down to the point where it may resolve to a single\nMetric in the data store or not. All operations that occur within the realm\nof a LabelSet can emit a vector of Metric entities to which the LabelSet may\nmatch.",
"type": "object",
@@ -7883,8 +7977,8 @@
}
},
"Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"type": "object",
"title": "A Route is a node that contains definitions of how to handle alerts.",
"properties": {
"active_time_intervals": {
"type": "array",
@@ -7926,6 +8020,12 @@
"type": "string"
}
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"receiver": {
"type": "string"
},
@@ -318,7 +318,6 @@ func (r ruleWithFolder) Fingerprint() fingerprint {
writeInt(*rule.PanelID)
}
writeString(rule.RuleGroup)
writeInt(int64(rule.RuleGroupIndex))
writeString(string(rule.NoDataState))
writeString(string(rule.ExecErrState))
if rule.Record != nil {
@@ -152,13 +152,14 @@ func TestRuleWithFolderFingerprint(t *testing.T) {
f2 := ruleWithFolder{rule: rule, folderTitle: uuid.NewString()}.Fingerprint()
require.NotEqual(t, f, f2)
})
t.Run("Version, Updated, IntervalSeconds, GUID and Annotations should be excluded from fingerprint", func(t *testing.T) {
t.Run("Version, Updated, IntervalSeconds, GUID, Annotations and RuleGroupIndex should be excluded from fingerprint", func(t *testing.T) {
cp := models.CopyRule(rule)
cp.Version++
cp.Updated = cp.Updated.Add(1 * time.Second)
cp.IntervalSeconds++
cp.Annotations = make(map[string]string)
cp.Annotations["test"] = "test"
cp.RuleGroupIndex++
cp.GUID = uuid.NewString()
f2 := ruleWithFolder{rule: cp, folderTitle: title}.Fingerprint()
+6 -5
View File
@@ -46,10 +46,11 @@ type CreateTeamCommand struct {
}
type UpdateTeamCommand struct {
ID int64
Name string
Email string
OrgID int64 `json:"-"`
ID int64
Name string
Email string
ExternalUID string
OrgID int64 `json:"-"`
}
type DeleteTeamCommand struct {
@@ -98,7 +99,7 @@ type TeamDTO struct {
OrgID int64 `json:"orgId" xorm:"org_id"`
Name string `json:"name"`
Email string `json:"email"`
ExternalUID string `json:"externalUID"`
ExternalUID string `json:"externalUID" xorm:"external_uid"`
IsProvisioned bool `json:"isProvisioned"`
AvatarURL string `json:"avatarUrl"`
MemberCount int64 `json:"memberCount"`
+4 -3
View File
@@ -111,9 +111,10 @@ func (ss *xormStore) Update(ctx context.Context, cmd *team.UpdateTeamCommand) er
}
t := team.Team{
Name: cmd.Name,
Email: cmd.Email,
Updated: time.Now(),
Name: cmd.Name,
Email: cmd.Email,
ExternalUID: cmd.ExternalUID,
Updated: time.Now(),
}
sess.MustCols("email")
+4 -1
View File
@@ -8666,6 +8666,9 @@
"Email": {
"type": "string"
},
"ExternalUID": {
"type": "string"
},
"ID": {
"type": "integer",
"format": "int64"
@@ -10647,4 +10650,4 @@
"name": "service_accounts"
}
]
}
}
+35 -13
View File
@@ -12999,6 +12999,12 @@
"properties": {
"alertmanager_config": {
"$ref": "#/definitions/Config"
},
"template_files": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
@@ -15855,6 +15861,20 @@
}
}
},
"GettableAlertmanagerUserConfig": {
"type": "object",
"properties": {
"alertmanager_config": {
"type": "string"
},
"template_files": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"GettableAlertmanagers": {
"type": "object",
"properties": {
@@ -16709,7 +16729,11 @@
"type": "object",
"properties": {
"equal": {
"$ref": "#/definitions/LabelNames"
"description": "A set of labels that must be equal between the source and target alert\nfor them to be a match.",
"type": "array",
"items": {
"type": "string"
}
},
"source_match": {
"description": "SourceMatch defines a set of labels that have to equal the given\nvalue for source alerts. Deprecated. Remove before v1.0 release.",
@@ -16884,17 +16908,6 @@
}
}
},
"LabelName": {
"description": "A LabelName is a key for a LabelSet or Metric. It has a value associated\ntherewith.",
"type": "string"
},
"LabelNames": {
"type": "array",
"title": "LabelNames is a sortable LabelName slice. In implements sort.Interface.",
"items": {
"$ref": "#/definitions/LabelName"
}
},
"LabelSet": {
"description": "A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet\nmay be fully-qualified down to the point where it may resolve to a single\nMetric in the data store or not. All operations that occur within the realm\nof a LabelSet can emit a vector of Metric entities to which the LabelSet may\nmatch.",
"type": "object",
@@ -20027,8 +20040,8 @@
}
},
"Route": {
"description": "A Route is a node that contains definitions of how to handle alerts. This is modified\nfrom the upstream alertmanager in that it adds the ObjectMatchers property.",
"type": "object",
"title": "A Route is a node that contains definitions of how to handle alerts.",
"properties": {
"active_time_intervals": {
"type": "array",
@@ -20070,6 +20083,12 @@
"type": "string"
}
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"provenance": {
"$ref": "#/definitions/Provenance"
},
"receiver": {
"type": "string"
},
@@ -22411,6 +22430,9 @@
"Email": {
"type": "string"
},
"ExternalUID": {
"type": "string"
},
"ID": {
"type": "integer",
"format": "int64"
@@ -87,8 +87,7 @@ export const getAllOptionEditors = () => {
name: 'Boolean',
description: 'Allows boolean values input',
editor(props) {
const { id, ...rest } = props; // Remove id from properties passed into switch
return <Switch {...rest} onChange={(e) => props.onChange(e.currentTarget.checked)} />;
return <Switch {...props} onChange={(e) => props.onChange(e.currentTarget.checked)} />;
},
};
@@ -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 = {
@@ -1,4 +1,4 @@
import { render } from 'test/test-utils';
import { render, waitFor } from 'test/test-utils';
import { byLabelText, byRole } from 'testing-library-selector';
import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
@@ -47,7 +47,7 @@ const ui = {
alertingFactory.dataSource.mimir().build({ meta: { alerting: true } });
describe.skip('ImportToGMARules', () => {
describe('ImportToGMARules', () => {
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleCreate]);
testWithFeatureToggles(['alertingImportYAMLUI', 'alertingMigrationUI']);
@@ -62,8 +62,9 @@ describe.skip('ImportToGMARules', () => {
it('should render datasource options', async () => {
const { user } = render(<ImportToGMARules />);
// Wait for the data source picker to be ready
// Wait for the data source picker to be ready and enabled
const dsPicker = await ui.dsImport.dsPicker.find();
await waitFor(() => expect(dsPicker).toBeEnabled());
await user.click(dsPicker);
await user.click(await ui.dsImport.mimirDsOption.find());
@@ -91,8 +92,12 @@ describe.skip('ImportToGMARules', () => {
it('should show confirmation dialog when importing from data source', async () => {
const { user } = render(<ImportToGMARules />);
// Wait for the data source picker to be enabled
const dsPicker = ui.dsImport.dsPicker.get();
await waitFor(() => expect(dsPicker).toBeEnabled());
// Select a data source
await user.click(ui.dsImport.dsPicker.get());
await user.click(dsPicker);
await user.click(await ui.dsImport.mimirDsOption.find());
// Click the import button
@@ -3,11 +3,14 @@ import { Suspense, lazy } from 'react';
import { config } from '@grafana/runtime';
import RulesFilterV1 from './RulesFilter.v1';
import { SupportedView } from './RulesViewModeSelector';
const RulesFilterV2 = lazy(() => import('./RulesFilter.v2'));
interface RulesFilerProps {
onClear?: () => void;
viewMode?: SupportedView;
onViewModeChange?: (viewMode: SupportedView) => void;
}
const RulesFilter = (props: RulesFilerProps) => {
@@ -26,7 +26,7 @@ import { alertStateToReadable } from '../../../utils/rules';
import { PopupCard } from '../../HoverCard';
import { MultipleDataSourcePicker } from '../MultipleDataSourcePicker';
import { RulesViewModeSelector } from './RulesViewModeSelector';
import { RulesViewModeSelector, SupportedView } from './RulesViewModeSelector';
const RuleTypeOptions: SelectableValue[] = [
{ label: 'Alert ', value: PromRuleType.Alerting },
@@ -44,6 +44,8 @@ const canRenderContactPointSelector = contextSrv.hasPermission(AccessControlActi
interface RulesFilerProps {
onClear?: () => void;
viewMode?: SupportedView;
onViewModeChange?: (viewMode: SupportedView) => void;
}
const RuleStateOptions = Object.entries(PromAlertingRuleState).map(([key, value]) => ({
@@ -51,7 +53,7 @@ const RuleStateOptions = Object.entries(PromAlertingRuleState).map(([key, value]
value,
}));
const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => {
const RulesFilter = ({ onClear = () => undefined, viewMode, onViewModeChange }: RulesFilerProps) => {
const styles = useStyles2(getStyles);
const { pluginsFilterEnabled } = usePluginsFilterStatus();
const { filterState, hasActiveFilters, searchQuery, setSearchQuery, updateFilters } = useRulesFilter();
@@ -317,7 +319,7 @@ const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => {
<Label>
<Trans i18nKey="alerting.rules-filter.view-as">View as</Trans>
</Label>
<RulesViewModeSelector />
<RulesViewModeSelector viewMode={viewMode} onViewModeChange={onViewModeChange} />
</div>
</Stack>
{hasActiveFilters && (
@@ -1,3 +1,5 @@
import { useCallback } from 'react';
import { SelectableValue } from '@grafana/data';
import { RadioButtonGroup } from '@grafana/ui';
@@ -15,24 +17,58 @@ const ViewOptions: Array<SelectableValue<SupportedView>> = [
{ icon: 'list-ul', label: 'List', value: 'list' },
];
function RulesViewModeSelectorV2() {
interface RulesViewModeSelectorV2Props {
viewMode?: SupportedView;
onViewModeChange?: (viewMode: SupportedView) => void;
}
/**
* Selecting a view mode is no longer a simple toggle relying on the URL query params.
* We now need to check if the current filters are compatible with the grouped view.
* If they are, we show the grouped view by default.
* If they are not, we show the list view.
* Use the complementary {@link useListViewMode} hook to get the current view mode and a handler for changing it.
*/
function RulesViewModeSelectorV2({ viewMode, onViewModeChange }: RulesViewModeSelectorV2Props) {
return <RadioButtonGroup options={ViewOptions} value={viewMode} onChange={onViewModeChange} />;
}
export function useListViewMode() {
const [queryParams, updateQueryParams] = useURLSearchParams();
const { hasActiveFilters } = useRulesFilter();
const wantsListView = queryParams.get('view') === 'list';
const { activeFilters } = useRulesFilter();
const selectedViewOption = hasActiveFilters || wantsListView ? 'list' : 'grouped';
const queryStringView: SupportedView = queryParams.get('view') === 'list' ? 'list' : 'grouped';
/* If we change to the grouped view, we just remove the "list" and "search" params */
const handleViewChange = (view: SupportedView) => {
if (view === 'list') {
updateQueryParams({ view });
const areFiltersGroupedViewCompatible = activeFilters.every(
(filter) => filter === 'groupName' || filter === 'namespace'
);
const showListView = areFiltersGroupedViewCompatible === false || queryStringView === 'list';
const handleViewChange = useCallback(
(view: SupportedView) => {
if (view === 'grouped') {
// When switching to grouped view, preserve filters only if they are grouped-view compatible
if (areFiltersGroupedViewCompatible) {
// Only remove view parameter, keep search (preserve group/namespace filters)
updateQueryParams({ view: undefined });
} else {
// Clear both view and search (clear all filters)
updateQueryParams({ view: undefined, search: undefined });
}
} else {
updateQueryParams({ view });
}
trackRulesListViewChange({ view });
} else {
updateQueryParams({ view: undefined, search: undefined });
}
};
},
[updateQueryParams, areFiltersGroupedViewCompatible]
);
return <RadioButtonGroup options={ViewOptions} value={selectedViewOption} onChange={handleViewChange} />;
const viewMode: SupportedView = showListView ? 'list' : 'grouped';
return {
viewMode,
handleViewChange,
};
}
const LegacyViewOptions: Array<SelectableValue<LegacySupportedView>> = [
@@ -66,4 +102,15 @@ function viewParamToLegacyView(viewParam: string | null): LegacySupportedView {
return 'grouped';
}
export const RulesViewModeSelector = shouldUseAlertingListViewV2() ? RulesViewModeSelectorV2 : RulesViewModeSelectorV1;
interface RulesViewModeSelectorProps {
viewMode?: SupportedView;
onViewModeChange?: (viewMode: SupportedView) => void;
}
export function RulesViewModeSelector({ viewMode, onViewModeChange }: RulesViewModeSelectorProps) {
if (shouldUseAlertingListViewV2()) {
return <RulesViewModeSelectorV2 viewMode={viewMode} onViewModeChange={onViewModeChange} />;
}
return <RulesViewModeSelectorV1 />;
}
@@ -38,6 +38,10 @@ export function useRulesFilter() {
}, [searchQuery]);
const hasActiveFilters = useMemo(() => Object.values(filterState).some((filter) => !isEmpty(filter)), [filterState]);
const activeFilters = useMemo(() => {
return chain(filterState).omitBy(isEmpty).keys().filter(isRuleFilterKey).value();
}, [filterState]);
const updateFilters = useCallback(
(newFilter: RulesFilter) => {
const newSearchQuery = applySearchFilterToQuery(searchQuery, newFilter);
@@ -86,7 +90,7 @@ export function useRulesFilter() {
}
}, [queryParams, updateFilters, filterState, updateQueryParams]);
return { filterState, hasActiveFilters, searchQuery, setSearchQuery, updateFilters };
return { filterState, hasActiveFilters, activeFilters, searchQuery, setSearchQuery, updateFilters };
}
export const useFilteredRules = (namespaces: CombinedRuleNamespace[], filterState: RulesFilter) => {
@@ -356,3 +360,20 @@ const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filterState: Rules
return ds?.name && filterState?.dataSourceNames?.includes(ds.name);
});
};
const RULES_FILTER_KEYS: Set<keyof RulesFilter> = new Set([
'freeFormWords',
'namespace',
'groupName',
'ruleName',
'ruleState',
'ruleType',
'dataSourceNames',
'labels',
'ruleHealth',
'dashboardUid',
'plugins',
'contactPoint',
]);
const isRuleFilterKey = (key: string): key is keyof RulesFilter => RULES_FILTER_KEYS.has(key as keyof RulesFilter);
@@ -22,14 +22,12 @@ import {
RuleWithOrigin,
useFilteredRulesIteratorProvider,
} from './hooks/useFilteredRulesIterator';
import { FRONTEND_LIST_PAGE_SIZE, getApiGroupPageSize } from './paginationLimits';
interface FilterViewProps {
filterState: RulesFilter;
}
const FRONTENT_PAGE_SIZE = 100;
const API_PAGE_SIZE = 2000;
export function FilterView({ filterState }: FilterViewProps) {
// ⚠️ We use a key to force the component to unmount and remount when the filter state changes
// filterState is a complex object including arrays and is constructed from URL params
@@ -79,10 +77,10 @@ function FilterViewResults({ filterState }: FilterViewProps) {
* ⚠️ Make sure we are returning / using a "iterator" and not an "iterable" since the iterable is only a blueprint
* and the iterator will allow us to exhaust the iterable in a stateful way
*/
const { iterable, abortController } = getFilteredRulesIterator(filterState, API_PAGE_SIZE);
const { iterable, abortController } = getFilteredRulesIterator(filterState, getApiGroupPageSize(true));
const rulesBatchIterator = iterable
.pipe(
bufferCountOrTime(FRONTENT_PAGE_SIZE, 1000),
bufferCountOrTime(FRONTEND_LIST_PAGE_SIZE, 1000),
onFinished(() => setDoneSearching(true))
)
[Symbol.asyncIterator]();
@@ -98,7 +96,7 @@ function FilterViewResults({ filterState }: FilterViewProps) {
let loadedRulesCount = 0;
while (loadedRulesCount < FRONTENT_PAGE_SIZE) {
while (loadedRulesCount < FRONTEND_LIST_PAGE_SIZE) {
const nextRulesBatch = await rulesIterator.next();
if (nextRulesBatch.done) {
return;
@@ -10,7 +10,7 @@ import { setPrometheusRules } from '../mocks/server/configure';
import { alertingFactory } from '../mocks/server/db';
import { GroupedView } from './GroupedView';
import { DATA_SOURCE_GROUP_PAGE_SIZE } from './PaginatedDataSourceLoader';
import { FRONTED_GROUPED_PAGE_SIZE } from './paginationLimits';
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
@@ -67,7 +67,7 @@ describe('RuleList - GroupedView', () => {
const mimirNamespace = await ui.namespace(/test-mimir-namespace/).find(mimirSection);
const firstPageGroups = await ui.group(/test-group-([1-9]|[1-3][0-9]|40)/).findAll(mimirNamespace);
expect(firstPageGroups).toHaveLength(DATA_SOURCE_GROUP_PAGE_SIZE);
expect(firstPageGroups).toHaveLength(FRONTED_GROUPED_PAGE_SIZE);
expect(firstPageGroups[0]).toHaveTextContent('test-group-1');
expect(firstPageGroups[24]).toHaveTextContent('test-group-25');
expect(firstPageGroups[39]).toHaveTextContent('test-group-40');
@@ -79,7 +79,7 @@ describe('RuleList - GroupedView', () => {
const secondPageGroups = await ui.group(/test-group-(4[1-9]|[5-7][0-9]|80)/).findAll(mimirNamespace);
expect(secondPageGroups).toHaveLength(DATA_SOURCE_GROUP_PAGE_SIZE);
expect(secondPageGroups).toHaveLength(FRONTED_GROUPED_PAGE_SIZE);
expect(secondPageGroups[0]).toHaveTextContent('test-group-41');
expect(secondPageGroups[24]).toHaveTextContent('test-group-65');
expect(secondPageGroups[39]).toHaveTextContent('test-group-80');
@@ -14,16 +14,32 @@ import { DataSourceSection } from './components/DataSourceSection';
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
export function GroupedView() {
interface GroupedViewProps {
groupFilter?: string;
namespaceFilter?: string;
}
export function GroupedView({ groupFilter, namespaceFilter }: GroupedViewProps) {
const externalRuleSources = useMemo(() => getExternalRulesSources(), []);
return (
<Stack direction="column" gap={1} role="list">
<DataSourceErrorBoundary rulesSourceIdentifier={GrafanaRulesSource}>
<PaginatedGrafanaLoader />
<PaginatedGrafanaLoader
groupFilter={groupFilter}
namespaceFilter={namespaceFilter}
key={`${groupFilter}-${namespaceFilter}`}
/>
</DataSourceErrorBoundary>
{externalRuleSources.map((ruleSource) => {
return <DataSourceLoader key={ruleSource.uid} rulesSourceIdentifier={ruleSource} />;
return (
<DataSourceLoader
key={ruleSource.uid}
rulesSourceIdentifier={ruleSource}
groupFilter={groupFilter}
namespaceFilter={namespaceFilter}
/>
);
})}
</Stack>
);
@@ -31,13 +47,15 @@ export function GroupedView() {
interface DataSourceLoaderProps {
rulesSourceIdentifier: DataSourceRulesSourceIdentifier;
groupFilter?: string;
namespaceFilter?: string;
}
export function GrafanaDataSourceLoader() {
return <DataSourceSection name="Grafana" application="grafana" uid="grafana" isLoading={true} />;
}
function DataSourceLoader({ rulesSourceIdentifier }: DataSourceLoaderProps) {
function DataSourceLoader({ rulesSourceIdentifier, groupFilter, namespaceFilter }: DataSourceLoaderProps) {
const { data: dataSourceInfo, isLoading, error } = useDiscoverDsFeaturesQuery({ uid: rulesSourceIdentifier.uid });
const { uid, name } = rulesSourceIdentifier;
@@ -55,9 +73,10 @@ function DataSourceLoader({ rulesSourceIdentifier }: DataSourceLoaderProps) {
return (
<DataSourceErrorBoundary rulesSourceIdentifier={rulesSourceIdentifier}>
<PaginatedDataSourceLoader
key={rulesSourceIdentifier.uid}
rulesSourceIdentifier={rulesSourceIdentifier}
application={dataSourceInfo.application}
groupFilter={groupFilter}
namespaceFilter={namespaceFilter}
/>
</DataSourceErrorBoundary>
);

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