diff --git a/.betterer.results b/.betterer.results
index d45139329f8..7e495f6d882 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -1969,6 +1969,11 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "2"]
],
+ "public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx:5381": [
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "2"]
+ ],
"public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
],
@@ -2092,7 +2097,10 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with ", "9"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "10"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "11"],
- [0, 0, 0, "No untranslated strings. Wrap text with ", "12"]
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "12"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "13"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "14"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "15"]
],
"public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
@@ -2100,6 +2108,11 @@ exports[`better eslint`] = {
"public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
+ "public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx:5381": [
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
+ [0, 0, 0, "No untranslated strings. Wrap text with ", "2"]
+ ],
"public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
diff --git a/.drone.yml b/.drone.yml
index 9a9595e7701..07831c07af2 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -193,6 +193,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -276,6 +277,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -383,6 +385,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -521,6 +524,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -618,6 +622,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -1062,6 +1067,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -1414,6 +1420,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -1538,6 +1545,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -2095,6 +2103,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -3791,6 +3800,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -4822,6 +4832,7 @@ steps:
from_secret: github-app-installation-id
GITHUB_APP_PRIVATE_KEY:
from_secret: github-app-private-key
+ failure: ignore
image: us-docker.pkg.dev/grafanalabs-global/docker-deployment-tools-prod/github-app-secret-writer:2024-11-05-v11688112090.1-83920c59
name: github-app-generate-token
volumes:
@@ -5740,6 +5751,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
-hmac: e97f7a0c3923b506dad6bf861bb1ea440a8f072ee3744742eec35e7278c3581c
+hmac: 04ba0c9b8e69705a28a24ba03de14ece0b15c4b44f6262fcbc6a9ee874b5a9db
...
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 8a020a54703..317d2b57729 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -67,6 +67,11 @@
reviewers: ["team:grafana/plugins-platform-frontend"],
matchPackageNames: ["@locker/{/,}**"],
},
+ {
+ groupName: "augurs",
+ matchPackageNames: ["@bsull/augurs"],
+ reviewers: ["sd2k"],
+ },
],
pin: {
enabled: false,
diff --git a/.gitignore b/.gitignore
index a5ae3592a84..ac5ad6f5f7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@ __debug_bin*
/devenv/docker/blocks/saml-enterprise
# This is the new place of the block, but I leave the previous here for a while
/devenv/docker/blocks/auth/saml-enterprise
+/devenv/docker/blocks/auth/signer
/tmp
tools/phantomjs/phantomjs
diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod
index a50b6c21745..820aabfed79 100644
--- a/apps/alerting/notifications/go.mod
+++ b/apps/alerting/notifications/go.mod
@@ -4,8 +4,8 @@ go 1.23.1
require (
github.com/grafana/grafana-app-sdk v0.23.1
- k8s.io/apimachinery v0.31.1
- k8s.io/apiserver v0.31.1
+ k8s.io/apimachinery v0.31.3
+ k8s.io/apiserver v0.31.3
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340
)
@@ -26,6 +26,7 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
@@ -36,7 +37,7 @@ require (
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -49,43 +50,45 @@ require (
github.com/onsi/gomega v1.34.1 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.60.0 // indirect
+ github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
+ github.com/stretchr/testify v1.10.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.etcd.io/bbolt v1.3.10 // indirect
go.etcd.io/etcd/api/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/v3 v3.5.14 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
- go.opentelemetry.io/otel v1.31.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 // indirect
- go.opentelemetry.io/otel/metric v1.31.0 // indirect
- go.opentelemetry.io/otel/sdk v1.31.0 // indirect
- go.opentelemetry.io/otel/trace v1.31.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
+ go.opentelemetry.io/otel v1.32.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect
+ go.opentelemetry.io/otel/metric v1.32.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.32.0 // indirect
+ go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
- golang.org/x/net v0.30.0 // indirect
- golang.org/x/oauth2 v0.23.0 // indirect
- golang.org/x/sys v0.26.0 // indirect
- golang.org/x/term v0.25.0 // indirect
- golang.org/x/text v0.19.0 // indirect
+ golang.org/x/net v0.31.0 // indirect
+ golang.org/x/oauth2 v0.24.0 // indirect
+ golang.org/x/sys v0.27.0 // indirect
+ golang.org/x/term v0.26.0 // indirect
+ golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.6.0 // indirect
+ golang.org/x/tools v0.27.0 // indirect
google.golang.org/genproto v0.0.0-20240820151423-278611b39280 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
google.golang.org/grpc v1.67.1 // indirect
- google.golang.org/protobuf v1.35.1 // indirect
+ google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- k8s.io/api v0.31.1 // indirect
- k8s.io/client-go v0.31.1 // indirect
- k8s.io/component-base v0.31.1 // indirect
+ k8s.io/api v0.31.3 // indirect
+ k8s.io/client-go v0.31.3 // indirect
+ k8s.io/component-base v0.31.3 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum
index 7b660a3834e..766cfbe43da 100644
--- a/apps/alerting/notifications/go.sum
+++ b/apps/alerting/notifications/go.sum
@@ -28,7 +28,7 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
@@ -48,7 +48,7 @@ github.com/grafana/grafana-app-sdk v0.23.1 h1:BRpUG0bA0oVxjthkmO2thuJBo3nbjaRSSm
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@@ -83,7 +83,7 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA=
+github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -94,8 +94,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
@@ -111,15 +110,13 @@ go.etcd.io/etcd/pkg/v3 v3.5.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M=
go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA=
go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
-go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
-go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU=
-go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
-go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
-go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
-go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
+go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
+go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
+go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
+go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -127,7 +124,7 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -135,39 +132,34 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
-golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
-golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
+golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
-golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20240820151423-278611b39280 h1:oKt8r1ZvaPqBe3oeGTdyx1iNjuBS+VJcc9QdU1CD3d8=
-google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
+google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
-google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
-google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -180,14 +172,11 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
-k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
-k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
-k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
-k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
-k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
-k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
-k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
+k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8=
+k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4=
+k8s.io/apiserver v0.31.3 h1:+1oHTtCB+OheqFEz375D0IlzHZ5VeQKX1KGXnx+TTuY=
+k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4=
+k8s.io/component-base v0.31.3 h1:DMCXXVx546Rfvhj+3cOm2EUxhS+EyztH423j+8sOwhQ=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
diff --git a/docs/sources/dashboards/share-dashboards-panels/shared-dashboards/index.md b/docs/sources/dashboards/share-dashboards-panels/shared-dashboards/index.md
index 2bfba846315..b46ef7b477d 100644
--- a/docs/sources/dashboards/share-dashboards-panels/shared-dashboards/index.md
+++ b/docs/sources/dashboards/share-dashboards-panels/shared-dashboards/index.md
@@ -239,6 +239,7 @@ guaranteed because plugin developers can override this functionality. The follow
### Unsupported
- Graphite
+- Dynatrace
### Unconfirmed
@@ -258,7 +259,6 @@ guaranteed because plugin developers can override this functionality. The follow
- Datadog
- Dataset
- Druid
-- Dynatrace
- GitHub
- Google BigQuery
- Grafana for YNAB
diff --git a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md
index 9f56def9011..bb1d2cd02aa 100644
--- a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md
+++ b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md
@@ -19,13 +19,18 @@ refs:
destination: /docs/grafana//panels-visualizations/query-transform-data/calculation-types/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/calculation-types/
+ configure-legends:
+ - pattern: /docs/grafana/
+ destination: /docs/grafana//panels-visualizations/configure-legend/
+ - pattern: /docs/grafana-cloud/
+ destination: /docs/grafana-cloud/visualizations/panels-visualizations/configure-legend/
---
# Pie chart
A pie chart is a graph that displays data as segments of a circle proportional to the whole, making it look like a sliced pie. Each slice corresponds to a value or measurement.
-{{< figure src="/static/img/docs/pie-chart-panel/pie-chart-example.png" max-width="1200px" lightbox="true" alt="Pie charts" >}}
+
The pie chart visualization is ideal when you have data that adds up to a total and you want to show the proportion of each value compared to other slices, as well as to the whole of the pie.
@@ -98,116 +103,85 @@ If you want to display only the values from a given field (or column), once the

-## Panel options
-
-{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="" >}}
-
-## Value options
-
-Use the following options to refine the value in your visualization.
-
-### Show
-
-Choose how much information to show.
+## Configuration options
-- **Calculate -** Reduces each value to a single value per series.
-- **All values -** Displays every value from a single series.
+{{< docs/shared lookup="visualizations/config-options-intro.md" source="grafana" version="" >}}
-### Calculation
+### Panel options
-Select a calculation to reduce each series when Calculate has been selected. For information about available calculations, refer to [Calculation types](ref:calculation-types).
+{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="" >}}
-### Limit
+### Value options
-When displaying every value from a single series, this limits the number of values displayed.
+Use the following options to refine the value in your visualization.
-### Fields
+
-Select which field or fields to display in the visualization. Each field name is available on the list, or you can select one of the following options:
+| Option | Description |
+| ------ | ----------- |
+| Show | Set how much information to show. Choose from:
**Calculate** - Reduces each value to a single value per series.
**All values** - Displays every value from a single series.
|
+| Calculation | If you chose **Calculate** as your **Show** option, select a calculation to reduce each series. For information about available calculations, refer to [Calculation types](ref:calculation-types). |
+| Limit | If you chose **All values** as your **Show** option, enter a value to limit the number of values displayed. |
+| Fields | Select which field or fields to display in the visualization. Each field name is available on the list, or you can select one of the following options:
**Numeric fields** - All fields with numerical values.
**All fields** - All fields that are not removed by transformations.
**Time** - All fields with time values.
|
-- **Numeric fields -** All fields with numerical values.
-- **All fields -** All fields that are not removed by transformations.
-- **Time -** All fields with time values.
+
-## Pie chart options
+### Pie chart options
Use these options to refine how your visualization looks.
-### Pie chart type
-
-Select the pie chart display style.
+#### Pie chart type
-### Pie
+Select the pie chart display style. Choose from **Pie** or **Donut**.
-
+
-### Donut
-
-
-
-### Labels
+#### Labels
Select labels to display on the pie chart. You can select more than one.
-- **Name -** The series or field name.
-- **Percent -** The percentage of the whole.
-- **Value -** The raw numerical value.
+- **Name** - The series or field name.
+- **Percent** - The percentage of the whole.
+- **Value** - The raw numerical value.
Labels are displayed in white over the body of the chart. You might need to select darker chart colors to make them more visible. Long names or numbers might be clipped.
-The following example shows a pie chart with **Name** and **Percent** labels displayed.
-
-
-
-## Tooltip options
-
-{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}}
-
-## Legend options
-
-Use these settings to define how the legend appears in your visualization. For more information about the legend, refer to [Configure a legend]({{< relref "../../configure-legend" >}}).
-
-### Visibility
-
-Toggle the switch to turn the legend on or off.
-
-### Mode
-
-Use these settings to define how the legend appears in your visualization.
-
-- **List -** Displays the legend as a list. This is a default display mode of the legend.
-- **Table -** Displays the legend as a table.
+The following example shows a pie chart with **Name** and **Percent** labels displayed:
-### Placement
+{{< figure src="/static/img/docs/pie-chart-panel/pie-chart-labels-7-5.png" alt="Pie chart labels" max-width="350px" >}}
-Choose where to display the legend.
+### Tooltip options
-- **Bottom -** Below the graph.
-- **Right -** To the right of the graph.
+{{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" leveloffset="+1" >}}
-#### Width
+### Legend options
-Control how wide the legend is when placed on the right side of the visualization. This option is only displayed if you set the legend placement to **Right**.
+Use these settings to define how the legend appears in your visualization. For more information about the legend, refer to [Configure a legend](ref:configure-legends).
-### Values
+
-Select values to display in the legend. You can select more than one.
+| Option | Description |
+| ------ | ----------- |
+| Visibility | Toggle the switch to turn the legend on or off. |
+| Mode | Use these settings to define how the legend appears in your visualization. Choose from:
**List** - Displays the legend as a list. This is a default display mode of the legend.
**Table** - Displays the legend as a table.
|
+| Placement | Select where to display the legend. Choose **Bottom** or **Right**. |
+| Width | Control how wide the legend is when placed on the right side of the visualization. This option is only displayed if you set the legend placement to **Right**. |
+| Legend values | Select values to display in the legend. You can select more than one:
**Percent** - The percentage of the whole.
**Value** - The raw numerical value.
|
-- **Percent:** The percentage of the whole.
-- **Value:** The raw numerical value.
+
-## Standard options
+### Standard options
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}}
-## Data links
+### Data links
{{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}}
-## Value mappings
+### Value mappings
{{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}}
-## Field overrides
+### Field overrides
{{< docs/shared lookup="visualizations/overrides-options.md" source="grafana" version="" >}}
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index 3f7c27beb59..e628d8caa8d 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -70,13 +70,14 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes |
| `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes |
| `accessActionSets` | Introduces action sets for resource permissions. Also ensures that all folder editors and admins can create subfolders without needing any additional permissions. | Yes |
-| `newDashboardSharingComponent` | Enables the new sharing drawer design | |
+| `newDashboardSharingComponent` | Enables the new sharing drawer design | Yes |
| `notificationBanner` | Enables the notification banner UI and API | Yes |
| `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | |
| `pinNavItems` | Enables pinning of nav items | Yes |
| `openSearchBackendFlowEnabled` | Enables the backend query flow for Open Search datasource plugin | Yes |
| `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes |
| `cloudwatchMetricInsightsCrossAccount` | Enables cross account observability for Cloudwatch Metric Insights query builder | Yes |
+| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | Yes |
| `singleTopNav` | Unifies the top search bar and breadcrumb bar into one | Yes |
| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | |
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes |
@@ -205,7 +206,6 @@ Experimental features might be changed or removed without prior notice.
| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs |
| `alertingApiServer` | Register Alerting APIs with the K8s API server |
| `dataplaneAggregator` | Enable grafana dataplane aggregator |
-| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture |
| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying |
| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources |
| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards |
diff --git a/docs/sources/setup-grafana/installation/_index.md b/docs/sources/setup-grafana/installation/_index.md
index dd6ad422c61..940a80aa91c 100644
--- a/docs/sources/setup-grafana/installation/_index.md
+++ b/docs/sources/setup-grafana/installation/_index.md
@@ -46,7 +46,7 @@ Installation of Grafana on other operating systems is possible, but is not recom
Grafana requires the minimum system resources:
- Minimum recommended memory: 512 MB
-- Minimum recommended CPU: 1
+- Minimum recommended CPU: 1 core
Some features might require more memory or CPUs, including:
diff --git a/e2e/dashboards-suite/dashboard-public-templating.spec.ts b/e2e/dashboards-suite/dashboard-public-templating.spec.ts
index 4a4e660d44e..8040f4ce56e 100644
--- a/e2e/dashboards-suite/dashboard-public-templating.spec.ts
+++ b/e2e/dashboards-suite/dashboard-public-templating.spec.ts
@@ -7,7 +7,7 @@ describe('Create a public dashboard with template variables shows a template var
it('Create a public dashboard with template variables shows a template variable warning', () => {
// Opening a dashboard with template variables
- e2e.flows.openDashboard({ uid: 'HYaGDGIMk' });
+ e2e.flows.openDashboard({ uid: 'HYaGDGIMk', queryParams: { '__feature.newDashboardSharingComponent': false } });
// Open sharing modal
e2e.components.NavToolbar.shareDashboard().click();
diff --git a/e2e/dashboards-suite/snapshot-create.spec.ts b/e2e/dashboards-suite/snapshot-create.spec.ts
index aff04b6b805..ee07081bfd0 100644
--- a/e2e/dashboards-suite/snapshot-create.spec.ts
+++ b/e2e/dashboards-suite/snapshot-create.spec.ts
@@ -10,7 +10,7 @@ describe('Snapshots', () => {
cy.intercept({
pathname: '/api/ds/query',
}).as('query');
- e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
+ e2e.flows.openDashboard({ uid: 'ZqZnVvFZz', queryParams: { '__feature.newDashboardSharingComponent': false } });
cy.wait('@query');
const panelsToCheck = [
diff --git a/go.mod b/go.mod
index 077c92aba10..dcc684cee4c 100644
--- a/go.mod
+++ b/go.mod
@@ -478,6 +478,8 @@ require github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs
require github.com/openzipkin/zipkin-go v0.4.3 // @grafana/oss-big-tent
+require github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20241209165425-c324376999f7 // @grafana/alerting-backend
+
require (
cloud.google.com/go/longrunning v0.6.0 // indirect
github.com/at-wat/mqtt-go v0.19.4 // indirect
diff --git a/go.sum b/go.sum
index 5a287842840..2f8b49fc192 100644
--- a/go.sum
+++ b/go.sum
@@ -2327,6 +2327,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
github.com/grafana/grafana-plugin-sdk-go v0.260.1 h1:KzbooQP9mv/9CPsn+SoUwGuomA8oUxO0iuIq6Rg/ekE=
github.com/grafana/grafana-plugin-sdk-go v0.260.1/go.mod h1:JriieK5Oc5v120QKhMs/LO55N0P3YI2ttEiVT1wfYsw=
+github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20241209165425-c324376999f7 h1:JFB5dvs0XwBh/RiDNA5OrqcF3eWCQmTYBm6Hy79PDMQ=
+github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20241209165425-c324376999f7/go.mod h1:AVvGgNqHsruJINRjKkhhY5NZMh5ke6Ei2bywuQ4Uuus=
github.com/grafana/grafana/apps/playlist v0.0.0-20241105090059-facca37f4d1f h1:zZN/Jy7PjoqtrMiBRV5O3x4xAArcSbUznuyAPACrKXI=
github.com/grafana/grafana/apps/playlist v0.0.0-20241105090059-facca37f4d1f/go.mod h1:e97Zxn1WX4Wn9TXEvwTjMNwU6yrjX/K7uVNSCZyEwxY=
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI=
diff --git a/go.work.sum b/go.work.sum
index 0195001fd44..2ed4636f78b 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -1231,6 +1231,8 @@ github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWe
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mithrandie/readline-csvq v1.3.0 h1:VTJEOGouJ8j27jJCD4kBBbNTxM0OdBvE1aY1tMhlqE8=
github.com/mithrandie/readline-csvq v1.3.0/go.mod h1:FKyYqDgf/G4SNov7SMFXRWO6LQLXIOeTog/NB97FZl0=
+github.com/moby/moby v26.0.0+incompatible h1:2n9/cIWkxiEI1VsWgTGgXhxIWUbv42PyxEP9L+RReC0=
+github.com/moby/moby v26.0.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=
@@ -1701,6 +1703,7 @@ go.opentelemetry.io/contrib/propagators/b3 v1.23.0/go.mod h1:Gyz7V7XghvwTq+mIhLF
go.opentelemetry.io/contrib/propagators/b3 v1.27.0 h1:IjgxbomVrV9za6bRi8fWCNXENs0co37SZedQilP2hm0=
go.opentelemetry.io/contrib/propagators/b3 v1.27.0/go.mod h1:Dv9obQz25lCisDvvs4dy28UPh974CxkahRDUPsY7y9E=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/bridge/opencensus v1.26.0 h1:DZzxj9QjznMVoehskOJnFP2gsTCWtDTFBDvFhPAY7nc=
go.opentelemetry.io/otel/bridge/opencensus v1.26.0/go.mod h1:rJiX0KrF5m8Tm1XE8jLczpAv5zUaDcvhKecFG0ZoFG4=
go.opentelemetry.io/otel/bridge/opencensus v1.27.0 h1:ao9aGGHd+G4YfjBpGs6vbkvt5hoC67STlJA9fCnOAcs=
@@ -1745,6 +1748,7 @@ go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDH
go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08=
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
@@ -1790,6 +1794,7 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -1803,12 +1808,14 @@ golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1875,6 +1882,7 @@ google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjr
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
diff --git a/package.json b/package.json
index ed0d15ea660..4fa95ba0d77 100644
--- a/package.json
+++ b/package.json
@@ -251,7 +251,7 @@
"yargs": "^17.5.1"
},
"dependencies": {
- "@bsull/augurs": "^0.6.0",
+ "@bsull/augurs": "^0.7.0",
"@emotion/css": "11.13.5",
"@emotion/react": "11.13.5",
"@fingerprintjs/fingerprintjs": "^3.4.2",
@@ -274,8 +274,8 @@
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/saga-icons": "workspace:*",
- "@grafana/scenes": "5.28.1",
- "@grafana/scenes-react": "5.28.1",
+ "@grafana/scenes": "^5.30.0",
+ "@grafana/scenes-react": "^5.30.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx
index 45d597378c4..da5f2075564 100644
--- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx
+++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx
@@ -34,6 +34,7 @@ interface BaseProps {
menu?: ReactElement | (() => ReactElement);
dragClass?: string;
dragClassCancel?: string;
+ onDragStart?: (e: React.PointerEvent) => void;
selectionId?: string;
/**
* Use only to indicate loading or streaming data in the panel.
@@ -142,6 +143,7 @@ export function PanelChrome({
onFocus,
onMouseMove,
onMouseEnter,
+ onDragStart,
showMenuAlways = false,
}: PanelChromeProps) {
const theme = useTheme2();
@@ -312,6 +314,7 @@ export function PanelChrome({
className={cx(styles.headerContainer, dragClass)}
style={headerStyles}
data-testid="header-container"
+ onPointerDown={onDragStart}
onPointerUp={onSelect}
>
{statusMessage && (
diff --git a/pkg/api/README.md b/pkg/api/README.md
index 08358933378..b3aee79ff7c 100644
--- a/pkg/api/README.md
+++ b/pkg/api/README.md
@@ -12,6 +12,8 @@ Developers modifying the HTTP API endpoints need to make sure to add the necessa
The following route defines a `PATCH` endpoint under the `/serviceaccounts/{serviceAccountId}` path with tag `service_accounts` (used for grouping together several routes) and operation ID `updateServiceAccount` (used for uniquely identifying routes and associate parameters and response with them).
+> For enterprise endpoints make sure you add the `enterprise` tag as well.
+
```go
// swagger:route PATCH /serviceaccounts/{serviceAccountId} service_accounts updateServiceAccount
diff --git a/pkg/build/cmd/publishaws.go b/pkg/build/cmd/publishaws.go
index aa52ef6debc..f48f821a0ed 100644
--- a/pkg/build/cmd/publishaws.go
+++ b/pkg/build/cmd/publishaws.go
@@ -15,7 +15,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/aws/aws-sdk-go/service/marketplacecatalog"
- "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/urfave/cli/v2"
@@ -60,9 +60,9 @@ type AwsMarketplacePublishingService struct {
}
type AwsMarketplaceDocker interface {
- ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error)
+ ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
ImageTag(ctx context.Context, source string, target string) error
- ImagePush(ctx context.Context, image string, options types.ImagePushOptions) (io.ReadCloser, error)
+ ImagePush(ctx context.Context, image string, options image.PushOptions) (io.ReadCloser, error)
}
type AwsMarketplaceRegistry interface {
@@ -172,8 +172,8 @@ func (s *AwsMarketplacePublishingService) Login(ctx context.Context) error {
return err
}
-func (s *AwsMarketplacePublishingService) PullImage(ctx context.Context, image string, version string) error {
- reader, err := s.docker.ImagePull(ctx, fmt.Sprintf("%s:%s", image, version), types.ImagePullOptions{
+func (s *AwsMarketplacePublishingService) PullImage(ctx context.Context, img string, version string) error {
+ reader, err := s.docker.ImagePull(ctx, fmt.Sprintf("%s:%s", img, version), image.PullOptions{
Platform: imagePlatform,
})
if err != nil {
@@ -201,7 +201,7 @@ func (s *AwsMarketplacePublishingService) TagImage(ctx context.Context, image st
}
func (s *AwsMarketplacePublishingService) PushToMarketplace(ctx context.Context, repo string, version string) error {
- reader, err := s.docker.ImagePush(ctx, fmt.Sprintf("%s/%s:%s", marketplaceRegistryUrl, repo, version), types.ImagePushOptions{
+ reader, err := s.docker.ImagePush(ctx, fmt.Sprintf("%s/%s:%s", marketplaceRegistryUrl, repo, version), image.PushOptions{
RegistryAuth: s.auth,
})
if err != nil {
diff --git a/pkg/build/cmd/publishaws_test.go b/pkg/build/cmd/publishaws_test.go
index fc8f2615336..626f5f4c5f4 100644
--- a/pkg/build/cmd/publishaws_test.go
+++ b/pkg/build/cmd/publishaws_test.go
@@ -14,7 +14,7 @@ import (
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/aws/aws-sdk-go/service/marketplacecatalog"
- "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/image"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
)
@@ -168,13 +168,13 @@ type mockAwsMarketplaceDocker struct {
ImagePushError error
}
-func (m *mockAwsMarketplaceDocker) ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) {
+func (m *mockAwsMarketplaceDocker) ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader([]byte(""))), m.ImagePullError
}
func (m *mockAwsMarketplaceDocker) ImageTag(ctx context.Context, source string, target string) error {
return m.ImageTagError
}
-func (m *mockAwsMarketplaceDocker) ImagePush(ctx context.Context, image string, options types.ImagePushOptions) (io.ReadCloser, error) {
+func (m *mockAwsMarketplaceDocker) ImagePush(ctx context.Context, image string, options image.PushOptions) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader([]byte(""))), m.ImagePushError
}
diff --git a/pkg/build/go.mod b/pkg/build/go.mod
index cca4e0474e4..bde874811f5 100644
--- a/pkg/build/go.mod
+++ b/pkg/build/go.mod
@@ -5,7 +5,7 @@ go 1.23.1
// Override docker/docker to avoid:
// go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires
// github.com/docker/docker@v0.0.0-00010101000000-000000000000: invalid version: unknown revision 000000000000
-replace github.com/docker/docker => github.com/moby/moby v25.0.2+incompatible
+replace github.com/docker/docker => github.com/moby/moby v26.0.0+incompatible
// contains openapi encoder fixes. remove ASAP
replace cuelang.org/go => github.com/grafana/cue v0.0.0-20230926092038-971951014e3f // @grafana/grafana-as-code
@@ -103,6 +103,7 @@ require (
github.com/distribution/reference v0.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/sosodev/duration v1.2.0 // indirect
github.com/vektah/gqlparser/v2 v2.5.11 // indirect
diff --git a/pkg/build/go.sum b/pkg/build/go.sum
index b32d80c077c..7117ad2ce56 100644
--- a/pkg/build/go.sum
+++ b/pkg/build/go.sum
@@ -169,8 +169,10 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/moby/moby v25.0.2+incompatible h1:g2oKRI7vgWkiPHZbBghaPbcV/SuKP1g/YLx0I2nxFT4=
-github.com/moby/moby v25.0.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/moby v26.0.0+incompatible h1:2n9/cIWkxiEI1VsWgTGgXhxIWUbv42PyxEP9L+RReC0=
+github.com/moby/moby v26.0.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
diff --git a/pkg/server/module_server_test.go b/pkg/server/module_server_test.go
index 272fe56c53d..ec7b97b1230 100644
--- a/pkg/server/module_server_test.go
+++ b/pkg/server/module_server_test.go
@@ -32,6 +32,10 @@ func TestIntegrationWillRunInstrumentationServerWhenTargetHasNoHttpServer(t *tes
if dbType == "sqlite3" {
t.Skip("skipping - sqlite not supported for storage server target")
}
+ // TODO - fix this test for postgres
+ if dbType == "postgres" {
+ t.Skip("skipping - test not working with postgres in Drone. Works locally.")
+ }
_, cfg := db.InitTestDBWithCfg(t)
cfg.HTTPPort = "3001"
diff --git a/pkg/services/accesscontrol/dualwrite/reconciler.go b/pkg/services/accesscontrol/dualwrite/reconciler.go
index 2be38d90aac..930327340fb 100644
--- a/pkg/services/accesscontrol/dualwrite/reconciler.go
+++ b/pkg/services/accesscontrol/dualwrite/reconciler.go
@@ -55,19 +55,19 @@ func NewZanzanaReconciler(cfg *setting.Cfg, client zanzana.Client, store db.DB,
newResourceReconciler(
"managed folder permissions",
managedPermissionsCollector(store, zanzana.KindFolders),
- zanzanaCollector(zanzana.FolderRelations),
+ zanzanaCollector(zanzana.RelationsFolder),
client,
),
newResourceReconciler(
"managed dashboard permissions",
managedPermissionsCollector(store, zanzana.KindDashboards),
- zanzanaCollector(zanzana.ResourceRelations),
+ zanzanaCollector(zanzana.RelationsResouce),
client,
),
newResourceReconciler(
"role permissions",
rolePermissionsCollector(store),
- zanzanaCollector(zanzana.FolderRelations),
+ zanzanaCollector(zanzana.RelationsFolder),
client,
),
newResourceReconciler(
diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go
index 0517a0be84a..5ec718d9933 100644
--- a/pkg/services/authz/zanzana/common/info.go
+++ b/pkg/services/authz/zanzana/common/info.go
@@ -11,11 +11,15 @@ type TypeInfo struct {
Relations []string
}
+func (t TypeInfo) IsValidRelation(relation string) bool {
+ return isValidRelation(relation, t.Relations)
+}
+
var typedResources = map[string]TypeInfo{
FormatGroupResource(
folderalpha1.FolderResourceInfo.GroupResource().Group,
folderalpha1.FolderResourceInfo.GroupResource().Resource,
- ): {Type: "folder", Relations: append(ResourceRelations, RelationCreate)},
+ ): {Type: "folder", Relations: RelationsFolder},
}
func GetTypeInfo(group, resource string) (TypeInfo, bool) {
@@ -24,19 +28,19 @@ func GetTypeInfo(group, resource string) (TypeInfo, bool) {
}
var VerbMapping = map[string]string{
- utils.VerbGet: RelationRead,
- utils.VerbList: RelationRead,
- utils.VerbWatch: RelationRead,
+ utils.VerbGet: RelationGet,
+ utils.VerbList: RelationGet,
+ utils.VerbWatch: RelationGet,
utils.VerbCreate: RelationCreate,
- utils.VerbUpdate: RelationWrite,
- utils.VerbPatch: RelationWrite,
+ utils.VerbUpdate: RelationUpdate,
+ utils.VerbPatch: RelationUpdate,
utils.VerbDelete: RelationDelete,
utils.VerbDeleteCollection: RelationDelete,
}
var RelationToVerbMapping = map[string]string{
- RelationRead: utils.VerbGet,
+ RelationGet: utils.VerbGet,
RelationCreate: utils.VerbCreate,
- RelationWrite: utils.VerbUpdate,
+ RelationUpdate: utils.VerbUpdate,
RelationDelete: utils.VerbDelete,
}
diff --git a/pkg/services/authz/zanzana/common/tuple.go b/pkg/services/authz/zanzana/common/tuple.go
index 294427631a8..cc94052438c 100644
--- a/pkg/services/authz/zanzana/common/tuple.go
+++ b/pkg/services/authz/zanzana/common/tuple.go
@@ -16,9 +16,12 @@ const (
TypeRenderService string = "render"
TypeTeam string = "team"
TypeRole string = "role"
- TypeFolder string = "folder"
- TypeResource string = "resource"
- TypeNamespace string = "namespace"
+)
+
+const (
+ TypeFolder string = "folder"
+ TypeResource string = "resource"
+ TypeGroupResouce string = "group_resource"
)
const (
@@ -31,44 +34,74 @@ const (
RelationSetEdit string = "edit"
RelationSetAdmin string = "admin"
- RelationRead string = "read"
- RelationWrite string = "write"
- RelationCreate string = "create"
- RelationDelete string = "delete"
- RelationPermissionsRead string = "permissions_read"
- RelationPermissionsWrite string = "permissions_write"
+ RelationGet string = "get"
+ RelationUpdate string = "update"
+ RelationCreate string = "create"
+ RelationDelete string = "delete"
RelationFolderResourceSetView string = "resource_" + RelationSetView
RelationFolderResourceSetEdit string = "resource_" + RelationSetEdit
RelationFolderResourceSetAdmin string = "resource_" + RelationSetAdmin
- RelationFolderResourceRead string = "resource_" + RelationRead
- RelationFolderResourceWrite string = "resource_" + RelationWrite
- RelationFolderResourceCreate string = "resource_" + RelationCreate
- RelationFolderResourceDelete string = "resource_" + RelationDelete
- RelationFolderResourcePermissionsRead string = "resource_" + RelationPermissionsRead
- RelationFolderResourcePermissionsWrite string = "resource_" + RelationPermissionsWrite
+ RelationFolderResourceGet string = "resource_" + RelationGet
+ RelationFolderResourceUpdate string = "resource_" + RelationUpdate
+ RelationFolderResourceCreate string = "resource_" + RelationCreate
+ RelationFolderResourceDelete string = "resource_" + RelationDelete
)
-var ResourceRelations = []string{
- RelationRead,
- RelationWrite,
+// RelationsGroupResource are relations that can be added on type "group_resource".
+var RelationsGroupResource = []string{
+ RelationGet,
+ RelationUpdate,
+ RelationCreate,
RelationDelete,
- RelationPermissionsRead,
- RelationPermissionsWrite,
}
-var FolderRelations = append(
- ResourceRelations,
- RelationCreate,
- RelationFolderResourceRead,
- RelationFolderResourceWrite,
+// RelationsResource are relations that can be added on type "resource".
+var RelationsResource = []string{
+ RelationGet,
+ RelationUpdate,
+ RelationDelete,
+}
+
+// RelationsFolderResource are relations that can be added on type "folder" for child resources.
+var RelationsFolderResource = []string{
+ RelationFolderResourceGet,
+ RelationFolderResourceUpdate,
RelationFolderResourceCreate,
RelationFolderResourceDelete,
- RelationFolderResourcePermissionsRead,
- RelationFolderResourcePermissionsWrite,
+}
+
+// RelationsFolder are relations that can be added on type "folder".
+var RelationsFolder = append(
+ RelationsFolderResource,
+ RelationGet,
+ RelationUpdate,
+ RelationCreate,
+ RelationDelete,
)
+func IsGroupResourceRelation(relation string) bool {
+ return isValidRelation(relation, RelationsGroupResource)
+}
+
+func IsFolderResourceRelation(relation string) bool {
+ return isValidRelation(relation, RelationsFolderResource)
+}
+
+func IsResourceRelation(relation string) bool {
+ return isValidRelation(relation, RelationsResource)
+}
+
+func isValidRelation(relation string, valid []string) bool {
+ for _, r := range valid {
+ if r == relation {
+ return true
+ }
+ }
+ return false
+}
+
func FolderResourceRelation(relation string) string {
return fmt.Sprintf("%s_%s", TypeResource, relation)
}
@@ -85,8 +118,8 @@ func NewFolderIdent(name string) string {
return fmt.Sprintf("%s:%s", TypeFolder, name)
}
-func NewNamespaceResourceIdent(group, resource string) string {
- return fmt.Sprintf("%s:%s", TypeNamespace, FormatGroupResource(group, resource))
+func NewGroupResourceIdent(group, resource string) string {
+ return fmt.Sprintf("%s:%s", TypeGroupResouce, FormatGroupResource(group, resource))
}
func FormatGroupResource(group, resource string) string {
@@ -139,11 +172,11 @@ func NewFolderResourceTuple(subject, relation, group, resource, folder string) *
}
}
-func NewNamespaceResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey {
+func NewGroupResourceTuple(subject, relation, group, resource string) *openfgav1.TupleKey {
return &openfgav1.TupleKey{
User: subject,
Relation: relation,
- Object: NewNamespaceResourceIdent(group, resource),
+ Object: NewGroupResourceIdent(group, resource),
}
}
@@ -258,10 +291,18 @@ func AddRenderContext(req *openfgav1.CheckRequest) {
req.ContextualTuples.TupleKeys = append(req.ContextualTuples.TupleKeys, &openfgav1.TupleKey{
User: req.TupleKey.User,
- Relation: "view",
- Object: NewNamespaceResourceIdent(
+ Relation: RelationSetView,
+ Object: NewGroupResourceIdent(
dashboardalpha1.DashboardResourceInfo.GroupResource().Group,
dashboardalpha1.DashboardResourceInfo.GroupResource().Resource,
),
})
}
+
+func NewResourceContext(group, resource string) *structpb.Struct {
+ return &structpb.Struct{
+ Fields: map[string]*structpb.Value{
+ "requested_group": structpb.NewStringValue(FormatGroupResource(group, resource)),
+ },
+ }
+}
diff --git a/pkg/services/authz/zanzana/schema/README.md b/pkg/services/authz/zanzana/schema/README.md
index e2e82966b36..4462e0f18e0 100644
--- a/pkg/services/authz/zanzana/schema/README.md
+++ b/pkg/services/authz/zanzana/schema/README.md
@@ -2,10 +2,10 @@
Here's some notes about [OpenFGA authorization model](https://openfga.dev/docs/modeling/getting-started) (schema) using to model access control in Grafana.
-## Namespace level permissions
+## GroupResource level permissions
-A relation to a namespace object grant access to all objects of the GroupResource in the entire namespace.
-They take the form of `{ “user”: “user:1”, relation: “read”, object:”namespace:dashboard.grafana.app/dashboard” }`. This
+A relation to a group_resource object grants access to all objects of the GroupResource.
+They take the form of `{ “user”: “user:1”, relation: “read”, object:”group_resource:dashboard.grafana.app/dashboard” }`. This
example would grant `user:1` access to all `dashboard.grafana.app/dashboard` in the namespace.
## Folder level permissions
diff --git a/pkg/services/authz/zanzana/schema/schema_core.fga b/pkg/services/authz/zanzana/schema/schema_core.fga
index 86a3a212361..8dd5f382279 100644
--- a/pkg/services/authz/zanzana/schema/schema_core.fga
+++ b/pkg/services/authz/zanzana/schema/schema_core.fga
@@ -6,31 +6,15 @@ type service-account
type render
-type namespace
- relations
- define view: [user, service-account, render, team#member, role#assignee] or edit
- define edit: [user, service-account, team#member, role#assignee] or admin
- define admin: [user, service-account, team#member, role#assignee]
-
- define read: [user, service-account, render, team#member, role#assignee] or view
- define create: [user, service-account, team#member, role#assignee] or edit
- define write: [user, service-account, team#member, role#assignee] or edit
- define delete: [user, service-account, team#member, role#assignee] or edit
- define permissions_read: [user, service-account, team#member, role#assignee] or admin
- define permissions_write: [user, service-account, team#member, role#assignee] or admin
-
type role
relations
define assignee: [user, service-account, team#member, role#assignee]
type team
relations
- # Action sets
define admin: [user, service-account]
define member: [user, service-account] or admin
- define read: [role#assignee] or member
- define write: [role#assignee] or admin
+ define get: [role#assignee] or member
+ define update: [role#assignee] or admin
define delete: [role#assignee] or admin
- define permissions_read: [role#assignee] or admin
- define permissions_write: [role#assignee] or admin
diff --git a/pkg/services/authz/zanzana/schema/schema_folder.fga b/pkg/services/authz/zanzana/schema/schema_folder.fga
index 117bff01199..d3cbb26bb64 100644
--- a/pkg/services/authz/zanzana/schema/schema_folder.fga
+++ b/pkg/services/authz/zanzana/schema/schema_folder.fga
@@ -9,9 +9,7 @@ type folder
define edit: [user, service-account, team#member, role#assignee] or admin or edit from parent
define admin: [user, service-account, team#member, role#assignee] or admin from parent
- define read: [user, service-account, team#member, role#assignee] or view or read from parent
+ define get: [user, service-account, team#member, role#assignee] or view or get from parent
define create: [user, service-account, team#member, role#assignee] or edit or create from parent
- define write: [user, service-account, team#member, role#assignee] or edit or write from parent
+ define update: [user, service-account, team#member, role#assignee] or edit or update from parent
define delete: [user, service-account, team#member, role#assignee] or edit or delete from parent
- define permissions_read: [user, service-account, team#member, role#assignee] or admin or permissions_read from parent
- define permissions_write: [user, service-account, team#member, role#assignee] or admin or permissions_write from parent
diff --git a/pkg/services/authz/zanzana/schema/schema_resource.fga b/pkg/services/authz/zanzana/schema/schema_resource.fga
index 9e90bf0472b..b97831d2163 100644
--- a/pkg/services/authz/zanzana/schema/schema_resource.fga
+++ b/pkg/services/authz/zanzana/schema/schema_resource.fga
@@ -6,12 +6,21 @@ extend type folder
define resource_edit: [user, service-account, team#member, role#assignee] or resource_admin or resource_edit from parent
define resource_admin: [user, service-account, team#member, role#assignee] or resource_admin from parent
- define resource_read: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_view or resource_read from parent
+ define resource_get: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_view or resource_get from parent
define resource_create: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_create from parent
- define resource_write: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_write from parent
+ define resource_update: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_update from parent
define resource_delete: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_edit or resource_delete from parent
- define resource_permissions_read: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_permissions_read from parent
- define resource_permissions_write: [user with folder_group_filter, service-account with folder_group_filter, team#member with folder_group_filter, role#assignee with folder_group_filter] or resource_admin or resource_permissions_write from parent
+
+type group_resource
+ relations
+ define view: [user, service-account, render, team#member, role#assignee] or edit
+ define edit: [user, service-account, team#member, role#assignee] or admin
+ define admin: [user, service-account, team#member, role#assignee]
+
+ define get: [user, service-account, render, team#member, role#assignee] or view
+ define create: [user, service-account, team#member, role#assignee] or edit
+ define update: [user, service-account, team#member, role#assignee] or edit
+ define delete: [user, service-account, team#member, role#assignee] or edit
type resource
relations
@@ -19,11 +28,9 @@ type resource
define edit: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
define admin: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter]
- define read: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or view
- define write: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
+ define get: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or view
+ define update: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
define delete: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or edit
- define permissions_read: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
- define permissions_write: [user with group_filter, service-account with group_filter, team#member with group_filter, role#assignee with group_filter] or admin
condition group_filter(requested_group: string, group_resource: string) {
requested_group == group_resource
diff --git a/pkg/services/authz/zanzana/server/server_batch_check.go b/pkg/services/authz/zanzana/server/server_batch_check.go
index 86b78634a56..46ae37189a7 100644
--- a/pkg/services/authz/zanzana/server/server_batch_check.go
+++ b/pkg/services/authz/zanzana/server/server_batch_check.go
@@ -56,7 +56,7 @@ func (s *Server) batchCheckItem(
allowed, ok := groupResourceAccess[groupResource]
if !ok {
- res, err := s.checkNamespace(ctx, r.GetSubject(), relation, item.GetGroup(), item.GetResource(), store)
+ res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, item.GetGroup(), item.GetResource(), store)
if err != nil {
return nil, err
}
diff --git a/pkg/services/authz/zanzana/server/server_batch_check_test.go b/pkg/services/authz/zanzana/server/server_batch_check_test.go
index ae8390ba951..5da2d2f76ee 100644
--- a/pkg/services/authz/zanzana/server/server_batch_check_test.go
+++ b/pkg/services/authz/zanzana/server/server_batch_check_test.go
@@ -44,7 +44,7 @@ func testBatchCheck(t *testing.T, server *Server) {
assert.False(t, res.Groups[groupResource].Items["2"])
})
- t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through namespace", func(t *testing.T) {
+ t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(dashboardGroup, dashboardResource)
res, err := server.BatchCheck(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, []*authzextv1.BatchCheckItem{
{Name: "1", Folder: "1"},
@@ -108,7 +108,7 @@ func testBatchCheck(t *testing.T, server *Server) {
assert.False(t, res.Groups[groupResource].Items["2"])
})
- t.Run("user:7 should be able to read folder {1,2} through namespace access", func(t *testing.T) {
+ t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) {
groupResource := zanzana.FormatGroupResource(folderGroup, folderResource)
res, err := server.BatchCheck(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, []*authzextv1.BatchCheckItem{
{Name: "1"},
diff --git a/pkg/services/authz/zanzana/server/server_capabilities.go b/pkg/services/authz/zanzana/server/server_capabilities.go
index 4d63841137d..f80ea3c5fb2 100644
--- a/pkg/services/authz/zanzana/server/server_capabilities.go
+++ b/pkg/services/authz/zanzana/server/server_capabilities.go
@@ -20,9 +20,9 @@ func (s *Server) Capabilities(ctx context.Context, r *authzextv1.CapabilitiesReq
}
func (s *Server) capabilitiesTyped(ctx context.Context, r *authzextv1.CapabilitiesRequest, info common.TypeInfo, store *storeInfo) (*authzextv1.CapabilitiesResponse, error) {
- out := make([]string, 0, len(common.ResourceRelations))
+ out := make([]string, 0, len(common.RelationsResource))
for _, relation := range info.Relations {
- res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
+ res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
if err != nil {
return nil, err
}
@@ -46,9 +46,9 @@ func (s *Server) capabilitiesTyped(ctx context.Context, r *authzextv1.Capabiliti
}
func (s *Server) capabilitiesGeneric(ctx context.Context, r *authzextv1.CapabilitiesRequest, store *storeInfo) (*authzextv1.CapabilitiesResponse, error) {
- out := make([]string, 0, len(common.ResourceRelations))
- for _, relation := range common.ResourceRelations {
- res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
+ out := make([]string, 0, len(common.RelationsResource))
+ for _, relation := range common.RelationsResource {
+ res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
if err != nil {
return nil, err
}
diff --git a/pkg/services/authz/zanzana/server/server_capabilities_test.go b/pkg/services/authz/zanzana/server/server_capabilities_test.go
index c1d43d8a61a..f12bd2ea6d1 100644
--- a/pkg/services/authz/zanzana/server/server_capabilities_test.go
+++ b/pkg/services/authz/zanzana/server/server_capabilities_test.go
@@ -26,42 +26,42 @@ func testCapabilities(t *testing.T, server *Server) {
t.Run("user:1 should only be able to read and write resource:dashboards.grafana.app/dashboards/1", func(t *testing.T) {
res, err := server.Capabilities(context.Background(), newReq("user:1", dashboardGroup, dashboardResource, "1", "1"))
require.NoError(t, err)
- assert.Equal(t, []string{common.RelationRead, common.RelationWrite}, res.GetCapabilities())
+ assert.Equal(t, []string{common.RelationGet, common.RelationUpdate}, res.GetCapabilities())
})
- t.Run("user:2 should be able to read and write resource:dashboards.grafana.app/dashboards/1 through namespace", func(t *testing.T) {
+ t.Run("user:2 should be able to read and write resource:dashboards.grafana.app/dashboards/1 through group_resource", func(t *testing.T) {
res, err := server.Capabilities(context.Background(), newReq("user:2", dashboardGroup, dashboardResource, "1", "1"))
require.NoError(t, err)
- assert.Equal(t, []string{common.RelationRead, common.RelationWrite}, res.GetCapabilities())
+ assert.Equal(t, []string{common.RelationGet, common.RelationUpdate}, res.GetCapabilities())
})
t.Run("user:3 should be able to read resource:dashboards.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
res, err := server.Capabilities(context.Background(), newReq("user:3", dashboardGroup, dashboardResource, "1", "1"))
require.NoError(t, err)
- assert.Equal(t, []string{common.RelationRead}, res.GetCapabilities())
+ assert.Equal(t, []string{common.RelationGet}, res.GetCapabilities())
})
t.Run("user:4 should be able to read dashboards.grafana.app/dashboards in folder 1", func(t *testing.T) {
res, err := server.Capabilities(context.Background(), newReq("user:4", dashboardGroup, dashboardResource, "1", "1"))
require.NoError(t, err)
- assert.Equal(t, []string{common.RelationRead}, res.GetCapabilities())
+ assert.Equal(t, []string{common.RelationGet}, res.GetCapabilities())
})
t.Run("user:5 should be able to read, write, create and delete resource:dashboards.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
res, err := server.Capabilities(context.Background(), newReq("user:5", dashboardGroup, dashboardResource, "1", "1"))
require.NoError(t, err)
- assert.Equal(t, []string{common.RelationRead, common.RelationWrite, common.RelationDelete}, res.GetCapabilities())
+ assert.Equal(t, []string{common.RelationGet, common.RelationUpdate, common.RelationDelete}, res.GetCapabilities())
})
- t.Run("user:6 should be able to read folder 1 ", func(t *testing.T) {
+ t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
res, err := server.Capabilities(context.Background(), newReq("user:6", folderGroup, folderResource, "", "1"))
require.NoError(t, err)
- assert.Equal(t, []string{common.RelationRead}, res.GetCapabilities())
+ assert.Equal(t, []string{common.RelationGet}, res.GetCapabilities())
})
- t.Run("user:7 should be able to read folder one through namespace access", func(t *testing.T) {
+ t.Run("user:7 should be able to read folder one through group_resource access", func(t *testing.T) {
res, err := server.Capabilities(context.Background(), newReq("user:7", folderGroup, folderResource, "", "1"))
require.NoError(t, err)
- assert.Equal(t, []string{common.RelationRead}, res.GetCapabilities())
+ assert.Equal(t, []string{common.RelationGet}, res.GetCapabilities())
})
}
diff --git a/pkg/services/authz/zanzana/server/server_check.go b/pkg/services/authz/zanzana/server/server_check.go
index 792a9561760..f7e2f489a1b 100644
--- a/pkg/services/authz/zanzana/server/server_check.go
+++ b/pkg/services/authz/zanzana/server/server_check.go
@@ -7,7 +7,6 @@ import (
authzv1 "github.com/grafana/authlib/authz/proto/v1"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
- "google.golang.org/protobuf/types/known/structpb"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
)
@@ -22,9 +21,7 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
}
relation := common.VerbMapping[r.GetVerb()]
-
- // Check if subject has access through namespace
- res, err := s.checkNamespace(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
+ res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
if err != nil {
return nil, err
}
@@ -39,18 +36,23 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
return s.checkGeneric(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), r.GetName(), r.GetFolder(), store)
}
-// checkTyped performes check on the root "namespace". If subject has access through the namespace they have access to
-// every resource for that "GroupResource".
-func (s *Server) checkNamespace(ctx context.Context, subject, relation, group, resource string, store *storeInfo) (*authzv1.CheckResponse, error) {
+// checkGroupResource check if subject has access to the full "GroupResource", if they do they can access every object
+// within it.
+func (s *Server) checkGroupResource(ctx context.Context, subject, relation, group, resource string, store *storeInfo) (*authzv1.CheckResponse, error) {
+ if !common.IsGroupResourceRelation(relation) {
+ return &authzv1.CheckResponse{Allowed: false}, nil
+ }
+
req := &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
Relation: relation,
- Object: common.NewNamespaceResourceIdent(group, resource),
+ Object: common.NewGroupResourceIdent(group, resource),
},
}
+
if strings.HasPrefix(subject, fmt.Sprintf("%s:", common.TypeRenderService)) {
common.AddRenderContext(req)
}
@@ -63,8 +65,12 @@ func (s *Server) checkNamespace(ctx context.Context, subject, relation, group, r
return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
}
-// checkTyped performes checks on our typed resources e.g. folder.
+// checkTyped checks on our typed resources e.g. folder.
func (s *Server) checkTyped(ctx context.Context, subject, relation, name string, info common.TypeInfo, store *storeInfo) (*authzv1.CheckResponse, error) {
+ if !info.IsValidRelation(relation) {
+ return &authzv1.CheckResponse{Allowed: false}, nil
+ }
+
// Check if subject has direct access to resource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
@@ -86,27 +92,26 @@ func (s *Server) checkTyped(ctx context.Context, subject, relation, name string,
return &authzv1.CheckResponse{Allowed: false}, nil
}
-// checkGeneric check our generic "resource" type.
+// checkGeneric check our generic "resource" type. It checks:
+// 1. If subject has access as a sub resource for a folder.
+// 2. If subject has direct access to resource.
func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, resource, name, folder string, store *storeInfo) (*authzv1.CheckResponse, error) {
- groupResource := structpb.NewStringValue(common.FormatGroupResource(group, resource))
+ var (
+ resourceCtx = common.NewResourceContext(group, resource)
+ folderRelation = common.FolderResourceRelation(relation)
+ )
- // Create relation can only exist on namespace or folder level.
- // So we skip direct resource access check.
- if relation != common.RelationCreate {
- // Check if subject has direct access to resource
+ if folder != "" && common.IsFolderResourceRelation(folderRelation) {
+ // Check if subject has access as a sub resource for the folder
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
- Relation: relation,
- Object: common.NewResourceIdent(group, resource, name),
- },
- Context: &structpb.Struct{
- Fields: map[string]*structpb.Value{
- "requested_group": groupResource,
- },
+ Relation: common.FolderResourceRelation(relation),
+ Object: common.NewFolderIdent(folder),
},
+ Context: resourceCtx,
})
if err != nil {
@@ -114,28 +119,24 @@ func (s *Server) checkGeneric(ctx context.Context, subject, relation, group, res
}
if res.GetAllowed() {
- return &authzv1.CheckResponse{Allowed: true}, nil
+ return &authzv1.CheckResponse{Allowed: res.GetAllowed()}, nil
}
}
- if folder == "" {
+ if !common.IsResourceRelation(relation) {
return &authzv1.CheckResponse{Allowed: false}, nil
}
- // Check if subject has access as a sub resource for the folder
+ // Check if subject has direct access to resource
res, err := s.openfga.Check(ctx, &openfgav1.CheckRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: subject,
- Relation: common.FolderResourceRelation(relation),
- Object: common.NewFolderIdent(folder),
- },
- Context: &structpb.Struct{
- Fields: map[string]*structpb.Value{
- "requested_group": groupResource,
- },
+ Relation: relation,
+ Object: common.NewResourceIdent(group, resource, name),
},
+ Context: resourceCtx,
})
if err != nil {
diff --git a/pkg/services/authz/zanzana/server/server_check_test.go b/pkg/services/authz/zanzana/server/server_check_test.go
index 6d4c6e49a15..0177b8e22a3 100644
--- a/pkg/services/authz/zanzana/server/server_check_test.go
+++ b/pkg/services/authz/zanzana/server/server_check_test.go
@@ -35,7 +35,7 @@ func testCheck(t *testing.T, server *Server) {
assert.False(t, res.GetAllowed())
})
- t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through namespace", func(t *testing.T) {
+ t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/1 through group_resource", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "1", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
@@ -83,7 +83,7 @@ func testCheck(t *testing.T, server *Server) {
assert.True(t, res.GetAllowed())
})
- t.Run("user:7 should be able to read folder one through namespace access", func(t *testing.T) {
+ t.Run("user:7 should be able to read folder one through group_resource access", func(t *testing.T) {
res, err := server.Check(context.Background(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", "1"))
require.NoError(t, err)
assert.True(t, res.GetAllowed())
diff --git a/pkg/services/authz/zanzana/server/server_list.go b/pkg/services/authz/zanzana/server/server_list.go
index 513ebb2490c..0baa35ba020 100644
--- a/pkg/services/authz/zanzana/server/server_list.go
+++ b/pkg/services/authz/zanzana/server/server_list.go
@@ -6,7 +6,6 @@ import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
- "google.golang.org/protobuf/types/known/structpb"
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
@@ -23,15 +22,7 @@ func (s *Server) List(ctx context.Context, r *authzextv1.ListRequest) (*authzext
relation := common.VerbMapping[r.GetVerb()]
- res, err := s.checkNamespace(
- ctx,
- r.GetSubject(),
- relation,
- r.GetGroup(),
- r.GetResource(),
- store,
- )
-
+ res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, r.GetGroup(), r.GetResource(), store)
if err != nil {
return nil, err
}
@@ -55,8 +46,12 @@ func (s *Server) listObjects(ctx context.Context, req *openfgav1.ListObjectsRequ
}
func (s *Server) listTyped(ctx context.Context, subject, relation string, info common.TypeInfo, store *storeInfo) (*authzextv1.ListResponse, error) {
+ if !info.IsValidRelation(relation) {
+ return &authzextv1.ListResponse{}, nil
+ }
+
// List all resources user has access too
- listRes, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
+ res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: store.ID,
AuthorizationModelId: store.ModelID,
Type: info.Type,
@@ -68,50 +63,56 @@ func (s *Server) listTyped(ctx context.Context, subject, relation string, info c
}
return &authzextv1.ListResponse{
- Items: typedObjects(info.Type, listRes.GetObjects()),
+ Items: typedObjects(info.Type, res.GetObjects()),
}, nil
}
func (s *Server) listGeneric(ctx context.Context, subject, relation, group, resource string, store *storeInfo) (*authzextv1.ListResponse, error) {
- groupResource := structpb.NewStringValue(common.FormatGroupResource(group, resource))
+ var (
+ resourceCtx = common.NewResourceContext(group, resource)
+ folderRelation = common.FolderResourceRelation(relation)
+ )
// 1. List all folders subject has access to resource type in
- folders, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
- StoreId: store.ID,
- AuthorizationModelId: store.ModelID,
- Type: common.TypeFolder,
- Relation: common.FolderResourceRelation(relation),
- User: subject,
- Context: &structpb.Struct{
- Fields: map[string]*structpb.Value{
- "requested_group": groupResource,
- },
- },
- })
- if err != nil {
- return nil, err
+ var folders []string
+ if common.IsFolderResourceRelation(folderRelation) {
+ res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
+ StoreId: store.ID,
+ AuthorizationModelId: store.ModelID,
+ Type: common.TypeFolder,
+ Relation: folderRelation,
+ User: subject,
+ Context: resourceCtx,
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ folders = res.GetObjects()
}
// 2. List all resource directly assigned to subject
- direct, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
- StoreId: store.ID,
- AuthorizationModelId: store.ModelID,
- Type: common.TypeResource,
- Relation: relation,
- User: subject,
- Context: &structpb.Struct{
- Fields: map[string]*structpb.Value{
- "requested_group": groupResource,
- },
- },
- })
- if err != nil {
- return nil, err
+ var resources []string
+ if common.IsResourceRelation(relation) {
+ res, err := s.listObjects(ctx, &openfgav1.ListObjectsRequest{
+ StoreId: store.ID,
+ AuthorizationModelId: store.ModelID,
+ Type: common.TypeResource,
+ Relation: relation,
+ User: subject,
+ Context: resourceCtx,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ resources = res.GetObjects()
}
return &authzextv1.ListResponse{
- Folders: folderObject(folders.GetObjects()),
- Items: directObjects(group, resource, direct.GetObjects()),
+ Folders: folderObject(folders),
+ Items: directObjects(group, resource, resources),
}, nil
}
diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go
index 595da83ad0a..a3b39913091 100644
--- a/pkg/services/authz/zanzana/server/server_test.go
+++ b/pkg/services/authz/zanzana/server/server_test.go
@@ -80,19 +80,19 @@ func setup(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server {
AuthorizationModelId: storeInf.ModelID,
Writes: &openfgav1.WriteRequestWrites{
TupleKeys: []*openfgav1.TupleKey{
- common.NewResourceTuple("user:1", "read", dashboardGroup, dashboardResource, "1"),
- common.NewResourceTuple("user:1", "write", dashboardGroup, dashboardResource, "1"),
- common.NewNamespaceResourceTuple("user:2", "read", dashboardGroup, dashboardResource),
- common.NewNamespaceResourceTuple("user:2", "write", dashboardGroup, dashboardResource),
- common.NewResourceTuple("user:3", "view", dashboardGroup, dashboardResource, "1"),
- common.NewFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "1"),
- common.NewFolderResourceTuple("user:4", "read", dashboardGroup, dashboardResource, "3"),
- common.NewFolderResourceTuple("user:5", "edit", dashboardGroup, dashboardResource, "1"),
- common.NewFolderTuple("user:6", "read", "1"),
- common.NewNamespaceResourceTuple("user:7", "read", folderGroup, folderResource),
+ common.NewResourceTuple("user:1", common.RelationGet, dashboardGroup, dashboardResource, "1"),
+ common.NewResourceTuple("user:1", common.RelationUpdate, dashboardGroup, dashboardResource, "1"),
+ common.NewGroupResourceTuple("user:2", common.RelationGet, dashboardGroup, dashboardResource),
+ common.NewGroupResourceTuple("user:2", common.RelationUpdate, dashboardGroup, dashboardResource),
+ common.NewResourceTuple("user:3", common.RelationSetView, dashboardGroup, dashboardResource, "1"),
+ common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "1"),
+ common.NewFolderResourceTuple("user:4", common.RelationGet, dashboardGroup, dashboardResource, "3"),
+ common.NewFolderResourceTuple("user:5", common.RelationSetEdit, dashboardGroup, dashboardResource, "1"),
+ common.NewFolderTuple("user:6", common.RelationGet, "1"),
+ common.NewGroupResourceTuple("user:7", common.RelationGet, folderGroup, folderResource),
common.NewFolderParentTuple("5", "4"),
common.NewFolderParentTuple("6", "5"),
- common.NewFolderResourceTuple("user:8", "edit", dashboardGroup, dashboardResource, "5"),
+ common.NewFolderResourceTuple("user:8", common.RelationSetEdit, dashboardGroup, dashboardResource, "5"),
common.NewFolderResourceTuple("user:9", "create", dashboardGroup, dashboardResource, "5"),
},
},
diff --git a/pkg/services/authz/zanzana/translations.go b/pkg/services/authz/zanzana/translations.go
index 8432614c7db..79e103cb5ee 100644
--- a/pkg/services/authz/zanzana/translations.go
+++ b/pkg/services/authz/zanzana/translations.go
@@ -56,18 +56,14 @@ var resourceTranslations = map[string]resourceTranslation{
group: folderGroup,
resource: folderResource,
mapping: map[string]actionMappig{
- "folders:read": newMapping(RelationRead),
- "folders:write": newMapping(RelationWrite),
- "folders:create": newMapping(RelationCreate),
- "folders:delete": newMapping(RelationDelete),
- "folders.permissions:read": newMapping(RelationPermissionsRead),
- "folders.permissions:write": newMapping(RelationPermissionsWrite),
- "dashboards:read": newScopedMapping(RelationRead, dashboardGroup, dashboardResource),
- "dashboards:write": newScopedMapping(RelationWrite, dashboardGroup, dashboardResource),
- "dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource),
- "dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource),
- "dashboards.permissions:read": newScopedMapping(RelationPermissionsRead, dashboardGroup, dashboardResource),
- "dashboards.permissions:write": newScopedMapping(RelationPermissionsWrite, dashboardGroup, dashboardResource),
+ "folders:read": newMapping(RelationGet),
+ "folders:write": newMapping(RelationUpdate),
+ "folders:create": newMapping(RelationCreate),
+ "folders:delete": newMapping(RelationDelete),
+ "dashboards:read": newScopedMapping(RelationGet, dashboardGroup, dashboardResource),
+ "dashboards:write": newScopedMapping(RelationUpdate, dashboardGroup, dashboardResource),
+ "dashboards:create": newScopedMapping(RelationCreate, dashboardGroup, dashboardResource),
+ "dashboards:delete": newScopedMapping(RelationDelete, dashboardGroup, dashboardResource),
},
},
KindDashboards: {
@@ -75,12 +71,10 @@ var resourceTranslations = map[string]resourceTranslation{
group: dashboardGroup,
resource: dashboardResource,
mapping: map[string]actionMappig{
- "dashboards:read": newMapping(RelationRead),
- "dashboards:write": newMapping(RelationWrite),
- "dashboards:create": newMapping(RelationCreate),
- "dashboards:delete": newMapping(RelationDelete),
- "dashboards.permissions:read": newMapping(RelationPermissionsRead),
- "dashboards.permissions:write": newMapping(RelationPermissionsWrite),
+ "dashboards:read": newMapping(RelationGet),
+ "dashboards:write": newMapping(RelationUpdate),
+ "dashboards:create": newMapping(RelationCreate),
+ "dashboards:delete": newMapping(RelationDelete),
},
},
}
diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go
index 512f1e6a07f..5160a3f83d9 100644
--- a/pkg/services/authz/zanzana/zanzana.go
+++ b/pkg/services/authz/zanzana/zanzana.go
@@ -18,7 +18,7 @@ const (
TypeRole = common.TypeRole
TypeFolder = common.TypeFolder
TypeResource = common.TypeResource
- TypeNamespace = common.TypeNamespace
+ TypeNamespace = common.TypeGroupResouce
)
const (
@@ -31,28 +31,25 @@ const (
RelationSetEdit = common.RelationSetEdit
RelationSetAdmin = common.RelationSetAdmin
- RelationRead = common.RelationRead
- RelationWrite = common.RelationWrite
- RelationCreate = common.RelationCreate
- RelationDelete = common.RelationDelete
- RelationPermissionsRead = common.RelationPermissionsRead
- RelationPermissionsWrite = common.RelationPermissionsWrite
+ RelationGet = common.RelationGet
+ RelationUpdate = common.RelationUpdate
+ RelationCreate = common.RelationCreate
+ RelationDelete = common.RelationDelete
RelationFolderResourceSetView = common.RelationFolderResourceSetView
RelationFolderResourceSetEdit = common.RelationFolderResourceSetEdit
RelationFolderResourceSetAdmin = common.RelationFolderResourceSetAdmin
- RelationFolderResourceRead = common.RelationFolderResourceRead
- RelationFolderResourceWrite = common.RelationFolderResourceWrite
- RelationFolderResourceCreate = common.RelationFolderResourceCreate
- RelationFolderResourceDelete = common.RelationFolderResourceDelete
- RelationFolderResourcePermissionsRead = common.RelationFolderResourcePermissionsRead
- RelationFolderResourcePermissionsWrite = common.RelationFolderResourcePermissionsWrite
+ RelationFolderResourceRead = common.RelationFolderResourceGet
+ RelationFolderResourceWrite = common.RelationFolderResourceUpdate
+ RelationFolderResourceCreate = common.RelationFolderResourceCreate
+ RelationFolderResourceDelete = common.RelationFolderResourceDelete
)
var (
- FolderRelations = common.FolderRelations
- ResourceRelations = common.ResourceRelations
+ RelationsFolder = common.RelationsFolder
+ RelationsFolderResource = common.RelationsFolder
+ RelationsResouce = common.RelationsResource
)
const (
@@ -98,7 +95,7 @@ func TranslateToResourceTuple(subject string, action, kind, name string) (*openf
}
if name == "*" {
- return common.NewNamespaceResourceTuple(subject, m.relation, translation.group, translation.resource), true
+ return common.NewGroupResourceTuple(subject, m.relation, translation.group, translation.resource), true
}
if translation.typ == TypeResource {
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 451bc5c2e82..730ad3741d0 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1248,7 +1248,7 @@ var (
Stage: FeatureStageGeneralAvailability,
Owner: grafanaSharingSquad,
FrontendOnly: true,
- Expression: "false", // disabled by default
+ Expression: "true", // enabled by default
},
{
Name: "alertingListViewV2",
@@ -1436,8 +1436,9 @@ var (
{
Name: "newFiltersUI",
Description: "Enables new combobox style UI for the Ad hoc filters variable in scenes architecture",
- Stage: FeatureStageExperimental,
+ Stage: FeatureStageGeneralAvailability,
Owner: grafanaDashboardsSquad,
+ Expression: "true", // enabled by default
},
{
Name: "lokiSendDashboardPanelNames",
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index 4e5cc8adc4b..78efd1d260b 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -188,7 +188,7 @@ cloudwatchMetricInsightsCrossAccount,GA,@grafana/aws-datasources,false,false,tru
prometheusAzureOverrideAudience,deprecated,@grafana/partner-datasources,false,false,false
alertingFilterV2,experimental,@grafana/alerting-squad,false,false,false
dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
-newFiltersUI,experimental,@grafana/dashboards-squad,false,false,false
+newFiltersUI,GA,@grafana/dashboards-squad,false,false,false
lokiSendDashboardPanelNames,experimental,@grafana/observability-logs,false,false,false
alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true
singleTopNav,GA,@grafana/grafana-frontend-platform,false,false,true
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index d62159c456e..324b18dee2f 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -2327,10 +2327,10 @@
{
"metadata": {
"name": "newDashboardSharingComponent",
- "resourceVersion": "1726241874335",
+ "resourceVersion": "1733231733564",
"creationTimestamp": "2024-05-03T15:02:18Z",
"annotations": {
- "grafana.app/updatedTimestamp": "2024-09-13 15:37:54.335099 +0000 UTC"
+ "grafana.app/updatedTimestamp": "2024-12-03 13:15:33.564083 +0000 UTC"
}
},
"spec": {
@@ -2338,7 +2338,7 @@
"stage": "GA",
"codeowner": "@grafana/sharing-squad",
"frontend": true,
- "expression": "false"
+ "expression": "true"
}
},
{
@@ -2358,13 +2358,17 @@
{
"metadata": {
"name": "newFiltersUI",
- "resourceVersion": "1724228641625",
- "creationTimestamp": "2024-08-30T12:48:13Z"
+ "resourceVersion": "1733391902652",
+ "creationTimestamp": "2024-08-30T12:48:13Z",
+ "annotations": {
+ "grafana.app/updatedTimestamp": "2024-12-05 09:45:02.652078 +0000 UTC"
+ }
},
"spec": {
"description": "Enables new combobox style UI for the Ad hoc filters variable in scenes architecture",
- "stage": "experimental",
- "codeowner": "@grafana/dashboards-squad"
+ "stage": "GA",
+ "codeowner": "@grafana/dashboards-squad",
+ "expression": "true"
}
},
{
diff --git a/pkg/services/sqlstore/sqlutil/sqlutil.go b/pkg/services/sqlstore/sqlutil/sqlutil.go
index 38bf06b2476..1a2008a1271 100644
--- a/pkg/services/sqlstore/sqlutil/sqlutil.go
+++ b/pkg/services/sqlstore/sqlutil/sqlutil.go
@@ -110,7 +110,8 @@ func sqLite3TestDB() (*TestDB, error) {
ret.ConnStr = "file:" + sqliteDb + "?cache=private&mode=rwc"
if os.Getenv("SQLITE_JOURNAL_MODE") != "false" {
- ret.ConnStr += "&_journal_mode=WAL"
+ // For tests, set sync=OFF for faster commits. Reference: https://www.sqlite.org/pragma.html#pragma_synchronous.
+ ret.ConnStr += "&_journal_mode=WAL&_synchronous=OFF"
}
ret.Path = sqliteDb
diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go
index 9ea3a321a36..341470812a7 100644
--- a/pkg/storage/unified/resource/search.go
+++ b/pkg/storage/unified/resource/search.go
@@ -8,7 +8,6 @@ import (
"sync"
"time"
- "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/hashicorp/golang-lru/v2/expirable"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@@ -171,7 +170,7 @@ func (s *searchSupport) Search(ctx context.Context, req *ResourceSearchRequest)
// init is called during startup. any failure will block startup and continued execution
func (s *searchSupport) init(ctx context.Context) error {
- _, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Init")
+ ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Init")
defer span.End()
start := time.Now().Unix()
@@ -214,6 +213,7 @@ func (s *searchSupport) init(ctx context.Context) error {
}()
end := time.Now().Unix()
+ s.log.Info("search index initialized", "duration_secs", end-start, "total_docs", s.search.TotalDocs())
if IndexMetrics != nil {
IndexMetrics.IndexCreationTime.WithLabelValues().Observe(float64(end - start))
}
@@ -277,7 +277,7 @@ func (s *searchSupport) handleEvent(ctx context.Context, evt *WrittenEvent) {
// record latency from when event was created to when it was indexed
latencySeconds := float64(time.Now().UnixMicro()-evt.ResourceVersion) / 1e6
if latencySeconds > 5 {
- logger.Warn("high index latency", "latency", latencySeconds)
+ s.log.Warn("high index latency", "latency", latencySeconds)
}
if IndexMetrics != nil {
IndexMetrics.IndexLatency.WithLabelValues(evt.Key.Resource).Observe(latencySeconds)
@@ -307,7 +307,7 @@ func (s *searchSupport) getOrCreateIndex(ctx context.Context, key NamespacedReso
}
func (s *searchSupport) build(ctx context.Context, nsr NamespacedResource, size int64, rv int64) (ResourceIndex, int64, error) {
- _, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Build")
+ ctx, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Build")
defer span.End()
builder, err := s.builders.get(ctx, nsr)
diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go
index 8339deefbc8..8d171135663 100644
--- a/pkg/storage/unified/resource/server.go
+++ b/pkg/storage/unified/resource/server.go
@@ -255,6 +255,12 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
}
}
+ err := s.Init(ctx)
+ if err != nil {
+ s.log.Error("error initializing resource server", "error", err)
+ return nil, err
+ }
+
return s, nil
}
@@ -294,16 +300,16 @@ func (s *server) Init(ctx context.Context) error {
}
}
- // Start watching for changes
- if s.initErr == nil {
- s.initErr = s.initWatcher()
- }
-
// initialize the search index
if s.initErr == nil && s.search != nil {
s.initErr = s.search.init(ctx)
}
+ // Start watching for changes
+ if s.initErr == nil {
+ s.initErr = s.initWatcher()
+ }
+
if s.initErr != nil {
s.log.Error("error initializing resource server", "error", s.initErr)
}
@@ -446,10 +452,6 @@ func (s *server) Create(ctx context.Context, req *CreateRequest) (*CreateRespons
ctx, span := s.tracer.Start(ctx, "storage_server.Create")
defer span.End()
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
-
rsp := &CreateResponse{}
user, ok := claims.From(ctx)
if !ok || user == nil {
@@ -488,10 +490,6 @@ func (s *server) Update(ctx context.Context, req *UpdateRequest) (*UpdateRespons
ctx, span := s.tracer.Start(ctx, "storage_server.Update")
defer span.End()
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
-
rsp := &UpdateResponse{}
user, ok := claims.From(ctx)
if !ok || user == nil {
@@ -542,10 +540,6 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons
ctx, span := s.tracer.Start(ctx, "storage_server.Delete")
defer span.End()
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
-
rsp := &DeleteResponse{}
if req.ResourceVersion < 0 {
return nil, apierrors.NewBadRequest("update must include the previous version")
@@ -634,9 +628,6 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons
}
func (s *server) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, error) {
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
user, ok := claims.From(ctx)
if !ok || user == nil {
return &ReadResponse{
@@ -693,9 +684,6 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err
}}, nil
}
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
if req.Limit < 1 {
req.Limit = 50 // default max 50 items in a page
}
@@ -786,10 +774,6 @@ func (s *server) initWatcher() error {
func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error {
ctx := srv.Context()
- if err := s.Init(ctx); err != nil {
- return err
- }
-
user, ok := claims.From(ctx)
if !ok || user == nil {
return apierrors.NewUnauthorized("no user found in context")
@@ -930,9 +914,6 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error {
}
func (s *server) Search(ctx context.Context, req *ResourceSearchRequest) (*ResourceSearchResponse, error) {
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
if s.search == nil {
return nil, fmt.Errorf("search index not configured")
}
@@ -941,25 +922,16 @@ func (s *server) Search(ctx context.Context, req *ResourceSearchRequest) (*Resou
// History implements ResourceServer.
func (s *server) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) {
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
return s.search.History(ctx, req)
}
// Origin implements ResourceServer.
func (s *server) Origin(ctx context.Context, req *OriginRequest) (*OriginResponse, error) {
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
return s.search.Origin(ctx, req)
}
// IsHealthy implements ResourceServer.
func (s *server) IsHealthy(ctx context.Context, req *HealthCheckRequest) (*HealthCheckResponse, error) {
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
return s.diagnostics.IsHealthy(ctx, req)
}
@@ -971,9 +943,6 @@ func (s *server) PutBlob(ctx context.Context, req *PutBlobRequest) (*PutBlobResp
Code: http.StatusNotImplemented,
}}, nil
}
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
rsp, err := s.blob.PutResourceBlob(ctx, req)
if err != nil {
@@ -1016,10 +985,6 @@ func (s *server) GetBlob(ctx context.Context, req *GetBlobRequest) (*GetBlobResp
}}, nil
}
- if err := s.Init(ctx); err != nil {
- return nil, err
- }
-
// The linked blob is stored in the resource metadata attributes
obj, status := s.getPartialObject(ctx, req.Resource, req.ResourceVersion)
if status != nil {
diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go
index b8047142fe6..932c69e3128 100644
--- a/pkg/storage/unified/search/bleve.go
+++ b/pkg/storage/unified/search/bleve.go
@@ -85,9 +85,6 @@ func (b *bleveBackend) BuildIndex(ctx context.Context,
// The builder will write all documents before returning
builder func(index resource.ResourceIndex) (int64, error),
) (resource.ResourceIndex, error) {
- b.cacheMu.Lock()
- defer b.cacheMu.Unlock()
-
_, span := b.tracer.Start(ctx, tracingPrexfixBleve+"BuildIndex")
defer span.End()
@@ -99,9 +96,9 @@ func (b *bleveBackend) BuildIndex(ctx context.Context,
if size > b.opts.FileThreshold {
dir := filepath.Join(b.opts.Root, key.Namespace, fmt.Sprintf("%s.%s", key.Resource, key.Group))
index, err = bleve.New(dir, mapper)
- if err == nil {
- b.log.Info("TODO, check last RV so we can see if the numbers have changed", "dir", dir)
- }
+
+ // TODO, check last RV so we can see if the numbers have changed
+
resource.IndexMetrics.IndexTenants.WithLabelValues(key.Namespace, "file").Inc()
} else {
index, err = bleve.NewMemOnly(mapper)
@@ -137,7 +134,9 @@ func (b *bleveBackend) BuildIndex(ctx context.Context,
return nil, err
}
+ b.cacheMu.Lock()
b.cache[key] = idx
+ b.cacheMu.Unlock()
return idx, nil
}
diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go
index ae207870164..965930dcf54 100644
--- a/pkg/storage/unified/sql/backend.go
+++ b/pkg/storage/unified/sql/backend.go
@@ -123,7 +123,7 @@ func (b *backend) Stop(_ context.Context) error {
// GetResourceStats implements Backend.
func (b *backend) GetResourceStats(ctx context.Context, namespace string, minCount int) ([]resource.ResourceStats, error) {
- _, span := b.tracer.Start(ctx, tracePrefix+".GetResourceStats")
+ ctx, span := b.tracer.Start(ctx, tracePrefix+".GetResourceStats")
defer span.End()
req := &sqlStatsRequest{
diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go
index 0b675139d31..60f43905176 100644
--- a/pkg/tests/testinfra/testinfra.go
+++ b/pkg/tests/testinfra/testinfra.go
@@ -461,9 +461,14 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
}
}
}
- logSection, err := getOrCreateSection("database")
+
+ dbSection, err := getOrCreateSection("database")
+ require.NoError(t, err)
+ _, err = dbSection.NewKey("query_retries", fmt.Sprintf("%d", queryRetries))
+ require.NoError(t, err)
+ _, err = dbSection.NewKey("max_open_conn", "2")
require.NoError(t, err)
- _, err = logSection.NewKey("query_retries", fmt.Sprintf("%d", queryRetries))
+ _, err = dbSection.NewKey("max_idle_conn", "2")
require.NoError(t, err)
cfgPath := filepath.Join(cfgDir, "test.ini")
diff --git a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go
index 96f3f53b952..c761b271f88 100644
--- a/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go
+++ b/pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go
@@ -121,8 +121,8 @@ func writeErrorResponse(rw http.ResponseWriter, code int, msg string) {
errorBody := map[string]string{
"error": msg,
}
- json, _ := json.Marshal(errorBody)
- _, err := rw.Write(json)
+ jsonRes, _ := json.Marshal(errorBody)
+ _, err := rw.Write(jsonRes)
if err != nil {
backend.Logger.Error("Unable to write HTTP response", "error", err)
}
diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go
index 6dedac9bdf8..a8584dfc59a 100644
--- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go
+++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/handler_checkhealth.go
@@ -16,7 +16,7 @@ import (
func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
err := e.Ping()
if err != nil {
- logCheckHealthError(ctx, e.dsInfo, err, e.log)
+ logCheckHealthError(ctx, e.dsInfo, err)
if strings.EqualFold(req.PluginContext.User.Role, "Admin") {
return ErrToHealthCheckResult(err)
}
@@ -73,7 +73,8 @@ func ErrToHealthCheckResult(err error) (*backend.CheckHealthResult, error) {
return res, nil
}
-func logCheckHealthError(_ context.Context, dsInfo DataSourceInfo, err error, logger log.Logger) {
+func logCheckHealthError(ctx context.Context, dsInfo DataSourceInfo, err error) {
+ logger := log.DefaultLogger.FromContext(ctx)
configSummary := map[string]any{
"config_url_length": len(dsInfo.URL),
"config_user_length": len(dsInfo.User),
@@ -104,8 +105,8 @@ func logCheckHealthError(_ context.Context, dsInfo DataSourceInfo, err error, lo
}
configSummaryJson, marshalError := json.Marshal(configSummary)
if marshalError != nil {
- logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error", "plugin_id", "grafana-postgresql-datasource")
+ logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error")
return
}
- logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error_detailed", "plugin_id", "grafana-postgresql-datasource", "details", string(configSummaryJson))
+ logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error_detailed", "details", string(configSummaryJson))
}
diff --git a/pkg/tsdb/mssql/kerberos/kerberos.go b/pkg/tsdb/mssql/kerberos/kerberos.go
index 207f324672a..abd9303231b 100644
--- a/pkg/tsdb/mssql/kerberos/kerberos.go
+++ b/pkg/tsdb/mssql/kerberos/kerberos.go
@@ -73,8 +73,6 @@ func Krb5ParseAuthCredentials(host string, port string, db string, user string,
krb5DriverParams += "krb5-dnslookupkdc=" + kerberosAuth.EnableDNSLookupKDC + ";"
}
- logger.Info(fmt.Sprintf("final krb connstr: %s", krb5DriverParams))
-
return krb5DriverParams
}
diff --git a/pkg/tsdb/mssql/sqleng/handler_checkhealth.go b/pkg/tsdb/mssql/sqleng/handler_checkhealth.go
new file mode 100644
index 00000000000..ade94fe7349
--- /dev/null
+++ b/pkg/tsdb/mssql/sqleng/handler_checkhealth.go
@@ -0,0 +1,93 @@
+package sqleng
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net"
+ "strings"
+
+ "github.com/grafana/grafana-plugin-sdk-go/backend"
+ "github.com/grafana/grafana-plugin-sdk-go/backend/log"
+)
+
+func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
+ if err := e.db.Ping(); err != nil {
+ logCheckHealthError(ctx, e.dsInfo, err)
+ if strings.EqualFold(req.PluginContext.User.Role, "Admin") {
+ return ErrToHealthCheckResult(err)
+ }
+ return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: e.TransformQueryError(e.log, err).Error()}, nil
+ }
+ return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil
+}
+
+// ErrToHealthCheckResult converts error into user friendly health check message
+// This should be called with non nil error. If the err parameter is empty, we will send Internal Server Error
+func ErrToHealthCheckResult(err error) (*backend.CheckHealthResult, error) {
+ if err == nil {
+ return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "Internal Server Error"}, nil
+ }
+ res := &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: err.Error()}
+ details := map[string]string{
+ "verboseMessage": err.Error(),
+ "errorDetailsLink": "https://grafana.com/docs/grafana/latest/datasources/mssql",
+ }
+ var opErr *net.OpError
+ if errors.As(err, &opErr) {
+ res.Message = "Network error: Failed to connect to the server"
+ if opErr != nil && opErr.Err != nil {
+ res.Message += fmt.Sprintf(". Error message: %s", opErr.Err.Error())
+ }
+ }
+ if strings.HasPrefix(err.Error(), "mssql: ") {
+ res.Message = "Database error: Failed to connect to the mssql server"
+ if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil {
+ details["verboseMessage"] = unwrappedErr.Error()
+ }
+ }
+ detailBytes, marshalErr := json.Marshal(details)
+ if marshalErr != nil {
+ return res, nil
+ }
+ res.JSONDetails = detailBytes
+ return res, nil
+}
+
+func logCheckHealthError(ctx context.Context, dsInfo DataSourceInfo, err error) {
+ logger := log.DefaultLogger.FromContext(ctx)
+ configSummary := map[string]any{
+ "config_url_length": len(dsInfo.URL),
+ "config_user_length": len(dsInfo.User),
+ "config_database_length": len(dsInfo.Database),
+ "config_json_data_database_length": len(dsInfo.JsonData.Database),
+ "config_max_open_conns": dsInfo.JsonData.MaxOpenConns,
+ "config_max_idle_conns": dsInfo.JsonData.MaxIdleConns,
+ "config_conn_max_life_time": dsInfo.JsonData.ConnMaxLifetime,
+ "config_conn_timeout": dsInfo.JsonData.ConnectionTimeout,
+ "config_ssl_mode": dsInfo.JsonData.Mode,
+ "config_tls_configuration_method": dsInfo.JsonData.ConfigurationMethod,
+ "config_tls_skip_verify": dsInfo.JsonData.TlsSkipVerify,
+ "config_timezone": dsInfo.JsonData.Timezone,
+ "config_time_interval": dsInfo.JsonData.TimeInterval,
+ "config_enable_secure_proxy": dsInfo.JsonData.SecureDSProxy,
+ "config_allow_clear_text_passwords": dsInfo.JsonData.AllowCleartextPasswords,
+ "config_authentication_type": dsInfo.JsonData.AuthenticationType,
+ "config_ssl_root_cert_file_length": len(dsInfo.JsonData.RootCertFile),
+ "config_ssl_cert_file_length": len(dsInfo.JsonData.CertFile),
+ "config_ssl_key_file_length": len(dsInfo.JsonData.CertKeyFile),
+ "config_encrypt_length": len(dsInfo.JsonData.Encrypt),
+ "config_server_name_length": len(dsInfo.JsonData.Servername),
+ "config_password_length": len(dsInfo.DecryptedSecureJSONData["password"]),
+ "config_tls_ca_cert_length": len(dsInfo.DecryptedSecureJSONData["tlsCACert"]),
+ "config_tls_client_cert_length": len(dsInfo.DecryptedSecureJSONData["tlsClientCert"]),
+ "config_tls_client_key_length": len(dsInfo.DecryptedSecureJSONData["tlsClientKey"]),
+ }
+ configSummaryJson, marshalError := json.Marshal(configSummary)
+ if marshalError != nil {
+ logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error")
+ return
+ }
+ logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error_detailed", "details", string(configSummaryJson))
+}
diff --git a/pkg/tsdb/mssql/sqleng/handler_checkhealth_test.go b/pkg/tsdb/mssql/sqleng/handler_checkhealth_test.go
new file mode 100644
index 00000000000..de41ffe1c58
--- /dev/null
+++ b/pkg/tsdb/mssql/sqleng/handler_checkhealth_test.go
@@ -0,0 +1,60 @@
+package sqleng
+
+import (
+ "errors"
+ "net"
+ "testing"
+
+ "github.com/grafana/grafana-plugin-sdk-go/backend"
+ mssql "github.com/microsoft/go-mssqldb"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestErrToHealthCheckResult(t *testing.T) {
+ tests := []struct {
+ name string
+ err error
+ want *backend.CheckHealthResult
+ }{
+ {
+ name: "without error",
+ want: &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: "Internal Server Error"},
+ },
+ {
+ name: "network error",
+ err: errors.Join(errors.New("foo"), &net.OpError{Op: "read", Net: "tcp", Err: errors.New("some op")}),
+ want: &backend.CheckHealthResult{
+ Status: backend.HealthStatusError,
+ Message: "Network error: Failed to connect to the server. Error message: some op",
+ JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/mssql","verboseMessage":"foo\nread tcp: some op"}`),
+ },
+ },
+ {
+ name: "db error",
+ err: errors.Join(errors.New("foo"), &mssql.Error{Message: "error foo occurred in mssql server"}),
+ want: &backend.CheckHealthResult{
+ Status: backend.HealthStatusError,
+ Message: "foo\nmssql: error foo occurred in mssql server",
+ JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/mssql","verboseMessage":"foo\nmssql: error foo occurred in mssql server"}`),
+ },
+ },
+ {
+ name: "regular error",
+ err: errors.New("internal server error"),
+ want: &backend.CheckHealthResult{
+ Status: backend.HealthStatusError,
+ Message: "internal server error",
+ JSONDetails: []byte(`{"errorDetailsLink":"https://grafana.com/docs/grafana/latest/datasources/mssql","verboseMessage":"internal server error"}`),
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ErrToHealthCheckResult(tt.err)
+ require.Nil(t, err)
+ assert.Equal(t, string(tt.want.JSONDetails), string(got.JSONDetails))
+ require.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/pkg/tsdb/mssql/sqleng/sql_engine.go b/pkg/tsdb/mssql/sqleng/sql_engine.go
index 153c5351258..27e34e52c11 100644
--- a/pkg/tsdb/mssql/sqleng/sql_engine.go
+++ b/pkg/tsdb/mssql/sqleng/sql_engine.go
@@ -152,15 +152,6 @@ func (e *DataSourceHandler) Dispose() {
e.log.Debug("DB disposed")
}
-func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
- err := e.db.Ping()
-
- if err != nil {
- return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: e.TransformQueryError(e.log, err).Error()}, nil
- }
- return &backend.CheckHealthResult{Status: backend.HealthStatusOk, Message: "Database Connection OK"}, nil
-}
-
func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
result := backend.NewQueryDataResponse()
ch := make(chan DBDataResponse, len(req.Queries))
diff --git a/pkg/tsdb/mysql/sqleng/handler_checkhealth.go b/pkg/tsdb/mysql/sqleng/handler_checkhealth.go
index d224bb9d7ee..fea2b0b8f8f 100644
--- a/pkg/tsdb/mysql/sqleng/handler_checkhealth.go
+++ b/pkg/tsdb/mysql/sqleng/handler_checkhealth.go
@@ -16,7 +16,7 @@ import (
func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
err := e.db.Ping()
if err != nil {
- logCheckHealthError(ctx, e.dsInfo, err, e.log)
+ logCheckHealthError(ctx, e.dsInfo, err)
if strings.EqualFold(req.PluginContext.User.Role, "Admin") {
return ErrToHealthCheckResult(err)
}
@@ -63,7 +63,8 @@ func ErrToHealthCheckResult(err error) (*backend.CheckHealthResult, error) {
return res, nil
}
-func logCheckHealthError(_ context.Context, dsInfo DataSourceInfo, err error, logger log.Logger) {
+func logCheckHealthError(ctx context.Context, dsInfo DataSourceInfo, err error) {
+ logger := log.DefaultLogger.FromContext(ctx)
configSummary := map[string]any{
"config_url_length": len(dsInfo.URL),
"config_user_length": len(dsInfo.User),
@@ -94,8 +95,8 @@ func logCheckHealthError(_ context.Context, dsInfo DataSourceInfo, err error, lo
}
configSummaryJson, marshalError := json.Marshal(configSummary)
if marshalError != nil {
- logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error", "plugin_id", "mysql")
+ logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error")
return
}
- logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error_detailed", "plugin_id", "mysql", "details", string(configSummaryJson))
+ logger.Error("Check health failed", "error", err, "message_type", "ds_config_health_check_error_detailed", "details", string(configSummaryJson))
}
diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json
index d2c775085d5..cdd858d9dcc 100644
--- a/public/api-enterprise-spec.json
+++ b/public/api-enterprise-spec.json
@@ -2211,6 +2211,113 @@
}
}
},
+ "/scim/users/": {
+ "get": {
+ "tags": [
+ "users",
+ "enterprise"
+ ],
+ "summary": "Fetches all users in UserSchema format.",
+ "operationId": "getUsers",
+ "responses": {
+ "200": {
+ "$ref": "#/responses/getUsers"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "404": {
+ "$ref": "#/responses/notFoundError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "users",
+ "enterprise"
+ ],
+ "summary": "Creates user.",
+ "operationId": "createUser",
+ "responses": {
+ "200": {
+ "$ref": "#/responses/okResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "403": {
+ "$ref": "#/responses/forbiddenError"
+ },
+ "409": {
+ "$ref": "#/responses/conflictError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "user",
+ "enterprise"
+ ],
+ "summary": "Deletes user.",
+ "operationId": "deleteUser",
+ "responses": {
+ "200": {
+ "$ref": "#/responses/okResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "404": {
+ "$ref": "#/responses/notFoundError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
+ "/scim/users/:id": {
+ "get": {
+ "tags": [
+ "user",
+ "enterprise"
+ ],
+ "summary": "Gets user by id.",
+ "operationId": "getUser",
+ "responses": {
+ "200": {
+ "$ref": "#/responses/okResponse"
+ },
+ "400": {
+ "$ref": "#/responses/badRequestError"
+ },
+ "401": {
+ "$ref": "#/responses/unauthorisedError"
+ },
+ "403": {
+ "$ref": "#/responses/forbiddenError"
+ },
+ "500": {
+ "$ref": "#/responses/internalServerError"
+ }
+ }
+ }
+ },
"/teams/{teamId}/groups": {
"get": {
"tags": [
diff --git a/public/api-merged.json b/public/api-merged.json
index 1978029ed6c..e9b63723b69 100644
--- a/public/api-merged.json
+++ b/public/api-merged.json
@@ -8609,7 +8609,8 @@
"/scim/users/": {
"get": {
"tags": [
- "users"
+ "users",
+ "enterprise"
],
"summary": "Fetches all users in UserSchema format.",
"operationId": "getUsers",
@@ -8633,7 +8634,8 @@
},
"post": {
"tags": [
- "users"
+ "users",
+ "enterprise"
],
"summary": "Creates user.",
"operationId": "createUser",
@@ -8660,7 +8662,8 @@
},
"delete": {
"tags": [
- "user"
+ "user",
+ "enterprise"
],
"summary": "Deletes user.",
"operationId": "deleteUser",
@@ -8686,7 +8689,8 @@
"/scim/users/:id": {
"get": {
"tags": [
- "user"
+ "user",
+ "enterprise"
],
"summary": "Gets user by id.",
"operationId": "getUser",
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
index 35a90efe365..2b4693d3a38 100644
--- a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
@@ -1,14 +1,16 @@
import { css } from '@emotion/css';
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
import { GrafanaTheme2, VariableHide } from '@grafana/data';
import {
CustomVariable,
EmbeddedScene,
PanelBuilders,
+ SceneComponentProps,
SceneControlsSpacer,
SceneFlexItem,
SceneFlexLayout,
+ SceneObjectBase,
SceneQueryRunner,
SceneReactObject,
SceneRefreshPicker,
@@ -16,7 +18,9 @@ import {
SceneTimeRange,
SceneVariableSet,
TextBoxVariable,
+ VariableDependencyConfig,
VariableValueSelectors,
+ sceneGraph,
useUrlSync,
} from '@grafana/scenes';
import { GraphDrawStyle, VisibilityMode } from '@grafana/schema/dist/esm/index';
@@ -36,14 +40,13 @@ import {
import { Trans } from 'app/core/internationalization';
import { LogMessages, logInfo } from '../../../Analytics';
-import { DataSourceInformation } from '../../../home/Insights';
import { alertStateHistoryDatasource, useRegisterHistoryRuntimeDataSource } from './CentralHistoryRuntimeDataSource';
import { HistoryEventsListObject } from './EventListSceneObject';
-export const LABELS_FILTER = 'labelsFilter';
-export const STATE_FILTER_TO = 'stateFilterTo';
-export const STATE_FILTER_FROM = 'stateFilterFrom';
+export const LABELS_FILTER = 'LABELS_FILTER';
+export const STATE_FILTER_TO = 'STATE_FILTER_TO';
+export const STATE_FILTER_FROM = 'STATE_FILTER_FROM';
/**
*
* This scene shows the history of the alert state changes.
@@ -67,74 +70,72 @@ export const CentralAlertHistoryScene = () => {
logInfo(LogMessages.loadedCentralAlertStateHistory);
}, []);
- // create the variables for the filters
- // textbox variable for filtering by labels
- const labelsFilterVariable = new TextBoxVariable({
- name: LABELS_FILTER,
- label: 'Labels: ',
- });
- //custom variable for filtering by the current state
- const transitionsToFilterVariable = new CustomVariable({
- name: STATE_FILTER_TO,
- value: StateFilterValues.all,
- label: 'End state:',
- hide: VariableHide.dontHide,
- query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending}`,
- });
- //custom variable for filtering by the previous state
- const transitionsFromFilterVariable = new CustomVariable({
- name: STATE_FILTER_FROM,
- value: StateFilterValues.all,
- label: 'Start state:',
- hide: VariableHide.dontHide,
- query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending}`,
- });
-
useRegisterHistoryRuntimeDataSource(); // register the runtime datasource for the history api.
- const scene = new EmbeddedScene({
- controls: [
- new SceneReactObject({
- component: LabelFilter,
- }),
- new SceneReactObject({
- component: FilterInfo,
- }),
- new VariableValueSelectors({}),
- new SceneReactObject({
- component: ClearFilterButton,
- props: {
- labelsFilterVariable,
- transitionsToFilterVariable,
- transitionsFromFilterVariable,
- },
- }),
- new SceneControlsSpacer(),
- new SceneTimePicker({}),
- new SceneRefreshPicker({}),
- ],
- // use default time range as from 1 hour ago to now, as the limit of the history api is 5000 events,
- // and using a wider time range might lead to showing gaps in the events list and the chart.
- $timeRange: new SceneTimeRange({
- from: 'now-1h',
- to: 'now',
- }),
- $variables: new SceneVariableSet({
- variables: [labelsFilterVariable, transitionsFromFilterVariable, transitionsToFilterVariable],
- }),
- body: new SceneFlexLayout({
- direction: 'column',
- children: [
- new SceneFlexItem({
- ySizing: 'content',
- body: getEventsSceneObject(alertStateHistoryDatasource),
+ const scene = useMemo(() => {
+ // create the variables for the filters
+ // textbox variable for filtering by labels
+ const labelsFilterVariable = new TextBoxVariable({
+ name: LABELS_FILTER,
+ label: 'Labels: ',
+ });
+
+ //custom variable for filtering by the current state
+ const transitionsToFilterVariable = new CustomVariable({
+ name: STATE_FILTER_TO,
+ value: StateFilterValues.all,
+ label: 'End state:',
+ hide: VariableHide.dontHide,
+ query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending}`,
+ });
+
+ //custom variable for filtering by the previous state
+ const transitionsFromFilterVariable = new CustomVariable({
+ name: STATE_FILTER_FROM,
+ value: StateFilterValues.all,
+ label: 'Start state:',
+ hide: VariableHide.dontHide,
+ query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending}`,
+ });
+
+ return new EmbeddedScene({
+ controls: [
+ new SceneReactObject({
+ component: LabelFilter,
}),
- new SceneFlexItem({
- body: new HistoryEventsListObject(),
+ new SceneReactObject({
+ component: FilterInfo,
}),
+ new VariableValueSelectors({}),
+ new ClearFilterButtonScenesObject({}),
+ new SceneControlsSpacer(),
+ new SceneTimePicker({}),
+ new SceneRefreshPicker({}),
],
- }),
- });
+ // use default time range as from 1 hour ago to now, as the limit of the history api is 5000 events,
+ // and using a wider time range might lead to showing gaps in the events list and the chart.
+ $timeRange: new SceneTimeRange({
+ from: 'now-1h',
+ to: 'now',
+ }),
+ $variables: new SceneVariableSet({
+ variables: [labelsFilterVariable, transitionsFromFilterVariable, transitionsToFilterVariable],
+ }),
+ body: new SceneFlexLayout({
+ direction: 'column',
+ children: [
+ new SceneFlexItem({
+ ySizing: 'content',
+ body: getEventsSceneObject(),
+ }),
+ new SceneFlexItem({
+ body: new HistoryEventsListObject({}),
+ }),
+ ],
+ }),
+ });
+ }, []);
+
// we need to call this to sync the url with the scene state
const isUrlSyncInitialized = useUrlSync(scene);
@@ -147,22 +148,11 @@ export const CentralAlertHistoryScene = () => {
/**
* Creates a SceneFlexItem with a timeseries panel that shows the events.
* The query uses a runtime datasource that fetches the events from the history api.
- * @param alertStateHistoryDataSource the datasource information for the runtime datasource
*/
-function getEventsSceneObject(alertStateHistoryDataSource: DataSourceInformation) {
- return new EmbeddedScene({
- controls: [],
- body: new SceneFlexLayout({
- direction: 'column',
- children: [
- new SceneFlexItem({
- ySizing: 'content',
- body: new SceneFlexLayout({
- children: [getEventsScenesFlexItem(alertStateHistoryDataSource)],
- }),
- }),
- ],
- }),
+function getEventsSceneObject() {
+ return new SceneFlexLayout({
+ direction: 'column',
+ children: [getEventsScenesFlexItem()],
});
}
@@ -171,15 +161,15 @@ function getEventsSceneObject(alertStateHistoryDataSource: DataSourceInformation
* @param datasource the datasource information for the runtime datasource
* @returns the SceneQueryRunner
*/
-function getSceneQuery(datasource: DataSourceInformation) {
+function getQueryRunnerForAlertHistoryDataSource() {
const query = new SceneQueryRunner({
- datasource: datasource,
+ datasource: alertStateHistoryDatasource,
queries: [
{
refId: 'A',
- expr: '',
- queryType: 'range',
- step: '10s',
+ labels: '${LABELS_FILTER}',
+ stateFrom: '${STATE_FILTER_FROM}',
+ stateTo: '${STATE_FILTER_TO}',
},
],
});
@@ -189,7 +179,7 @@ function getSceneQuery(datasource: DataSourceInformation) {
* This function creates a SceneFlexItem with a timeseries panel that shows the events.
* The query uses a runtime datasource that fetches the events from the history api.
*/
-export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
+export function getEventsScenesFlexItem() {
return new SceneFlexItem({
minHeight: 300,
body: PanelBuilders.timeseries()
@@ -197,7 +187,7 @@ export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
.setDescription(
'Each alert event represents an alert instance that changed its state at a particular point in time. The history of the data is displayed over a period of time.'
)
- .setData(getSceneQuery(datasource))
+ .setData(getQueryRunnerForAlertHistoryDataSource())
.setColor({ mode: 'continuous-BlPu' })
.setCustomFieldConfig('fillOpacity', 100)
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars)
@@ -213,47 +203,49 @@ export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
.setCustomFieldConfig('scaleDistribution', { type: ScaleDistribution.Linear })
.setOption('legend', { showLegend: false, displayMode: LegendDisplayMode.Hidden })
.setOption('tooltip', { mode: TooltipDisplayMode.Single })
-
.setNoValue('No events found')
.build(),
});
}
-/*
- * This component shows a button to clear the filters.
- * It is shown when the filters are active.
- * props:
- * labelsFilterVariable: the textbox variable for filtering by labels
- * transitionsToFilterVariable: the custom variable for filtering by the current state
- * transitionsFromFilterVariable: the custom variable for filtering by the previous state
- */
-function ClearFilterButton({
- labelsFilterVariable,
- transitionsToFilterVariable,
- transitionsFromFilterVariable,
-}: {
- labelsFilterVariable: TextBoxVariable;
- transitionsToFilterVariable: CustomVariable;
- transitionsFromFilterVariable: CustomVariable;
-}) {
- // get the current values of the filters
- const valueInLabelsFilter = labelsFilterVariable.getValue();
- //todo: use parsePromQLStyleMatcherLooseSafe to validate the label filter and check the lenghtof the result
- const valueInTransitionsFilter = transitionsToFilterVariable.getValue();
- const valueInTransitionsFromFilter = transitionsFromFilterVariable.getValue();
+export class ClearFilterButtonScenesObject extends SceneObjectBase {
+ public static Component = ClearFilterButtonObjectRenderer;
+
+ protected _variableDependency = new VariableDependencyConfig(this, {
+ variableNames: [LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO],
+ });
+}
+
+export function ClearFilterButtonObjectRenderer({ model }: SceneComponentProps) {
+ // This make sure the component is re-rendered when the variables change
+ model.useState();
+
+ const labelsFilter = sceneGraph.interpolate(model, '${LABELS_FILTER}');
+ const stateTo = sceneGraph.interpolate(model, '${STATE_FILTER_TO}');
+ const stateFrom = sceneGraph.interpolate(model, '${STATE_FILTER_FROM}');
+
// if no filter is active, return null
- if (
- !valueInLabelsFilter &&
- valueInTransitionsFilter === StateFilterValues.all &&
- valueInTransitionsFromFilter === StateFilterValues.all
- ) {
+ if (!labelsFilter && stateTo === StateFilterValues.all && stateFrom === StateFilterValues.all) {
return null;
}
+
const onClearFilter = () => {
- labelsFilterVariable.setValue('');
- transitionsToFilterVariable.changeValueTo(StateFilterValues.all);
- transitionsFromFilterVariable.changeValueTo(StateFilterValues.all);
+ const labelsFiltersVariable = sceneGraph.lookupVariable(LABELS_FILTER, model);
+ if (labelsFiltersVariable instanceof TextBoxVariable) {
+ labelsFiltersVariable.setValue('');
+ }
+
+ const stateToFilterVariable = sceneGraph.lookupVariable(STATE_FILTER_TO, model);
+ if (stateToFilterVariable instanceof CustomVariable) {
+ stateToFilterVariable.changeValueTo(StateFilterValues.all);
+ }
+
+ const stateFromFilterVariable = sceneGraph.lookupVariable(STATE_FILTER_FROM, model);
+ if (stateFromFilterVariable instanceof CustomVariable) {
+ stateFromFilterVariable.changeValueTo(StateFilterValues.all);
+ }
};
+
return (
-
+
);
diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
index 9a0a0fa8f72..10edc9904d7 100644
--- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
+++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts
@@ -4,6 +4,8 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardScene } from '../scene/DashboardScene';
import { VizPanelLinks } from '../scene/PanelLinks';
+import { getLayoutManagerFor } from './utils';
+
function getTimePicker(scene: DashboardScene) {
return scene.state.controls?.state.timePicker;
}
@@ -53,4 +55,5 @@ export const dashboardSceneGraph = {
getVizPanels,
getDataLayers,
getCursorSync,
+ getLayoutManagerFor,
};
diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts
index 09792696251..3e34ec23362 100644
--- a/public/app/features/dashboard-scene/utils/utils.ts
+++ b/public/app/features/dashboard-scene/utils/utils.ts
@@ -18,6 +18,7 @@ import { DashboardScene } from '../scene/DashboardScene';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
+import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types';
export const NEW_PANEL_HEIGHT = 8;
export const NEW_PANEL_WIDTH = 12;
@@ -220,6 +221,7 @@ export function getDefaultVizPanel(): VizPanel {
pluginId: 'timeseries',
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
hoverHeaderOffset: 0,
+ $behaviors: [],
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
}),
@@ -279,3 +281,16 @@ export function activateSceneObjectAndParentTree(so: SceneObject): CancelActivat
* Useful when rendering a scene object out of context of it's parent
*/
export const activateInActiveParents = activateSceneObjectAndParentTree;
+
+export function getLayoutManagerFor(sceneObject: SceneObject): DashboardLayoutManager {
+ let parent = sceneObject.parent;
+
+ while (parent) {
+ if (isDashboardLayoutManager(parent)) {
+ return parent;
+ }
+ parent = parent.parent;
+ }
+
+ throw new Error('Could not find layout manager for scene object');
+}
diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx
index debeed4e2a2..9106be64a5d 100644
--- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx
+++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx
@@ -1,5 +1,7 @@
import * as React from 'react';
+import { Box } from '@grafana/ui';
+
import { OptionsPaneCategory } from './OptionsPaneCategory';
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
@@ -57,6 +59,10 @@ export class OptionsPaneCategoryDescriptor {
return this.props.customRender();
}
+ if (this.props.id === '') {
+ return {this.items.map((item) => item.render(searchQuery))};
+ }
+
return (
{this.items.map((item) => item.render(searchQuery))}
diff --git a/public/app/features/logs/components/LogRows.test.tsx b/public/app/features/logs/components/LogRows.test.tsx
index 439bf7087fc..cd861e2e37d 100644
--- a/public/app/features/logs/components/LogRows.test.tsx
+++ b/public/app/features/logs/components/LogRows.test.tsx
@@ -275,4 +275,26 @@ describe('Popover menu', () => {
expect(onClickFilterOutString).toHaveBeenCalledTimes(1);
expect(onClickFilterString).toHaveBeenCalledTimes(1);
});
+ describe('Interacting with log details', () => {
+ it('Allows text selection even if the popover menu is not available', async () => {
+ setup({
+ onClickFilterOutString: undefined,
+ onClickFilterString: undefined,
+ });
+ await userEvent.click(screen.getByText('log message 1'));
+ expect(screen.queryByText('Copy selection')).not.toBeInTheDocument();
+ expect(screen.queryByText(/details/)).not.toBeInTheDocument();
+ });
+
+ it('Displays Log Details if there is no text selection', async () => {
+ jest.spyOn(document, 'getSelection').mockReturnValue(null);
+ setup({
+ onClickFilterOutString: undefined,
+ onClickFilterString: undefined,
+ });
+ await userEvent.click(screen.getByText('log message 1'));
+ expect(screen.queryByText('Copy selection')).not.toBeInTheDocument();
+ expect(screen.getByText(/details/)).toBeInTheDocument();
+ });
+ });
});
diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx
index 96d1bd2e71e..94b35c7d81d 100644
--- a/public/app/features/logs/components/LogRows.tsx
+++ b/public/app/features/logs/components/LogRows.tsx
@@ -115,13 +115,15 @@ class UnThemedLogRows extends PureComponent {
}
handleSelection = (e: MouseEvent, row: LogRowModel): boolean => {
- if (this.popoverMenuSupported() === false) {
- return false;
- }
const selection = document.getSelection()?.toString();
if (!selection) {
return false;
}
+ if (this.popoverMenuSupported() === false) {
+ // This signals onRowClick inside LogRow to skip the event because the user is selecting text
+ return selection ? true : false;
+ }
+
if (!this.logRowsRef.current) {
return false;
}
@@ -247,7 +249,7 @@ class UnThemedLogRows extends PureComponent {
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
pinned={this.props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)}
isFilterLabelActive={this.props.isFilterLabelActive}
- handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
+ handleTextSelection={this.handleSelection}
{...rest}
/>
))}
@@ -270,7 +272,7 @@ class UnThemedLogRows extends PureComponent {
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
pinned={this.props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)}
isFilterLabelActive={this.props.isFilterLabelActive}
- handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
+ handleTextSelection={this.handleSelection}
{...rest}
/>
))}
diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureAuth.test.ts b/public/app/plugins/datasource/mssql/azureauth/AzureAuth.test.ts
index ae8d516966e..2d1a75fa02f 100644
--- a/public/app/plugins/datasource/mssql/azureauth/AzureAuth.test.ts
+++ b/public/app/plugins/datasource/mssql/azureauth/AzureAuth.test.ts
@@ -1,92 +1,115 @@
-import { AzureAuthType, AzureCloud, AzureCredentialsType, ConcealedSecretType } from '../types';
+import {
+ AzureCredentials,
+ AzureCloud,
+ ConcealedSecret,
+ AzureClientSecretCredentials,
+ instanceOfAzureCredential,
+ updateDatasourceCredentials,
+} from '@grafana/azure-sdk';
+import { config } from '@grafana/runtime';
import {
- configWithManagedIdentityEnabled,
- configWithManagedIdentityDisabled,
dataSourceSettingsWithMsiCredentials,
dataSourceSettingsWithClientSecretOnServer,
dataSourceSettingsWithClientSecretInSecureJSONData,
} from './AzureAuth.testMocks';
-import { getDefaultCredentials, getSecret, getCredentials, updateCredentials } from './AzureCredentialsConfig';
+import { getDefaultCredentials, getCredentials } from './AzureCredentialsConfig';
// NOTE: @ts-ignores are used to ignore the type errors that are thrown when passing in the mocks.
// This is because the mocks are partials of the actual types, so the types are not complete.
-export const CLIENT_SECRET_SYMBOL: ConcealedSecretType = Symbol('Concealed client secret');
+export const CLIENT_SECRET_SYMBOL: ConcealedSecret = Symbol('Concealed client secret');
export const CLIENT_SECRET_STRING = 'XXXX-super-secret-secret-XXXX';
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'), // Keep the rest of the actual module
+}));
+
describe('AzureAuth', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ });
+
describe('AzureCredentialsConfig', () => {
it('`getDefaultCredentials()` should return the correct credentials based on whether the managed identity is enabled', () => {
- const resultForManagedIdentityEnabled = getDefaultCredentials(true, AzureCloud.Public);
- const resultForManagedIdentityDisabled = getDefaultCredentials(false, AzureCloud.Public);
+ jest.mocked(config).azure.managedIdentityEnabled = true;
+ const resultForManagedIdentityEnabled = getDefaultCredentials();
+
+ jest.mocked(config).azure.managedIdentityEnabled = false;
+ const resultForManagedIdentityDisabled = getDefaultCredentials();
expect(resultForManagedIdentityEnabled).toEqual({ authType: 'msi' });
expect(resultForManagedIdentityDisabled).toEqual({ authType: 'clientsecret', azureCloud: 'AzureCloud' });
});
- it("`getSecret()` should correctly return the client secret if it's not concealed", () => {
- const resultFromServerSideSecret = getSecret(false, CLIENT_SECRET_STRING);
- expect(resultFromServerSideSecret).toBe(CLIENT_SECRET_STRING);
-
- const resultFromSecureJSONDataSecret = typeof getSecret(true, '');
- expect(resultFromSecureJSONDataSecret).toBe('symbol');
- });
-
describe('getCredentials()', () => {
it('should return the correct managed identity credentials', () => {
- // If `dataSourceSettings.authType === AzureAuthType.MSI` && `config.azure.managedIdentityEnabled === true`.
+ // If `dataSourceSettings.authType === 'msi'` && `config.azure.managedIdentityEnabled === true`.
+ jest.mocked(config).azure.managedIdentityEnabled = true;
const resultForManagedIdentityEnabled = getCredentials(
// @ts-ignore
- dataSourceSettingsWithMsiCredentials,
- configWithManagedIdentityEnabled
+ dataSourceSettingsWithMsiCredentials
);
- expect(resultForManagedIdentityEnabled).toEqual({ authType: AzureAuthType.MSI });
+ expect(resultForManagedIdentityEnabled).toEqual({ authType: 'msi' });
- // If `dataSourceSettings.authType === AzureAuthType.MSI` but `config.azure.managedIdentityEnabled !== true`.
+ // If `dataSourceSettings.authType === 'msi'` but `config.azure.managedIdentityEnabled !== true`.
// Default to basic client secret credentials.
+ jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForManagedIdentityEnabledInJSONButDisabledInConfig = getCredentials(
// @ts-ignore
- dataSourceSettingsWithMsiCredentials,
- configWithManagedIdentityDisabled
+ dataSourceSettingsWithMsiCredentials
);
expect(resultForManagedIdentityEnabledInJSONButDisabledInConfig).toEqual({
- authType: AzureAuthType.CLIENT_SECRET,
+ authType: 'clientsecret',
azureCloud: 'AzureCloud',
});
});
it('should return the correct client secret credentials', () => {
const basicExpectedResult = {
- authType: AzureAuthType.CLIENT_SECRET,
+ authType: 'clientsecret',
azureCloud: 'AzureCloud',
tenantId: 'XXXX-tenant-id-XXXX',
clientId: 'XXXX-client-id-XXXX',
};
- // If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == true`,
+ // If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == true`,
// i.e. the client secret is stored on the server.
+ jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForClientSecretCredentialsOnServer = getCredentials(
// @ts-ignore
- dataSourceSettingsWithClientSecretOnServer,
- configWithManagedIdentityDisabled
+ dataSourceSettingsWithClientSecretOnServer
);
// Here we test the properties separately because the client secret is a symbol,
// and since JS symobls are unique, we test via the `typeof` operator.
- expect(resultForClientSecretCredentialsOnServer.authType).toEqual(AzureAuthType.CLIENT_SECRET);
- expect(resultForClientSecretCredentialsOnServer.azureCloud).toEqual('AzureCloud');
- expect(resultForClientSecretCredentialsOnServer.tenantId).toEqual('XXXX-tenant-id-XXXX');
- expect(resultForClientSecretCredentialsOnServer.clientId).toEqual('XXXX-client-id-XXXX');
- expect(typeof resultForClientSecretCredentialsOnServer.clientSecret).toEqual('symbol');
+ expect(resultForClientSecretCredentialsOnServer.authType).toEqual('clientsecret');
+ expect(
+ instanceOfAzureCredential(
+ 'clientsecret',
+ resultForClientSecretCredentialsOnServer
+ )
+ ).toEqual(true);
+ expect((resultForClientSecretCredentialsOnServer as AzureClientSecretCredentials).azureCloud).toEqual(
+ 'AzureCloud'
+ );
+ expect((resultForClientSecretCredentialsOnServer as AzureClientSecretCredentials).tenantId).toEqual(
+ 'XXXX-tenant-id-XXXX'
+ );
+ expect((resultForClientSecretCredentialsOnServer as AzureClientSecretCredentials).clientId).toEqual(
+ 'XXXX-client-id-XXXX'
+ );
+ expect(typeof (resultForClientSecretCredentialsOnServer as AzureClientSecretCredentials).clientSecret).toEqual(
+ 'symbol'
+ );
- // If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == false`,
+ // If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == false`,
// i.e. the client secret is stored in the secureJson.
+ jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForClientSecretCredentialsInSecureJSON = getCredentials(
// @ts-ignore
- dataSourceSettingsWithClientSecretInSecureJSONData,
- configWithManagedIdentityDisabled
+ dataSourceSettingsWithClientSecretInSecureJSONData
);
expect(resultForClientSecretCredentialsInSecureJSON).toEqual({
...basicExpectedResult,
@@ -97,66 +120,68 @@ describe('AzureAuth', () => {
describe('updateCredentials()', () => {
it('should update the credentials for managed service identity correctly', () => {
- // If `dataSourceSettings.authType === AzureAuthType.MSI` && `config.azure.managedIdentityEnabled === true`.
- const resultForMsiCredentials = updateCredentials(
+ // If `dataSourceSettings.authType === 'msi'` && `config.azure.managedIdentityEnabled === true`.
+ jest.mocked(config).azure.managedIdentityEnabled = true;
+ const resultForMsiCredentials = updateDatasourceCredentials(
// @ts-ignore
dataSourceSettingsWithMsiCredentials,
- configWithManagedIdentityEnabled,
{
- authType: AzureAuthType.MSI,
+ authType: 'msi',
}
);
expect(resultForMsiCredentials).toEqual({ jsonData: { azureCredentials: { authType: 'msi' } } });
- // If `dataSourceSettings.authType === AzureAuthType.MSI` but `config.azure.managedIdentityEnabled !== true`.
+ // If `dataSourceSettings.authType === 'msi'` but `config.azure.managedIdentityEnabled !== true`.
+ jest.mocked(config).azure.managedIdentityEnabled = false;
expect(() =>
- updateCredentials(
+ updateDatasourceCredentials(
// @ts-ignore
dataSourceSettingsWithMsiCredentials,
- configWithManagedIdentityDisabled,
{
- authType: AzureAuthType.MSI,
+ authType: 'msi',
}
)
).toThrow('Managed Identity authentication is not enabled in Grafana config.');
});
it('should update the credentials for client secret correctly', () => {
- const basicClientSecretCredentials: AzureCredentialsType = {
- authType: AzureAuthType.CLIENT_SECRET,
- azureCloud: 'AzureCloud',
+ const basicClientSecretCredentials: AzureCredentials = {
+ authType: 'clientsecret',
+ azureCloud: AzureCloud.Public,
tenantId: 'XXXX-tenant-id-XXXX',
clientId: 'XXXX-client-id-XXXX',
};
- // If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == true`.
- const resultForClientSecretCredentials1 = updateCredentials(
+ // If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == true`.
+ jest.mocked(config).azure.managedIdentityEnabled = false;
+ const resultForClientSecretCredentials1 = updateDatasourceCredentials(
// @ts-ignore
dataSourceSettingsWithClientSecretOnServer,
- configWithManagedIdentityDisabled,
basicClientSecretCredentials
);
- expect(resultForClientSecretCredentials1).toEqual({
- jsonData: {
- azureCredentials: { ...basicClientSecretCredentials },
- },
- secureJsonData: { azureClientSecret: undefined },
- secureJsonFields: { azureClientSecret: false },
+
+ expect(resultForClientSecretCredentials1.jsonData.azureCredentials).toEqual(basicClientSecretCredentials);
+ expect(resultForClientSecretCredentials1.secureJsonData).toEqual({ azureClientSecret: undefined });
+ expect(resultForClientSecretCredentials1.secureJsonFields).toEqual({
+ azureClientSecret: false,
+ clientSecret: false,
});
- // If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == false`.
- const resultForClientSecretCredentials2 = updateCredentials(
+ // If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == false`.
+ jest.mocked(config).azure.managedIdentityEnabled = false;
+ const resultForClientSecretCredentials2 = updateDatasourceCredentials(
// @ts-ignore
dataSourceSettingsWithClientSecretInSecureJSONData,
- configWithManagedIdentityDisabled,
{ ...basicClientSecretCredentials, clientSecret: 'XXXX-super-secret-secret-XXXX' }
);
- expect(resultForClientSecretCredentials2).toEqual({
- jsonData: {
- azureCredentials: { ...basicClientSecretCredentials },
- },
- secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX' },
- secureJsonFields: { azureClientSecret: false },
+
+ expect(resultForClientSecretCredentials2.jsonData.azureCredentials).toEqual(basicClientSecretCredentials);
+ expect(resultForClientSecretCredentials2.secureJsonData).toEqual({
+ azureClientSecret: 'XXXX-super-secret-secret-XXXX',
+ });
+ expect(resultForClientSecretCredentials2.secureJsonFields).toEqual({
+ azureClientSecret: false,
+ clientSecret: false,
});
});
});
diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts b/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts
index 8c4fda9035e..137a41cc909 100644
--- a/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts
+++ b/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts
@@ -1,8 +1,6 @@
-import { DataSourceSettings } from '@grafana/data';
+import { AzureDataSourceSettings } from '@grafana/azure-sdk';
import { GrafanaBootConfig } from '@grafana/runtime';
-import { AzureAuthSecureJSONDataType, AzureAuthJSONDataType, AzureAuthType } from '../types';
-
export const configWithManagedIdentityEnabled: Partial = {
azure: {
managedIdentityEnabled: true,
@@ -24,31 +22,22 @@ export const configWithManagedIdentityDisabled: Partial = {
},
};
-export const dataSourceSettingsWithMsiCredentials: Partial<
- DataSourceSettings
-> = {
- jsonData: { azureCredentials: { authType: AzureAuthType.MSI } },
+export const dataSourceSettingsWithMsiCredentials: Partial = {
+ jsonData: { azureCredentials: { authType: 'msi' } },
};
-const basicJSONData = {
+// Will return symbol as the secret is concealed
+export const dataSourceSettingsWithClientSecretOnServer: Partial = {
jsonData: {
- azureCredentials: {
- authType: AzureAuthType.CLIENT_SECRET,
- tenantId: 'XXXX-tenant-id-XXXX',
- clientId: 'XXXX-client-id-XXXX',
- },
+ azureCredentials: { authType: 'clientsecret', clientId: 'XXXX-client-id-XXXX', tenantId: 'XXXX-tenant-id-XXXX' },
},
+ secureJsonFields: { azureClientSecret: true },
};
-
-// Will return symbol as the secret is concealed
-export const dataSourceSettingsWithClientSecretOnServer: Partial<
- DataSourceSettings
-> = { ...basicJSONData, secureJsonFields: { azureClientSecret: true } };
-
// Will return the secret as a string from the secureJsonData
-export const dataSourceSettingsWithClientSecretInSecureJSONData: Partial<
- DataSourceSettings
-> = {
- ...basicJSONData,
- secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX', password: undefined },
+export const dataSourceSettingsWithClientSecretInSecureJSONData: Partial = {
+ jsonData: {
+ azureCredentials: { authType: 'clientsecret', clientId: 'XXXX-client-id-XXXX', tenantId: 'XXXX-tenant-id-XXXX' },
+ },
+ secureJsonFields: { azureClientSecret: false },
+ secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX' },
};
diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx b/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx
index 2213abff501..ba93261c515 100644
--- a/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx
+++ b/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx
@@ -1,24 +1,25 @@
import { useMemo } from 'react';
import { useEffectOnce } from 'react-use';
+import { AzureCredentials, AzureCloud, updateDatasourceCredentials } from '@grafana/azure-sdk';
+import { SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types';
-import { AzureCredentialsType } from '../types';
-
-import { KnownAzureClouds } from './AzureCredentials';
-import { getCredentials, updateCredentials } from './AzureCredentialsConfig';
+import { getCredentials } from './AzureCredentialsConfig';
import { AzureCredentialsForm } from './AzureCredentialsForm';
+export const KnownAzureClouds: Array> = [{ value: AzureCloud.Public, label: 'Azure' }];
+
export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
const { dataSourceConfig: dsSettings, onChange } = props;
const managedIdentityEnabled = config.azure.managedIdentityEnabled;
const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled;
- const credentials = useMemo(() => getCredentials(dsSettings, config), [dsSettings]);
+ const credentials = useMemo(() => getCredentials(dsSettings), [dsSettings]);
- const onCredentialsChange = (credentials: AzureCredentialsType): void => {
- onChange(updateCredentials(dsSettings, config, credentials));
+ const onCredentialsChange = (credentials: AzureCredentials): void => {
+ onChange(updateDatasourceCredentials(dsSettings, credentials));
};
// The auth type needs to be set on the first load of the data source
diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureCredentials.ts b/public/app/plugins/datasource/mssql/azureauth/AzureCredentials.ts
deleted file mode 100644
index 123feb53bab..00000000000
--- a/public/app/plugins/datasource/mssql/azureauth/AzureCredentials.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { SelectableValue } from '@grafana/data';
-
-import { AzureCredentialsType, AzureAuthType } from '../types';
-
-export enum AzureCloud {
- Public = 'AzureCloud',
- None = '',
-}
-
-export const KnownAzureClouds: Array> = [{ value: AzureCloud.Public, label: 'Azure' }];
-
-export function isCredentialsComplete(credentials: AzureCredentialsType): boolean {
- switch (credentials.authType) {
- case AzureAuthType.MSI:
- return true;
- case AzureAuthType.CLIENT_SECRET:
- return !!(credentials.azureCloud && credentials.tenantId && credentials.clientId && credentials.clientSecret);
- case AzureAuthType.AD_PASSWORD:
- return !!(credentials.clientId && credentials.password && credentials.userId);
- }
-}
diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsConfig.ts b/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsConfig.ts
index acb493712ff..435edadbf34 100644
--- a/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsConfig.ts
+++ b/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsConfig.ts
@@ -1,167 +1,26 @@
-import { DataSourceSettings } from '@grafana/data';
-import { GrafanaBootConfig } from '@grafana/runtime';
-
import {
- AzureCloud,
- AzureCredentialsType,
- ConcealedSecretType,
- AzureAuthSecureJSONDataType,
- AzureAuthJSONDataType,
- AzureAuthType,
-} from '../types';
-
-export const getDefaultCredentials = (managedIdentityEnabled: boolean, cloud: string): AzureCredentialsType => {
- if (managedIdentityEnabled) {
- return { authType: AzureAuthType.MSI };
+ AzureCredentials,
+ AzureDataSourceSettings,
+ getDatasourceCredentials,
+ getDefaultAzureCloud,
+} from '@grafana/azure-sdk';
+import { config } from '@grafana/runtime';
+
+export const getDefaultCredentials = (): AzureCredentials => {
+ if (config.azure.managedIdentityEnabled) {
+ return { authType: 'msi' };
} else {
- return { authType: AzureAuthType.CLIENT_SECRET, azureCloud: cloud };
+ return { authType: 'clientsecret', azureCloud: getDefaultAzureCloud() };
}
};
-export const getSecret = (
- storedServerSide: boolean,
- secret: string | symbol | undefined
-): undefined | string | ConcealedSecretType => {
- const concealedSecret: ConcealedSecretType = Symbol('Concealed client secret');
- if (storedServerSide) {
- // The secret is concealed server side, so return the symbol
- return concealedSecret;
- } else {
- return typeof secret === 'string' && secret.length > 0 ? secret : undefined;
+export const getCredentials = (dsSettings: AzureDataSourceSettings): AzureCredentials => {
+ const credentials = getDatasourceCredentials(dsSettings);
+ if (credentials) {
+ return credentials;
}
-};
-
-export const getCredentials = (
- dsSettings: DataSourceSettings,
- bootConfig: GrafanaBootConfig
-): AzureCredentialsType => {
- // JSON data
- const credentials = dsSettings.jsonData?.azureCredentials;
-
- // Secure JSON data/fields
- const clientSecretStoredServerSide = dsSettings.secureJsonFields?.azureClientSecret;
- const clientSecret = dsSettings.secureJsonData?.azureClientSecret;
- const passwordStoredServerSide = dsSettings.secureJsonFields?.password;
- const password = dsSettings.secureJsonData?.password;
-
- // BootConfig data
- const managedIdentityEnabled = !!bootConfig.azure?.managedIdentityEnabled;
- const cloud = bootConfig.azure?.cloud || AzureCloud.Public;
// If no credentials saved, then return empty credentials
// of type based on whether the managed identity enabled
- if (!credentials) {
- return getDefaultCredentials(managedIdentityEnabled, cloud);
- }
-
- switch (credentials.authType) {
- case AzureAuthType.MSI:
- if (managedIdentityEnabled) {
- return {
- authType: AzureAuthType.MSI,
- };
- } else {
- // If authentication type is managed identity but managed identities were disabled in Grafana config,
- // then we should fallback to an empty app registration (client secret) configuration
- return {
- authType: AzureAuthType.CLIENT_SECRET,
- azureCloud: cloud,
- };
- }
- case AzureAuthType.CLIENT_SECRET:
- return {
- authType: AzureAuthType.CLIENT_SECRET,
- azureCloud: credentials.azureCloud || cloud,
- tenantId: credentials.tenantId,
- clientId: credentials.clientId,
- clientSecret: getSecret(clientSecretStoredServerSide, clientSecret),
- };
- case AzureAuthType.AD_PASSWORD:
- return {
- authType: AzureAuthType.AD_PASSWORD,
- userId: credentials.userId,
- clientId: credentials.clientId,
- password: getSecret(passwordStoredServerSide, password),
- };
- }
-};
-
-export const updateCredentials = (
- dsSettings: DataSourceSettings,
- bootConfig: GrafanaBootConfig,
- credentials: AzureCredentialsType
-): DataSourceSettings => {
- // BootConfig data
- const managedIdentityEnabled = !!bootConfig.azure?.managedIdentityEnabled;
- const cloud = bootConfig.azure?.cloud || AzureCloud.Public;
-
- switch (credentials.authType) {
- case AzureAuthType.MSI:
- if (!managedIdentityEnabled) {
- throw new Error('Managed Identity authentication is not enabled in Grafana config.');
- }
-
- dsSettings = {
- ...dsSettings,
- jsonData: {
- ...dsSettings.jsonData,
- azureCredentials: {
- authType: AzureAuthType.MSI,
- },
- },
- };
-
- return dsSettings;
-
- case AzureAuthType.CLIENT_SECRET:
- dsSettings = {
- ...dsSettings,
- jsonData: {
- ...dsSettings.jsonData,
- azureCredentials: {
- authType: AzureAuthType.CLIENT_SECRET,
- azureCloud: credentials.azureCloud || cloud,
- tenantId: credentials.tenantId,
- clientId: credentials.clientId,
- },
- },
- secureJsonData: {
- ...dsSettings.secureJsonData,
- azureClientSecret:
- typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
- ? credentials.clientSecret
- : undefined,
- },
- secureJsonFields: {
- ...dsSettings.secureJsonFields,
- azureClientSecret: typeof credentials.clientSecret === 'symbol',
- },
- };
-
- return dsSettings;
-
- case AzureAuthType.AD_PASSWORD:
- return {
- ...dsSettings,
- jsonData: {
- ...dsSettings.jsonData,
- azureCredentials: {
- authType: AzureAuthType.AD_PASSWORD,
- userId: credentials.userId,
- clientId: credentials.clientId,
- },
- },
- secureJsonData: {
- ...dsSettings.secureJsonData,
- password:
- typeof credentials.password === 'string' && credentials.password.length > 0
- ? credentials.password
- : undefined,
- },
- secureJsonFields: {
- ...dsSettings.secureJsonFields,
- password: typeof credentials.password === 'symbol',
- },
- };
- }
+ return getDefaultCredentials();
};
diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx b/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx
index 442f75bb7fa..ba2124a3c38 100644
--- a/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx
+++ b/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx
@@ -1,16 +1,15 @@
import { ChangeEvent } from 'react';
+import { AzureCredentials, AzureAuthType } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data';
import { Button, Field, Select, Input } from '@grafana/ui/src/components';
-import { AzureCredentialsType, AzureAuthType } from '../types';
-
export interface Props {
managedIdentityEnabled: boolean;
azureEntraPasswordCredentialsEnabled: boolean;
- credentials: AzureCredentialsType;
+ credentials: AzureCredentials;
azureCloudOptions?: SelectableValue[];
- onCredentialsChange: (updatedCredentials: AzureCredentialsType) => void;
+ onCredentialsChange: (updatedCredentials: AzureCredentials) => void;
disabled?: boolean;
}
@@ -26,9 +25,89 @@ export const AzureCredentialsForm = (props: Props) => {
const onAuthTypeChange = (selected: SelectableValue) => {
if (onCredentialsChange) {
- const updated: AzureCredentialsType = {
+ const updated: AzureCredentials = {
+ ...credentials,
+ authType: selected.value || 'msi',
+ };
+ onCredentialsChange(updated);
+ }
+ };
+
+ const onAzureCloudChange = (selected: SelectableValue) => {
+ if (credentials.authType === 'clientsecret') {
+ const updated: AzureCredentials = {
+ ...credentials,
+ azureCloud: selected.value,
+ };
+ onCredentialsChange(updated);
+ }
+ };
+
+ const onTenantIdChange = (event: ChangeEvent) => {
+ if (credentials.authType === 'clientsecret') {
+ const updated: AzureCredentials = {
+ ...credentials,
+ tenantId: event.target.value,
+ };
+ onCredentialsChange(updated);
+ }
+ };
+
+ const onClientIdChange = (event: ChangeEvent) => {
+ if (credentials.authType === 'clientsecret' || credentials.authType === 'ad-password') {
+ const updated: AzureCredentials = {
+ ...credentials,
+ clientId: event.target.value,
+ };
+ onCredentialsChange(updated);
+ }
+ };
+
+ const onClientSecretChange = (event: ChangeEvent) => {
+ if (credentials.authType === 'clientsecret') {
+ const updated: AzureCredentials = {
+ ...credentials,
+ clientSecret: event.target.value,
+ };
+ onCredentialsChange(updated);
+ }
+ };
+
+ const onClientSecretReset = () => {
+ if (credentials.authType === 'clientsecret') {
+ const updated: AzureCredentials = {
+ ...credentials,
+ clientSecret: '',
+ };
+ onCredentialsChange(updated);
+ }
+ };
+
+ const onUserIdChange = (event: ChangeEvent) => {
+ if (credentials.authType === 'ad-password') {
+ const updated: AzureCredentials = {
+ ...credentials,
+ userId: event.target.value,
+ };
+ onCredentialsChange(updated);
+ }
+ };
+
+ const onPasswordChange = (event: ChangeEvent) => {
+ if (credentials.authType === 'ad-password') {
+ const updated: AzureCredentials = {
+ ...credentials,
+ password: event.target.value,
+ };
+ onCredentialsChange(updated);
+ }
+ };
+
+ const onPasswordReset = () => {
+ if (credentials.authType === 'ad-password') {
+ const updated: AzureCredentials = {
...credentials,
- authType: selected.value || AzureAuthType.MSI,
+ password: '',
};
onCredentialsChange(updated);
}
@@ -36,33 +115,23 @@ export const AzureCredentialsForm = (props: Props) => {
const authTypeOptions: Array> = [
{
- value: AzureAuthType.CLIENT_SECRET,
+ value: 'clientsecret',
label: 'App Registration',
},
];
if (managedIdentityEnabled) {
authTypeOptions.push({
- value: AzureAuthType.MSI,
+ value: 'msi',
label: 'Managed Identity',
});
}
if (azureEntraPasswordCredentialsEnabled) {
authTypeOptions.push({
- value: AzureAuthType.AD_PASSWORD,
+ value: 'ad-password',
label: 'Azure Entra Password',
});
}
- const onInputChange = ({ property, value }: { property: keyof AzureCredentialsType; value: string }) => {
- if (onCredentialsChange) {
- const updated: AzureCredentialsType = {
- ...credentials,
- [property]: value,
- };
- onCredentialsChange(updated);
- }
- };
-
return (