Merge branch 'main' of https://github.com/grafana/grafana into kristina/filter-time-transformation
This commit is contained in:
+103
-103
@@ -25,7 +25,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-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.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: lint-starlark
|
||||
trigger:
|
||||
event:
|
||||
@@ -437,7 +437,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -446,21 +446,21 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-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.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: test-backend
|
||||
- commands:
|
||||
- apk add --update build-base
|
||||
@@ -469,7 +469,7 @@ steps:
|
||||
| grep -o '\(.*\)/' | sort -u)
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: test-backend-integration
|
||||
trigger:
|
||||
event:
|
||||
@@ -524,7 +524,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- echo $(/usr/bin/github-app-external-token) > /github-app/token
|
||||
@@ -569,16 +569,16 @@ steps:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go run scripts/modowners/modowners.go check go.mod
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: validate-modfile
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make swagger-validate
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: validate-openapi-spec
|
||||
trigger:
|
||||
event:
|
||||
@@ -655,7 +655,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- '# It is required that code generated from Thema/CUE be committed and in sync
|
||||
@@ -665,7 +665,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -674,7 +674,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- yarn install --immutable || yarn install --immutable
|
||||
@@ -712,7 +712,7 @@ steps:
|
||||
- /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64
|
||||
-a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu
|
||||
-a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7
|
||||
-a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.5 --yarn-cache=$$YARN_CACHE_FOLDER
|
||||
-a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.7 --yarn-cache=$$YARN_CACHE_FOLDER
|
||||
--build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.21.3
|
||||
--tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{
|
||||
.version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD
|
||||
@@ -1110,7 +1110,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
@@ -1124,7 +1124,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -1133,14 +1133,14 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: wire-install
|
||||
- commands:
|
||||
- dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
@@ -1161,7 +1161,7 @@ steps:
|
||||
GRAFANA_TEST_DB: postgres
|
||||
PGPASSWORD: grafanatest
|
||||
POSTGRES_HOST: postgres
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: postgres-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://mysql80:3306 -timeout 120s
|
||||
@@ -1182,7 +1182,7 @@ steps:
|
||||
environment:
|
||||
GRAFANA_TEST_DB: mysql
|
||||
MYSQL_HOST: mysql80
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: mysql-8.0-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://redis:6379 -timeout 120s
|
||||
@@ -1198,7 +1198,7 @@ steps:
|
||||
- wait-for-redis
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: redis-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://memcached:11211 -timeout 120s
|
||||
@@ -1214,7 +1214,7 @@ steps:
|
||||
- wait-for-memcached
|
||||
environment:
|
||||
MEMCACHED_HOSTS: memcached:11211
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: memcached-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://mimir_backend:8080 -timeout 120s
|
||||
@@ -1230,7 +1230,7 @@ steps:
|
||||
environment:
|
||||
AM_TENANT_ID: test
|
||||
AM_URL: http://mimir_backend:8080
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: remote-alertmanager-integration-tests
|
||||
trigger:
|
||||
event:
|
||||
@@ -1312,7 +1312,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
trigger:
|
||||
event:
|
||||
@@ -1433,7 +1433,7 @@ steps:
|
||||
&& return 1; fi
|
||||
depends_on:
|
||||
- clone-enterprise
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: swagger-gen
|
||||
trigger:
|
||||
event:
|
||||
@@ -1538,7 +1538,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- '# It is required that code generated from Thema/CUE be committed and in sync
|
||||
@@ -1549,7 +1549,7 @@ steps:
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on:
|
||||
- clone-enterprise
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -1559,14 +1559,14 @@ steps:
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on:
|
||||
- clone-enterprise
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: wire-install
|
||||
- commands:
|
||||
- apk add --update build-base
|
||||
@@ -1574,7 +1574,7 @@ steps:
|
||||
- go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES}
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: sqlite-benchmark-integration-tests
|
||||
- commands:
|
||||
- apk add --update build-base
|
||||
@@ -1586,7 +1586,7 @@ steps:
|
||||
GRAFANA_TEST_DB: postgres
|
||||
PGPASSWORD: grafanatest
|
||||
POSTGRES_HOST: postgres
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: postgres-benchmark-integration-tests
|
||||
- commands:
|
||||
- apk add --update build-base
|
||||
@@ -1597,7 +1597,7 @@ steps:
|
||||
environment:
|
||||
GRAFANA_TEST_DB: mysql
|
||||
MYSQL_HOST: mysql80
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: mysql-8.0-benchmark-integration-tests
|
||||
trigger:
|
||||
event:
|
||||
@@ -1669,7 +1669,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
trigger:
|
||||
branch: main
|
||||
@@ -1852,7 +1852,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -1861,21 +1861,21 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-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.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: test-backend
|
||||
- commands:
|
||||
- apk add --update build-base
|
||||
@@ -1884,7 +1884,7 @@ steps:
|
||||
| grep -o '\(.*\)/' | sort -u)
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: test-backend-integration
|
||||
trigger:
|
||||
branch: main
|
||||
@@ -1929,22 +1929,22 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go run scripts/modowners/modowners.go check go.mod
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: validate-modfile
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make swagger-validate
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: validate-openapi-spec
|
||||
- commands:
|
||||
- ./bin/build verify-drone
|
||||
@@ -2076,7 +2076,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- '# It is required that code generated from Thema/CUE be committed and in sync
|
||||
@@ -2086,7 +2086,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -2095,7 +2095,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- yarn install --immutable || yarn install --immutable
|
||||
@@ -2132,7 +2132,7 @@ steps:
|
||||
- /src/grafana-build artifacts -a targz:grafana:linux/amd64 -a targz:grafana:linux/arm64
|
||||
-a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu
|
||||
-a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7
|
||||
-a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.5 --yarn-cache=$$YARN_CACHE_FOLDER
|
||||
-a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.7 --yarn-cache=$$YARN_CACHE_FOLDER
|
||||
--build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.21.3
|
||||
--tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{
|
||||
.version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD
|
||||
@@ -2607,7 +2607,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
@@ -2621,7 +2621,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -2630,14 +2630,14 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: wire-install
|
||||
- commands:
|
||||
- dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
@@ -2658,7 +2658,7 @@ steps:
|
||||
GRAFANA_TEST_DB: postgres
|
||||
PGPASSWORD: grafanatest
|
||||
POSTGRES_HOST: postgres
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: postgres-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://mysql80:3306 -timeout 120s
|
||||
@@ -2679,7 +2679,7 @@ steps:
|
||||
environment:
|
||||
GRAFANA_TEST_DB: mysql
|
||||
MYSQL_HOST: mysql80
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: mysql-8.0-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://redis:6379 -timeout 120s
|
||||
@@ -2695,7 +2695,7 @@ steps:
|
||||
- wait-for-redis
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: redis-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://memcached:11211 -timeout 120s
|
||||
@@ -2711,7 +2711,7 @@ steps:
|
||||
- wait-for-memcached
|
||||
environment:
|
||||
MEMCACHED_HOSTS: memcached:11211
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: memcached-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://mimir_backend:8080 -timeout 120s
|
||||
@@ -2727,7 +2727,7 @@ steps:
|
||||
environment:
|
||||
AM_TENANT_ID: test
|
||||
AM_URL: http://mimir_backend:8080
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: remote-alertmanager-integration-tests
|
||||
trigger:
|
||||
branch: main
|
||||
@@ -2996,7 +2996,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -3005,21 +3005,21 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-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.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: test-backend
|
||||
- commands:
|
||||
- apk add --update build-base
|
||||
@@ -3028,7 +3028,7 @@ steps:
|
||||
| grep -o '\(.*\)/' | sort -u)
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: test-backend-integration
|
||||
trigger:
|
||||
branch:
|
||||
@@ -3071,22 +3071,22 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go run scripts/modowners/modowners.go check go.mod
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: validate-modfile
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make swagger-validate
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: validate-openapi-spec
|
||||
trigger:
|
||||
branch:
|
||||
@@ -3165,7 +3165,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- echo $DRONE_RUNNER_NAME
|
||||
@@ -3179,7 +3179,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -3188,14 +3188,14 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: wire-install
|
||||
- commands:
|
||||
- dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
@@ -3216,7 +3216,7 @@ steps:
|
||||
GRAFANA_TEST_DB: postgres
|
||||
PGPASSWORD: grafanatest
|
||||
POSTGRES_HOST: postgres
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: postgres-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://mysql80:3306 -timeout 120s
|
||||
@@ -3237,7 +3237,7 @@ steps:
|
||||
environment:
|
||||
GRAFANA_TEST_DB: mysql
|
||||
MYSQL_HOST: mysql80
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: mysql-8.0-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://redis:6379 -timeout 120s
|
||||
@@ -3253,7 +3253,7 @@ steps:
|
||||
- wait-for-redis
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: redis-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://memcached:11211 -timeout 120s
|
||||
@@ -3269,7 +3269,7 @@ steps:
|
||||
- wait-for-memcached
|
||||
environment:
|
||||
MEMCACHED_HOSTS: memcached:11211
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: memcached-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://mimir_backend:8080 -timeout 120s
|
||||
@@ -3285,7 +3285,7 @@ steps:
|
||||
environment:
|
||||
AM_TENANT_ID: test
|
||||
AM_URL: http://mimir_backend:8080
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: remote-alertmanager-integration-tests
|
||||
trigger:
|
||||
branch:
|
||||
@@ -3385,7 +3385,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- ./bin/build artifacts docker fetch --edition oss
|
||||
@@ -3517,7 +3517,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- ./bin/build artifacts docker fetch --edition oss
|
||||
@@ -3658,7 +3658,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- ./bin/build artifacts packages --artifacts-editions=oss --tag $${DRONE_TAG} --src-bucket
|
||||
@@ -3750,7 +3750,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- yarn install --immutable || yarn install --immutable
|
||||
@@ -3850,7 +3850,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- depends_on:
|
||||
- compile-build-cmd
|
||||
@@ -3947,7 +3947,7 @@ steps:
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- ./bin/build publish grafana-com --edition oss ${DRONE_TAG}
|
||||
@@ -4009,7 +4009,7 @@ steps:
|
||||
from_secret: grafana_api_key
|
||||
GCP_KEY_BASE64:
|
||||
from_secret: gcp_key_base64
|
||||
GO_VERSION: 1.23.5
|
||||
GO_VERSION: 1.23.7
|
||||
GPG_PASSPHRASE:
|
||||
from_secret: packages_gpg_passphrase
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -4084,7 +4084,7 @@ steps:
|
||||
from_secret: grafana_api_key
|
||||
GCP_KEY_BASE64:
|
||||
from_secret: gcp_key_base64
|
||||
GO_VERSION: 1.23.5
|
||||
GO_VERSION: 1.23.7
|
||||
GPG_PASSPHRASE:
|
||||
from_secret: packages_gpg_passphrase
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -4201,7 +4201,7 @@ steps:
|
||||
from_secret: grafana_api_key
|
||||
GCP_KEY_BASE64:
|
||||
from_secret: gcp_key_base64
|
||||
GO_VERSION: 1.23.5
|
||||
GO_VERSION: 1.23.7
|
||||
GPG_PASSPHRASE:
|
||||
from_secret: packages_gpg_passphrase
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -4352,7 +4352,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -4361,21 +4361,21 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-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.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: test-backend
|
||||
- commands:
|
||||
- apk add --update build-base
|
||||
@@ -4384,7 +4384,7 @@ steps:
|
||||
| grep -o '\(.*\)/' | sort -u)
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: test-backend-integration
|
||||
trigger:
|
||||
cron:
|
||||
@@ -4438,7 +4438,7 @@ steps:
|
||||
from_secret: grafana_api_key
|
||||
GCP_KEY_BASE64:
|
||||
from_secret: gcp_key_base64
|
||||
GO_VERSION: 1.23.5
|
||||
GO_VERSION: 1.23.7
|
||||
GPG_PASSPHRASE:
|
||||
from_secret: packages_gpg_passphrase
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -4582,7 +4582,7 @@ steps:
|
||||
from_secret: grafana_api_key
|
||||
GCP_KEY_BASE64:
|
||||
from_secret: gcp_key_base64
|
||||
GO_VERSION: 1.23.5
|
||||
GO_VERSION: 1.23.7
|
||||
GPG_PASSPHRASE:
|
||||
from_secret: packages_gpg_passphrase
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -4689,7 +4689,7 @@ steps:
|
||||
- export GITHUB_TOKEN=$(cat /github-app/token)
|
||||
- dagger run --silent /src/grafana-build artifacts -a $${ARTIFACTS} --grafana-ref=$${GRAFANA_REF}
|
||||
--enterprise-ref=$${ENTERPRISE_REF} --grafana-repo=$${GRAFANA_REPO} --version=$${VERSION}
|
||||
--go-version=1.23.5
|
||||
--go-version=1.23.7
|
||||
depends_on:
|
||||
- github-app-generate-token
|
||||
environment:
|
||||
@@ -4710,7 +4710,7 @@ steps:
|
||||
from_secret: grafana_api_key
|
||||
GCP_KEY_BASE64:
|
||||
from_secret: gcp_key_base64
|
||||
GO_VERSION: 1.23.5
|
||||
GO_VERSION: 1.23.7
|
||||
GPG_PASSPHRASE:
|
||||
from_secret: packages_gpg_passphrase
|
||||
GPG_PRIVATE_KEY:
|
||||
@@ -4848,7 +4848,7 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-cue
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-cue
|
||||
- commands:
|
||||
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
|
||||
@@ -4857,14 +4857,14 @@ steps:
|
||||
- apk add --update make
|
||||
- CODEGEN_VERIFY=1 make gen-jsonnet
|
||||
depends_on: []
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: verify-gen-jsonnet
|
||||
- commands:
|
||||
- apk add --update make
|
||||
- make gen-go
|
||||
depends_on:
|
||||
- verify-gen-cue
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: wire-install
|
||||
- commands:
|
||||
- dockerize -wait tcp://postgres:5432 -timeout 120s
|
||||
@@ -4885,7 +4885,7 @@ steps:
|
||||
GRAFANA_TEST_DB: postgres
|
||||
PGPASSWORD: grafanatest
|
||||
POSTGRES_HOST: postgres
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: postgres-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://mysql80:3306 -timeout 120s
|
||||
@@ -4906,7 +4906,7 @@ steps:
|
||||
environment:
|
||||
GRAFANA_TEST_DB: mysql
|
||||
MYSQL_HOST: mysql80
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: mysql-8.0-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://redis:6379 -timeout 120s
|
||||
@@ -4922,7 +4922,7 @@ steps:
|
||||
- wait-for-redis
|
||||
environment:
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: redis-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://memcached:11211 -timeout 120s
|
||||
@@ -4938,7 +4938,7 @@ steps:
|
||||
- wait-for-memcached
|
||||
environment:
|
||||
MEMCACHED_HOSTS: memcached:11211
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: memcached-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://mimir_backend:8080 -timeout 120s
|
||||
@@ -4954,7 +4954,7 @@ steps:
|
||||
environment:
|
||||
AM_TENANT_ID: test
|
||||
AM_URL: http://mimir_backend:8080
|
||||
image: golang:1.23.5-alpine
|
||||
image: golang:1.23.7-alpine
|
||||
name: remote-alertmanager-integration-tests
|
||||
trigger:
|
||||
event:
|
||||
@@ -5257,7 +5257,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.23.5-alpine
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.23.7-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
|
||||
@@ -5295,7 +5295,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.23.5-alpine
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.23.7-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
|
||||
@@ -5564,6 +5564,6 @@ kind: secret
|
||||
name: gcr_credentials
|
||||
---
|
||||
kind: signature
|
||||
hmac: f16a4715c7a4e6a4ffb1fe041b42fb966310fd5da455239614e9a239493aff82
|
||||
hmac: 33f2e5615dfd7889899f9f8f16f7716190fa637fe98f1efd7e29607f8946be7d
|
||||
|
||||
...
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.5'
|
||||
go-version: '1.23.7'
|
||||
cache: true
|
||||
|
||||
- name: Validate go.mod
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.5'
|
||||
go-version: '1.23.7'
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
+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.23.5-alpine
|
||||
ARG GO_IMAGE=golang:1.23.7-alpine
|
||||
|
||||
# Default to building locally
|
||||
ARG GO_SRC=go-builder
|
||||
|
||||
@@ -8,7 +8,7 @@ WIRE_TAGS = "oss"
|
||||
include .bingo/Variables.mk
|
||||
|
||||
GO = go
|
||||
GO_VERSION = 1.23.5
|
||||
GO_VERSION = 1.23.7
|
||||
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.23.4
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.31.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/apps/alerting/notifications
|
||||
|
||||
go 1.23.5
|
||||
go 1.23.7
|
||||
|
||||
replace github.com/grafana/grafana => ../../..
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/apps/investigations
|
||||
|
||||
go 1.23.4
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.31.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/apps/playlist
|
||||
|
||||
go 1.23.4
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/grafana/grafana-app-sdk v0.31.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module high-card
|
||||
|
||||
go 1.22.4
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.20.2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module utf8-support
|
||||
|
||||
go 1.22.4
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23.5
|
||||
FROM golang:1.23.7
|
||||
|
||||
ADD main.go /go/src/webhook/main.go
|
||||
|
||||
|
||||
@@ -20,8 +20,19 @@ Label-Based Access Control (LBAC) allows fine-grained access control to data sou
|
||||
|
||||
## Supported Data Sources
|
||||
|
||||
### Feature availability
|
||||
|
||||
LBAC for data sources is currently generally available for `Loki` and in **experimental** for `Prometheus`. Support for additional data sources may be added in future updates.
|
||||
|
||||
| Data source | [Grafana Cloud](/docs/grafana-cloud) | [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}) |
|
||||
| ----------- | ------------------------------------ | ----------------------------------------------------------------------------- |
|
||||
| Loki | GA | GA |
|
||||
| Prometheus | PrivatePreview | PrivatePreview |
|
||||
|
||||
{{% admonition type="note" %}}
|
||||
For enterprise this feature requires Grafana Enterprise Metrics (GEM) or Grafana Enterprise Logs (GEL) to function.
|
||||
{{% /admonition %}}
|
||||
|
||||
**LBAC for data sources offers:**
|
||||
|
||||
- Team-based access control using `LogQL` rules.
|
||||
|
||||
@@ -117,6 +117,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `improvedExternalSessionHandling` | Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves. |
|
||||
| `elasticsearchCrossClusterSearch` | Enables cross cluster search in the Elasticsearch datasource |
|
||||
| `improvedExternalSessionHandlingSAML` | Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly. |
|
||||
| `alertRuleRestore` | Enables the alert rule restore feature |
|
||||
|
||||
## Experimental feature toggles
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana
|
||||
|
||||
go 1.23.5
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1 // @grafana/observability-traces-and-profiling
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
go 1.23.5
|
||||
go 1.23.7
|
||||
|
||||
// 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.23.1
|
||||
go 1.23.7
|
||||
|
||||
require k8s.io/code-generator v0.32.0
|
||||
|
||||
|
||||
@@ -255,4 +255,5 @@ export interface FeatureToggles {
|
||||
newShareReportDrawer?: boolean;
|
||||
rendererDisableAppPluginsPreload?: boolean;
|
||||
assetSriChecks?: boolean;
|
||||
alertRuleRestore?: boolean;
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ export enum PluginExtensionPoints {
|
||||
DataSourceConfig = 'grafana/datasources/config',
|
||||
ExploreToolbarAction = 'grafana/explore/toolbar/action',
|
||||
UserProfileTab = 'grafana/user/profile/tab',
|
||||
TraceViewDetails = 'grafana/traceview/details',
|
||||
}
|
||||
|
||||
export type PluginExtensionPanelContext = {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/aggregator
|
||||
|
||||
go 1.23.5
|
||||
|
||||
toolchain go1.23.6
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/emicklei/go-restful/v3 v3.11.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/apimachinery
|
||||
|
||||
go 1.23.1
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 // @grafana/identity-access-team
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/apiserver
|
||||
|
||||
go 1.23.4
|
||||
|
||||
toolchain go1.23.6
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/build
|
||||
|
||||
go 1.23.1
|
||||
go 1.23.7
|
||||
|
||||
// Override docker/docker to avoid:
|
||||
// go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/build/wire
|
||||
|
||||
go 1.23.1
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/codegen
|
||||
|
||||
go 1.23.1
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
cuelang.org/go v0.11.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/plugins/codegen
|
||||
|
||||
go 1.23.1
|
||||
go 1.23.7
|
||||
|
||||
replace github.com/grafana/grafana/pkg/codegen => ../../codegen
|
||||
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/promlib
|
||||
|
||||
go 1.23.5
|
||||
|
||||
toolchain go1.23.6
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/semconv
|
||||
|
||||
go 1.23.1
|
||||
go 1.23.7
|
||||
|
||||
require go.opentelemetry.io/otel v1.34.0
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/fullstorydev/grpchan/inprocgrpc"
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
@@ -71,12 +70,7 @@ func NewServer(cfg setting.ZanzanaServerSettings, openfga OpenFGAServer, logger
|
||||
}
|
||||
|
||||
func (s *Server) IsHealthy(ctx context.Context) (bool, error) {
|
||||
// FIXME: get back to openfga.IsReady() when issue is fixed
|
||||
// https://github.com/openfga/openfga/issues/2251
|
||||
_, err := s.openfga.ListStores(ctx, &openfgav1.ListStoresRequest{
|
||||
PageSize: wrapperspb.Int32(1),
|
||||
})
|
||||
return err == nil, nil
|
||||
return s.openfga.IsReady(ctx)
|
||||
}
|
||||
|
||||
func (s *Server) getContextuals(subject string) (*openfgav1.ContextualTupleKeys, error) {
|
||||
|
||||
@@ -1782,6 +1782,13 @@ var (
|
||||
Owner: grafanaFrontendOpsWG,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "alertRuleRestore",
|
||||
Description: "Enables the alert rule restore feature",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
Owner: grafanaAlertingSquad,
|
||||
Expression: "true", // enabled by default
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -236,3 +236,4 @@ alertingRuleVersionHistoryRestore,GA,@grafana/alerting-squad,false,false,true
|
||||
newShareReportDrawer,experimental,@grafana/sharing-squad,false,false,false
|
||||
rendererDisableAppPluginsPreload,experimental,@grafana/sharing-squad,false,false,true
|
||||
assetSriChecks,experimental,@grafana/frontend-ops,false,false,true
|
||||
alertRuleRestore,preview,@grafana/alerting-squad,false,false,false
|
||||
|
||||
|
@@ -954,4 +954,8 @@ const (
|
||||
// FlagAssetSriChecks
|
||||
// Enables SRI checks for Grafana JavaScript assets
|
||||
FlagAssetSriChecks = "assetSriChecks"
|
||||
|
||||
// FlagAlertRuleRestore
|
||||
// Enables the alert rule restore feature
|
||||
FlagAlertRuleRestore = "alertRuleRestore"
|
||||
)
|
||||
|
||||
@@ -108,6 +108,22 @@
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertRuleRestore",
|
||||
"resourceVersion": "1741127758142",
|
||||
"creationTimestamp": "2025-03-04T22:29:36Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-03-04 22:35:58.1421143 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the alert rule restore feature",
|
||||
"stage": "preview",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertStateHistoryLokiOnly",
|
||||
|
||||
@@ -1930,8 +1930,9 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
|
||||
Cfg: setting.UnifiedAlertingSettings{
|
||||
BaseInterval: time.Second * 10,
|
||||
},
|
||||
FolderService: folderService,
|
||||
Bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
FolderService: folderService,
|
||||
Bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
FeatureToggles: featuremgmt.WithFeatures(),
|
||||
}
|
||||
user := &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
|
||||
@@ -157,7 +157,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceU
|
||||
rulesToDelete = append(rulesToDelete, uid...)
|
||||
}
|
||||
if len(rulesToDelete) > 0 {
|
||||
err := srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.GetOrgID(), rulesToDelete...)
|
||||
err := srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.GetOrgID(), ngmodels.NewUserUID(c.SignedInUser), rulesToDelete...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -461,7 +461,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey
|
||||
UIDs = append(UIDs, rule.UID)
|
||||
}
|
||||
|
||||
if err = srv.store.DeleteAlertRulesByUID(tranCtx, c.SignedInUser.GetOrgID(), UIDs...); err != nil {
|
||||
if err = srv.store.DeleteAlertRulesByUID(tranCtx, c.SignedInUser.GetOrgID(), ngmodels.NewUserUID(c.SignedInUser), UIDs...); err != nil {
|
||||
return fmt.Errorf("failed to delete rules: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
||||
deleteCommands := getRecordedCommand(ruleStore)
|
||||
require.Len(t, deleteCommands, 1)
|
||||
cmd := deleteCommands[0]
|
||||
actualUIDs := cmd.Params[1].([]string)
|
||||
actualUIDs := cmd.Params[2].([]string)
|
||||
require.Len(t, actualUIDs, len(expectedRules))
|
||||
for _, rule := range expectedRules {
|
||||
require.Containsf(t, actualUIDs, rule.UID, "Rule %s was expected to be deleted but it wasn't", rule.UID)
|
||||
|
||||
@@ -28,7 +28,7 @@ type RuleStore interface {
|
||||
// and return the map of uuid to id.
|
||||
InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.AlertRule) ([]ngmodels.AlertRuleKeyWithId, error)
|
||||
UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.UpdateRule) error
|
||||
DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error
|
||||
DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *ngmodels.UserUID, ruleUID ...string) error
|
||||
|
||||
// IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace uids
|
||||
IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error)
|
||||
|
||||
@@ -556,7 +556,7 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity
|
||||
})
|
||||
}
|
||||
}
|
||||
if err := service.deleteRules(ctx, user.GetOrgID(), delta.Delete...); err != nil {
|
||||
if err := service.deleteRules(ctx, user, delta.Delete...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -749,7 +749,7 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, user ident
|
||||
// This is different from deleting groups. We delete the rules directly rather than persisting a delta here to keep the semantics the same.
|
||||
// TODO: Either persist a delta here as a breaking change, or deprecate this endpoint in favor of the group endpoint.
|
||||
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
return service.deleteRules(ctx, user.GetOrgID(), rule)
|
||||
return service.deleteRules(ctx, user, rule)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -775,18 +775,18 @@ func (service *AlertRuleService) checkLimitsTransactionCtx(ctx context.Context,
|
||||
}
|
||||
|
||||
// deleteRules deletes a set of target rules and associated data, while checking for database consistency.
|
||||
func (service *AlertRuleService) deleteRules(ctx context.Context, orgID int64, targets ...*models.AlertRule) error {
|
||||
func (service *AlertRuleService) deleteRules(ctx context.Context, user identity.Requester, targets ...*models.AlertRule) error {
|
||||
uids := make([]string, 0, len(targets))
|
||||
for _, tgt := range targets {
|
||||
if tgt != nil {
|
||||
uids = append(uids, tgt.UID)
|
||||
}
|
||||
}
|
||||
if err := service.ruleStore.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil {
|
||||
if err := service.ruleStore.DeleteAlertRulesByUID(ctx, user.GetOrgID(), models.NewUserUID(user), uids...); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, uid := range uids {
|
||||
if err := service.provenanceStore.DeleteProvenance(ctx, &models.AlertRule{UID: uid}, orgID); err != nil {
|
||||
if err := service.provenanceStore.DeleteProvenance(ctx, &models.AlertRule{UID: uid}, user.GetOrgID()); err != nil {
|
||||
// We failed to clean up the record, but this doesn't break things. Log it and move on.
|
||||
service.log.Warn("Failed to delete provenance record for rule: %w", err)
|
||||
}
|
||||
|
||||
@@ -1726,7 +1726,7 @@ func TestDeleteRuleGroup(t *testing.T) {
|
||||
func TestDeleteRuleGroups(t *testing.T) {
|
||||
orgID1 := rand.Int63()
|
||||
orgID2 := rand.Int63()
|
||||
u := &user.SignedInUser{OrgID: orgID1}
|
||||
u := &user.SignedInUser{OrgID: orgID1, UserUID: "test-test"}
|
||||
|
||||
// Create groups across different orgs and namespaces
|
||||
groupKey1 := models.AlertRuleGroupKey{
|
||||
@@ -1805,6 +1805,7 @@ func TestDeleteRuleGroups(t *testing.T) {
|
||||
// Verify only rules from group1 in org1 were deleted
|
||||
deletes := getDeletedRules(t, ruleStore)
|
||||
require.Len(t, deletes, 1)
|
||||
require.Equal(t, "test-test", deletes[0].userID)
|
||||
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
|
||||
})
|
||||
|
||||
@@ -2045,8 +2046,9 @@ func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
|
||||
}
|
||||
|
||||
type deleteRuleOperation struct {
|
||||
orgID int64
|
||||
uids []string
|
||||
orgID int64
|
||||
userID string
|
||||
uids []string
|
||||
}
|
||||
|
||||
func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation {
|
||||
@@ -2058,12 +2060,20 @@ func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOpera
|
||||
orgID, ok := q.Params[0].(int64)
|
||||
require.True(t, ok, "orgID parameter should be int64")
|
||||
|
||||
uids, ok := q.Params[1].([]string)
|
||||
uid := ""
|
||||
userUID, ok := q.Params[1].(*models.UserUID)
|
||||
require.True(t, ok, "parameter should be UserUID")
|
||||
if userUID != nil {
|
||||
uid = string(*userUID)
|
||||
}
|
||||
|
||||
uids, ok := q.Params[2].([]string)
|
||||
require.True(t, ok, "uids parameter should be []string")
|
||||
|
||||
operations = append(operations, deleteRuleOperation{
|
||||
orgID: orgID,
|
||||
uids: uids,
|
||||
orgID: orgID,
|
||||
userID: uid,
|
||||
uids: uids,
|
||||
})
|
||||
}
|
||||
return operations
|
||||
@@ -2077,9 +2087,10 @@ func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRul
|
||||
Cfg: setting.UnifiedAlertingSettings{
|
||||
BaseInterval: time.Second * 10,
|
||||
},
|
||||
Logger: log.NewNopLogger(),
|
||||
FolderService: folderService,
|
||||
Bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
Logger: log.NewNopLogger(),
|
||||
FolderService: folderService,
|
||||
Bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
FeatureToggles: featuremgmt.WithFeatures(),
|
||||
}
|
||||
// store := fakes.NewRuleStore(t)
|
||||
quotas := MockQuotaChecker{}
|
||||
|
||||
@@ -35,7 +35,7 @@ type RuleStore interface {
|
||||
GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error)
|
||||
InsertAlertRules(ctx context.Context, user *models.UserUID, rule []models.AlertRule) ([]models.AlertRuleKeyWithId, error)
|
||||
UpdateAlertRules(ctx context.Context, user *models.UserUID, rule []models.UpdateRule) error
|
||||
DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error
|
||||
DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *models.UserUID, ruleUID ...string) error
|
||||
GetAlertRulesGroupByRuleUID(ctx context.Context, query *models.GetAlertRulesGroupByRuleUIDQuery) ([]*models.AlertRule, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ var (
|
||||
)
|
||||
|
||||
// DeleteAlertRulesByUID is a handler for deleting an alert rule.
|
||||
func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error {
|
||||
func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *ngmodels.UserUID, ruleUID ...string) error {
|
||||
if len(ruleUID) == 0 {
|
||||
return nil
|
||||
}
|
||||
logger := st.Logger.New("org_id", orgID, "rule_uids", ruleUID)
|
||||
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
rows, err := sess.Table(alertRule{}).Where("org_id = ?", orgID).In("uid", ruleUID).Delete(alertRule{})
|
||||
@@ -57,12 +60,6 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI
|
||||
})
|
||||
}
|
||||
|
||||
rows, err = sess.Table(alertRuleVersion{}).Where("rule_org_id = ?", orgID).In("rule_uid", ruleUID).Delete(alertRule{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("Deleted alert rule versions", "count", rows)
|
||||
|
||||
rows, err = sess.Table("alert_instance").Where("rule_org_id = ?", orgID).In("rule_uid", ruleUID).Delete(alertRule{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -75,10 +72,78 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI
|
||||
}
|
||||
logger.Debug("Deleted alert rule state", "count", rows)
|
||||
|
||||
var versions []alertRuleVersion
|
||||
if st.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertRuleRestore) {
|
||||
versions, err = st.getLatestVersionOfRulesByUID(ctx, orgID, ruleUID)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get latest version of deleted alert rules. The recovery will not be possible", "error", err)
|
||||
}
|
||||
for idx := range versions {
|
||||
version := &versions[idx]
|
||||
version.ID = 0
|
||||
version.RuleUID = ""
|
||||
version.Created = TimeNow()
|
||||
version.CreatedBy = nil
|
||||
if user != nil {
|
||||
version.CreatedBy = util.Pointer(string(*user))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows, err = sess.Table(alertRuleVersion{}).Where("rule_org_id = ?", orgID).In("rule_uid", ruleUID).Delete(alertRule{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("Deleted alert rule versions", "count", rows)
|
||||
|
||||
if len(versions) > 0 {
|
||||
_, err = sess.Insert(versions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to persist deleted rule for recovery: %w", err)
|
||||
}
|
||||
logger.Debug("Inserted alert rule versions for recovery", "count", len(versions))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (st DBstore) getLatestVersionOfRulesByUID(ctx context.Context, orgID int64, ruleUIDs []string) ([]alertRuleVersion, error) {
|
||||
var result []alertRuleVersion
|
||||
err := st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
args, in := getINSubQueryArgs(ruleUIDs)
|
||||
// take only the latest versions of each rule by GUID
|
||||
rows, err := sess.SQL(fmt.Sprintf(`
|
||||
SELECT v1.* FROM alert_rule_version AS v1
|
||||
INNER JOIN (
|
||||
SELECT rule_guid, MAX(id) AS id
|
||||
FROM alert_rule_version
|
||||
WHERE rule_org_id = ?
|
||||
AND rule_uid IN (%s)
|
||||
GROUP BY rule_guid
|
||||
) AS v2 ON v1.rule_guid = v2.rule_guid AND v1.id = v2.id
|
||||
`, strings.Join(in, ",")), append([]any{orgID}, args...)...).Rows(new(alertRuleVersion))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result = make([]alertRuleVersion, 0, len(ruleUIDs))
|
||||
for rows.Next() {
|
||||
rule := new(alertRuleVersion)
|
||||
err = rows.Scan(rule)
|
||||
if err != nil {
|
||||
st.Logger.Error("Invalid rule version found in DB store, ignoring it", "func", "getLatestVersionOfRulesByUID", "error", err)
|
||||
continue
|
||||
}
|
||||
result = append(result, *rule)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace. Returns all rules that belong to the namespaces
|
||||
func (st DBstore) IncreaseVersionForAllRulesInNamespaces(ctx context.Context, orgID int64, namespaceUIDs []string) ([]ngmodels.AlertRuleKeyWithVersion, error) {
|
||||
var keys []ngmodels.AlertRuleKeyWithVersion
|
||||
@@ -820,7 +885,7 @@ func (st DBstore) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs [
|
||||
}
|
||||
}
|
||||
|
||||
if err := st.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil {
|
||||
if err := st.DeleteAlertRulesByUID(ctx, orgID, ngmodels.NewUserUID(user), uids...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/testutil"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
@@ -685,7 +686,6 @@ func TestIntegration_DeleteInFolder(t *testing.T) {
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
|
||||
rule := createRule(t, store, nil)
|
||||
|
||||
t.Run("should not be able to delete folder without permissions to delete rules", func(t *testing.T) {
|
||||
@@ -714,6 +714,9 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) {
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
cfg.UnifiedAlerting.BaseInterval = 1 * time.Second
|
||||
cfg.UnifiedAlerting.RuleVersionRecordLimit = -1
|
||||
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
logger := log.New("test-dbstore")
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, &fakeBus{})
|
||||
@@ -742,7 +745,7 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
err := store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, rule.UID)
|
||||
err := store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, &models.AlertingUserUID, rule.UID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, called)
|
||||
})
|
||||
@@ -769,7 +772,7 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) {
|
||||
require.Len(t, savedInstances, 1)
|
||||
|
||||
// Delete the rule
|
||||
err = store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, rule.UID)
|
||||
err = store.DeleteAlertRulesByUID(context.Background(), rule.OrgID, &models.AlertingUserUID, rule.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now there should be no alert rule state
|
||||
@@ -780,6 +783,71 @@ func TestIntegration_DeleteAlertRulesByUID(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, savedInstances)
|
||||
})
|
||||
|
||||
t.Run("should remove all version and insert one with empty rule_uid", func(t *testing.T) {
|
||||
orgID := int64(rand.Intn(1000))
|
||||
gen = gen.With(gen.WithOrgID(orgID))
|
||||
// Create a new store to pass the custom bus to check the signal
|
||||
b := &fakeBus{}
|
||||
logger := log.New("test-dbstore")
|
||||
|
||||
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
|
||||
store.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertRuleRestore)
|
||||
|
||||
result, err := store.InsertAlertRules(context.Background(), &models.AlertingUserUID, gen.GenerateMany(3))
|
||||
uids := make([]string, 0, len(result))
|
||||
for _, rule := range result {
|
||||
uids = append(uids, rule.UID)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
rules, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{OrgID: orgID, RuleUIDs: uids})
|
||||
require.NoError(t, err)
|
||||
|
||||
updates := make([]models.UpdateRule, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
rule2 := models.CopyRule(rule, gen.WithTitle(util.GenerateShortUID()))
|
||||
updates = append(updates, models.UpdateRule{
|
||||
Existing: rule,
|
||||
New: *rule2,
|
||||
})
|
||||
}
|
||||
err = store.UpdateAlertRules(context.Background(), &models.AlertingUserUID, updates)
|
||||
require.NoError(t, err)
|
||||
|
||||
versions, err := store.GetAlertRuleVersions(context.Background(), orgID, rules[0].GUID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, versions, 2)
|
||||
|
||||
err = store.DeleteAlertRulesByUID(context.Background(), orgID, util.Pointer(models.UserUID("test")), uids...)
|
||||
require.NoError(t, err)
|
||||
|
||||
guids := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
guids = append(guids, rule.GUID)
|
||||
}
|
||||
|
||||
_ = sqlStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||
var versions []alertRuleVersion
|
||||
err = sess.Table(alertRuleVersion{}).Where(`rule_uid = ''`).In("rule_guid", guids).Find(&versions)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, versions, len(rules)) // should be one version per GUID
|
||||
|
||||
for _, version := range versions {
|
||||
assert.Equal(t, "", version.RuleUID)
|
||||
assert.Equal(t, "test", *version.CreatedBy)
|
||||
// Remove the GUID from guids
|
||||
for i, guid := range guids {
|
||||
if guid == version.RuleGUID {
|
||||
guids = append(guids[:i], guids[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure that guids is empty
|
||||
assert.Empty(t, guids, "Some rules are left unrecoverable")
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationInsertAlertRules(t *testing.T) {
|
||||
@@ -1894,11 +1962,12 @@ func createTestStore(
|
||||
bus bus.Bus,
|
||||
) *DBstore {
|
||||
return &DBstore{
|
||||
SQLStore: sqlStore,
|
||||
FolderService: folderService,
|
||||
Logger: logger,
|
||||
Cfg: cfg,
|
||||
Bus: bus,
|
||||
SQLStore: sqlStore,
|
||||
FolderService: folderService,
|
||||
Logger: logger,
|
||||
Cfg: cfg,
|
||||
Bus: bus,
|
||||
FeatureToggles: featuremgmt.WithFeatures(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,10 +102,10 @@ func (f *RuleStore) GetRecordedCommands(predicate func(cmd any) (any, bool)) []a
|
||||
return result
|
||||
}
|
||||
|
||||
func (f *RuleStore) DeleteAlertRulesByUID(_ context.Context, orgID int64, UIDs ...string) error {
|
||||
func (f *RuleStore) DeleteAlertRulesByUID(_ context.Context, orgID int64, user *models.UserUID, UIDs ...string) error {
|
||||
f.RecordedOps = append(f.RecordedOps, GenericRecordedQuery{
|
||||
Name: "DeleteAlertRulesByUID",
|
||||
Params: []any{orgID, UIDs},
|
||||
Params: []any{orgID, user, UIDs},
|
||||
})
|
||||
|
||||
rules := f.Rules[orgID]
|
||||
|
||||
@@ -20,6 +20,7 @@ type FakeOrgService struct {
|
||||
ExpectedOrgUsers []*org.OrgUserDTO
|
||||
ExpectedSearchOrgUsersResult *org.SearchOrgUsersQueryResult
|
||||
ExpectedOrgListResponse OrgListResponse
|
||||
SearchOrgUsersFn func(context.Context, *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error)
|
||||
}
|
||||
|
||||
func NewOrgServiceFake() *FakeOrgService {
|
||||
@@ -98,6 +99,9 @@ func (f *FakeOrgService) RemoveOrgUser(ctx context.Context, cmd *org.RemoveOrgUs
|
||||
}
|
||||
|
||||
func (f *FakeOrgService) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) {
|
||||
if f.SearchOrgUsersFn != nil {
|
||||
return f.SearchOrgUsersFn(ctx, query)
|
||||
}
|
||||
return f.ExpectedSearchOrgUsersResult, f.ExpectedError
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -100,16 +101,28 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use
|
||||
return nil, user.ErrEmptyUsernameAndEmail.Errorf("user cannot be created with empty username and email")
|
||||
}
|
||||
|
||||
cmdOrg := org.GetOrgIDForNewUserCommand{
|
||||
Email: cmd.Email,
|
||||
Login: cmd.Login,
|
||||
OrgID: cmd.OrgID,
|
||||
OrgName: cmd.OrgName,
|
||||
SkipOrgSetup: cmd.SkipOrgSetup,
|
||||
}
|
||||
orgID, err := s.orgService.GetIDForNewUser(ctx, cmdOrg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// if the user is provisioned, use the org ID from the requester
|
||||
var orgID int64
|
||||
var err error
|
||||
if cmd.IsProvisioned {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgID = requester.GetOrgID()
|
||||
} else {
|
||||
cmdOrg := org.GetOrgIDForNewUserCommand{
|
||||
Email: cmd.Email,
|
||||
Login: cmd.Login,
|
||||
OrgID: cmd.OrgID,
|
||||
OrgName: cmd.OrgName,
|
||||
SkipOrgSetup: cmd.SkipOrgSetup,
|
||||
}
|
||||
orgID, err = s.orgService.GetIDForNewUser(ctx, cmdOrg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if cmd.Email == "" {
|
||||
cmd.Email = cmd.Login
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/storage/unified/apistore
|
||||
|
||||
go 1.23.5
|
||||
go 1.23.7
|
||||
|
||||
replace (
|
||||
github.com/grafana/grafana => ../../../..
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/storage/unified/resource
|
||||
|
||||
go 1.23.5
|
||||
go 1.23.7
|
||||
|
||||
replace (
|
||||
github.com/grafana/grafana => ../../../..
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module github.com/grafana/grafana/pkg/util/xorm
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
go 1.23.7
|
||||
|
||||
require (
|
||||
github.com/googleapis/go-sql-spanner v1.11.1
|
||||
|
||||
@@ -43,11 +43,18 @@ export const AnnoKeyFolderUrl = 'grafana.app/folderUrl';
|
||||
export const AnnoKeyMessage = 'grafana.app/message';
|
||||
export const AnnoKeySlug = 'grafana.app/slug';
|
||||
|
||||
// Identify where values came from
|
||||
export const AnnoKeyRepoName = 'grafana.app/repoName';
|
||||
export const AnnoKeyRepoPath = 'grafana.app/repoPath';
|
||||
export const AnnoKeyRepoHash = 'grafana.app/repoHash';
|
||||
export const AnnoKeyRepoTimestamp = 'grafana.app/repoTimestamp';
|
||||
export enum ManagerKind {
|
||||
Repo = 'repo',
|
||||
Terraform = 'terraform',
|
||||
Kubectl = 'kubectl',
|
||||
Plugin = 'plugin',
|
||||
}
|
||||
|
||||
export const AnnoKeyManagerKind = 'grafana.app/managedBy';
|
||||
export const AnnoKeyManagerIdentity = 'grafana.app/managerId';
|
||||
export const AnnoKeySourcePath = 'grafana.app/sourcePath';
|
||||
export const AnnoKeySourceChecksum = 'grafana.app/sourceChecksum';
|
||||
export const AnnoKeySourceTimestamp = 'grafana.app/sourceTimestamp';
|
||||
|
||||
export const AnnoKeySavedFromUI = 'grafana.app/saved-from-ui';
|
||||
export const AnnoKeyDashboardNotFound = 'grafana.app/dashboard-not-found';
|
||||
@@ -66,10 +73,11 @@ type GrafanaAnnotations = {
|
||||
[AnnoKeyFolder]?: string;
|
||||
[AnnoKeySlug]?: string;
|
||||
|
||||
[AnnoKeyRepoName]?: string;
|
||||
[AnnoKeyRepoPath]?: string;
|
||||
[AnnoKeyRepoHash]?: string;
|
||||
[AnnoKeyRepoTimestamp]?: string;
|
||||
[AnnoKeyManagerKind]?: ManagerKind;
|
||||
[AnnoKeyManagerIdentity]?: string;
|
||||
[AnnoKeySourcePath]?: string;
|
||||
[AnnoKeySourceChecksum]?: string;
|
||||
[AnnoKeySourceTimestamp]?: string;
|
||||
};
|
||||
|
||||
// Annotations provided by the front-end client
|
||||
|
||||
@@ -42,6 +42,7 @@ import { VariablesChanged } from 'app/features/variables/types';
|
||||
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, AnnoKeySourcePath, ManagerKind } from '../../apiserver/types';
|
||||
import { DashboardEditPane } from '../edit-pane/DashboardEditPane';
|
||||
import { PanelEditor } from '../panel-edit/PanelEditor';
|
||||
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
|
||||
@@ -741,6 +742,35 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
getDashboardChanges(saveTimeRange?: boolean, saveVariables?: boolean, saveRefresh?: boolean): DashboardChangeInfo {
|
||||
return this._serializer.getDashboardChangesFromScene(this, { saveTimeRange, saveVariables, saveRefresh });
|
||||
}
|
||||
|
||||
getManagerKind(): ManagerKind | undefined {
|
||||
return this.state.meta.k8s?.annotations?.[AnnoKeyManagerKind];
|
||||
}
|
||||
|
||||
isManaged() {
|
||||
return Boolean(this.getManagerKind());
|
||||
}
|
||||
|
||||
isManagedRepository() {
|
||||
return Boolean(this.getManagerKind() === ManagerKind.Repo);
|
||||
}
|
||||
|
||||
getPath() {
|
||||
return this.state.meta.k8s?.annotations?.[AnnoKeySourcePath];
|
||||
}
|
||||
|
||||
setManager(kind: ManagerKind, id: string) {
|
||||
this.setState({
|
||||
meta: {
|
||||
k8s: {
|
||||
annotations: {
|
||||
[AnnoKeyManagerKind]: kind,
|
||||
[AnnoKeyManagerIdentity]: id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DashboardVariableDependency implements SceneVariableDependencyConfigLike {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Badge } from '@grafana/ui';
|
||||
import { AnnoKeyManagerIdentity, AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types';
|
||||
import { DashboardMeta } from 'app/types';
|
||||
|
||||
export default function ManagedDashboardNavBarBadge({ meta }: { meta: DashboardMeta }) {
|
||||
const obj = meta.k8s;
|
||||
if (!obj?.annotations) {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = 'Provisioned';
|
||||
const kind = obj.annotations?.[AnnoKeyManagerKind];
|
||||
const id = obj.annotations?.[AnnoKeyManagerIdentity];
|
||||
switch (kind) {
|
||||
case ManagerKind.Terraform:
|
||||
text = 'Terraform';
|
||||
case ManagerKind.Kubectl:
|
||||
text = 'Kubectl';
|
||||
case ManagerKind.Plugin:
|
||||
text = `Plugin: ${id}`;
|
||||
}
|
||||
return <Badge color="darkgrey" icon="exchange-alt" text={text} key="provisioned-dashboard-button-badge" />;
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import { isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
|
||||
import ManagedDashboardNavBarBadge from './ManagedDashboardNavBarBadge';
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardScene;
|
||||
@@ -69,7 +70,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
const isViewingPanel = Boolean(viewPanelScene);
|
||||
const isEditedPanelDirty = usePanelEditDirty(editPanel);
|
||||
const isEditingLibraryPanel = editPanel && isLibraryPanel(editPanel.state.panelRef.resolve());
|
||||
const isNew = !Boolean(uid);
|
||||
const isNew = !Boolean(uid || dashboard.isManaged());
|
||||
|
||||
const hasCopiedPanel = store.exists(LS_PANEL_COPY_KEY);
|
||||
// Means we are not in settings view, fullscreen panel or edit panel
|
||||
@@ -77,6 +78,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
const isEditingAndShowingDashboard = isEditing && isShowingDashboard;
|
||||
const showScopesSelector = config.featureToggles.scopeFilters && !isEditing;
|
||||
const dashboardNewLayouts = config.featureToggles.dashboardNewLayouts;
|
||||
const isManaged = Boolean(dashboard.isManaged());
|
||||
|
||||
if (!isEditingPanel) {
|
||||
// This adds the presence indicators in enterprise
|
||||
@@ -125,6 +127,16 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
});
|
||||
}
|
||||
|
||||
if (isManaged && meta.canEdit) {
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: true,
|
||||
render: () => {
|
||||
return <ManagedDashboardNavBarBadge meta={meta} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isDevEnv = config.buildInfo.env === 'development';
|
||||
|
||||
toolbarActions.push({
|
||||
@@ -548,7 +560,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
}
|
||||
|
||||
// If we only can save as copy
|
||||
if (canSaveAs && !meta.canSave && !meta.canMakeEditable) {
|
||||
if (canSaveAs && !meta.canSave && !meta.canMakeEditable && !isManaged) {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
getGridItemKeyForPanelId,
|
||||
getDashboardSceneFor,
|
||||
} from '../../utils/utils';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
@@ -180,22 +179,6 @@ export class DefaultGridLayoutManager
|
||||
return panels;
|
||||
}
|
||||
|
||||
public hasVizPanels(): boolean {
|
||||
for (const child of this.state.grid.state.children) {
|
||||
if (child instanceof DashboardGridItem) {
|
||||
return true;
|
||||
} else if (child instanceof SceneGridRow) {
|
||||
for (const rowChild of child.state.children) {
|
||||
if (rowChild instanceof DashboardGridItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public addNewRow(): SceneGridRow {
|
||||
const id = dashboardSceneGraph.getNextPanelId(this);
|
||||
|
||||
@@ -224,17 +207,6 @@ export class DefaultGridLayoutManager
|
||||
return row;
|
||||
}
|
||||
|
||||
public addNewTab() {
|
||||
const shouldAddTab = this.hasVizPanels();
|
||||
const tabsLayout = TabsLayoutManager.createFromLayout(this);
|
||||
|
||||
if (shouldAddTab) {
|
||||
tabsLayout.addNewTab();
|
||||
}
|
||||
|
||||
getDashboardSceneFor(this).switchLayout(tabsLayout);
|
||||
}
|
||||
|
||||
public editModeChanged(isEditing: boolean) {
|
||||
const updateResizeAndDragging = () => {
|
||||
this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing });
|
||||
|
||||
+1
-40
@@ -4,14 +4,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
|
||||
|
||||
import { joinCloneKeys } from '../../utils/clone';
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import {
|
||||
getDashboardSceneFor,
|
||||
getGridItemKeyForPanelId,
|
||||
getPanelIdForVizPanel,
|
||||
getVizPanelKeyForPanelId,
|
||||
} from '../../utils/utils';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
import { getGridItemKeyForPanelId, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
||||
|
||||
@@ -111,16 +104,6 @@ export class ResponsiveGridLayoutManager
|
||||
return panels;
|
||||
}
|
||||
|
||||
public hasVizPanels(): boolean {
|
||||
for (const child of this.state.layout.state.children) {
|
||||
if (child instanceof ResponsiveGridItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
|
||||
return this.clone({
|
||||
layout: this.state.layout.clone({
|
||||
@@ -143,28 +126,6 @@ export class ResponsiveGridLayoutManager
|
||||
});
|
||||
}
|
||||
|
||||
public addNewRow() {
|
||||
const shouldAddRow = this.hasVizPanels();
|
||||
const rowsLayout = RowsLayoutManager.createFromLayout(this);
|
||||
|
||||
if (shouldAddRow) {
|
||||
rowsLayout.addNewRow();
|
||||
}
|
||||
|
||||
getDashboardSceneFor(this).switchLayout(rowsLayout);
|
||||
}
|
||||
|
||||
public addNewTab() {
|
||||
const shouldAddTab = this.hasVizPanels();
|
||||
const tabsLayout = TabsLayoutManager.createFromLayout(this);
|
||||
|
||||
if (shouldAddTab) {
|
||||
tabsLayout.addNewTab();
|
||||
}
|
||||
|
||||
getDashboardSceneFor(this).switchLayout(tabsLayout);
|
||||
}
|
||||
|
||||
public getOptions(): OptionsPaneItemDescriptor[] {
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
@@ -65,16 +65,6 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
return panels;
|
||||
}
|
||||
|
||||
public hasVizPanels(): boolean {
|
||||
for (const row of this.state.rows) {
|
||||
if (row.getLayout().hasVizPanels()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@@ -85,16 +85,6 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public hasVizPanels(): boolean {
|
||||
for (const tab of this.state.tabs) {
|
||||
if (tab.getLayout().hasVizPanels()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public addNewTab() {
|
||||
const currentTab = new TabItem();
|
||||
this.setState({ tabs: [...this.state.tabs, currentTab], currentTabIndex: this.state.tabs.length });
|
||||
|
||||
@@ -39,11 +39,6 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
|
||||
*/
|
||||
getVizPanels(): VizPanel[];
|
||||
|
||||
/**
|
||||
* Check if the layout has viz panels
|
||||
*/
|
||||
hasVizPanels(): boolean;
|
||||
|
||||
/**
|
||||
* Notify the layout manager that the edit mode has changed
|
||||
* @param isEditing
|
||||
|
||||
@@ -514,6 +514,7 @@ export class Explore extends PureComponent<Props, ExploreState> {
|
||||
dataFrames={dataFrames}
|
||||
splitOpenFn={this.onSplitOpen('traceView')}
|
||||
scrollElement={this.scrollElement}
|
||||
timeRange={queryResponse.timeRange}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
)
|
||||
|
||||
@@ -3,8 +3,8 @@ import userEvent from '@testing-library/user-event';
|
||||
import { createRef } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { DataFrame, MutableDataFrame } from '@grafana/data';
|
||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataFrame, MutableDataFrame, TimeRange } from '@grafana/data';
|
||||
import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime';
|
||||
|
||||
import { configureStore } from '../../../store/configureStore';
|
||||
|
||||
@@ -25,6 +25,7 @@ function getTraceView(frames: DataFrame[]) {
|
||||
traceProp={transformDataFrames(frames[0])!}
|
||||
datasource={undefined}
|
||||
topOfViewRef={topOfViewRef}
|
||||
timeRange={{} as TimeRange}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
@@ -47,6 +48,11 @@ function renderTraceViewNew() {
|
||||
|
||||
describe('TraceView', () => {
|
||||
beforeAll(() => {
|
||||
setPluginLinksHook(() => ({
|
||||
isLoading: false,
|
||||
links: [],
|
||||
}));
|
||||
|
||||
setDataSourceSrv({
|
||||
getInstanceSettings() {
|
||||
return undefined;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
LinkModel,
|
||||
mapInternalLinkToExplore,
|
||||
SplitOpen,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { getTraceToLogsOptions, TraceToMetricsData, TraceToProfilesData } from '@grafana/o11y-ds-frontend';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
@@ -67,6 +68,7 @@ type Props = {
|
||||
focusedSpanId?: string;
|
||||
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
|
||||
spanFilters?: SearchProps;
|
||||
timeRange: TimeRange;
|
||||
};
|
||||
|
||||
export function TraceView(props: Props) {
|
||||
@@ -238,6 +240,8 @@ export function TraceView(props: Props) {
|
||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||
redrawListView={redrawListView}
|
||||
setRedrawListView={setRedrawListView}
|
||||
timeRange={props.timeRange}
|
||||
app={exploreId ? CoreApp.Explore : CoreApp.Unknown}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -2,6 +2,8 @@ import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
import { configureStore } from '../../../store/configureStore';
|
||||
|
||||
import { frameOld } from './TraceView.test';
|
||||
@@ -11,6 +13,7 @@ jest.mock('@grafana/runtime', () => {
|
||||
return {
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
reportInteraction: jest.fn(),
|
||||
usePluginLinks: jest.fn().mockReturnValue({ isLoading: false, links: [] }),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -19,7 +22,7 @@ function renderTraceViewContainer(frames = [frameOld]) {
|
||||
|
||||
const { container, baseElement } = render(
|
||||
<Provider store={store}>
|
||||
<TraceViewContainer exploreId="left" dataFrames={frames} splitOpenFn={() => {}} />
|
||||
<TraceViewContainer exploreId="left" dataFrames={frames} splitOpenFn={() => {}} timeRange={{} as TimeRange} />
|
||||
</Provider>
|
||||
);
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { DataFrame, SplitOpen } from '@grafana/data';
|
||||
import { DataFrame, SplitOpen, TimeRange } from '@grafana/data';
|
||||
import { PanelChrome } from '@grafana/ui';
|
||||
import { StoreState, useSelector } from 'app/types';
|
||||
|
||||
@@ -12,12 +12,13 @@ interface Props {
|
||||
splitOpenFn: SplitOpen;
|
||||
exploreId: string;
|
||||
scrollElement?: Element;
|
||||
timeRange: TimeRange;
|
||||
}
|
||||
|
||||
export function TraceViewContainer(props: Props) {
|
||||
// At this point we only show single trace
|
||||
const frame = props.dataFrames[0];
|
||||
const { dataFrames, splitOpenFn, exploreId, scrollElement } = props;
|
||||
const { dataFrames, splitOpenFn, exploreId, scrollElement, timeRange } = props;
|
||||
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
|
||||
const datasource = useSelector(
|
||||
(state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined
|
||||
@@ -36,6 +37,7 @@ export function TraceViewContainer(props: Props) {
|
||||
scrollElement={scrollElement}
|
||||
traceProp={traceProp}
|
||||
datasource={datasource}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</PanelChrome>
|
||||
);
|
||||
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
import { CoreApp, TimeRange } from '@grafana/data';
|
||||
import { usePluginLinks } from '@grafana/runtime';
|
||||
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
||||
|
||||
import { SpanLinkType } from '../../types/links';
|
||||
import { TraceSpan } from '../../types/trace';
|
||||
|
||||
import { getSpanDetailLinkButtons, getProfileLinkButtonsContext } from './SpanDetailLinkButtons';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
usePluginLinks: jest.fn().mockReturnValue({ isLoading: false, links: [] }),
|
||||
}));
|
||||
|
||||
const span = {
|
||||
process: {
|
||||
serviceName: 'test-service',
|
||||
},
|
||||
tags: [{ key: 'pyroscope.profile.id', value: 'test-profile' }],
|
||||
} as TraceSpan;
|
||||
|
||||
const createSpanLink = jest.fn();
|
||||
const timeRange = {
|
||||
from: new Date(0),
|
||||
to: new Date(1000),
|
||||
} as unknown as TimeRange;
|
||||
|
||||
describe('getSpanDetailLinkButtons', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return empty buttons when createSpanLink is not provided', () => {
|
||||
const result = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink: undefined,
|
||||
datasourceType: 'test',
|
||||
traceToProfilesOptions: undefined,
|
||||
timeRange,
|
||||
app: CoreApp.Explore,
|
||||
});
|
||||
|
||||
expect(result.logLinkButton).toBeNull();
|
||||
expect(result.profileLinkButtons).toBeNull();
|
||||
expect(result.sessionLinkButton).toBeNull();
|
||||
});
|
||||
|
||||
it('should create log link button when logs link exists', () => {
|
||||
createSpanLink.mockReturnValue([{ type: SpanLinkType.Logs, href: '/logs', title: 'Logs' }]);
|
||||
|
||||
const result = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink,
|
||||
datasourceType: 'test',
|
||||
traceToProfilesOptions: undefined,
|
||||
timeRange,
|
||||
app: CoreApp.Explore,
|
||||
});
|
||||
|
||||
expect(result.logLinkButton).toBeDefined();
|
||||
expect(result.profileLinkButtons).toBeNull();
|
||||
expect(result.sessionLinkButton).toBeNull();
|
||||
});
|
||||
|
||||
it('should create profile link button when profiles link exists', () => {
|
||||
createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]);
|
||||
|
||||
const result = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink,
|
||||
datasourceType: 'test',
|
||||
traceToProfilesOptions: {
|
||||
datasourceUid: 'test-uid',
|
||||
profileTypeId: 'test-type',
|
||||
customQuery: false,
|
||||
},
|
||||
timeRange,
|
||||
app: CoreApp.Explore,
|
||||
});
|
||||
|
||||
expect(result.logLinkButton).toBeNull();
|
||||
expect(result.profileLinkButtons).toBeDefined();
|
||||
expect(result.sessionLinkButton).toBeNull();
|
||||
});
|
||||
|
||||
it('should create session link button when session link exists', () => {
|
||||
createSpanLink.mockReturnValue([{ type: SpanLinkType.Session, href: '/session', title: 'Session' }]);
|
||||
|
||||
const result = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink,
|
||||
datasourceType: 'test',
|
||||
traceToProfilesOptions: undefined,
|
||||
timeRange,
|
||||
app: CoreApp.Explore,
|
||||
});
|
||||
|
||||
expect(result.logLinkButton).toBeNull();
|
||||
expect(result.profileLinkButtons).toBeNull();
|
||||
expect(result.sessionLinkButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create profile drilldown button when plugin link exists', () => {
|
||||
createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]);
|
||||
(usePluginLinks as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
links: [
|
||||
{
|
||||
pluginId: 'grafana-pyroscope-app',
|
||||
title: 'Open in Profiles Drilldown',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink,
|
||||
datasourceType: 'test',
|
||||
traceToProfilesOptions: {
|
||||
datasourceUid: 'test-uid',
|
||||
profileTypeId: 'test-type',
|
||||
customQuery: false,
|
||||
},
|
||||
timeRange,
|
||||
app: CoreApp.Explore,
|
||||
});
|
||||
|
||||
expect(result.profileLinkButtons).toBeDefined();
|
||||
// Should render both the original profile link and the drilldown button
|
||||
expect(result.profileLinkButtons?.props.children).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not create profile drilldown button when not in Explore', () => {
|
||||
createSpanLink.mockReturnValue([{ type: SpanLinkType.Profiles, href: '/profiles', title: RelatedProfilesTitle }]);
|
||||
(usePluginLinks as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
links: [
|
||||
{
|
||||
pluginId: 'grafana-pyroscope-app',
|
||||
title: 'Open in Profiles Drilldown',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink,
|
||||
datasourceType: 'test',
|
||||
traceToProfilesOptions: {
|
||||
datasourceUid: 'test-uid',
|
||||
profileTypeId: 'test-type',
|
||||
customQuery: false,
|
||||
},
|
||||
timeRange,
|
||||
app: CoreApp.Dashboard,
|
||||
});
|
||||
|
||||
expect(result.profileLinkButtons).toBeDefined();
|
||||
// Should only render the original profile link
|
||||
expect(result.profileLinkButtons?.props.children).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProfileLinkButtonsContext', () => {
|
||||
const traceToProfilesOptions = {
|
||||
datasourceUid: 'test-uid',
|
||||
profileTypeId: 'test-type',
|
||||
customQuery: false,
|
||||
};
|
||||
|
||||
it('should create context with all properties', () => {
|
||||
const context = getProfileLinkButtonsContext(span, traceToProfilesOptions, timeRange);
|
||||
|
||||
expect(context).toEqual({
|
||||
serviceName: 'test-service',
|
||||
profileTypeId: 'test-type',
|
||||
spanSelector: 'test-profile',
|
||||
explorationType: 'flame-graph',
|
||||
timeRange: {
|
||||
from: new Date(0).toISOString(),
|
||||
to: new Date(1000).toISOString(),
|
||||
},
|
||||
datasource: {
|
||||
uid: 'test-uid',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing traceToProfilesOptions', () => {
|
||||
const context = getProfileLinkButtonsContext(span, undefined, timeRange);
|
||||
|
||||
expect(context).toEqual({
|
||||
serviceName: 'test-service',
|
||||
profileTypeId: '',
|
||||
spanSelector: 'test-profile',
|
||||
explorationType: 'flame-graph',
|
||||
timeRange: {
|
||||
from: new Date(0).toISOString(),
|
||||
to: new Date(1000).toISOString(),
|
||||
},
|
||||
datasource: {
|
||||
uid: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing service name', () => {
|
||||
const spanWithoutService = {
|
||||
process: {},
|
||||
tags: [{ key: 'pyroscope.profile.id', value: 'test-profile' }],
|
||||
} as TraceSpan;
|
||||
|
||||
const context = getProfileLinkButtonsContext(spanWithoutService, traceToProfilesOptions, timeRange);
|
||||
|
||||
expect(context.serviceName).toBe('');
|
||||
});
|
||||
|
||||
it('should handle missing profile ID tag', () => {
|
||||
const spanWithoutProfileId = {
|
||||
process: { serviceName: 'test-service' },
|
||||
tags: [],
|
||||
} as unknown as TraceSpan;
|
||||
|
||||
const context = getProfileLinkButtonsContext(spanWithoutProfileId, traceToProfilesOptions, timeRange);
|
||||
|
||||
expect(context.spanSelector).toBe('');
|
||||
});
|
||||
});
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { CoreApp, IconName, PluginExtensionPoints, RawTimeRange, TimeRange } from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, locationService, reportInteraction, usePluginLinks } from '@grafana/runtime';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { Button, DataLinkButton } from '@grafana/ui';
|
||||
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
||||
|
||||
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
||||
import { SpanLinkFunc } from '../../types';
|
||||
import { SpanLinkDef, SpanLinkType } from '../../types/links';
|
||||
import { TraceSpan } from '../../types/trace';
|
||||
|
||||
export type ProfilesButtonContext = {
|
||||
serviceName: string;
|
||||
profileTypeId: string;
|
||||
spanSelector: string;
|
||||
explorationType: string;
|
||||
timeRange: RawTimeRange;
|
||||
datasource: DataSourceRef;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
span: TraceSpan;
|
||||
traceToProfilesOptions?: TraceToProfilesOptions;
|
||||
datasourceType: string;
|
||||
timeRange: TimeRange;
|
||||
createSpanLink?: SpanLinkFunc;
|
||||
app: CoreApp;
|
||||
};
|
||||
|
||||
export const getSpanDetailLinkButtons = (props: Props) => {
|
||||
const { span, createSpanLink, traceToProfilesOptions, timeRange, datasourceType, app } = props;
|
||||
|
||||
let logLinkButton: JSX.Element | null = null;
|
||||
let profileLinkButton: JSX.Element | null = null;
|
||||
let sessionLinkButton: JSX.Element | null = null;
|
||||
if (createSpanLink) {
|
||||
const links = createSpanLink(span);
|
||||
const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs);
|
||||
if (links && logsLink && logsLink.length > 0) {
|
||||
logLinkButton = createLinkButton(logsLink[0], SpanLinkType.Logs, 'Logs for this span', 'gf-logs', datasourceType);
|
||||
}
|
||||
const profilesLink = links?.filter(
|
||||
(link) => link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle
|
||||
);
|
||||
if (links && profilesLink && profilesLink.length > 0) {
|
||||
profileLinkButton = createLinkButton(
|
||||
profilesLink[0],
|
||||
SpanLinkType.Profiles,
|
||||
'Profiles for this span',
|
||||
'link',
|
||||
datasourceType
|
||||
);
|
||||
}
|
||||
const sessionLink = links?.filter((link) => link.type === SpanLinkType.Session);
|
||||
if (links && sessionLink && sessionLink.length > 0) {
|
||||
sessionLinkButton = createLinkButton(
|
||||
sessionLink[0],
|
||||
SpanLinkType.Session,
|
||||
'Session for this span',
|
||||
'frontend-observability',
|
||||
datasourceType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let profileLinkButtons = profileLinkButton;
|
||||
if (profileLinkButton) {
|
||||
// ensure we have a profile link
|
||||
const profilesDrilldownPluginId = 'grafana-pyroscope-app';
|
||||
const context = getProfileLinkButtonsContext(span, traceToProfilesOptions, timeRange);
|
||||
|
||||
// if in explore, use the plugin extension point to get the link
|
||||
// note: plugin extension point links are not currently supported in panel plugins
|
||||
if (app === CoreApp.Explore) {
|
||||
const extensionPointId = PluginExtensionPoints.TraceViewDetails;
|
||||
const { links } = usePluginLinks({ extensionPointId, context, limitPerPlugin: 1 });
|
||||
const link = links && links.length > 0 ? links.find((link) => link.pluginId === profilesDrilldownPluginId) : null;
|
||||
const label = 'Open in Profiles Drilldown';
|
||||
|
||||
// if we have a plugin link, add a button to open in Grafana Profiles Drilldown
|
||||
if (link) {
|
||||
const profileDrilldownLinkButton = (
|
||||
<Button
|
||||
icon="link"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (link && link.onClick) {
|
||||
reportInteraction('grafana_traces_trace_view_span_link_clicked', {
|
||||
datasourceType,
|
||||
grafana_version: config.buildInfo.version,
|
||||
type: SpanLinkType.ProfilesDrilldown,
|
||||
location: 'spanDetails',
|
||||
});
|
||||
|
||||
link.onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
|
||||
profileLinkButtons = (
|
||||
<>
|
||||
{profileLinkButton}
|
||||
{profileDrilldownLinkButton}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { profileLinkButtons, logLinkButton, sessionLinkButton };
|
||||
};
|
||||
|
||||
export const getProfileLinkButtonsContext = (
|
||||
span: TraceSpan,
|
||||
traceToProfilesOptions: TraceToProfilesOptions | undefined,
|
||||
timeRange: TimeRange
|
||||
) => {
|
||||
const spanSelector = span.tags.filter((tag) => tag.key === pyroscopeProfileIdTagKey);
|
||||
const context: ProfilesButtonContext = {
|
||||
serviceName: span.process.serviceName ?? '',
|
||||
profileTypeId: traceToProfilesOptions?.profileTypeId ?? '',
|
||||
spanSelector: spanSelector.length === 1 && spanSelector[0].value ? spanSelector[0].value : '',
|
||||
explorationType: 'flame-graph',
|
||||
timeRange: {
|
||||
from: timeRange.from.toISOString(),
|
||||
to: timeRange.to.toISOString(),
|
||||
},
|
||||
datasource: { uid: traceToProfilesOptions?.datasourceUid },
|
||||
};
|
||||
return context;
|
||||
};
|
||||
|
||||
const createLinkButton = (
|
||||
link: SpanLinkDef,
|
||||
type: SpanLinkType,
|
||||
title: string,
|
||||
icon: IconName,
|
||||
datasourceType: string,
|
||||
className?: string
|
||||
) => {
|
||||
return (
|
||||
<DataLinkButton
|
||||
link={{
|
||||
...link,
|
||||
title: title,
|
||||
target: '_blank',
|
||||
origin: link.field,
|
||||
onClick: (event: React.MouseEvent) => {
|
||||
// DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation
|
||||
// In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking
|
||||
// this interaction will not be tracked with link right clicks
|
||||
reportInteraction('grafana_traces_trace_view_span_link_clicked', {
|
||||
datasourceType,
|
||||
grafana_version: config.buildInfo.version,
|
||||
type,
|
||||
location: 'spanDetails',
|
||||
});
|
||||
|
||||
if (link.onClick) {
|
||||
link.onClick?.(event);
|
||||
} else {
|
||||
locationService.push(link.href);
|
||||
}
|
||||
},
|
||||
}}
|
||||
buttonProps={{ icon, className }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+12
-1
@@ -19,7 +19,7 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { createDataFrame, DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { data } from '@grafana/flamegraph';
|
||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataSourceSrv, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime';
|
||||
|
||||
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
||||
import traceGenerator from '../../demo/trace-generators';
|
||||
@@ -70,6 +70,12 @@ describe('<SpanDetail>', () => {
|
||||
createFocusSpanLink: jest.fn().mockReturnValue({}),
|
||||
traceFlameGraphs: { [span.spanID]: createDataFrame(data) },
|
||||
setRedrawListView: jest.fn(),
|
||||
timeRange: {
|
||||
raw: {
|
||||
from: 0,
|
||||
to: 1000000000000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
span.tags = [
|
||||
@@ -156,6 +162,11 @@ describe('<SpanDetail>', () => {
|
||||
props.logsToggle.mockReset();
|
||||
props.logItemToggle.mockReset();
|
||||
|
||||
setPluginLinksHook(() => ({
|
||||
isLoading: false,
|
||||
links: [],
|
||||
}));
|
||||
|
||||
setDataSourceSrv({
|
||||
getList() {
|
||||
return [pyroSettings];
|
||||
|
||||
+17
-63
@@ -15,29 +15,26 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { SpanStatusCode } from '@opentelemetry/api';
|
||||
import cx from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
CoreApp,
|
||||
DataFrame,
|
||||
dateTimeFormat,
|
||||
GrafanaTheme2,
|
||||
IconName,
|
||||
LinkModel,
|
||||
TimeRange,
|
||||
TraceKeyValuePair,
|
||||
TraceLog,
|
||||
} from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { DataLinkButton, Divider, Icon, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer';
|
||||
import { Divider, Icon, TextArea, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
|
||||
import { autoColor } from '../../Theme';
|
||||
import LabeledList from '../../common/LabeledList';
|
||||
import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span';
|
||||
import { SpanLinkFunc, TNil } from '../../types';
|
||||
import { SpanLinkDef, SpanLinkType } from '../../types/links';
|
||||
import { TraceLink, TraceSpan, TraceSpanReference } from '../../types/trace';
|
||||
import { formatDuration } from '../utils';
|
||||
|
||||
@@ -46,6 +43,7 @@ import AccordianLogs from './AccordianLogs';
|
||||
import AccordianReferences from './AccordianReferences';
|
||||
import AccordianText from './AccordianText';
|
||||
import DetailState from './DetailState';
|
||||
import { getSpanDetailLinkButtons } from './SpanDetailLinkButtons';
|
||||
import SpanFlameGraph from './SpanFlameGraph';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
@@ -168,6 +166,8 @@ export type SpanDetailProps = {
|
||||
traceFlameGraphs: TraceFlameGraphs;
|
||||
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
||||
setRedrawListView: (redraw: {}) => void;
|
||||
timeRange: TimeRange;
|
||||
app: CoreApp;
|
||||
};
|
||||
|
||||
export default function SpanDetail(props: SpanDetailProps) {
|
||||
@@ -193,6 +193,8 @@ export default function SpanDetail(props: SpanDetailProps) {
|
||||
setTraceFlameGraphs,
|
||||
traceToProfilesOptions,
|
||||
setRedrawListView,
|
||||
timeRange,
|
||||
app,
|
||||
} = props;
|
||||
const {
|
||||
isTagsOpen,
|
||||
@@ -289,62 +291,14 @@ export default function SpanDetail(props: SpanDetailProps) {
|
||||
});
|
||||
}
|
||||
|
||||
const createLinkButton = (link: SpanLinkDef, type: SpanLinkType, title: string, icon: IconName) => {
|
||||
return (
|
||||
<DataLinkButton
|
||||
link={{
|
||||
...link,
|
||||
title: title,
|
||||
target: '_blank',
|
||||
origin: link.field,
|
||||
onClick: (event: React.MouseEvent) => {
|
||||
// DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation
|
||||
// In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking
|
||||
// this interaction will not be tracked with link right clicks
|
||||
reportInteraction('grafana_traces_trace_view_span_link_clicked', {
|
||||
datasourceType: datasourceType,
|
||||
grafana_version: config.buildInfo.version,
|
||||
type,
|
||||
location: 'spanDetails',
|
||||
});
|
||||
|
||||
if (link.onClick) {
|
||||
link.onClick?.(event);
|
||||
} else {
|
||||
locationService.push(link.href);
|
||||
}
|
||||
},
|
||||
}}
|
||||
buttonProps={{ icon }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
let logLinkButton: JSX.Element | null = null;
|
||||
let profileLinkButton: JSX.Element | null = null;
|
||||
let sessionLinkButton: JSX.Element | null = null;
|
||||
if (createSpanLink) {
|
||||
const links = createSpanLink(span);
|
||||
const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs);
|
||||
if (links && logsLink && logsLink.length > 0) {
|
||||
logLinkButton = createLinkButton(logsLink[0], SpanLinkType.Logs, 'Logs for this span', 'gf-logs');
|
||||
}
|
||||
const profilesLink = links?.filter(
|
||||
(link) => link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle
|
||||
);
|
||||
if (links && profilesLink && profilesLink.length > 0) {
|
||||
profileLinkButton = createLinkButton(profilesLink[0], SpanLinkType.Profiles, 'Profiles for this span', 'link');
|
||||
}
|
||||
const sessionLink = links?.filter((link) => link.type === SpanLinkType.Session);
|
||||
if (links && sessionLink && sessionLink.length > 0) {
|
||||
sessionLinkButton = createLinkButton(
|
||||
sessionLink[0],
|
||||
SpanLinkType.Session,
|
||||
'Session for this span',
|
||||
'frontend-observability'
|
||||
);
|
||||
}
|
||||
}
|
||||
const { profileLinkButtons, logLinkButton, sessionLinkButton } = getSpanDetailLinkButtons({
|
||||
span,
|
||||
createSpanLink,
|
||||
datasourceType,
|
||||
traceToProfilesOptions,
|
||||
timeRange,
|
||||
app,
|
||||
});
|
||||
|
||||
const focusSpanLink = createFocusSpanLink(traceID, spanID);
|
||||
return (
|
||||
@@ -359,7 +313,7 @@ export default function SpanDetail(props: SpanDetailProps) {
|
||||
</div>
|
||||
<div className={styles.linkList}>
|
||||
{logLinkButton}
|
||||
{profileLinkButton}
|
||||
{profileLinkButtons}
|
||||
{sessionLinkButton}
|
||||
</div>
|
||||
<Divider spacing={1} />
|
||||
|
||||
+14
@@ -16,6 +16,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { createTheme } from '@grafana/data';
|
||||
import { setPluginLinksHook } from '@grafana/runtime';
|
||||
|
||||
import DetailState from './SpanDetail/DetailState';
|
||||
import { UnthemedSpanDetailRow, SpanDetailRowProps } from './SpanDetailRow';
|
||||
@@ -47,12 +48,25 @@ const setup = (propOverrides?: SpanDetailRowProps) => {
|
||||
traceStartTime: 1000,
|
||||
theme: createTheme(),
|
||||
traceFlameGraphs: {},
|
||||
timeRange: {
|
||||
raw: {
|
||||
from: 0,
|
||||
to: 1000000000000,
|
||||
},
|
||||
},
|
||||
...propOverrides,
|
||||
};
|
||||
return render(<UnthemedSpanDetailRow {...(props as SpanDetailRowProps)} />);
|
||||
};
|
||||
|
||||
describe('SpanDetailRow tests', () => {
|
||||
beforeEach(() => {
|
||||
setPluginLinksHook(() => ({
|
||||
isLoading: false,
|
||||
links: [],
|
||||
}));
|
||||
});
|
||||
|
||||
it('renders without exploding', () => {
|
||||
expect(() => setup()).not.toThrow();
|
||||
});
|
||||
|
||||
+7
-1
@@ -16,7 +16,7 @@ import { css } from '@emotion/css';
|
||||
import classNames from 'classnames';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||
import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui';
|
||||
@@ -103,6 +103,8 @@ export type SpanDetailRowProps = {
|
||||
traceFlameGraphs: TraceFlameGraphs;
|
||||
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
||||
setRedrawListView: (redraw: {}) => void;
|
||||
timeRange: TimeRange;
|
||||
app: CoreApp;
|
||||
};
|
||||
|
||||
export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
|
||||
@@ -146,6 +148,8 @@ export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
|
||||
traceFlameGraphs,
|
||||
setTraceFlameGraphs,
|
||||
setRedrawListView,
|
||||
timeRange,
|
||||
app,
|
||||
} = this.props;
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
@@ -193,6 +197,8 @@ export class UnthemedSpanDetailRow extends PureComponent<SpanDetailRowProps> {
|
||||
traceFlameGraphs={traceFlameGraphs}
|
||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||
setRedrawListView={setRedrawListView}
|
||||
timeRange={timeRange}
|
||||
app={app}
|
||||
/>
|
||||
</div>
|
||||
</TimelineRow.Cell>
|
||||
|
||||
+7
-1
@@ -18,7 +18,7 @@ import memoizeOne from 'memoize-one';
|
||||
import * as React from 'react';
|
||||
import { RefObject } from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||
import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
@@ -109,6 +109,8 @@ type TVirtualizedTraceViewOwnProps = {
|
||||
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
||||
redrawListView: {};
|
||||
setRedrawListView: (redraw: {}) => void;
|
||||
timeRange: TimeRange;
|
||||
app: CoreApp;
|
||||
};
|
||||
|
||||
export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline;
|
||||
@@ -557,6 +559,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
||||
traceFlameGraphs,
|
||||
setTraceFlameGraphs,
|
||||
setRedrawListView,
|
||||
timeRange,
|
||||
app,
|
||||
} = this.props;
|
||||
const detailState = detailStates.get(spanID);
|
||||
if (!trace || !detailState) {
|
||||
@@ -598,6 +602,8 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
||||
traceFlameGraphs={traceFlameGraphs}
|
||||
setTraceFlameGraphs={setTraceFlameGraphs}
|
||||
setRedrawListView={setRedrawListView}
|
||||
timeRange={timeRange}
|
||||
app={app}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { PureComponent, RefObject } from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkModel, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||
import { CoreApp, GrafanaTheme2, LinkModel, TimeRange, TraceKeyValuePair, TraceLog } from '@grafana/data';
|
||||
import { SpanBarOptions, TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
@@ -112,6 +112,8 @@ export type TProps = {
|
||||
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
|
||||
redrawListView: {};
|
||||
setRedrawListView: (redraw: {}) => void;
|
||||
timeRange: TimeRange;
|
||||
app: CoreApp;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum SpanLinkType {
|
||||
Traces = 'trace',
|
||||
Metrics = 'metric',
|
||||
Profiles = 'profile',
|
||||
ProfilesDrilldown = 'profile-drilldown',
|
||||
Session = 'session',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { useMemo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo, useRef, useEffect, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { SQLEditor } from '@grafana/plugin-ui';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { ExpressionQuery } from '../types';
|
||||
|
||||
// Account for Monaco editor's border to prevent clipping
|
||||
const EDITOR_BORDER_ADJUSTMENT = 2; // 1px border on top and bottom
|
||||
|
||||
interface Props {
|
||||
refIds: Array<SelectableValue<string>>;
|
||||
query: ExpressionQuery;
|
||||
@@ -13,8 +18,10 @@ interface Props {
|
||||
|
||||
export const SqlExpr = ({ onChange, refIds, query }: Props) => {
|
||||
const vars = useMemo(() => refIds.map((v) => v.value!), [refIds]);
|
||||
|
||||
const initialQuery = `select * from ${vars[0]} limit 1`;
|
||||
const styles = useStyles2(getStyles);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ height: 0 });
|
||||
|
||||
const onEditorChange = (expression: string) => {
|
||||
onChange({
|
||||
@@ -23,5 +30,37 @@ export const SqlExpr = ({ onChange, refIds, query }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
return <SQLEditor query={query.expression || initialQuery} onChange={onEditorChange}></SQLEditor>;
|
||||
// Set up resize observer to handle container resizing
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
const { height } = entries[0].contentRect;
|
||||
setDimensions({ height });
|
||||
});
|
||||
|
||||
resizeObserver.observe(containerRef.current);
|
||||
return () => resizeObserver.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.editorContainer}>
|
||||
<SQLEditor
|
||||
query={query.expression || initialQuery}
|
||||
onChange={onEditorChange}
|
||||
height={dimensions.height - EDITOR_BORDER_ADJUSTMENT}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
editorContainer: css({
|
||||
height: '240px',
|
||||
resize: 'vertical',
|
||||
overflow: 'auto',
|
||||
minHeight: '100px',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -53,6 +53,7 @@ function GraphiteQueryEditorContent() {
|
||||
icon="pen"
|
||||
variant="secondary"
|
||||
aria-label="Toggle editor mode"
|
||||
tooltip={state?.queryModel?.error}
|
||||
onClick={() => {
|
||||
dispatch(actions.toggleEditorMode());
|
||||
}}
|
||||
|
||||
@@ -744,6 +744,9 @@ describe('graphiteDatasource', () => {
|
||||
params: [{ multiple: true }],
|
||||
},
|
||||
updateText: () => {},
|
||||
render: () => {
|
||||
return '';
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ export class GraphiteDatasource
|
||||
target: query.target || '',
|
||||
textEditor: false,
|
||||
},
|
||||
getTemplateSrv()
|
||||
this.templateSrv
|
||||
);
|
||||
graphiteQuery.parseTarget();
|
||||
|
||||
|
||||
@@ -77,6 +77,21 @@ export default class GraphiteQuery {
|
||||
|
||||
try {
|
||||
this.parseTargetRecursive(astNode, null);
|
||||
if (this.target.target) {
|
||||
const oldQuery = this.target.target;
|
||||
const newQuery = this.generateQueryString();
|
||||
|
||||
// Spaces, quotes, and commas are used when rendering the AST back into a string.
|
||||
// We are removing these for less false positives of query changes.
|
||||
const sanitizeQuery = (o: string): string => o.replace(/\s|'|"|,/g, '');
|
||||
const oldSanitized = sanitizeQuery(oldQuery);
|
||||
const newSanitized = sanitizeQuery(newQuery);
|
||||
if (oldSanitized && newSanitized && oldSanitized !== newSanitized) {
|
||||
throw new Error(
|
||||
`Failed to make a visual query builder query that is equivalent to the query.\nOriginal query: ${oldQuery}\nQuery builder query: ${newQuery}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
console.error('error parsing target:', err.message);
|
||||
@@ -181,16 +196,19 @@ export default class GraphiteQuery {
|
||||
arrayMove(this.functions, index, index + offset);
|
||||
}
|
||||
|
||||
updateModelTarget(targets: any) {
|
||||
generateQueryString(): string {
|
||||
const wrapFunction = (target: string, func: FuncInstance) => {
|
||||
return func.render(target, (value: string) => {
|
||||
return this.templateSrv ? this.templateSrv.replace(value, this.scopedVars) : value;
|
||||
});
|
||||
};
|
||||
const metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.?select metric$/, '');
|
||||
return reduce(this.functions, wrapFunction, metricPath);
|
||||
}
|
||||
|
||||
updateModelTarget(targets: any) {
|
||||
if (!this.target.textEditor) {
|
||||
const metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.?select metric$/, '');
|
||||
this.target.target = reduce(this.functions, wrapFunction, metricPath);
|
||||
this.target.target = this.generateQueryString();
|
||||
}
|
||||
|
||||
this.updateRenderedTarget(this.target, targets);
|
||||
|
||||
@@ -258,6 +258,20 @@ describe('Graphite query model', () => {
|
||||
ctx.queryModel.updateModelTarget(targets);
|
||||
expect(ctx.queryModel.target.target).toContain(nestedFunctionAsParam);
|
||||
});
|
||||
|
||||
//This is not preferred behavior. The query builder cannot parse `maxSeries(sum(testSeries1), sum(testSeries2))` and when it can, remove this test
|
||||
it('should return an error when visual query builder query does not match raw query', () => {
|
||||
jest.spyOn(console, 'error').mockImplementation();
|
||||
ctx.target = {
|
||||
refId: 'A',
|
||||
target: 'maxSeries(sum(testSeries1), sum(testSeries2))',
|
||||
};
|
||||
ctx.targets = [ctx.target];
|
||||
ctx.queryModel = new GraphiteQuery(ctx.datasource, ctx.target, ctx.templateSrv);
|
||||
expect(ctx.queryModel.error).toBe(
|
||||
'Failed to make a visual query builder query that is equivalent to the query.\nOriginal query: maxSeries(sum(testSeries1), sum(testSeries2))\nQuery builder query: maxSeries(sumSeries(sumSeries(testSeries1), testSeries2))'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { Action, Dispatch } from 'redux';
|
||||
|
||||
import { DataQuery, TimeRange } from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { TemplateSrv } from '../../../../features/templating/template_srv';
|
||||
import { GraphiteDatasource } from '../datasource';
|
||||
@@ -57,7 +56,7 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
|
||||
state = {
|
||||
...state,
|
||||
...deps,
|
||||
queryModel: new GraphiteQuery(deps.datasource, deps.target, getTemplateSrv()),
|
||||
queryModel: new GraphiteQuery(deps.datasource, deps.target, state.templateSrv),
|
||||
supportsTags: deps.datasource.supportsTags,
|
||||
paused: false,
|
||||
removeTagValue: '-- remove tag --',
|
||||
|
||||
@@ -52,6 +52,7 @@ export const TracesPanel = ({ data, options, replaceVariables }: PanelProps<Trac
|
||||
focusedSpanId={options.focusedSpanId}
|
||||
createFocusSpanLink={options.createFocusSpanLink}
|
||||
spanFilters={replaceSearchVariables(replaceVariables, options.spanFilters)}
|
||||
timeRange={data.timeRange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.20.3-windowsservercore-1809
|
||||
FROM golang:1.23.7-windowsservercore-1809
|
||||
|
||||
SHELL ["powershell", "-command"]
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ global variables
|
||||
"""
|
||||
|
||||
grabpl_version = "v3.1.2"
|
||||
golang_version = "1.23.5"
|
||||
golang_version = "1.23.7"
|
||||
|
||||
# nodejs_version should match what's in ".nvmrc", but without the v prefix.
|
||||
nodejs_version = "22.11.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module github.com/grafana/grafana/scripts/go-workspace
|
||||
|
||||
go 1.23.1
|
||||
go 1.23.7
|
||||
|
||||
require golang.org/x/mod v0.20.0
|
||||
|
||||
Reference in New Issue
Block a user