Merge branch 'main' into leeoniya/state-timeline-fix-dual-time-multi-series
This commit is contained in:
@@ -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,6 +1,6 @@
|
||||
module bra
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.4
|
||||
|
||||
tool github.com/unknwon/bra
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module cog
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.4
|
||||
|
||||
tool github.com/grafana/cog/cmd/cli
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module cue
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.4
|
||||
|
||||
tool cuelang.org/go/cmd/cue
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
module jb
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.4
|
||||
|
||||
tool github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module lefthook
|
||||
|
||||
go 1.24.3
|
||||
go 1.24.4
|
||||
|
||||
tool github.com/evilmartians/lefthook
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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,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
@@ -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
@@ -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,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,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,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
|
||||
|
||||
|
||||
+3
-1
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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(() => {
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './saveDashboard';
|
||||
export * from './importDashboard';
|
||||
export * from './editPaneActions';
|
||||
export * from './selectPanel';
|
||||
export * from './panelPosition';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { e2e } from '../..';
|
||||
|
||||
export const verifyPanelsStackedVertically = () => {
|
||||
let initialOffset = 0;
|
||||
e2e.components.Panels.Panel.title('New panel').each((el) => {
|
||||
if (!initialOffset) {
|
||||
initialOffset = el.offset().top;
|
||||
} else {
|
||||
const elOffset = el.offset().top;
|
||||
expect(elOffset).to.be.greaterThan(initialOffset);
|
||||
initialOffset = elOffset;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,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
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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,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,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,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,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
|
||||
|
||||
@@ -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
@@ -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,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})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -8666,6 +8666,9 @@
|
||||
"Email": {
|
||||
"type": "string"
|
||||
},
|
||||
"ExternalUID": {
|
||||
"type": "string"
|
||||
},
|
||||
"ID": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -10647,4 +10650,4 @@
|
||||
"name": "service_accounts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+35
-13
@@ -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 = {
|
||||
|
||||
+9
-4
@@ -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 && (
|
||||
|
||||
+61
-14
@@ -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
Reference in New Issue
Block a user