Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f87389f10d | ||
|
|
f7d9d22963 | ||
|
|
29cf10f1fb | ||
|
|
8998b1fde4 | ||
|
|
0f698d08d3 | ||
|
|
c847f1fa4b | ||
|
|
157cab192c | ||
|
|
dbb4eb7e12 | ||
|
|
c9fe5c3669 | ||
|
|
e89efd7068 | ||
|
|
3cb03b888f | ||
|
|
d04f51f7ed | ||
|
|
b74465e428 | ||
|
|
f3ed3a999d | ||
|
|
612af5ef55 |
+9
-11
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
+10
-10
@@ -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 }) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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{
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
+8
-37
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+79
-21
@@ -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);
|
||||
|
||||
+22
-13
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user