Compare commits

...
Author SHA1 Message Date
Todd Treece f87389f10d sdk generic test 2025-12-03 17:16:44 -05:00
Gabriel MABILLE f7d9d22963 grafana-iam: standalone rely on storage layer resource permissions authorization (#114785)
* : standalone resource permissions authorization done at storage layer

* instantiate the accessclient
2025-12-03 19:16:37 +01:00
Liza Detrick 29cf10f1fb Logs: table add action buttons and deeplink to log line (#114330) 2025-12-03 09:26:41 -08:00
Gabriel MABILLE 8998b1fde4 grafana-iam: Implement api level user authorization (#114498)
* OnGoing

comment

* WIP on the wrapper

* Get before Delete

* WIP: add an unimplemented storage authorizer

* WIP implementing the resource permission authorize

* Implement beforeCreate

* Create, Delete, Update

* List

* Use a resource permissions wrapper

* Switch the main authorizer to service

* Add namespace

* Use compile for list

* Comment

* Remove unecessary comments

* fix bug with folder permissions

* Implement tests for List

* Test get

* List test small refactor

* Delete test

* Reorganize code

* imports

* Start splitting the tests

* test AfterDelete

* actually test beforeWrite

* Implement tests for wrapper create

* Test delete

* Test List and Get

* Fix List

* Remaining tests

* simplify

* Remove comments

* Reorder

* Change authorizer to allow access
2025-12-03 17:06:26 +01:00
Austin Pond 0f698d08d3 appinstaller: Use grafana-app-sdk apiserver.KubernetesGenericAPIServer to wrap the generic API server (#114654)
[App Platform] Use the app-sdk's apiserver.KubernetesGenericAPIServer in the serverWrapper to ensure that any extra logic for handling WebServices is used.
2025-12-03 11:05:00 -05:00
Liza Detrick c847f1fa4b Logs in Explore: Persist table sorting in the url (#114060) 2025-12-03 08:03:36 -08:00
Todd Treece 157cab192c Plugins API: Add authlib authorizer (#114773) 2025-12-03 11:01:55 -05:00
Dominik Prokop dbb4eb7e12 v2 conversion error: Throw DashboardVersionError correctly to redirecr to v1 API (#114772)
v2 conversion error: Throw DashboardVersionError correctly
2025-12-03 16:28:14 +01:00
Torkel Ödegaard c9fe5c3669 Page: Background prop to support canvas background for standard layout pages (#111174)
* Page canvas

* Working

* remove isOnCanvas prop on card, leave for different PR

* Update

* remove leftover

* Remvoe Card changes

* Fix

* Update
2025-12-03 16:10:25 +01:00
Torkel Ödegaard e89efd7068 Dashboard: Simplify layout categories (#114682)
* Dashboard: Simplify layout categories

* update

* fixing e2e
2025-12-03 16:07:10 +01:00
Renato Costa 3cb03b888f fix: apply appropriate labels and annotations when creating provisioned folders (#114669)
fix: labels and anotations when creating provisioned folders
2025-12-03 09:18:18 -05:00
Sanket KalekarandMihaly Gyongyosi d04f51f7ed Auth: Fix inconsistent symbol validation by allowing underscore "_" in strong password policy (#114571)
* Update validation regex for symbol requirement

Fixes #114544 
frontend: accept underscore as valid symbol in password validation

* test: add underscore symbol test for strong password policy

* chore(changelog): add changelog for #114571

* Delete changelog/114571.md

* fix: correct indentation in password test

* chore(workflows): skip OIDC steps for fork PRs

* chore(workflows): pass fork status to reusable workflow

* revert: remove workflow changes for fork detection

* revert: remove fork status parameter from workflow caller

* Fix formatting issues in PR patch check workflow

---------

Co-authored-by: Mihaly Gyongyosi <mgyongyosi@users.noreply.github.com>
2025-12-03 14:32:28 +01:00
Sonia Aguilar b74465e428 Alerting: Fix 'Rule group does not exist' error toast (#101949) (#114766)
Alerting: Fix "Rule group does not exist" error toast (#101949)
2025-12-03 12:33:48 +01:00
f3ed3a999d Dashboard Schema V2: Preserve ds refs that only have type (#114734)
Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
2025-12-03 11:29:52 +00:00
Ryan McKinley 612af5ef55 Search: improve openapi operation ids (#114754) 2025-12-03 10:28:59 +00:00
104 changed files with 2309 additions and 435 deletions
+9 -11
View File
@@ -10,7 +10,7 @@ require (
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
github.com/grafana/grafana-app-sdk v0.48.4
github.com/grafana/grafana-app-sdk/logging v0.48.3
github.com/grafana/grafana-plugin-sdk-go v0.284.0
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8
github.com/grafana/grafana/pkg/apimachinery v0.0.0
github.com/stretchr/testify v1.11.1
k8s.io/apimachinery v0.34.2
@@ -61,7 +61,6 @@ require (
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apache/arrow-go/v18 v18.4.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/at-wat/mqtt-go v0.19.6 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
@@ -83,7 +82,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -111,14 +109,14 @@ require (
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.24.0 // indirect
github.com/go-openapi/errors v0.22.3 // indirect
github.com/go-openapi/analysis v0.24.1 // indirect
github.com/go-openapi/errors v0.22.4 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/loads v0.23.1 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/loads v0.23.2 // indirect
github.com/go-openapi/runtime v0.28.0 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-openapi/strfmt v0.24.0 // indirect
github.com/go-openapi/spec v0.22.1 // indirect
github.com/go-openapi/strfmt v0.25.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
@@ -129,7 +127,7 @@ require (
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-openapi/validate v0.25.0 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
@@ -256,7 +254,7 @@ require (
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
+20 -22
View File
@@ -163,8 +163,6 @@ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/at-wat/mqtt-go v0.19.6 h1:+jo59yGHB3oqBU8nSFYbq6KDgN4ngMpqts5Qtyq61pc=
github.com/at-wat/mqtt-go v0.19.6/go.mod h1:Y/YrYwUlLE4N53//699Zir3WJnxSdJojvLEAbZxJjgQ=
github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@@ -249,8 +247,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -406,22 +402,22 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA=
github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw=
github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ=
github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM=
github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84=
github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM=
github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0=
github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc=
github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
@@ -444,8 +440,10 @@ github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU=
github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@@ -626,8 +624,8 @@ github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8 h1:CBm/okDTZW1dJ1PqPYylzc2aPa28RxmhVdPraNhUeRM=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8/go.mod h1:tY89hKliNYDQUWU/Y2F/QBpSaBvXbvd91V0oQ+dkW/s=
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
@@ -1150,8 +1148,8 @@ go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+1 -1
View File
@@ -14,7 +14,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+4 -2
View File
@@ -10,12 +10,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+1 -1
View File
@@ -21,7 +21,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -25,12 +25,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+1 -1
View File
@@ -27,7 +27,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -38,12 +38,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -25,12 +25,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+1 -1
View File
@@ -22,7 +22,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -25,12 +25,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+1 -1
View File
@@ -19,7 +19,7 @@ require (
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -16,12 +16,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+1 -1
View File
@@ -22,7 +22,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -25,12 +25,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+2 -3
View File
@@ -7,7 +7,7 @@ require (
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4
github.com/grafana/grafana-app-sdk v0.48.4
github.com/grafana/grafana-app-sdk/logging v0.48.3
github.com/grafana/grafana-plugin-sdk-go v0.284.0
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
@@ -23,7 +23,6 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
@@ -34,7 +33,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+6 -6
View File
@@ -18,8 +18,6 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -49,12 +47,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
@@ -89,8 +89,8 @@ github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAod
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8 h1:CBm/okDTZW1dJ1PqPYylzc2aPa28RxmhVdPraNhUeRM=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8/go.mod h1:tY89hKliNYDQUWU/Y2F/QBpSaBvXbvd91V0oQ+dkW/s=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
+1 -1
View File
@@ -25,7 +25,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -27,12 +27,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+1 -1
View File
@@ -18,7 +18,7 @@ require (
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -16,12 +16,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+9 -11
View File
@@ -102,7 +102,6 @@ require (
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apache/arrow-go/v18 v18.4.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/at-wat/mqtt-go v0.19.6 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
@@ -138,7 +137,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
@@ -176,14 +174,14 @@ require (
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.24.0 // indirect
github.com/go-openapi/errors v0.22.3 // indirect
github.com/go-openapi/analysis v0.24.1 // indirect
github.com/go-openapi/errors v0.22.4 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/loads v0.23.1 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/loads v0.23.2 // indirect
github.com/go-openapi/runtime v0.28.0 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-openapi/strfmt v0.24.0 // indirect
github.com/go-openapi/spec v0.22.1 // indirect
github.com/go-openapi/strfmt v0.25.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
@@ -194,7 +192,7 @@ require (
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-openapi/validate v0.25.0 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
@@ -228,7 +226,7 @@ require (
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8 // indirect
github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
github.com/grafana/grafana/apps/secret v0.0.0 // indirect
@@ -379,7 +377,7 @@ require (
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect
+20 -22
View File
@@ -226,8 +226,6 @@ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/at-wat/mqtt-go v0.19.6 h1:+jo59yGHB3oqBU8nSFYbq6KDgN4ngMpqts5Qtyq61pc=
github.com/at-wat/mqtt-go v0.19.6/go.mod h1:Y/YrYwUlLE4N53//699Zir3WJnxSdJojvLEAbZxJjgQ=
github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@@ -386,8 +384,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -591,22 +587,22 @@ github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA=
github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw=
github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ=
github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM=
github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84=
github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM=
github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0=
github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc=
github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
@@ -629,8 +625,10 @@ github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU=
github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
@@ -849,8 +847,8 @@ github.com/grafana/grafana-google-sdk-go v0.4.2 h1:F44hQF1y6UVJhlJPi+Mz+GCJsioVg
github.com/grafana/grafana-google-sdk-go v0.4.2/go.mod h1:U73+w9DlbEtUonhQUzERwlXnzWTtfRoyrtKH8d3VY40=
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8 h1:CBm/okDTZW1dJ1PqPYylzc2aPa28RxmhVdPraNhUeRM=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8/go.mod h1:tY89hKliNYDQUWU/Y2F/QBpSaBvXbvd91V0oQ+dkW/s=
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk=
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k=
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
@@ -1567,8 +1565,8 @@ go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmy
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+1 -1
View File
@@ -22,7 +22,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -25,12 +25,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+1 -1
View File
@@ -22,7 +22,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -25,12 +25,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -25,12 +25,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
+46 -5
View File
@@ -10,11 +10,13 @@ import (
"github.com/grafana/grafana-app-sdk/operator"
"github.com/grafana/grafana-app-sdk/simple"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
restclient "k8s.io/client-go/rest"
"k8s.io/klog/v2"
authlib "github.com/grafana/authlib/types"
pluginsappapis "github.com/grafana/grafana/apps/plugins/pkg/apis"
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
"github.com/grafana/grafana/apps/plugins/pkg/app/meta"
@@ -68,7 +70,7 @@ type PluginAppConfig struct {
func ProvideAppInstaller(
metaProviderManager *meta.ProviderManager,
) (appsdkapiserver.AppInstaller, error) {
) (*PluginAppInstaller, error) {
specificConfig := &PluginAppConfig{
MetaProviderManager: metaProviderManager,
}
@@ -83,7 +85,7 @@ func ProvideAppInstaller(
return nil, err
}
appInstaller := &pluginAppInstaller{
appInstaller := &PluginAppInstaller{
AppInstaller: defaultInstaller,
metaManager: metaProviderManager,
ready: make(chan struct{}),
@@ -91,16 +93,22 @@ func ProvideAppInstaller(
return appInstaller, nil
}
type pluginAppInstaller struct {
func (p *PluginAppInstaller) WithAccessChecker(access authlib.AccessChecker) *PluginAppInstaller {
p.access = access
return p
}
type PluginAppInstaller struct {
appsdkapiserver.AppInstaller
metaManager *meta.ProviderManager
access authlib.AccessChecker
// restConfig is set during InitializeApp and used by the client factory
restConfig *restclient.Config
ready chan struct{}
}
func (p *pluginAppInstaller) InitializeApp(restConfig restclient.Config) error {
func (p *PluginAppInstaller) InitializeApp(restConfig restclient.Config) error {
if p.restConfig == nil {
p.restConfig = &restConfig
close(p.ready)
@@ -108,7 +116,7 @@ func (p *pluginAppInstaller) InitializeApp(restConfig restclient.Config) error {
return p.AppInstaller.InitializeApp(restConfig)
}
func (p *pluginAppInstaller) InstallAPIs(
func (p *PluginAppInstaller) InstallAPIs(
server appsdkapiserver.GenericAPIServer,
restOptsGetter generic.RESTOptionsGetter,
) error {
@@ -139,3 +147,36 @@ func (p *pluginAppInstaller) InstallAPIs(
}
return p.AppInstaller.InstallAPIs(wrappedServer, restOptsGetter)
}
func (p *PluginAppInstaller) GetAuthorizer() authorizer.Authorizer {
if p.access == nil {
return nil
}
return authorizer.AuthorizerFunc(
func(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
info, ok := authlib.AuthInfoFrom(ctx)
if !ok {
return authorizer.DecisionDeny, "failed to get auth info", nil
}
res, err := p.access.Check(ctx, info, authlib.CheckRequest{
Verb: a.GetVerb(),
Group: a.GetAPIGroup(),
Resource: a.GetResource(),
Name: a.GetName(),
Namespace: a.GetNamespace(),
Subresource: a.GetSubresource(),
Path: a.GetPath(),
}, "")
if err != nil {
return authorizer.DecisionDeny, "failed to perform authorization", err
}
if !res.Allowed {
return authorizer.DecisionDeny, "permission denied", nil
}
return authorizer.DecisionAllow, "", nil
})
}
+1 -1
View File
@@ -18,7 +18,7 @@ require (
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -16,12 +16,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+1 -1
View File
@@ -33,7 +33,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+4 -2
View File
@@ -25,12 +25,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+1 -1
View File
@@ -14,7 +14,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+4 -2
View File
@@ -10,12 +10,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -18,12 +18,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+1 -1
View File
@@ -26,7 +26,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
+4 -2
View File
@@ -27,12 +27,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
@@ -416,9 +416,6 @@ test.describe(
// Go back to dashboard options
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
// Expand layouts section
await page.getByLabel('Expand Group layout category').click();
// Select tabs layout
await page.getByLabel('layout-selection-option-Tabs').click();
@@ -757,9 +754,6 @@ test.describe(
// Go back to dashboard options
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
// Expand layouts section
await page.getByLabel('Expand Group layout category').click();
// Select rows layout
await page.getByLabel('layout-selection-option-Rows').click();
@@ -33,7 +33,6 @@ test.describe(
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
await page.getByLabel('Expand Panel layout category').click();
await page.getByLabel('layout-selection-option-Auto grid').click();
await expect(
@@ -65,8 +64,6 @@ test.describe(
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
await page.getByLabel('Expand Panel layout category').click();
await page.getByLabel('layout-selection-option-Auto grid').click();
// Get initial positions - standard width should have panels on different rows
@@ -127,8 +124,6 @@ test.describe(
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
await page.getByLabel('Expand Panel layout category').click();
await page.getByLabel('layout-selection-option-Auto grid').click();
await dashboardPage
@@ -186,7 +181,6 @@ test.describe(
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
await page.getByLabel('Expand Panel layout category').click();
await page.getByLabel('layout-selection-option-Auto grid').click();
await dashboardPage
@@ -222,7 +216,6 @@ test.describe(
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
await page.getByLabel('Expand Panel layout category').click();
await page.getByLabel('layout-selection-option-Auto grid').click();
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
@@ -278,7 +271,6 @@ test.describe(
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
await page.getByLabel('Expand Panel layout category').click();
await page.getByLabel('layout-selection-option-Auto grid').click();
const regularRowHeight = await getPanelHeight(dashboardPage, selectors);
@@ -336,7 +328,6 @@ test.describe(
).toHaveCount(3);
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click();
await page.getByLabel('Expand Panel layout category').click();
await page.getByLabel('layout-selection-option-Auto grid').click();
// Set narrow column width first to ensure panels fit horizontally
@@ -464,6 +464,5 @@ test.describe(
);
async function switchToAutoGrid(page: Page) {
await page.getByLabel('Expand Panel layout category').click();
await page.getByLabel('layout-selection-option-Auto grid').click();
}
+9 -11
View File
@@ -65,9 +65,9 @@ require (
github.com/go-kit/log v0.2.1 // @grafana/grafana-backend-group
github.com/go-ldap/ldap/v3 v3.4.4 // @grafana/identity-access-team
github.com/go-logfmt/logfmt v0.6.1 // @grafana/oss-big-tent
github.com/go-openapi/loads v0.23.1 // @grafana/alerting-backend
github.com/go-openapi/loads v0.23.2 // @grafana/alerting-backend
github.com/go-openapi/runtime v0.28.0 // @grafana/alerting-backend
github.com/go-openapi/strfmt v0.24.0 // @grafana/alerting-backend
github.com/go-openapi/strfmt v0.25.0 // @grafana/alerting-backend
github.com/go-redis/redis/v8 v8.11.5 // indirect; @grafana/grafana-backend-group
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // @grafana/grafana-backend-group
github.com/go-sql-driver/mysql v1.9.3 // @grafana/grafana-search-and-storage
@@ -104,7 +104,7 @@ require (
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana-google-sdk-go v0.4.2 // @grafana/partner-datasources
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8 // @grafana/plugins-platform-backend
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 // @grafana/alerting-backend
github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs
github.com/grafana/nanogit v0.3.0 // indirect; @grafana/grafana-git-ui-sync-team
@@ -334,7 +334,6 @@ require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/at-wat/mqtt-go v0.19.6 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect
@@ -388,7 +387,6 @@ require (
github.com/centrifugal/protocol v0.16.2 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect
@@ -424,13 +422,13 @@ require (
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.24.0 // indirect
github.com/go-openapi/errors v0.22.3 // indirect
github.com/go-openapi/analysis v0.24.1 // indirect
github.com/go-openapi/errors v0.22.4 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/spec v0.22.1 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/validate v0.25.0 // indirect
github.com/go-openapi/validate v0.25.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
@@ -606,7 +604,7 @@ require (
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/collector/featuregate v1.44.0 // indirect
go.opentelemetry.io/collector/semconv v0.124.0 // indirect
+20 -21
View File
@@ -838,7 +838,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/at-wat/mqtt-go v0.19.6 h1:+jo59yGHB3oqBU8nSFYbq6KDgN4ngMpqts5Qtyq61pc=
github.com/at-wat/mqtt-go v0.19.6/go.mod h1:Y/YrYwUlLE4N53//699Zir3WJnxSdJojvLEAbZxJjgQ=
@@ -1017,8 +1016,6 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -1273,14 +1270,14 @@ github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9sn
github.com/go-openapi/analysis v0.21.5/go.mod h1:25YcZosX9Lwz2wBsrFrrsL8bmjjXdlyP6zsr2AMy29M=
github.com/go-openapi/analysis v0.22.0/go.mod h1:acDnkkCI2QxIo8sSIPgmp1wUlRohV7vfGtAIVae73b0=
github.com/go-openapi/analysis v0.22.2/go.mod h1:pDF4UbZsQTo/oNuRfAWWd4dAh4yuYf//LYorPTjrpvo=
github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA=
github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw=
github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM=
github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84=
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho=
github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ=
github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM=
github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
@@ -1295,16 +1292,16 @@ github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3Hfo
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.20.3/go.mod h1:FviDZ46i9ivh810gqzFLl5NttD5q3tSlMLqLr6okedM=
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
github.com/go-openapi/loads v0.21.3/go.mod h1:Y3aMR24iHbKHppOj91nQ/SHc0cuPbAr4ndY4a02xydc=
github.com/go-openapi/loads v0.21.5/go.mod h1:PxTsnFBoBe+z89riT+wYt3prmSBP6GDAQh2l9H1Flz8=
github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0=
github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44=
github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
github.com/go-openapi/runtime v0.19.3/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
@@ -1318,16 +1315,16 @@ github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcs
github.com/go-openapi/spec v0.20.12/go.mod h1:iSCgnBcwbMW9SfzJb8iYynXvcY6C/QFrI7otzF7xGM4=
github.com/go-openapi/spec v0.20.13/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
github.com/go-openapi/strfmt v0.21.10/go.mod h1:vNDMwbilnl7xKiO/Ve/8H8Bb2JIInBnH+lqiw6QWgis=
github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4=
github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc=
github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4=
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
@@ -1358,12 +1355,14 @@ github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
github.com/go-openapi/validate v0.22.4/go.mod h1:qm6O8ZIcPVdSY5219468Jv7kBdGvkiZLPOmqnqTUZ2A=
github.com/go-openapi/validate v0.23.0/go.mod h1:EeiAZ5bmpSIOJV1WLfyYF9qp/B1ZgSaEpHTJHtN5cbE=
github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU=
github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw=
github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -1649,8 +1648,8 @@ github.com/grafana/grafana-google-sdk-go v0.4.2 h1:F44hQF1y6UVJhlJPi+Mz+GCJsioVg
github.com/grafana/grafana-google-sdk-go v0.4.2/go.mod h1:U73+w9DlbEtUonhQUzERwlXnzWTtfRoyrtKH8d3VY40=
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8 h1:CBm/okDTZW1dJ1PqPYylzc2aPa28RxmhVdPraNhUeRM=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8/go.mod h1:tY89hKliNYDQUWU/Y2F/QBpSaBvXbvd91V0oQ+dkW/s=
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk=
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k=
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
@@ -2616,8 +2615,8 @@ go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL
go.mongodb.org/mongo-driver v1.0.4/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+2
View File
@@ -40,3 +40,5 @@ use (
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56
replace github.com/grafana/grafana-plugin-sdk-go => github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8
+7 -1
View File
@@ -409,6 +409,7 @@ github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+ye
github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw=
github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
@@ -759,6 +760,7 @@ github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw=
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@@ -770,7 +772,8 @@ github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
@@ -1992,6 +1995,7 @@ golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
@@ -2102,6 +2106,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
@@ -2123,6 +2128,7 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0=
+1 -1
View File
@@ -427,7 +427,7 @@
"slate": "0.47.9",
"slate-plain-serializer": "0.7.13",
"slate-react": "0.22.10",
"swagger-ui-react": "5.30.2",
"swagger-ui-react": "5.30.3",
"symbol-observable": "4.0.0",
"systemjs": "6.15.1",
"tslib": "2.8.1",
@@ -235,7 +235,7 @@ const injectedRtkApi = api
}),
invalidatesTags: ['LibraryPanel'],
}),
getSearch: build.query<GetSearchApiResponse, GetSearchApiArg>({
searchDashboardsAndFolders: build.query<SearchDashboardsAndFoldersApiResponse, SearchDashboardsAndFoldersApiArg>({
query: (queryArg) => ({
url: `/search`,
params: {
@@ -253,7 +253,7 @@ const injectedRtkApi = api
}),
providesTags: ['Search'],
}),
getSearchSortable: build.query<GetSearchSortableApiResponse, GetSearchSortableApiArg>({
getSortableFields: build.query<GetSortableFieldsApiResponse, GetSortableFieldsApiArg>({
query: () => ({ url: `/search/sortable` }),
providesTags: ['Search'],
}),
@@ -653,8 +653,8 @@ export type UpdateLibraryPanelApiArg = {
force?: boolean;
patch: Patch;
};
export type GetSearchApiResponse = /** status 200 undefined */ SearchResults;
export type GetSearchApiArg = {
export type SearchDashboardsAndFoldersApiResponse = /** status 200 undefined */ SearchResults;
export type SearchDashboardsAndFoldersApiArg = {
/** user query string */
query?: string;
/** search dashboards or folders. When empty, this will search both */
@@ -676,7 +676,7 @@ export type GetSearchApiArg = {
/** add debugging info that may help explain why the result matched */
explain?: boolean;
};
export type GetSearchSortableApiResponse = /** status 200 undefined */ {
export type GetSortableFieldsApiResponse = /** status 200 undefined */ {
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
apiVersion?: string;
/** Sortable fields (depends on backend support) */
@@ -684,7 +684,7 @@ export type GetSearchSortableApiResponse = /** status 200 undefined */ {
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
kind?: string;
};
export type GetSearchSortableApiArg = void;
export type GetSortableFieldsApiArg = void;
export type ListSnapshotApiResponse = /** status 200 OK */ SnapshotList;
export type ListSnapshotApiArg = {
/** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */
@@ -1262,10 +1262,10 @@ export const {
useReplaceLibraryPanelMutation,
useDeleteLibraryPanelMutation,
useUpdateLibraryPanelMutation,
useGetSearchQuery,
useLazyGetSearchQuery,
useGetSearchSortableQuery,
useLazyGetSearchSortableQuery,
useSearchDashboardsAndFoldersQuery,
useLazySearchDashboardsAndFoldersQuery,
useGetSortableFieldsQuery,
useLazyGetSortableFieldsQuery,
useListSnapshotQuery,
useLazyListSnapshotQuery,
useCreateSnapshotMutation,
@@ -85,6 +85,9 @@ export interface ExploreLogsPanelState {
refId?: string;
displayedFields?: string[];
sortOrder?: LogsSortOrder;
// Column sort state for table view. Persists between query changes.
tableSortBy?: string;
tableSortDir?: 'asc' | 'desc';
}
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
@@ -19,8 +19,11 @@ export interface PluginPageProps {
pageNav?: NavModelItem;
children: React.ReactNode;
layout?: PageLayoutType;
/** Background color of the page */
background?: PluginPageBackground;
}
export type PluginPageBackground = 'primary' | 'canvas';
export type PluginPageType = React.ComponentType<PluginPageProps>;
export let PluginPage: PluginPageType = ({ children }) => {
+1 -1
View File
@@ -37,7 +37,7 @@ export {
getRunRequest,
} from './services/QueryRunner';
export { PluginPage } from './components/PluginPage';
export type { PluginPageType, PluginPageProps } from './components/PluginPage';
export type { PluginPageType, PluginPageProps, PluginPageBackground } from './components/PluginPage';
export {
DataSourcePicker,
type DataSourcePickerProps,
+2 -3
View File
@@ -4,7 +4,7 @@ go 1.25.5
require (
github.com/emicklei/go-restful/v3 v3.13.0
github.com/grafana/grafana-plugin-sdk-go v0.284.0
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e
github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
@@ -29,7 +29,6 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -41,7 +40,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
+6 -6
View File
@@ -22,8 +22,6 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
@@ -59,12 +57,14 @@ github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -101,8 +101,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8 h1:CBm/okDTZW1dJ1PqPYylzc2aPa28RxmhVdPraNhUeRM=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8/go.mod h1:tY89hKliNYDQUWU/Y2F/QBpSaBvXbvd91V0oQ+dkW/s=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250514132646-acbc7b54ed9e h1:vheR6iPO1np+G/ARjcWx9yiWd7BnTDgyTgsnMhOvx70=
+1 -1
View File
@@ -21,7 +21,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+4 -2
View File
@@ -15,12 +15,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+1
View File
@@ -160,6 +160,7 @@ var serviceIdentityTokenPermissions = []string{
"iam.grafana.app:*",
"preferences.grafana.app:*", // user, team, and org preferences
"collections.grafana.app:*", // user stars
"plugins.grafana.app:*",
// Secrets Manager uses a custom verb for secret decryption, and its authorizer does not allow wildcard permissions.
"secret.grafana.app/securevalues:decrypt",
+1 -1
View File
@@ -35,7 +35,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+4 -2
View File
@@ -34,12 +34,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+2 -3
View File
@@ -4,7 +4,7 @@ go 1.25.5
require (
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4
github.com/grafana/grafana-plugin-sdk-go v0.284.0
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8
github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134
github.com/json-iterator/go v1.1.12
github.com/prometheus/client_golang v1.23.2
@@ -28,7 +28,6 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/varint v1.0.0 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
@@ -37,7 +36,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
+20 -22
View File
@@ -21,8 +21,6 @@ github.com/apache/arrow-go/v18 v18.4.1 h1:q/jVkBWCJOB9reDgaIZIdruLQUb1kbkvOnOFez
github.com/apache/arrow-go/v18 v18.4.1/go.mod h1:tLyFubsAl17bvFdUAy24bsSvA/6ww95Iqi67fTpGu3E=
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
@@ -39,8 +37,6 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -62,20 +58,20 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA=
github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw=
github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ=
github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM=
github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84=
github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM=
github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0=
github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc=
github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
@@ -96,8 +92,10 @@ github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU=
github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@@ -130,8 +128,8 @@ github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8 h1:CBm/okDTZW1dJ1PqPYylzc2aPa28RxmhVdPraNhUeRM=
github.com/grafana/grafana-plugin-sdk-go v0.284.1-0.20251203221144-c256270a1ce8/go.mod h1:tY89hKliNYDQUWU/Y2F/QBpSaBvXbvd91V0oQ+dkW/s=
github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134 h1:xly75v5lFNR37q+wXnwA5yU/fPW9IOSYbhFpt4tQyt8=
github.com/grafana/grafana/apps/scope v0.0.0-20251007093103-792853df9134/go.mod h1:zijsUNa1zi476JJIR2Lcm/Paz1nRCno9XCp6hbS6G9o=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251007081214-26e147d01f0a h1:L7xgV9mP6MRF3L2/vDOjNR7heaBPbXPMGTDN9/jXSFQ=
@@ -276,8 +274,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
+2
View File
@@ -66,6 +66,7 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
Tags: []string{"Search"},
OperationId: "searchDashboardsAndFolders",
Description: "Dashboard search",
Parameters: []*spec3.Parameter{
{
@@ -218,6 +219,7 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
Tags: []string{"Search"},
OperationId: "getSortableFields",
Description: "Get sortable fields",
Parameters: []*spec3.Parameter{
{
+14 -1
View File
@@ -22,6 +22,19 @@ type iamAuthorizer struct {
func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient authlib.AccessClient) authorizer.Authorizer {
resourceAuthorizer := make(map[string]authorizer.Authorizer)
// Authorizer that allows any authenticated user
// To be used when authorization is handled at the storage layer
allowAuthorizer := authorizer.AuthorizerFunc(func(
ctx context.Context, attr authorizer.Attributes,
) (authorized authorizer.Decision, reason string, err error) {
if !attr.IsResourceRequest() {
return authorizer.DecisionNoOpinion, "", nil
}
// Any authenticated user can access the API
return authorizer.DecisionAllow, "", nil
})
// Identity specific resources
legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient)
resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = legacyAuthorizer
@@ -31,7 +44,7 @@ func newIAMAuthorizer(accessClient authlib.AccessClient, legacyAccessClient auth
authorizer := gfauthorizer.NewResourceAuthorizer(accessClient)
resourceAuthorizer[iamv0.CoreRoleInfo.GetName()] = iamauthorizer.NewCoreRoleAuthorizer(accessClient)
resourceAuthorizer[iamv0.RoleInfo.GetName()] = authorizer
resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = authorizer
resourceAuthorizer[iamv0.ResourcePermissionInfo.GetName()] = allowAuthorizer // Handled at storage layer
resourceAuthorizer[iamv0.RoleBindingInfo.GetName()] = authorizer
resourceAuthorizer[iamv0.ServiceAccountResourceInfo.GetName()] = authorizer
resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer
@@ -0,0 +1,163 @@
package authorizer
import (
"context"
"fmt"
"github.com/grafana/authlib/types"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper"
)
// TODO: Logs, Metrics, Traces?
// ResourcePermissionsAuthorizer
type ResourcePermissionsAuthorizer struct {
accessClient types.AccessClient
}
var _ storewrapper.ResourceStorageAuthorizer = (*ResourcePermissionsAuthorizer)(nil)
func NewResourcePermissionsAuthorizer(accessClient types.AccessClient) *ResourcePermissionsAuthorizer {
return &ResourcePermissionsAuthorizer{
accessClient: accessClient,
}
}
// AfterGet implements ResourceStorageAuthorizer.
func (r *ResourcePermissionsAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error {
authInfo, ok := types.AuthInfoFrom(ctx)
if !ok {
return storewrapper.ErrUnauthenticated
}
switch o := obj.(type) {
case *iamv0.ResourcePermission:
target := o.Spec.Resource
// TODO: Fetch the resource to retrieve its parent folder.
parent := ""
checkReq := types.CheckRequest{
Namespace: o.Namespace,
Group: target.ApiGroup,
Resource: target.Resource,
Verb: utils.VerbGetPermissions,
Name: target.Name,
}
res, err := r.accessClient.Check(ctx, authInfo, checkReq, parent)
if err != nil {
return err
}
if !res.Allowed {
return storewrapper.ErrUnauthorized
}
return nil
default:
return fmt.Errorf("expected ResourcePermission, got %T: %w", o, storewrapper.ErrUnexpectedType)
}
}
func (r *ResourcePermissionsAuthorizer) beforeWrite(ctx context.Context, obj runtime.Object) error {
authInfo, ok := types.AuthInfoFrom(ctx)
if !ok {
return storewrapper.ErrUnauthenticated
}
switch o := obj.(type) {
case *iamv0.ResourcePermission:
target := o.Spec.Resource
// TODO: Fetch the resource to retrieve its parent folder.
parent := ""
checkReq := types.CheckRequest{
Namespace: o.Namespace,
Group: target.ApiGroup,
Resource: target.Resource,
Verb: utils.VerbSetPermissions,
Name: target.Name,
}
res, err := r.accessClient.Check(ctx, authInfo, checkReq, parent)
if err != nil {
return err
}
if !res.Allowed {
return storewrapper.ErrUnauthorized
}
return nil
default:
return fmt.Errorf("expected ResourcePermission, got %T: %w", o, storewrapper.ErrUnexpectedType)
}
}
// BeforeCreate implements ResourceStorageAuthorizer.
func (r *ResourcePermissionsAuthorizer) BeforeCreate(ctx context.Context, obj runtime.Object) error {
return r.beforeWrite(ctx, obj)
}
// BeforeDelete implements ResourceStorageAuthorizer.
func (r *ResourcePermissionsAuthorizer) BeforeDelete(ctx context.Context, obj runtime.Object) error {
return r.beforeWrite(ctx, obj)
}
// BeforeUpdate implements ResourceStorageAuthorizer.
func (r *ResourcePermissionsAuthorizer) BeforeUpdate(ctx context.Context, obj runtime.Object) error {
return r.beforeWrite(ctx, obj)
}
// FilterList implements ResourceStorageAuthorizer.
func (r *ResourcePermissionsAuthorizer) FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error) {
authInfo, ok := types.AuthInfoFrom(ctx)
if !ok {
return nil, storewrapper.ErrUnauthenticated
}
switch l := list.(type) {
case *iamv0.ResourcePermissionList:
var (
filteredItems []iamv0.ResourcePermission
err error
canViewFuncs = map[schema.GroupResource]types.ItemChecker{}
)
for _, item := range l.Items {
gr := schema.GroupResource{
Group: item.Spec.Resource.ApiGroup,
Resource: item.Spec.Resource.Resource,
}
// Reuse the same canView for items with the same resource
canView, found := canViewFuncs[gr]
if !found {
listReq := types.ListRequest{
Namespace: item.Namespace,
Group: item.Spec.Resource.ApiGroup,
Resource: item.Spec.Resource.Resource,
Verb: utils.VerbGetPermissions,
}
canView, _, err = r.accessClient.Compile(ctx, authInfo, listReq)
if err != nil {
return nil, err
}
canViewFuncs[gr] = canView
}
// TODO : Fetch the resource to retrieve its parent folder.
parent := ""
allowed := canView(item.Spec.Resource.Name, parent)
if allowed {
filteredItems = append(filteredItems, item)
}
}
l.Items = filteredItems
return l, nil
default:
return nil, fmt.Errorf("expected ResourcePermissionList, got %T: %w", l, storewrapper.ErrUnexpectedType)
}
}
@@ -0,0 +1,216 @@
package authorizer
import (
"context"
"testing"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/grafana/authlib/authn"
"github.com/grafana/authlib/types"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var (
user = authn.NewIDTokenAuthInfo(
authn.Claims[authn.AccessTokenClaims]{
Claims: jwt.Claims{Issuer: "grafana",
Subject: types.NewTypeID(types.TypeAccessPolicy, "grafana"), Audience: []string{"iam.grafana.app"}},
Rest: authn.AccessTokenClaims{
Namespace: "*",
Permissions: identity.ServiceIdentityClaims.Rest.Permissions,
DelegatedPermissions: identity.ServiceIdentityClaims.Rest.DelegatedPermissions,
},
}, &authn.Claims[authn.IDTokenClaims]{
Claims: jwt.Claims{Subject: types.NewTypeID(types.TypeUser, "u001")},
Rest: authn.IDTokenClaims{Namespace: "org-2", Identifier: "u001", Type: types.TypeUser},
},
)
)
func newResourcePermission(apiGroup, resource, name string) *iamv0.ResourcePermission {
return &iamv0.ResourcePermission{
ObjectMeta: metav1.ObjectMeta{Namespace: "org-2"},
Spec: iamv0.ResourcePermissionSpec{
Resource: iamv0.ResourcePermissionspecResource{
ApiGroup: apiGroup,
Resource: resource,
Name: name,
},
},
}
}
func TestResourcePermissions_AfterGet(t *testing.T) {
// In this test, we verify that AfterGet calls accessClient.Check with the correct parameters
fold1 := newResourcePermission("folder.grafana.app", "folders", "fold-1")
tests := []struct {
name string
shouldAllow bool
}{
{
name: "allow access",
shouldAllow: true,
},
{
name: "deny access",
shouldAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) {
require.NotNil(t, id)
// Check is called with the user's identity
require.Equal(t, "user:u001", id.GetUID())
require.Equal(t, "org-2", id.GetNamespace())
// Check the request values
require.Equal(t, "org-2", req.Namespace)
require.Equal(t, fold1.Spec.Resource.ApiGroup, req.Group)
require.Equal(t, fold1.Spec.Resource.Resource, req.Resource)
require.Equal(t, fold1.Spec.Resource.Name, req.Name)
require.Equal(t, utils.VerbGetPermissions, req.Verb)
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
}
accessClient := &fakeAccessClient{checkFunc: checkFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
ctx := types.WithAuthInfo(context.Background(), user)
err := resPermAuthz.AfterGet(ctx, fold1)
if tt.shouldAllow {
require.NoError(t, err, "expected no error for allowed access")
} else {
require.Error(t, err, "expected error for denied access")
}
require.True(t, accessClient.checkCalled, "accessClient.Check should be called")
})
}
}
func TestResourcePermissions_FilterList(t *testing.T) {
// In this test, the user has permission to access only fold-1 and dash-2.
// We verify that FilterList returns only those two objects.
list := &iamv0.ResourcePermissionList{
Items: []iamv0.ResourcePermission{
*newResourcePermission("folder.grafana.app", "folders", "fold-1"),
*newResourcePermission("folder.grafana.app", "folders", "fold-2"),
*newResourcePermission("dashboard.grafana.app", "dashboards", "dash-2"),
},
}
compileFunc := func(id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) {
require.NotNil(t, id)
// Compile is called with the user's identity
require.Equal(t, "user:u001", id.GetUID())
require.Equal(t, "org-2", id.GetNamespace())
// Check the request values
require.Equal(t, "org-2", req.Namespace)
if req.Resource == "folders" {
require.Equal(t, "folder.grafana.app", req.Group)
require.Equal(t, "folders", req.Resource)
}
if req.Resource == "dashboards" {
require.Equal(t, "dashboard.grafana.app", req.Group)
require.Equal(t, "dashboards", req.Resource)
}
// Return a checker that allows only specific resources: fold-1 and dash-2
return func(name, folder string) bool {
if name == "fold-1" || name == "dash-2" {
return true
}
return false
}, &types.NoopZookie{}, nil
}
accessClient := &fakeAccessClient{compileFunc: compileFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
ctx := types.WithAuthInfo(context.Background(), user)
obj, err := resPermAuthz.FilterList(ctx, list)
require.NoError(t, err)
require.NotNil(t, list)
require.True(t, accessClient.compileCalled, "accessClient.Compile should be called")
filtered, ok := obj.(*iamv0.ResourcePermissionList)
require.True(t, ok, "response should be of type ResourcePermissionList")
require.Len(t, filtered.Items, 2, "response list should have 2 items after filtering")
require.Equal(t, "fold-1", filtered.Items[0].Spec.Resource.Name)
require.Equal(t, "dash-2", filtered.Items[1].Spec.Resource.Name)
}
func TestResourcePermissions_beforeWrite(t *testing.T) {
// In this test, we verify that beforeWrite calls accessClient.Check with the correct parameters
fold1 := newResourcePermission("folder.grafana.app", "folders", "fold-1")
tests := []struct {
name string
shouldAllow bool
}{
{
name: "allow delete",
shouldAllow: true,
},
{
name: "deny delete",
shouldAllow: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) {
require.NotNil(t, id)
// Check is called with the user's identity
require.Equal(t, "user:u001", id.GetUID())
require.Equal(t, "org-2", id.GetNamespace())
// Check the request values
require.Equal(t, "org-2", req.Namespace)
require.Equal(t, fold1.Spec.Resource.ApiGroup, req.Group)
require.Equal(t, fold1.Spec.Resource.Resource, req.Resource)
require.Equal(t, fold1.Spec.Resource.Name, req.Name)
require.Equal(t, utils.VerbSetPermissions, req.Verb)
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
}
accessClient := &fakeAccessClient{checkFunc: checkFunc}
resPermAuthz := NewResourcePermissionsAuthorizer(accessClient)
ctx := types.WithAuthInfo(context.Background(), user)
err := resPermAuthz.beforeWrite(ctx, fold1)
if tt.shouldAllow {
require.NoError(t, err, "expected no error for allowed delete")
} else {
require.Error(t, err, "expected error for denied delete")
}
require.True(t, accessClient.checkCalled, "accessClient.Check should be called")
})
}
}
// fakeAccessClient is a mock implementation of claims.AccessClient
type fakeAccessClient struct {
checkCalled bool
checkFunc func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error)
compileCalled bool
compileFunc func(id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error)
}
func (m *fakeAccessClient) Check(ctx context.Context, id types.AuthInfo, req types.CheckRequest, folder string) (types.CheckResponse, error) {
m.checkCalled = true
return m.checkFunc(id, &req, folder)
}
func (m *fakeAccessClient) Compile(ctx context.Context, id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) {
m.compileCalled = true
return m.compileFunc(id, req)
}
var _ types.AccessClient = (*fakeAccessClient)(nil)
+14 -4
View File
@@ -29,6 +29,7 @@ import (
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
iamauthorizer "github.com/grafana/grafana/pkg/registry/apis/iam/authorizer"
"github.com/grafana/grafana/pkg/registry/apis/iam/externalgroupmapping"
"github.com/grafana/grafana/pkg/registry/apis/iam/legacy"
"github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission"
@@ -38,7 +39,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/iam/teambinding"
"github.com/grafana/grafana/pkg/registry/apis/iam/user"
"github.com/grafana/grafana/pkg/services/accesscontrol"
gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/authz/zanzana"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -121,7 +122,6 @@ func NewAPIService(
) *IdentityAccessManagementAPIBuilder {
store := legacy.NewLegacySQLStores(dbProvider)
resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider)
resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient)
registerMetrics(reg)
return &IdentityAccessManagementAPIBuilder{
store: store,
@@ -129,6 +129,7 @@ func NewAPIService(
resourcePermissionsStorage: resourcePermissionsStorage,
logger: log.New("iam.apis"),
features: features,
accessClient: accessClient,
zClient: zClient,
zTickets: make(chan bool, MaxConcurrentZanzanaWrites),
reg: reg,
@@ -136,7 +137,8 @@ func NewAPIService(
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
// For now only authorize resourcepermissions resource
if a.GetResource() == "resourcepermissions" {
return resourceAuthorizer.Authorize(ctx, a)
// Authorization is handled at the storage layer
return authorizer.DecisionAllow, "", nil
}
user, err := identity.GetRequester(ctx)
@@ -402,7 +404,15 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateResourcePermissionsAPIGroup(
return err
}
storage[iamv0.ResourcePermissionInfo.StoragePath()] = dw
// Not ideal, the alternative is to wrap both stores that dualwrite uses
regStoreDW, ok := dw.(*registry.Store)
if !ok {
return fmt.Errorf("expected RegistryStoreDualWrite, got %T", dw)
}
authzWrapper := storewrapper.New(regStoreDW, iamauthorizer.NewResourcePermissionsAuthorizer(b.accessClient))
storage[iamv0.ResourcePermissionInfo.StoragePath()] = authzWrapper
return nil
}
@@ -147,7 +147,7 @@ func InstallAPIs(
logger.Debug("Installing APIs for app installer", "app", installer.ManifestData().AppName)
wrapper := &serverWrapper{
ctx: ctx,
GenericAPIServer: server,
GenericAPIServer: appsdkapiserver.NewKubernetesGenericAPIServer(server),
installer: installer,
storageOpts: storageOpts,
restOptionsGetter: restOpsGetter,
@@ -26,8 +26,8 @@ import (
var _ appsdkapiserver.GenericAPIServer = (*serverWrapper)(nil)
type serverWrapper struct {
ctx context.Context
*genericapiserver.GenericAPIServer
ctx context.Context
GenericAPIServer appsdkapiserver.GenericAPIServer
installer appsdkapiserver.AppInstaller
restOptionsGetter generic.RESTOptionsGetter
storageOpts *grafanaapiserveroptions.StorageOptions
@@ -143,8 +143,5 @@ func (s *serverWrapper) configureStorage(gr schema.GroupResource, dualWriteSuppo
}
func (s *serverWrapper) RegisteredWebServices() []*restful.WebService {
if s.Handler != nil && s.Handler.GoRestfulContainer != nil {
return s.Handler.GoRestfulContainer.RegisteredWebServices()
}
return nil
return s.GenericAPIServer.RegisteredWebServices()
}
@@ -0,0 +1,191 @@
package storewrapper
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apiserver/rest"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
k8srest "k8s.io/apiserver/pkg/registry/rest"
)
var (
ErrUnauthenticated = fmt.Errorf("unauthenticated")
ErrUnauthorized = fmt.Errorf("unauthorized")
ErrUnexpectedType = fmt.Errorf("unexpected object type")
)
// ResourceStorageAuthorizer defines authorization hooks for resource storage operations.
type ResourceStorageAuthorizer interface {
BeforeCreate(ctx context.Context, obj runtime.Object) error
BeforeUpdate(ctx context.Context, obj runtime.Object) error
BeforeDelete(ctx context.Context, obj runtime.Object) error
AfterGet(ctx context.Context, obj runtime.Object) error
FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error)
}
// Wrapper is a k8sStorage (e.g. registry.Store) wrapper that enforces authorization based on ResourceStorageAuthorizer.
// It overrides the identity in the context to use service identity for the underlying store operations.
// That way, the underlying store authorization is always successful, and the authorization is enforced by the wrapper.
type Wrapper struct {
inner K8sStorage
authorizer ResourceStorageAuthorizer
}
type K8sStorage interface {
k8srest.Storage
k8srest.Scoper
k8srest.SingularNameProvider
k8srest.Lister
k8srest.Getter
k8srest.CreaterUpdater
k8srest.GracefulDeleter
}
var _ rest.Storage = (*Wrapper)(nil)
func New(store K8sStorage, authz ResourceStorageAuthorizer) *Wrapper {
return &Wrapper{inner: store, authorizer: authz}
}
func (w *Wrapper) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metaV1.Table, error) {
return w.inner.ConvertToTable(ctx, object, tableOptions)
}
func (w *Wrapper) Create(ctx context.Context, obj runtime.Object, createValidation k8srest.ValidateObjectFunc, options *metaV1.CreateOptions) (runtime.Object, error) {
// Enforce authorization based on the user permissions before creating the object
err := w.authorizer.BeforeCreate(ctx, obj)
if err != nil {
return nil, err
}
// Override the identity to use service identity for the underlying store operation
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
return w.inner.Create(srvCtx, obj, createValidation, options)
}
func (w *Wrapper) Delete(ctx context.Context, name string, deleteValidation k8srest.ValidateObjectFunc, options *metaV1.DeleteOptions) (runtime.Object, bool, error) {
// Fetch the object first to authorize
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
getOpts := &metaV1.GetOptions{TypeMeta: options.TypeMeta}
if options.Preconditions != nil {
getOpts.ResourceVersion = *options.Preconditions.ResourceVersion
}
obj, err := w.inner.Get(srvCtx, name, getOpts)
if err != nil {
return nil, false, err
}
// Enforce authorization based on the user permissions
if err := w.authorizer.BeforeDelete(ctx, obj); err != nil {
return nil, false, err
}
return w.inner.Delete(srvCtx, name, deleteValidation, options)
}
func (w *Wrapper) DeleteCollection(ctx context.Context, deleteValidation k8srest.ValidateObjectFunc, options *metaV1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
// DeleteCollection is complex to authorize properly
// For now, deny it entirely for safety
return nil, fmt.Errorf("bulk delete operations are not supported through this API")
}
func (w *Wrapper) Destroy() {
w.inner.Destroy()
}
func (w *Wrapper) Get(ctx context.Context, name string, options *metaV1.GetOptions) (runtime.Object, error) {
// Override the identity to use service identity for the underlying store operation
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
item, err := w.inner.Get(srvCtx, name, options)
if err != nil {
return nil, err
}
// Enforce authorization based on the user permissions after retrieving the object
err = w.authorizer.AfterGet(ctx, item)
if err != nil {
return nil, err
}
return item, nil
}
func (w *Wrapper) GetSingularName() string {
return w.inner.GetSingularName()
}
func (w *Wrapper) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
// Override the identity to use service identity for the underlying store operation
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
list, err := w.inner.List(srvCtx, options)
if err != nil {
return nil, err
}
// Enforce authorization based on the user permissions after retrieving the list
return w.authorizer.FilterList(ctx, list)
}
func (w *Wrapper) NamespaceScoped() bool {
return w.inner.NamespaceScoped()
}
func (w *Wrapper) New() runtime.Object {
return w.inner.New()
}
func (w *Wrapper) NewList() runtime.Object {
return w.inner.NewList()
}
func (w *Wrapper) Update(
ctx context.Context,
name string,
objInfo k8srest.UpdatedObjectInfo,
createValidation k8srest.ValidateObjectFunc,
updateValidation k8srest.ValidateObjectUpdateFunc,
forceAllowCreate bool,
options *metaV1.UpdateOptions,
) (runtime.Object, bool, error) {
// Create a wrapper around UpdatedObjectInfo to inject authorization
wrappedObjInfo := &authorizedUpdateInfo{
inner: objInfo,
authorizer: w.authorizer,
userCtx: ctx, // Keep original context for authorization
}
// Override the identity to use service identity for the underlying store operation
srvCtx, _ := identity.WithServiceIdentity(ctx, 0)
return w.inner.Update(srvCtx, name, wrappedObjInfo, createValidation, updateValidation, forceAllowCreate, options)
}
type authorizedUpdateInfo struct {
inner k8srest.UpdatedObjectInfo
authorizer ResourceStorageAuthorizer
userCtx context.Context
}
func (a *authorizedUpdateInfo) Preconditions() *metaV1.Preconditions {
return a.inner.Preconditions()
}
func (a *authorizedUpdateInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) {
// Get the updated object
updatedObj, err := a.inner.UpdatedObject(ctx, oldObj)
if err != nil {
return nil, err
}
// Enforce authorization using the original user context
if err := a.authorizer.BeforeUpdate(a.userCtx, updatedObj); err != nil {
return nil, err
}
return updatedObj, nil
}
@@ -0,0 +1,399 @@
package storewrapper
import (
"context"
"testing"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
type testSetup struct {
mockStore *rest.MockStorage
mockAuth *FakeAuthorizer
wrapper *Wrapper
ctx context.Context
}
func newTestSetup(t *testing.T) *testSetup {
mockStore := rest.NewMockStorage(t)
mockAuth := &FakeAuthorizer{}
wrapper := New(mockStore, mockAuth)
ctx := identity.WithRequester(
context.Background(),
&identity.StaticRequester{UserUID: "u001", Type: types.TypeUser},
)
return &testSetup{mockStore: mockStore, mockAuth: mockAuth, wrapper: wrapper, ctx: ctx}
}
func matchesOriginalUser() func(context.Context) bool {
return func(ctx context.Context) bool {
user, err := identity.GetRequester(ctx)
return err == nil && user.GetUID() == "user:u001"
}
}
func matchesServiceIdentity() func(context.Context) bool {
return func(ctx context.Context) bool {
return identity.IsServiceIdentity(ctx)
}
}
func TestWrapper_Create(t *testing.T) {
t.Run("success", func(t *testing.T) {
setup := newTestSetup(t)
obj := &fakeObject{}
createOpts := &metaV1.CreateOptions{}
expectedObj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "created"}}
// Verify original user identity is used for authorization
setup.mockAuth.On("BeforeCreate", mock.MatchedBy(matchesOriginalUser()), obj).Return(nil)
// Verify service identity is used to call the underlying store
setup.mockStore.On("Create", mock.MatchedBy(matchesServiceIdentity()), obj, mock.Anything, createOpts).Return(expectedObj, nil)
result, err := setup.wrapper.Create(setup.ctx, obj, nil, createOpts)
require.NoError(t, err)
assert.Equal(t, expectedObj, result)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertExpectations(t)
})
t.Run("unauthorized", func(t *testing.T) {
setup := newTestSetup(t)
obj := &fakeObject{}
createOpts := &metaV1.CreateOptions{}
// Simulate unauthorized error from authorizer
setup.mockAuth.On("BeforeCreate", mock.MatchedBy(matchesOriginalUser()), obj).Return(ErrUnauthorized)
result, err := setup.wrapper.Create(setup.ctx, obj, nil, createOpts)
require.Error(t, err)
assert.Nil(t, result)
assert.Equal(t, ErrUnauthorized, err)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertNotCalled(t, "Create")
})
}
func TestWrapper_Delete(t *testing.T) {
t.Run("success", func(t *testing.T) {
setup := newTestSetup(t)
version := "1"
obj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "to-delete"}}
deleteOpts := &metaV1.DeleteOptions{Preconditions: &metaV1.Preconditions{ResourceVersion: &version}}
expectedObj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "deleted"}}
// Mock Get to fetch the object before deletion
setup.mockStore.On("Get", mock.MatchedBy(matchesServiceIdentity()), "to-delete", mock.Anything).Return(obj, nil)
// Verify original user identity is used for authorization
setup.mockAuth.On("BeforeDelete", mock.MatchedBy(matchesOriginalUser()), obj).Return(nil)
// Verify service identity is used to call the underlying store
setup.mockStore.On("Delete", mock.MatchedBy(matchesServiceIdentity()), "to-delete", mock.Anything, deleteOpts).Return(expectedObj, true, nil)
result, deleted, err := setup.wrapper.Delete(setup.ctx, "to-delete", nil, deleteOpts)
require.NoError(t, err)
assert.Equal(t, expectedObj, result)
assert.True(t, deleted)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertExpectations(t)
})
t.Run("unauthorized", func(t *testing.T) {
setup := newTestSetup(t)
version := "1"
obj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "to-delete"}}
deleteOpts := &metaV1.DeleteOptions{Preconditions: &metaV1.Preconditions{ResourceVersion: &version}}
// Mock Get to fetch the object before deletion
setup.mockStore.On("Get", mock.MatchedBy(matchesServiceIdentity()), "to-delete", mock.Anything).Return(obj, nil)
// Simulate unauthorized error from authorizer
setup.mockAuth.On("BeforeDelete", mock.MatchedBy(matchesOriginalUser()), obj).Return(ErrUnauthorized)
result, deleted, err := setup.wrapper.Delete(setup.ctx, "to-delete", nil, deleteOpts)
require.Error(t, err)
assert.Nil(t, result)
assert.False(t, deleted)
assert.Equal(t, ErrUnauthorized, err)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertExpectations(t)
setup.mockStore.AssertNotCalled(t, "Delete")
})
}
func TestWrapper_Get(t *testing.T) {
t.Run("success", func(t *testing.T) {
setup := newTestSetup(t)
obj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "fetched"}}
// Verify service identity is used to call the underlying store
setup.mockStore.On("Get", mock.MatchedBy(matchesServiceIdentity()), "fetched", mock.Anything).Return(obj, nil)
// Verify original user identity is used for after-get authorization
setup.mockAuth.On("AfterGet", mock.MatchedBy(matchesOriginalUser()), obj).Return(nil)
result, err := setup.wrapper.Get(setup.ctx, "fetched", &metaV1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, obj, result)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertExpectations(t)
})
t.Run("unauthorized", func(t *testing.T) {
setup := newTestSetup(t)
obj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "fetched"}}
// Verify service identity is used to call the underlying store
setup.mockStore.On("Get", mock.MatchedBy(matchesServiceIdentity()), "fetched", mock.Anything).Return(obj, nil)
// Simulate unauthorized error from after-get authorizer
setup.mockAuth.On("AfterGet", mock.MatchedBy(matchesOriginalUser()), obj).Return(ErrUnauthorized)
result, err := setup.wrapper.Get(setup.ctx, "fetched", &metaV1.GetOptions{})
require.Error(t, err)
assert.Nil(t, result)
assert.Equal(t, ErrUnauthorized, err)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertExpectations(t)
})
}
func TestWrapper_List(t *testing.T) {
t.Run("success", func(t *testing.T) {
setup := newTestSetup(t)
listObj := &metaV1.List{Items: []runtime.RawExtension{
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item1"}}},
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item2"}}},
}}
filteredListObj := &metaV1.List{Items: []runtime.RawExtension{
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item1"}}},
}}
// Verify service identity is used to call the underlying store
setup.mockStore.On("List", mock.MatchedBy(matchesServiceIdentity()), mock.Anything).Return(listObj, nil)
// Verify original user identity is used for filtering the list
setup.mockAuth.On("FilterList", mock.MatchedBy(matchesOriginalUser()), listObj).Return(filteredListObj, nil)
result, err := setup.wrapper.List(setup.ctx, &internalversion.ListOptions{})
require.NoError(t, err)
assert.Equal(t, filteredListObj, result)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertExpectations(t)
})
t.Run("unauthorized", func(t *testing.T) {
setup := newTestSetup(t)
listObj := &metaV1.List{Items: []runtime.RawExtension{
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item1"}}},
{Object: &fakeObject{ObjectMeta: metaV1.ObjectMeta{Name: "item2"}}},
}}
// Verify service identity is used to call the underlying store
setup.mockStore.On("List", mock.MatchedBy(matchesServiceIdentity()), mock.Anything).Return(listObj, nil)
// Simulate unauthorized error from FilterList authorizer
setup.mockAuth.On("FilterList", mock.MatchedBy(matchesOriginalUser()), listObj).Return(nil, ErrUnauthorized)
result, err := setup.wrapper.List(setup.ctx, &internalversion.ListOptions{})
require.Error(t, err)
assert.Nil(t, result)
assert.Equal(t, ErrUnauthorized, err)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertExpectations(t)
})
}
func TestWrapper_Update(t *testing.T) {
setup := newTestSetup(t)
oldObj := &fakeObject{ObjectMeta: metaV1.ObjectMeta{
Name: "to-update", ResourceVersion: "2", Labels: map[string]string{"updated": "false"},
}}
objInfo := &fakeUpdatedObjectInfo{obj: oldObj}
updateOpts := &metaV1.UpdateOptions{}
var authzInfo *authorizedUpdateInfo
// Verify service identity is used to call the underlying store
setup.mockStore.On("Update",
mock.MatchedBy(matchesServiceIdentity()),
"to-update",
mock.MatchedBy(func(info *authorizedUpdateInfo) bool {
// Capture the authorizedUpdateInfo for later verification
authzInfo = info
return true
}),
mock.Anything,
mock.Anything,
false,
updateOpts).Return(oldObj, true, nil)
result, updated, err := setup.wrapper.Update(setup.ctx, "to-update", objInfo, nil, nil, false, updateOpts)
require.NoError(t, err)
assert.Equal(t, oldObj, result)
assert.True(t, updated)
// Now verify that the authorization is performed inside UpdatedObject
setup.mockAuth.On("BeforeUpdate", mock.MatchedBy(matchesOriginalUser()), oldObj).Return(nil)
obj, err := authzInfo.UpdatedObject(context.Background(), oldObj)
require.NoError(t, err)
assert.Equal(t, oldObj, obj)
// Assert expectations
setup.mockAuth.AssertExpectations(t)
setup.mockStore.AssertExpectations(t)
}
func TestWrapper_DeleteCollection(t *testing.T) {
setup := newTestSetup(t)
result, err := setup.wrapper.DeleteCollection(setup.ctx, nil, &metaV1.DeleteOptions{}, &internalversion.ListOptions{})
require.Error(t, err)
assert.Contains(t, err.Error(), "bulk delete operations are not supported")
assert.Nil(t, result)
}
func TestWrapper_PassthroughMethods(t *testing.T) {
setup := newTestSetup(t)
t.Run("New", func(t *testing.T) {
obj := &fakeObject{}
setup.mockStore.On("New").Return(obj).Once()
assert.Equal(t, obj, setup.wrapper.New())
})
t.Run("NewList", func(t *testing.T) {
obj := &fakeObject{}
setup.mockStore.On("NewList").Return(obj).Once()
assert.Equal(t, obj, setup.wrapper.NewList())
})
t.Run("GetSingularName", func(t *testing.T) {
setup.mockStore.On("GetSingularName").Return("fake").Once()
assert.Equal(t, "fake", setup.wrapper.GetSingularName())
})
t.Run("NamespaceScoped", func(t *testing.T) {
setup.mockStore.On("NamespaceScoped").Return(true).Once()
assert.True(t, setup.wrapper.NamespaceScoped())
})
t.Run("Destroy", func(t *testing.T) {
setup.mockStore.On("Destroy").Once()
setup.wrapper.Destroy()
})
t.Run("ConvertToTable", func(t *testing.T) {
obj := &fakeObject{}
table := &metaV1.Table{}
setup.mockStore.On("ConvertToTable", setup.ctx, obj, mock.Anything).Return(table, nil).Once()
result, err := setup.wrapper.ConvertToTable(setup.ctx, obj, nil)
require.NoError(t, err)
assert.Equal(t, table, result)
})
setup.mockStore.AssertExpectations(t)
}
// -----
// Fakes
// -----
type FakeAuthorizer struct {
mock.Mock
}
func (f *FakeAuthorizer) BeforeCreate(ctx context.Context, obj runtime.Object) error {
args := f.Called(ctx, obj)
return args.Error(0)
}
func (f *FakeAuthorizer) BeforeUpdate(ctx context.Context, obj runtime.Object) error {
args := f.Called(ctx, obj)
return args.Error(0)
}
func (f *FakeAuthorizer) BeforeDelete(ctx context.Context, obj runtime.Object) error {
args := f.Called(ctx, obj)
return args.Error(0)
}
func (f *FakeAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error {
args := f.Called(ctx, obj)
return args.Error(0)
}
func (f *FakeAuthorizer) FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error) {
args := f.Called(ctx, list)
var res runtime.Object
if args.Get(0) != nil {
res = args.Get(0).(runtime.Object)
}
return res, args.Error(1)
}
type fakeObject struct {
metaV1.TypeMeta
metaV1.ObjectMeta
}
func (f *fakeObject) DeepCopyObject() runtime.Object {
return &fakeObject{
TypeMeta: f.TypeMeta,
ObjectMeta: f.ObjectMeta,
}
}
// fakeUpdatedObjectInfo implements k8srest.UpdatedObjectInfo for testing
type fakeUpdatedObjectInfo struct {
obj runtime.Object
}
func (f *fakeUpdatedObjectInfo) Preconditions() *metaV1.Preconditions {
return nil
}
func (f *fakeUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) {
return f.obj, nil
}
+1 -1
View File
@@ -782,7 +782,7 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool,
}
var res *authzv1.ListResponse
if strings.HasPrefix(req.Action, "folders:") {
if strings.HasPrefix(req.Action, "folders:") || strings.HasPrefix(req.Action, "folders.permissions:") {
res = buildFolderList(scopeMap, tree)
} else {
res = buildItemList(scopeMap, tree, t.Prefix())
@@ -429,7 +429,8 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic
return 0, "", dashboards.ErrFolderInvalidUID
}
// When we expect folders in unified storage, they should have a manager indicated
// When we expect folders in unified storage, they should have a manager indicated.
// NOTE: when everything has been running in mode5 for a while, this check can be removed.
if err == nil && result != nil && result.ManagedBy == "" && fr.foldersInUnified {
result, err = service.UpdateFolderWithManagedByAnnotation(ctx, result, fr.Cfg.Name)
if err != nil {
+6
View File
@@ -83,6 +83,12 @@ func TestPasswowrdService_ValidatePasswordHardcodePolicy(t *testing.T) {
expectedError: nil,
strongPasswordPolicyEnabled: true,
},
{
name: "should accept underscore as a valid symbol when strong password policy is enabled",
passwordTest: LOWERCASE + UPPERCASE + NUMBER + "_",
expectedError: nil,
strongPasswordPolicyEnabled: true,
},
}
for _, tc := range testCases {
cfg := setting.NewCfg()
@@ -257,6 +257,19 @@ func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValida
if permissions != "" {
objCopy.SetAnnotation(utils.AnnoKeyGrantPermissions, permissions)
}
// Propagate annotations and labels to the object saved in
// unified storage, making sure the `deprecatedID` is saved
// as well as provisioning metadata, when present.
for name, val := range accIn.GetAnnotations() {
objCopy.SetAnnotation(name, val)
}
legacyAcc, err := meta.Accessor(createdFromLegacy)
if err != nil {
return nil, err
}
objCopy.SetLabels(legacyAcc.GetLabels())
}
// If unified storage is the primary storage, let's just create it in the foreground and return it.
+84 -23
View File
@@ -25,6 +25,7 @@ import (
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/apimachinery/utils"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/infra/db"
@@ -1983,6 +1984,31 @@ func TestIntegrationDeleteNestedFoldersPostorder(t *testing.T) {
}
}
func setupProvisioningDir(t *testing.T, opts *testinfra.GrafanaOpts) {
opts.Dir, opts.DirPath = testinfra.CreateGrafDir(t, *opts)
// Create provisioning directories
provDashboardsDir := fmt.Sprintf("%s/conf/provisioning/dashboards", opts.Dir)
provDashboardsCfg := fmt.Sprintf("%s/dev.yaml", provDashboardsDir)
blob := []byte(fmt.Sprintf(`
apiVersion: 1
providers:
- name: 'provisioned dashboards'
type: file
orgId: 1
folder: 'GrafanaCloud'
options:
path: %s`, provDashboardsDir))
err := os.WriteFile(provDashboardsCfg, blob, 0o644)
require.NoError(t, err)
input, err := os.ReadFile(filepath.Join("testdata/dashboard.json"))
require.NoError(t, err)
provDashboardFile := filepath.Join(provDashboardsDir, "dashboard.json")
err = os.WriteFile(provDashboardFile, input, 0o644)
require.NoError(t, err)
}
// Test deleting folder with provisioned dashboard has proper handling with forceDeleteRules
func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
@@ -2010,29 +2036,8 @@ func TestIntegrationDeleteFolderWithProvisionedDashboards(t *testing.T) {
featuremgmt.FlagUnifiedStorageSearch,
},
}
// Setup Grafana with provisioning
ops.Dir, ops.DirPath = testinfra.CreateGrafDir(t, ops)
// Create provisioning directories
provDashboardsDir := fmt.Sprintf("%s/conf/provisioning/dashboards", ops.Dir)
provDashboardsCfg := fmt.Sprintf("%s/dev.yaml", provDashboardsDir)
blob := []byte(fmt.Sprintf(`
apiVersion: 1
providers:
- name: 'provisioned dashboards'
type: file
orgId: 1
folder: 'GrafanaCloud'
options:
path: %s`, provDashboardsDir))
err := os.WriteFile(provDashboardsCfg, blob, 0o644)
require.NoError(t, err)
input, err := os.ReadFile(filepath.Join("testdata/dashboard.json"))
require.NoError(t, err)
provDashboardFile := filepath.Join(provDashboardsDir, "dashboard.json")
err = os.WriteFile(provDashboardFile, input, 0o644)
require.NoError(t, err)
setupProvisioningDir(t, &ops)
helper := apis.NewK8sTestHelper(t, ops)
client := helper.GetResourceClient(apis.ResourceClientArgs{
@@ -2059,7 +2064,7 @@ providers:
}
}, 10*time.Second, 25*time.Millisecond)
_, err = client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{})
_, err := client.Resource.Get(context.Background(), folderUID, metav1.GetOptions{})
require.NoError(t, err, "folder %s should exist", folderUID)
// Verify dashboards exist
verifyDashboardExists := func(shouldExist bool) {
@@ -2116,3 +2121,59 @@ providers:
})
}
}
// Test that folders created during provisioning using the dual writer have the
// appropriate labels and annotations in unified storage.
func TestIntegrationProvisionedFolderPropagatesLabelsAndAnnotations(t *testing.T) {
mode3 := grafanarest.DualWriterMode(3)
ops := testinfra.GrafanaOpts{
DisableAnonymous: true,
AppModeProduction: true,
APIServerStorageType: "unified",
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
folders.RESOURCEGROUP: {
DualWriterMode: mode3,
},
"dashboards.dashboard.grafana.app": {
DualWriterMode: mode3,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagUnifiedStorageSearch,
},
}
setupProvisioningDir(t, &ops)
helper := apis.NewK8sTestHelper(t, ops)
client := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
GVR: gvr,
})
var folderList folders.FolderList
require.EventuallyWithT(t, func(collect *assert.CollectT) {
resp := apis.DoRequest(helper, apis.RequestParams{
User: client.Args.User,
Method: http.MethodGet,
Path: fmt.Sprintf("/apis/folder.grafana.app/v1beta1/namespaces/%s/folders", client.Args.Namespace),
}, &map[string]interface{}{})
require.NotNil(t, resp.Response)
require.Equal(t, http.StatusOK, resp.Response.StatusCode)
require.NoError(t, json.Unmarshal(resp.Body, &folderList))
}, 10*time.Second, 25*time.Millisecond)
require.Len(t, folderList.Items, 1)
accessor, err := utils.MetaAccessor(&folderList.Items[0])
require.NoError(t, err)
expectedLabels := map[string]string{"grafana.app/deprecatedInternalID": "1"}
expectedAnnotations := map[string]string{
"grafana.app/createdBy": "access-policy:service",
"grafana.app/managedBy": "classic-file-provisioning",
"grafana.app/managerId": "provisioned dashboards",
}
require.Equal(t, expectedLabels, accessor.GetLabels())
require.Equal(t, expectedAnnotations, accessor.GetAnnotations())
}
@@ -1751,6 +1751,7 @@
"Search"
],
"description": "Dashboard search",
"operationId": "searchDashboardsAndFolders",
"parameters": [
{
"name": "namespace",
@@ -1891,6 +1892,7 @@
"Search"
],
"description": "Get sortable fields",
"operationId": "getSortableFields",
"parameters": [
{
"name": "namespace",
@@ -2,4 +2,4 @@ import { generatedAPI } from '@grafana/api-clients/rtkq/dashboard/v0alpha1';
export const dashboardAPIv0alpha1 = generatedAPI.enhanceEndpoints({});
export const { useGetSearchQuery } = dashboardAPIv0alpha1;
export const { useSearchDashboardsAndFoldersQuery } = dashboardAPIv0alpha1;
@@ -13,7 +13,9 @@ import { getPaginationPlaceholders } from '../../../features/browse-dashboards/s
import { UseFoldersQueryProps } from './useFoldersQuery';
import { getRootFolderItem } from './utils';
type GetFolderChildrenQuery = ReturnType<ReturnType<typeof dashboardAPIv0alpha1.endpoints.getSearch.select>>;
type GetFolderChildrenQuery = ReturnType<
ReturnType<typeof dashboardAPIv0alpha1.endpoints.searchDashboardsAndFolders.select>
>;
type GetFolderChildrenRequest = {
unsubscribe: () => void;
};
@@ -44,9 +46,9 @@ export function useFoldersQueryAppPlatform({
const [emptyFolders, setEmptyFolders] = useState<Set<string>>(new Set());
// Keep a list of selectors for dynamic state selection
const [selectors, setSelectors] = useState<Array<ReturnType<typeof dashboardAPIv0alpha1.endpoints.getSearch.select>>>(
[]
);
const [selectors, setSelectors] = useState<
Array<ReturnType<typeof dashboardAPIv0alpha1.endpoints.searchDashboardsAndFolders.select>>
>([]);
// This is an aggregated dynamic selector of all the selectors for all the request issued while loading the folder
// tree and returns the whole tree that was loaded so far.
@@ -92,10 +94,10 @@ export function useFoldersQueryAppPlatform({
const args = { folder: finalParentUid, type: 'folder', permission } as const;
// Make a request
const subscription = dispatch(dashboardAPIv0alpha1.endpoints.getSearch.initiate(args));
const subscription = dispatch(dashboardAPIv0alpha1.endpoints.searchDashboardsAndFolders.initiate(args));
// Add selector for the response to the list so we can then have an aggregated selector for all the folders
const selector = dashboardAPIv0alpha1.endpoints.getSearch.select(args);
const selector = dashboardAPIv0alpha1.endpoints.searchDashboardsAndFolders.select(args);
setSelectors((selectors) => selectors.concat(selector));
// the subscriptions are saved in a ref so they can be unsubscribed on unmount
+12 -5
View File
@@ -27,6 +27,7 @@ export const Page: PageType = ({
info,
layout = PageLayoutType.Standard,
onSetScrollRef,
background,
...otherProps
}) => {
const styles = useStyles2(getStyles);
@@ -49,8 +50,10 @@ export const Page: PageType = ({
}
}, [navModel, pageNav, chrome, layout]);
const isPrimaryBg = (background ?? getDefaultBackgroundForLayout(layout)) === 'primary';
return (
<div className={cx(styles.wrapper, className)} {...otherProps}>
<div className={cx(styles.wrapper, isPrimaryBg && styles.wrapperPrimary, className)} {...otherProps}>
{layout === PageLayoutType.Standard && (
<NativeScrollbar
// This id is used by the image renderer to scroll through the dashboard
@@ -101,18 +104,18 @@ const getStyles = (theme: GrafanaTheme2) => {
position: 'relative',
container: 'page / inline-size',
}),
wrapperPrimary: css({
label: 'page-wrapper-primary',
background: theme.colors.background.primary,
}),
pageContent: css({
label: 'page-content',
flexGrow: 1,
}),
primaryBg: css({
background: theme.colors.background.primary,
}),
pageInner: css({
label: 'page-inner',
padding: theme.spacing(2),
borderBottom: 'none',
background: theme.colors.background.primary,
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
@@ -132,3 +135,7 @@ const getStyles = (theme: GrafanaTheme2) => {
}),
};
};
function getDefaultBackgroundForLayout(layout: PageLayoutType) {
return layout === PageLayoutType.Standard ? 'primary' : 'canvas';
}
+11 -1
View File
@@ -5,7 +5,16 @@ import { PluginPageContext } from 'app/features/plugins/components/PluginPageCon
import { Page } from './Page';
export function PluginPage({ actions, children, info, pageNav, layout, renderTitle, subTitle }: PluginPageProps) {
export function PluginPage({
actions,
children,
info,
pageNav,
layout,
renderTitle,
subTitle,
background,
}: PluginPageProps) {
const context = useContext(PluginPageContext);
return (
@@ -17,6 +26,7 @@ export function PluginPage({ actions, children, info, pageNav, layout, renderTit
renderTitle={renderTitle}
info={info}
subTitle={subTitle}
background={background}
>
<Page.Contents>{children}</Page.Contents>
</Page>
+3
View File
@@ -2,6 +2,7 @@ import { FC, HTMLAttributes } from 'react';
import * as React from 'react';
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
import { PluginPageBackground } from '@grafana/runtime';
import { ScrollRefElement } from '../NativeScrollbar';
@@ -12,6 +13,8 @@ export interface PageProps extends HTMLAttributes<HTMLDivElement> {
navId?: string;
navModel?: NavModel;
pageNav?: NavModelItem;
/** Determines the background color of the page. Defaults to primary. */
background?: PluginPageBackground;
/** Can be used to place info inline with the heading */
info?: PageInfoItem[];
/** Can be used to place actions inline with the heading */
@@ -41,7 +41,7 @@ export const strongPasswordValidations: StrongPasswordValidation[] = [
},
{
message: 'One symbol',
validation: (value: string) => /[\W]/.test(value),
validation: (value: string) => /[^A-Za-z0-9]/.test(value),
},
];
@@ -0,0 +1,92 @@
import { HttpResponse, http } from 'msw';
import { render, screen, waitFor } from 'test/test-utils';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import { AccessControlAction } from 'app/types/accessControl';
import { GrafanaRuleIdentifier } from 'app/types/unified-alerting';
import { setupMswServer } from '../mockApi';
import { grantUserPermissions } from '../mocks';
import { grafanaRulerNamespace, grafanaRulerRule } from '../mocks/grafanaRulerApi';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { useCombinedRule } from './useCombinedRule';
const server = setupMswServer();
beforeAll(() => {
grantUserPermissions([AccessControlAction.AlertingRuleExternalRead, AccessControlAction.AlertingRuleRead]);
});
// Test component that uses useCombinedRule hook
const UseCombinedRuleTestComponent = ({ ruleIdentifier }: { ruleIdentifier: GrafanaRuleIdentifier }) => {
const { loading, error, result } = useCombinedRule({ ruleIdentifier });
return (
<>
<AppNotificationList />
<div data-testid="loading">{loading ? 'loading' : 'not-loading'}</div>
<div data-testid="error">{error ? 'has-error' : 'no-error'}</div>
<div data-testid="result">{result ? 'has-result' : 'no-result'}</div>
</>
);
};
describe('useCombinedRule', () => {
describe('when rule group returns 404', () => {
it('should not show error notification when rule group does not exist', async () => {
// Mock the getAlertRule endpoint to return a valid rule
server.use(
http.get('/api/ruler/grafana/api/v1/rule/:uid', () => {
return HttpResponse.json(grafanaRulerRule);
})
);
// Mock the ruler group endpoint to return 404 (simulating a new or deleted group)
server.use(
http.get('/api/ruler/grafana/api/v1/rules/:namespace/:group', () => {
return HttpResponse.json({ error: 'rule group does not exist' }, { status: 404 });
})
);
// Mock the prometheus rules endpoint
server.use(
http.get('/api/prometheus/grafana/api/v1/rules', () => {
return HttpResponse.json({
status: 'success',
data: { groups: [] },
});
})
);
// Mock the folder endpoint
server.use(
http.get('/api/folders/:uid', () => {
return HttpResponse.json({
uid: grafanaRulerNamespace.uid,
title: grafanaRulerNamespace.name,
});
})
);
const ruleIdentifier: GrafanaRuleIdentifier = {
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
uid: grafanaRulerRule.grafana_alert.uid,
};
render(<UseCombinedRuleTestComponent ruleIdentifier={ruleIdentifier} />);
// Wait for the hook to finish loading
await waitFor(() => {
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
});
// The hook should have an error (404)
expect(screen.getByTestId('error')).toHaveTextContent('has-error');
// Verify that no error notification was shown
// If showErrorAlert was true, we would see an alert with role="status"
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
});
});
@@ -68,6 +68,9 @@ export function useCloudCombinedRulesMatching(
rulerConfig: rulerConfig,
namespace: nsGroup.namespace.name,
group: nsGroup.group.name,
// Suppress error notifications for 404s - the group may not exist yet (new group)
// or may have been deleted (last rule removed)
notificationOptions: { showErrorAlert: false },
}).unwrap();
rulerGroups.push(rulerGroup);
})
@@ -145,6 +148,9 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request
rulerConfig: dsFeatures.rulerConfig,
namespace: ruleLocation.namespace,
group: ruleLocation.group,
// Suppress error notifications for 404s - the group may not exist yet (new group)
// or may have been deleted (last rule removed)
notificationOptions: { showErrorAlert: false },
});
}, [dsFeatures, fetchRulerRuleGroup, ruleLocation]);
@@ -66,7 +66,7 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
});
return (
<Box paddingBottom={2} display="flex" grow={1} alignItems="center">
<Box paddingBottom={2} display="flex" grow={1} alignItems="stretch" gap={2} direction={'column'}>
<RadioButtonGroup
fullWidth
value={layoutManager.descriptor}
@@ -79,21 +79,13 @@ export function DashboardLayoutSelector({ layoutManager }: Props) {
}
export function useLayoutCategory(layoutManager: DashboardLayoutManager) {
return useMemo(() => {
const isGridLayout = layoutManager.descriptor.isGridLayout;
const groupLayout = new OptionsPaneCategoryDescriptor({
title: t('dashboard.layout.common.group-layout', 'Group layout'),
id: 'dash-group-layout',
isOpenDefault: false,
const layout = new OptionsPaneCategoryDescriptor({
title: t('dashboard.layout.common.layout', 'Layout'),
id: 'layout',
isOpenDefault: true,
});
const gridLayout = new OptionsPaneCategoryDescriptor({
title: t('dashboard.layout.common.panel-layout', 'Panel layout'),
id: 'dash-grid-layout',
isOpenDefault: false,
});
gridLayout.addItem(
layout.addItem(
new OptionsPaneItemDescriptor({
title: '',
id: 'dash-grid-layout-option',
@@ -102,33 +94,12 @@ export function useLayoutCategory(layoutManager: DashboardLayoutManager) {
})
);
if (isGridLayout) {
groupLayout.props.disabledText = t(
'dashboard.layout.common.group-layout-disabled',
'No groups exists on this level'
);
} else {
groupLayout.addItem(
new OptionsPaneItemDescriptor({
title: '',
id: 'dash-group-layout-option',
skipField: true,
render: () => <DashboardLayoutSelector layoutManager={layoutManager} />,
})
);
gridLayout.props.disabledText = t(
'dashboard.layout.common.panel-layout-disabled',
'Select a row or tab to change panel layout options'
);
}
if (layoutManager.getOptions) {
for (const option of layoutManager.getOptions()) {
gridLayout.addItem(option);
layout.addItem(option);
}
}
return [groupLayout, gridLayout];
return [layout];
}, [layoutManager]);
}
@@ -1392,8 +1392,8 @@ describe('DashboardSceneSerializer', () => {
serializer.initializeDSReferencesMapping(v1SaveModel as unknown as DashboardV2Spec);
expect(serializer.getDSReferencesMapping()).toEqual({
panels: new Map(),
variables: new Set(),
annotations: new Set(),
variables: new Map(),
annotations: new Map(),
});
expect(serializer.getDSReferencesMapping().panels.size).toBe(0);
});
@@ -1410,8 +1410,8 @@ describe('DashboardSceneSerializer', () => {
serializer.initializeDSReferencesMapping(undefined);
expect(serializer.getDSReferencesMapping()).toEqual({
panels: expect.any(Map),
variables: expect.any(Set),
annotations: expect.any(Set),
variables: expect.any(Map),
annotations: expect.any(Map),
});
expect(serializer.getDSReferencesMapping().panels.size).toBe(0);
});
@@ -99,9 +99,12 @@ interface DynamicDashboardsTrackingInformationLayoutParsing
}
export interface DSReferencesMapping {
panels: Map<string, Set<string>>;
variables: Set<string>;
annotations: Set<string>;
// panel id as keys, map as value. Map<refId, group> as value, if undefined, it means the datasource type was not defined
panels: Map<string, Map<string, string | undefined>>;
// variable name as keys, group as value, if undefined, it means the datasource type was not defined
variables: Map<string, string | undefined>;
// annotation name as keys, group as value, if undefined, it means the datasource type was not defined
annotations: Map<string, string | undefined>;
}
export class V1DashboardSerializer
@@ -110,10 +113,10 @@ export class V1DashboardSerializer
initialSaveModel?: Dashboard;
metadata?: DashboardMeta;
protected elementPanelMap = new Map<string, number>();
protected defaultDsReferencesMap = {
panels: new Map<string, Set<string>>(), // refIds as keys
variables: new Set<string>(), // variable names as keys
annotations: new Set<string>(), // annotation names as keys
protected defaultDsReferencesMap: DSReferencesMapping = {
panels: new Map(),
variables: new Map(),
annotations: new Map(),
};
initializeElementMapping(saveModel: Dashboard | undefined) {
@@ -274,10 +277,10 @@ export class V2DashboardSerializer
metadata?: DashboardWithAccessInfo<DashboardV2Spec>['metadata'];
protected elementPanelMap = new Map<string, number>();
// map of elementId that will contain all the queries, variables and annotations that dont have a ds defined
protected defaultDsReferencesMap = {
panels: new Map<string, Set<string>>(), // refIds as keys
variables: new Set<string>(), // variable names as keys
annotations: new Set<string>(), // annotation names as keys
protected defaultDsReferencesMap: DSReferencesMapping = {
panels: new Map(),
variables: new Map(),
annotations: new Map(),
};
getElementPanelMapping() {
@@ -308,13 +311,9 @@ export class V2DashboardSerializer
return;
}
// initialize the object
this.defaultDsReferencesMap = {
panels: new Map<string, Set<string>>(),
variables: new Set<string>(),
annotations: new Set<string>(),
};
this.defaultDsReferencesMap = { panels: new Map(), variables: new Map(), annotations: new Map() };
// get all the element keys
// initialize autossigned panel queries ds references map
const elementKeys = Object.keys(saveModel?.elements || {});
elementKeys.forEach((key) => {
const elementPanel = saveModel?.elements[key];
@@ -324,14 +323,13 @@ export class V2DashboardSerializer
for (const query of panelQueries) {
if (!query.spec.query.datasource?.name) {
// Datasources without UID. Here we're saving elements with only type!
const elementId = this.getElementIdForPanel(elementPanel.spec.id);
if (!this.defaultDsReferencesMap.panels.has(elementId)) {
this.defaultDsReferencesMap.panels.set(elementId, new Set());
}
const panelDsqueries = this.defaultDsReferencesMap.panels.get(elementId) || new Map();
const datasourceType = query.spec.query.group || undefined;
const panelDsqueries = this.defaultDsReferencesMap.panels.get(elementId)!;
panelDsqueries.add(query.spec.refId);
panelDsqueries.set(query.spec.refId, datasourceType);
this.defaultDsReferencesMap.panels.set(elementId, panelDsqueries);
}
}
}
@@ -342,7 +340,8 @@ export class V2DashboardSerializer
for (const variable of saveModel.variables) {
// for query variables that dont have a ds defined add them to the list
if (variable.kind === 'QueryVariable' && !variable.spec.query.datasource?.name) {
this.defaultDsReferencesMap.variables.add(variable.spec.name);
const datasourceType = variable.spec.query.group || undefined;
this.defaultDsReferencesMap.variables.set(variable.spec.name, datasourceType);
}
}
}
@@ -351,7 +350,8 @@ export class V2DashboardSerializer
if (saveModel?.annotations) {
for (const annotation of saveModel.annotations) {
if (!annotation.spec.query?.datasource?.name) {
this.defaultDsReferencesMap.annotations.add(annotation.spec.name);
const datasourceType = annotation.spec.query.group || undefined;
this.defaultDsReferencesMap.annotations.set(annotation.spec.name, datasourceType);
}
}
}
@@ -470,6 +470,11 @@ describe('transformSceneToSaveModelSchemaV2', () => {
datasource: { uid: 'prometheus', type: 'prometheus' },
};
const queryWithOnlyDSType: SceneDataQuery = {
refId: 'C',
datasource: { type: 'prometheus' },
};
// Mock query runner with runtime-resolved datasource
const queryRunner = new SceneQueryRunner({
queries: [queryWithoutDS, queryWithDS],
@@ -477,7 +482,7 @@ describe('transformSceneToSaveModelSchemaV2', () => {
});
// Get a reference to the DS references mapping
const dsReferencesMap = new Set(['A']);
const dsReferencesMap = new Map<string, string | undefined>([['A', undefined]]);
// Test the query without DS originally - should return undefined
const resultA = getPersistedDSFor(queryWithoutDS, dsReferencesMap, 'query', queryRunner);
@@ -487,13 +492,17 @@ describe('transformSceneToSaveModelSchemaV2', () => {
const resultB = getPersistedDSFor(queryWithDS, dsReferencesMap, 'query', queryRunner);
expect(resultB).toEqual({ uid: 'prometheus', type: 'prometheus' });
// Test the query with only type defined - should return the type
const resultC = getPersistedDSFor(queryWithOnlyDSType, dsReferencesMap, 'query', queryRunner);
expect(resultC).toEqual({ type: 'prometheus' });
// Test a query with no DS originally but not in the mapping - should get the runner's datasource
const queryNotInMapping: SceneDataQuery = {
refId: 'C',
refId: 'D',
// No datasource, but not in mapping
};
const resultC = getPersistedDSFor(queryNotInMapping, dsReferencesMap, 'query', queryRunner);
expect(resultC).toEqual({ uid: 'default-ds', type: 'default' });
const resultD = getPersistedDSFor(queryNotInMapping, dsReferencesMap, 'query', queryRunner);
expect(resultD).toEqual({ uid: 'default-ds', type: 'default' });
});
});
@@ -511,8 +520,14 @@ describe('transformSceneToSaveModelSchemaV2', () => {
datasource: { uid: 'prometheus', type: 'prometheus' },
});
// Variable with only type defined
const variableWithOnlyDSType = new QueryVariable({
name: 'C',
datasource: { type: 'prometheus' },
});
// Get a reference to the DS references mapping
const dsReferencesMap = new Set(['A']);
const dsReferencesMap = new Map<string, string | undefined>([['A', undefined]]);
// Test the variable without DS originally - should return undefined
const resultA = getPersistedDSFor(variableWithoutDS, dsReferencesMap, 'variable');
@@ -522,13 +537,17 @@ describe('transformSceneToSaveModelSchemaV2', () => {
const resultB = getPersistedDSFor(variableWithDS, dsReferencesMap, 'variable');
expect(resultB).toEqual({ uid: 'prometheus', type: 'prometheus' });
// Test a variable with no DS originally but not in the mapping - should get empty object
// Test the variable with only type defined - should return the type
const resultC = getPersistedDSFor(variableWithOnlyDSType, dsReferencesMap, 'variable');
expect(resultC).toEqual({ type: 'prometheus' });
// Test a variable with no DS originally but not in the mapping - should return undefined
const variableNotInMapping = new QueryVariable({
name: 'C',
name: 'D',
// No datasource, but not in mapping
});
const resultC = getPersistedDSFor(variableNotInMapping, dsReferencesMap, 'variable');
expect(resultC).toEqual({});
const resultD = getPersistedDSFor(variableNotInMapping, dsReferencesMap, 'variable');
expect(resultD).toBeUndefined();
});
});
@@ -647,6 +666,11 @@ describe('getElementDatasource', () => {
refId: 'A',
};
const queryWithOnlyType: SceneDataQuery = {
refId: 'C',
datasource: { type: 'prometheus' },
};
// Mock query runner
const queryRunner = new SceneQueryRunner({
queries: [queryWithoutDS, queryWithDS],
@@ -655,9 +679,9 @@ describe('getElementDatasource', () => {
// Mock dsReferencesMapping
const dsReferencesMapping = {
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
variables: new Set<string>(),
annotations: new Set<string>(),
panels: new Map<string, Map<string, string>>([['panel-1', new Map<string, string>([['A', '']])]]),
variables: new Map<string, string>(),
annotations: new Map<string, string>(),
};
// Call the function with the panel and query with DS
@@ -667,6 +691,16 @@ describe('getElementDatasource', () => {
// Call the function with the panel and query without DS
const resultWithoutDS = getElementDatasource(vizPanel, queryWithoutDS, 'panel', queryRunner, dsReferencesMapping);
expect(resultWithoutDS).toBeUndefined();
// Call the function with the panel and query with only type
const resultWithOnlyType = getElementDatasource(
vizPanel,
queryWithOnlyType,
'panel',
queryRunner,
dsReferencesMapping
);
expect(resultWithOnlyType).toEqual({ type: 'prometheus' });
});
it('should handle variable datasources correctly', () => {
@@ -692,9 +726,9 @@ describe('getElementDatasource', () => {
// Mock dsReferencesMapping
const dsReferencesMapping = {
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
variables: new Set<string>(['A']),
annotations: new Set<string>(),
panels: new Map<string, Map<string, string>>([['panel-1', new Map<string, string>([['A', '']])]]),
variables: new Map<string, string>([['A', '']]),
annotations: new Map<string, string>(),
};
// Call the function with variables
@@ -781,11 +815,25 @@ describe('getElementDatasource', () => {
iconColor: 'blue',
};
// Create an annotation query with only type defined
const annotationLayerWithOnlyType = new dataLayers.AnnotationsDataLayer({
name: 'Annotation with only datasource type',
isEnabled: true,
isHidden: false,
query: {
name: 'Test Annotation',
enable: true,
hide: false,
iconColor: 'blue',
datasource: { type: 'prometheus' },
},
});
// Mock dsReferencesMapping
const dsReferencesMapping = {
panels: new Map([['panel-1', new Set(['A'])]]),
variables: new Set<string>(),
annotations: new Set<string>(['No DS Annotation']),
panels: new Map<string, Map<string, string>>([['panel-1', new Map<string, string>([['A', '']])]]),
variables: new Map<string, string>(),
annotations: new Map<string, string>(),
};
// Test with annotation that has datasource defined
@@ -807,6 +855,16 @@ describe('getElementDatasource', () => {
dsReferencesMapping
);
expect(resultWithoutDS).toBeUndefined();
// Test with annotation that has only type defined
const resultWithOnlyType = getElementDatasource(
annotationLayer,
annotationLayerWithOnlyType.state.query,
'annotation',
undefined,
dsReferencesMapping
);
expect(resultWithOnlyType).toEqual({ type: 'prometheus' });
});
it('should handle invalid input combinations', () => {
@@ -878,9 +936,9 @@ describe('getVizPanelQueries', () => {
// Mock dsReferencesMapping
const dsReferencesMapping = {
panels: new Map(new Set([['panel-1', new Set<string>(['A'])]])),
variables: new Set<string>(),
annotations: new Set<string>(),
panels: new Map<string, Map<string, string>>([['panel-1', new Map<string, string>([['A', '']])]]),
variables: new Map<string, string>(),
annotations: new Map<string, string>(),
};
const result = getVizPanelQueries(vizPanel, dsReferencesMapping);
@@ -808,13 +808,13 @@ export function getAutoAssignedDSRef(
element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer,
type: 'panels' | 'variables' | 'annotations',
elementMapReferences?: DSReferencesMapping
): Set<string> {
): Map<string, string | undefined> {
if (!elementMapReferences) {
return new Set();
return new Map();
}
if (type === 'panels' && isVizPanel(element)) {
const elementKey = dashboardSceneGraph.getElementIdentifierForVizPanel(element);
return elementMapReferences.panels.get(elementKey) || new Set();
return elementMapReferences.panels.get(elementKey) || new Map();
}
if (type === 'variables') {
@@ -830,20 +830,26 @@ export function getAutoAssignedDSRef(
}
/**
* Determines if a data source reference should be persisted for a query or variable
* Returns the datasource value that should be persisted for a panel query, variable or annotation
* - Undefined if the datasource was not defined in the initial save model
* - { type: string } if the datasource was autossigned defined by the initial group value
* - { uid: string, type: string } if the datasource was defined in the initial save model
*/
export function getPersistedDSFor<T extends SceneDataQuery | QueryVariable | AnnotationQuery>(
element: T,
autoAssignedDsRef: Set<string>,
autoAssignedDsRef: Map<string, string | undefined>,
type: 'query' | 'variable' | 'annotation',
context?: SceneQueryRunner
): DataSourceRef | undefined {
// Get the element identifier - refId for queries, name for variables
const elementId = getElementIdentifier(element, type);
// If the element is in the auto-assigned set, it didn't have a datasource specified
// If the ds was autossigned, return the datasource initial ds value.
if (autoAssignedDsRef?.has(elementId)) {
return undefined;
const dsType = autoAssignedDsRef.get(elementId);
// If the ds type was not undefined means the datasource was autossigned, so return the datasource with only the type
return dsType ? { type: dsType } : undefined;
}
// Return appropriate datasource reference based on element type
@@ -858,11 +864,11 @@ export function getPersistedDSFor<T extends SceneDataQuery | QueryVariable | Ann
}
if (type === 'variable' && 'state' in element && 'datasource' in element.state) {
return element.state.datasource || {};
return element.state.datasource || undefined;
}
if (type === 'annotation' && 'datasource' in element) {
return element.datasource || {};
return element.datasource || undefined;
}
return undefined;
@@ -920,11 +926,14 @@ function isQueryVariable(query: SceneDataQuery | QueryVariable | AnnotationQuery
}
/**
* Get the persisted datasource for a query or variable
* When a query or variable is created it could not have a datasource set
* Get the persisted datasource for a panel query, annotation or variable
* When a panel query, annotation or variable is created it could not have a datasource set
* we want to respect that and not overwrite it with the auto assigned datasources
* resolved in runtime
*
* - Undefined if the datasource was not defined in the initial save model
* - { type: string } if the datasource was autossigned defined by the initial group value
* - { uid: string, type: string } if the datasource was defined in the initial save model
*/
export function getElementDatasource(
element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer,
@@ -961,7 +970,7 @@ export function getElementDatasource(
const autoAssignedRefs = getAutoAssignedDSRef(element, 'annotations', dsReferencesMapping);
result = getPersistedDSFor(queryElement, autoAssignedRefs, 'annotation');
}
// Important: Only return the datasource if it's not in auto-assigned refs
// and if the result would not be an empty object
// Avoid returning an empty object
return Object.keys(result || {}).length > 0 ? result : undefined;
}
@@ -13,6 +13,10 @@ export function isV2StoredVersion(version: string | undefined): boolean {
return version === 'v2alpha1' || version === 'v2beta1';
}
export function isV0V1StoredVersion(version: string | undefined): boolean {
return version === 'v0alpha1' || version === 'v1alpha1' || version === 'v1beta1';
}
export function getDashboardsApiVersion(responseFormat?: 'v1' | 'v2') {
const isDashboardSceneEnabled = config.featureToggles.dashboardScene;
const isKubernetesDashboardsEnabled = config.featureToggles.kubernetesDashboards;
+4 -4
View File
@@ -317,7 +317,7 @@ describe('v2 dashboard API', () => {
});
describe('version error handling', () => {
it('should not throw DashboardVersionError for v0alpha1 conversion error and v2 spec', async () => {
it('should throw DashboardVersionError for v0alpha1 conversion error and v2 spec', async () => {
const mockDashboardWithError = {
...mockDashboardDto,
status: {
@@ -332,7 +332,7 @@ describe('v2 dashboard API', () => {
mockGet.mockResolvedValueOnce(mockDashboardWithError);
const api = new K8sDashboardV2API();
await expect(api.getDashboardDTO('test')).resolves.toBe(mockDashboardWithError);
await expect(api.getDashboardDTO('test')).rejects.toThrow('backend conversion not yet implemented');
});
it('should throw DashboardVersionError for v0alpha1 conversion error and v1 spec', async () => {
@@ -358,7 +358,7 @@ describe('v2 dashboard API', () => {
await expect(api.getDashboardDTO('test')).rejects.toThrow('backend conversion not yet implemented');
});
it('should not throw DashboardVersionError for v1beta1 conversion error and v2 spec', async () => {
it('should throw DashboardVersionError for v1beta1 conversion error and v2 spec', async () => {
const mockDashboardWithError = {
...mockDashboardDto,
status: {
@@ -373,7 +373,7 @@ describe('v2 dashboard API', () => {
mockGet.mockResolvedValueOnce(mockDashboardWithError);
const api = new K8sDashboardV2API();
await expect(api.getDashboardDTO('test')).resolves.toBe(mockDashboardWithError);
await expect(api.getDashboardDTO('test')).rejects.toThrow('backend conversion not yet implemented');
});
it('should throw DashboardVersionError for v1beta1 conversion error and v1 spec', async () => {
+3 -9
View File
@@ -24,7 +24,7 @@ import { DashboardDTO, SaveDashboardResponseDTO } from 'app/types/dashboard';
import { SaveDashboardCommand } from '../components/SaveDashboard/types';
import { DashboardAPI, DashboardVersionError, DashboardWithAccessInfo, ListDeletedDashboardsOptions } from './types';
import { isDashboardV2Spec } from './utils';
import { isV0V1StoredVersion } from './utils';
export const K8S_V2_DASHBOARD_API_CONFIG = {
group: 'dashboard.grafana.app',
@@ -44,16 +44,10 @@ export class K8sDashboardV2API
async getDashboardDTO(uid: string) {
try {
const dashboard = await this.client.subresource<DashboardWithAccessInfo<DashboardV2Spec>>(uid, 'dto');
// FOR /dto calls returning v2 spec we are ignoring the conversion status to avoid runtime errors caused by the status
// being saved for v2 resources that's been client-side converted to v2 and then PUT to the API server.
if (
!isDashboardV2Spec(dashboard.spec) &&
dashboard.status?.conversion?.failed &&
(dashboard.status.conversion.storedVersion === 'v1alpha1' ||
dashboard.status.conversion.storedVersion === 'v1beta1' ||
dashboard.status.conversion.storedVersion === 'v0alpha1')
) {
// This could come as conversion error from v0 or v2 to V1.
if (dashboard.status?.conversion?.failed && isV0V1StoredVersion(dashboard.status.conversion.storedVersion)) {
throw new DashboardVersionError(dashboard.status.conversion.storedVersion, dashboard.status.conversion.error);
}
@@ -75,6 +75,7 @@ describe('Logs', () => {
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost:3000/explore?test',
search: '?test',
},
writable: true,
});
+14 -1
View File
@@ -83,6 +83,7 @@ import LogsNavigation from './LogsNavigation';
import { LogsTableWrap, getLogsTableHeight } from './LogsTableWrap';
import { LogsVolumePanelList } from './LogsVolumePanelList';
import { SETTING_KEY_ROOT, SETTINGS_KEYS, visualisationTypeKey } from './utils/logs';
import { getExploreBaseUrl } from './utils/url';
interface Props extends Themeable2 {
width: number;
@@ -308,6 +309,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
refId: undefined,
displayedFields: undefined,
sortOrder: undefined,
tableSortBy: undefined,
tableSortDir: undefined,
})
);
});
@@ -324,6 +327,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
labelFieldName: logsPanelState.labelFieldName,
refId: logsPanelState.refId ?? panelState?.logs?.refId,
displayedFields: logsPanelState.displayedFields ?? panelState?.logs?.displayedFields,
tableSortBy: logsPanelState.tableSortBy ?? panelState?.logs?.tableSortBy,
tableSortDir: logsPanelState.tableSortDir ?? panelState?.logs?.tableSortDir,
})
);
}
@@ -334,6 +339,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
panelState?.logs?.columns,
panelState?.logs?.displayedFields,
panelState?.logs?.refId,
panelState?.logs?.tableSortBy,
panelState?.logs?.tableSortDir,
visualisationType,
]
);
@@ -611,7 +618,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
// append changed urlState to baseUrl
const serializedState = serializeStateToUrlParam(urlState);
const baseUrl = /.*(?=\/explore)/.exec(`${window.location.href}`)![0];
const baseUrl = getExploreBaseUrl();
const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState });
await createAndCopyShortLink(url);
@@ -996,6 +1003,10 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
panelState={panelState?.logs}
updatePanelState={updatePanelState}
datasourceType={props.datasourceType}
displayedFields={displayedFields}
exploreId={props.exploreId}
absoluteRange={props.absoluteRange}
logRows={props.logRows}
/>
</div>
)}
@@ -1050,6 +1061,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
onLogOptionsChange={onLogOptionsChange}
filterLevels={filterLevels}
timeRange={props.range}
exploreId={props.exploreId}
absoluteRange={props.absoluteRange}
/>
</div>
)}
@@ -1,7 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { ComponentProps } from 'react';
import { DataFrame, FieldType, LogsSortOrder, toUtc } from '@grafana/data';
import { DataFrame, FieldType, LogsSortOrder, toUtc, urlUtil } from '@grafana/data';
import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal';
import { config } from '@grafana/runtime';
import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields';
@@ -301,4 +301,141 @@ describe('LogsTable', () => {
});
});
});
describe('Sort persistence', () => {
it('should update URL with sort parameters when sort changes', async () => {
// Mock onSortByChange to update URL (simulating parent Explore component behavior)
const onSortByChange = jest.fn((sortBy) => {
const mockUrl = new URL(window.location.href);
if (sortBy && sortBy.length > 0) {
mockUrl.searchParams.set('tableSortBy', sortBy[0].displayName);
mockUrl.searchParams.set('tableSortDir', sortBy[0].desc ? 'desc' : 'asc');
} else {
// Remove sort params if no sort is applied
mockUrl.searchParams.delete('tableSortBy');
mockUrl.searchParams.delete('tableSortDir');
}
window.history.replaceState({}, '', mockUrl.toString());
});
setup({
tableSortBy: 'Time',
tableSortDir: 'desc',
onSortByChange,
columnsWithMeta: {
Time: { active: true, percentOfLinesWithLabel: 3, index: 0 },
line: { active: true, percentOfLinesWithLabel: 3, index: 1 },
},
});
await waitFor(() => {
const rows = screen.getAllByRole('row');
expect(rows.length).toBe(4);
});
// Verify the Time column has the sort indicator (arrow down for descending)
const timeColumnHeader = screen.getByRole('columnheader', { name: /Time/i });
const sortButton = timeColumnHeader.querySelector('button[title="Toggle SortBy"]');
expect(sortButton).toBeTruthy();
// Click to toggle sort (desc -> asc)
if (sortButton) {
fireEvent.click(sortButton);
}
await waitFor(() => {
expect(onSortByChange).toHaveBeenCalled();
});
// Verify URL was updated (callback was called and URL reflects the new sort state)
const currentUrl = new URL(window.location.href);
const tableSortBy = currentUrl.searchParams.get('tableSortBy');
const tableSortDir = currentUrl.searchParams.get('tableSortDir');
expect(onSortByChange).toHaveBeenCalled();
// Verify sort parameters are in URL after clicking
// The mock simulates parent component updating URL with sort state
if (tableSortBy && tableSortDir) {
expect(tableSortBy).toBe('Time');
expect(tableSortDir).toBe('desc');
}
});
});
describe('Selected log line', () => {
it('should handle selected log line from URL parameter', async () => {
// Use getMockLokiFrame which has proper structure with id field
const testFrame = getMockLokiFrame();
const logsFrame = parseLogsFrame(testFrame);
// Get the second ID from the parsed frame to test selection of non-first row
const secondId = logsFrame?.idField?.values[1];
// Mock URL search params to include selectedLine
const mockGetSearchParams = jest.spyOn(urlUtil, 'getUrlSearchParams');
mockGetSearchParams.mockReturnValue({
selectedLine: JSON.stringify({ id: secondId, row: 1 }),
});
// Verify selectedLine is in the mocked URL params
const params = urlUtil.getUrlSearchParams();
expect(params.selectedLine).toBeDefined();
expect(params.selectedLine).toContain(secondId);
});
it('should clear selectedLine URL parameter after render', async () => {
// Mock locationService.partial instead of window.history.replaceState
const partialSpy = jest.spyOn(require('@grafana/runtime').locationService, 'partial');
// Use getMockLokiFrame which has proper structure
const testFrame = getMockLokiFrame();
const logsFrame = parseLogsFrame(testFrame);
// Get the first ID from the parsed frame
const firstId = logsFrame?.idField?.values[0];
// Mock URL search params with matching id
const mockGetSearchParams = jest.spyOn(urlUtil, 'getUrlSearchParams');
mockGetSearchParams.mockReturnValue({
selectedLine: JSON.stringify({ id: firstId, row: 0 }),
});
setup({ logsFrame }, testFrame);
await waitFor(() => {
expect(partialSpy).toHaveBeenCalled();
// Verify that selectedLine is set to undefined
const callArgs = partialSpy.mock.calls[0];
expect(callArgs[0]).toEqual({ selectedLine: undefined });
expect(callArgs[1]).toBe(true); // replace parameter
});
});
});
describe('Table action buttons', () => {
it('should render action buttons in first column when exploreId is provided', async () => {
setup({
exploreId: 'test-explore',
});
await waitFor(() => {
const rows = screen.getAllByRole('row');
expect(rows.length).toBeGreaterThan(1); // header + data rows
});
// Verify buttons are in the first column
const rows = screen.getAllByRole('row');
const dataRows = rows.filter((row) => row.getAttribute('role') === 'row' && !row.getAttribute('aria-label'));
dataRows.forEach((row) => {
const cells = row.querySelectorAll('[role="cell"]');
const firstCell = cells[0];
// First cell should contain both action buttons
expect(firstCell.querySelector('button[aria-label="View log line"]')).toBeTruthy();
expect(firstCell.querySelector('button[aria-label="Copy link to log line"]')).toBeTruthy();
});
});
});
});
+196 -15
View File
@@ -1,7 +1,9 @@
import { useCallback, useEffect, useState } from 'react';
import { css } from '@emotion/css';
import { useCallback, useEffect, useState, useMemo } from 'react';
import { lastValueFrom } from 'rxjs';
import {
urlUtil,
applyFieldOverrides,
CustomTransformOperator,
DataFrame,
@@ -16,14 +18,26 @@ import {
TimeRange,
transformDataFrame,
ValueLinkConfig,
ExploreLogsPanelState,
AbsoluteTimeRange,
LogRowModel,
GrafanaTheme2,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { AdHocFilterItem, Table } from '@grafana/ui';
import { config, locationService } from '@grafana/runtime';
import {
AdHocFilterItem,
CustomCellRendererProps,
TableSortByFieldState,
Table,
TableCellDisplayMode,
useStyles2,
} from '@grafana/ui';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/internal';
import { LogsFrame } from 'app/features/logs/logsFrame';
import { getFieldLinksForExplore } from '../utils/links';
import { LogsTableActionButtons } from './LogsTableActionButtons';
import { FieldNameMeta } from './LogsTableWrap';
interface Props {
@@ -38,12 +52,86 @@ interface Props {
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
logsFrame: LogsFrame | null;
tableSortBy?: string;
tableSortDir?: 'asc' | 'desc';
onSortByChange?: (sortBy: TableSortByFieldState[]) => void;
displayedFields?: string[];
exploreId?: string;
panelState?: ExploreLogsPanelState;
absoluteRange?: AbsoluteTimeRange;
logRows?: LogRowModel[];
}
export function LogsTable(props: Props) {
const { timeZone, splitOpen, range, logsSortOrder, width, dataFrame, columnsWithMeta, logsFrame } = props;
const {
timeZone,
splitOpen,
range,
logsSortOrder,
width,
dataFrame,
columnsWithMeta,
logsFrame,
tableSortBy,
tableSortDir,
onSortByChange,
} = props;
const [tableFrame, setTableFrame] = useState<DataFrame | undefined>(undefined);
const [columnWidthMap, setColumnWidthMap] = useState<Record<string, number>>({});
const timeIndex = logsFrame?.timeField.index;
const styles = useStyles2(getStyles);
// Extract selected log ID from URL parameter
const selectedLogInfo = useMemo(() => {
const { selectedLine } = urlUtil.getUrlSearchParams();
const param = Array.isArray(selectedLine) ? selectedLine[0] : selectedLine;
if (typeof param !== 'string') {
return undefined;
}
try {
const { id, row } = JSON.parse(param);
return { id, row };
} catch (error) {
return undefined;
}
}, []);
// Set the initial row index based on the selected log ID if selectedLine is present in the URL
const initialRowIndex = useMemo(() => {
if (!selectedLogInfo || !tableFrame || !selectedLogInfo.id) {
return undefined;
}
// Search through all fields in tableFrame to find the one containing the ID
for (const field of tableFrame.fields) {
const lineIndex = field.values.findIndex((v: unknown) => v === selectedLogInfo.id);
if (lineIndex !== -1) {
return lineIndex;
}
}
return undefined;
}, [selectedLogInfo, tableFrame]);
// Clear the selectedLine URL parameter after table loads
useEffect(() => {
if (initialRowIndex !== undefined && tableFrame) {
// Remove selectedLine from URL using locationService (proper Grafana way)
locationService.partial({ selectedLine: undefined }, true);
}
}, [initialRowIndex, tableFrame]);
const onColumnResize = useCallback((fieldDisplayName: string, width: number) => {
if (width > 0) {
setColumnWidthMap((prev) => ({
...prev,
[fieldDisplayName]: width,
}));
}
}, []);
const prepareTableFrame = useCallback(
(frame: DataFrame): DataFrame => {
@@ -66,7 +154,21 @@ export function LogsTable(props: Props) {
},
});
// `getLinks` and `applyFieldOverrides` are taken from TableContainer.tsx
for (const field of frameWithOverrides.fields) {
for (const [index, field] of frameWithOverrides.fields.entries()) {
// Hide ID field from visualization (it's only needed for row matching)
if (logsFrame?.idField && (field.name === logsFrame.idField.name || field.name === 'id')) {
field.config = {
...field.config,
custom: {
...field.config.custom,
hideFrom: {
...field.config.custom?.hideFrom,
viz: true,
},
},
};
}
field.getLinks = (config: ValueLinkConfig) => {
return getFieldLinksForExplore({
field,
@@ -76,13 +178,44 @@ export function LogsTable(props: Props) {
dataFrame: sortedFrame!,
});
};
// For the first field (time), wrap the cell to include action buttons
const isFirstField = index === 0;
field.config = {
...field.config,
custom: {
inspect: true,
filterable: true, // This sets the columns to be filterable
width: getInitialFieldWidth(field),
width: columnWidthMap[field.name] ?? getInitialFieldWidth(field),
...field.config.custom,
cellOptions: isFirstField
? {
type: TableCellDisplayMode.Custom,
cellComponent: (cellProps: CustomCellRendererProps) => (
<>
<LogsTableActionButtons
{...cellProps}
logsFrame={logsFrame ?? undefined}
displayedFields={props.displayedFields}
exploreId={props.exploreId}
panelState={props.panelState}
absoluteRange={props.absoluteRange}
logRows={props.logRows}
rowIndex={cellProps.rowIndex}
/>
<span className={styles.firstColumnCell}>
{cellProps.field.display?.(cellProps.value).text ?? String(cellProps.value)}
</span>
</>
),
}
: field.config.custom?.cellOptions,
headerComponent: isFirstField
? (headerProps: { defaultContent: React.ReactNode }) => (
<div className={styles.firstColumnHeader}>{headerProps.defaultContent}</div>
)
: field.config.custom?.headerComponent,
},
// This sets the individual field value as filterable
filterable: isFieldFilterable(field, logsFrame?.bodyField.name ?? '', logsFrame?.timeField.name ?? ''),
@@ -94,7 +227,22 @@ export function LogsTable(props: Props) {
return frameWithOverrides;
},
[logsSortOrder, timeZone, splitOpen, range, logsFrame?.bodyField.name, logsFrame?.timeField.name, timeIndex]
[
logsSortOrder,
timeZone,
splitOpen,
range,
columnWidthMap,
logsFrame,
timeIndex,
styles.firstColumnCell,
styles.firstColumnHeader,
props.displayedFields,
props.exploreId,
props.panelState,
props.absoluteRange,
props.logRows,
]
);
useEffect(() => {
@@ -112,9 +260,24 @@ export function LogsTable(props: Props) {
// Add the label filters to the transformations
const transform = getLabelFiltersTransform(labelFilters);
if (transform) {
// Ensure ID field is always included for row matching
if (logsFrame?.idField?.name) {
transform.options.includeByName = {
...transform.options.includeByName,
[logsFrame.idField.name]: true,
};
}
transformations.push(transform);
} else {
// If no fields are filtered, filter the default fields, so we don't render all columns
// Always include ID field for row matching
const includeByName: Record<string, boolean> = {
[logsFrame.bodyField.name]: true,
[logsFrame.timeField.name]: true,
};
if (logsFrame?.idField?.name) {
includeByName[logsFrame.idField.name] = true;
}
transformations.push({
id: 'organize',
options: {
@@ -122,10 +285,7 @@ export function LogsTable(props: Props) {
[logsFrame.bodyField.name]: 0,
[logsFrame.timeField.name]: 1,
},
includeByName: {
[logsFrame.bodyField.name]: true,
[logsFrame.timeField.name]: true,
},
includeByName,
},
});
}
@@ -146,6 +306,7 @@ export function LogsTable(props: Props) {
prepareTableFrame,
logsFrame?.bodyField.name,
logsFrame?.timeField.name,
logsFrame?.idField?.name,
]);
if (!tableFrame) {
@@ -167,16 +328,24 @@ export function LogsTable(props: Props) {
}
};
// Use persisted sortBy if available, otherwise default to time field based on logsSortOrder
const defaultSortBy: TableSortByFieldState[] = [
{ displayName: logsFrame?.timeField.name || '', desc: logsSortOrder === LogsSortOrder.Descending },
];
const initialSortBy: TableSortByFieldState[] =
tableSortBy && tableSortDir ? [{ displayName: tableSortBy, desc: tableSortDir === 'desc' }] : defaultSortBy;
return (
<Table
data={tableFrame}
width={width}
onColumnResize={onColumnResize}
onCellFilterAdded={props.onClickFilterLabel && props.onClickFilterOutLabel ? onCellFilterAdded : undefined}
height={props.height}
footerOptions={{ show: true, reducer: ['count'], countRows: true }}
initialSortBy={[
{ displayName: logsFrame?.timeField.name || '', desc: logsSortOrder === LogsSortOrder.Descending },
]}
initialSortBy={initialSortBy}
onSortByChange={onSortByChange}
initialRowIndex={initialRowIndex}
/>
);
}
@@ -263,7 +432,19 @@ function getLabelFiltersTransform(labelFilters: Record<string, number>) {
function getInitialFieldWidth(field: Field): number | undefined {
if (field.type === FieldType.time) {
return 200;
return 230;
}
return undefined;
}
const getStyles = (theme: GrafanaTheme2) => ({
firstColumnHeader: css({
display: 'flex',
label: 'wrapper',
marginLeft: theme.spacing(7),
width: '100%',
}),
firstColumnCell: css({
paddingLeft: theme.spacing(7),
}),
});
@@ -0,0 +1,190 @@
import { css } from '@emotion/css';
import { useCallback, useState } from 'react';
import {
AbsoluteTimeRange,
ExploreLogsPanelState,
GrafanaTheme2,
LogRowModel,
serializeStateToUrlParam,
urlUtil,
} from '@grafana/data';
import { t } from '@grafana/i18n';
import { ClipboardButton, CustomCellRendererProps, IconButton, Modal, useTheme2 } from '@grafana/ui';
import { getLogsPermalinkRange } from 'app/core/utils/shortLinks';
import { getUrlStateFromPaneState } from 'app/features/explore/hooks/useStateSync';
import { LogsFrame } from 'app/features/logs/logsFrame';
import { getState } from 'app/store/store';
import { getExploreBaseUrl } from './utils/url';
interface Props extends CustomCellRendererProps {
logId?: string;
logsFrame?: LogsFrame;
exploreId?: string;
panelState?: ExploreLogsPanelState;
displayedFields?: string[];
absoluteRange?: AbsoluteTimeRange;
logRows?: LogRowModel[];
index?: number;
}
export function LogsTableActionButtons(props: Props) {
const { exploreId, absoluteRange, logRows, rowIndex, panelState, displayedFields, logsFrame, frame } = props;
const theme = useTheme2();
const [isInspecting, setIsInspecting] = useState(false);
// Get logId from the table frame (frame), not the original logsFrame, because
// the table frame is sorted/transformed and rowIndex refers to the table frame
const idFieldName = logsFrame?.idField?.name ?? 'id';
const idField = frame.fields.find((field) => field.name === idFieldName || field.name === 'id');
const logId = idField?.values[rowIndex];
const getLineValue = () => {
const bodyFieldName = logsFrame?.bodyField?.name;
const bodyField = bodyFieldName
? frame.fields.find((field) => field.name === bodyFieldName)
: frame.fields.find((field) => field.type === 'string');
return bodyField?.values[rowIndex];
};
const lineValue = getLineValue();
const styles = getStyles(theme);
// Generate link to the log line
const getText = useCallback(() => {
if (!logId || !exploreId || !absoluteRange || !logRows) {
return '';
}
try {
// Get the log row from the logRows array
const logRow = logRows.find((row) => row.rowId === logId);
if (!logRow) {
return '';
}
// Get the current explore state
const currentPaneState = getState().explore.panes[exploreId];
if (!currentPaneState) {
return '';
}
// Create URL state with log permalink information
const urlState = getUrlStateFromPaneState(currentPaneState);
// Preserve all panel state (columns, labelFieldName, etc.)
urlState.panelsState = {
...currentPaneState.panelsState,
logs: {
...panelState,
displayedFields: displayedFields ?? [],
},
};
// Calculate the time range for the permalink
urlState.range = getLogsPermalinkRange(logRow, logRows, absoluteRange);
// Create the full URL with selectedLine as a URL parameter (with id and row)
const serializedState = serializeStateToUrlParam(urlState);
const baseUrl = getExploreBaseUrl();
const url = urlUtil.renderUrl(`${baseUrl}/explore`, {
left: serializedState,
selectedLine: JSON.stringify({ id: logId, row: rowIndex }),
});
return url;
} catch (error) {
return '';
}
}, [absoluteRange, displayedFields, exploreId, logId, logRows, rowIndex, panelState]);
const handleViewClick = () => {
setIsInspecting(true);
};
return (
<>
<div className={styles.iconWrapper}>
<div className={styles.inspect}>
<IconButton
className={styles.inspectButton}
tooltip={t('explore.logs-table.action-buttons.view-log-line', 'View log line')}
variant="secondary"
aria-label={t('explore.logs-table.action-buttons.view-log-line', 'View log line')}
tooltipPlacement="top"
size="md"
name="eye"
onClick={handleViewClick}
tabIndex={0}
/>
</div>
<div className={styles.inspect}>
<ClipboardButton
className={styles.clipboardButton}
icon="share-alt"
variant="secondary"
fill="text"
size="md"
tooltip={t('explore.logs-table.action-buttons.copy-link', 'Copy link to log line')}
tooltipPlacement="top"
tabIndex={0}
aria-label={t('explore.logs-table.action-buttons.copy-link', 'Copy link to log line')}
getText={getText}
/>
</div>
</div>
{isInspecting && (
<Modal
onDismiss={() => setIsInspecting(false)}
isOpen={true}
title={t('explore.logs-table.action-buttons.inspect-value', 'Inspect value')}
>
<pre>{lineValue}</pre>
<Modal.ButtonRow>
<ClipboardButton icon="copy" getText={() => lineValue}>
{t('explore.logs-table.action-buttons.copy-to-clipboard', 'Copy to Clipboard')}
</ClipboardButton>
</Modal.ButtonRow>
</Modal>
)}
</>
);
}
export const getStyles = (theme: GrafanaTheme2) => ({
clipboardButton: css({
height: '100%',
lineHeight: '1',
padding: 0,
width: '20px',
}),
iconWrapper: css({
background: theme.colors.background.secondary,
boxShadow: theme.shadows.z2,
display: 'flex',
flexDirection: 'row',
height: '35px',
left: 0,
top: 0,
padding: `0 ${theme.spacing(0.5)}`,
position: 'absolute',
zIndex: 1,
}),
inspect: css({
'& button svg': {
marginRight: 'auto',
},
'&:hover': {
color: theme.colors.text.link,
cursor: 'pointer',
},
padding: '5px 3px',
}),
inspectButton: css({
borderRadius: theme.shape.radius.default,
display: 'inline-flex',
margin: 0,
overflow: 'hidden',
verticalAlign: 'middle',
}),
});
@@ -7,11 +7,13 @@ import {
ExploreLogsPanelState,
GrafanaTheme2,
Labels,
LogRowModel,
LogsSortOrder,
SelectableValue,
SplitOpen,
store,
TimeRange,
AbsoluteTimeRange,
} from '@grafana/data';
import { t } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
@@ -40,6 +42,10 @@ interface Props {
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
datasourceType?: string;
exploreId?: string;
displayedFields?: string[];
absoluteRange?: AbsoluteTimeRange;
logRows?: LogRowModel[];
}
type ActiveFieldMeta = {
@@ -278,6 +284,25 @@ export function LogsTableWrap(props: Props) {
const styles = useStyles2(getStyles, height, sidebarWidth);
const onSortByChange = useCallback(
(sortBy: Array<{ displayName: string; desc?: boolean }>) => {
// Transform from Table format to URL format - only store the first sort column
if (sortBy.length > 0) {
// Defer the Redux dispatch to avoid updating ExploreActions during Table's render cycle
// Even though this is called from an event handler, the synchronous Redux dispatch
// can cause ExploreActions (which subscribes to panes state) to re-render while
// Table is still rendering, triggering the React warning
setTimeout(() => {
updatePanelState({
tableSortBy: sortBy[0].displayName,
tableSortDir: sortBy[0].desc ? 'desc' : 'asc',
});
}, 0);
}
},
[updatePanelState]
);
if (!columnsWithMeta) {
return null;
}
@@ -512,6 +537,13 @@ export function LogsTableWrap(props: Props) {
dataFrame={currentDataFrame}
columnsWithMeta={columnsWithMeta}
height={height}
tableSortBy={panelState?.tableSortBy}
tableSortDir={panelState?.tableSortDir}
onSortByChange={onSortByChange}
displayedFields={props.displayedFields}
exploreId={props.exploreId}
absoluteRange={props.absoluteRange}
logRows={props.logRows}
/>
</div>
</div>
@@ -0,0 +1,10 @@
/**
* Gets the base URL before the /explore path.
* Used for constructing explore URLs with permalinks.
*
* @returns The base URL (e.g., "http://localhost:3000" or "https://grafana.com")
*/
export function getExploreBaseUrl(): string {
const match = /.*(?=\/explore)/.exec(window.location.href);
return match ? match[0] : window.location.origin;
}

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