From 82b81c2b75b60eee02de067a08c14aacb1762cce Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Tue, 25 Nov 2025 09:48:57 -0500 Subject: [PATCH 01/94] Rudderstack Event: When click dashboard list item, add uid to rudderstack event (#114374) NameCell: add uid to rudderstack event --- public/app/features/browse-dashboards/components/NameCell.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx index 4ee8ceba594..0049fb8f829 100644 --- a/public/app/features/browse-dashboards/components/NameCell.tsx +++ b/public/app/features/browse-dashboards/components/NameCell.tsx @@ -97,6 +97,7 @@ export function NameCell({ row: { original: data }, onFolderClick, treeID }: Nam itemKind: item.kind, parent: item.parentUID ? 'folder' : 'root', source: 'browseDashboardsPage_BrowseView', + uid: item.uid, }); }} href={item.url} From 5694d6371ee9989409f7259295b1cf821cd1f022 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 25 Nov 2025 15:46:07 +0000 Subject: [PATCH 02/94] Plugins: Upgrade `github.com/grafana/grafana-plugin-sdk-go` v0.283.0 => v0.284.0 (#114400) * upgraded github.com/grafana/grafana-plugin-sdk-go v0.283.0 => v0.284.0 * make update-workspace * apply fixes * nolint for FieldTypeNullableJSON instead * fmt * fix lint issues --- apps/advisor/go.mod | 2 +- apps/advisor/go.sum | 4 ++-- apps/dashboard/go.mod | 2 +- apps/dashboard/go.sum | 4 ++-- apps/iam/go.mod | 2 +- apps/iam/go.sum | 4 ++-- apps/plugins/go.mod | 2 +- apps/plugins/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- go.work.sum | 9 +++++++++ pkg/aggregator/go.mod | 2 +- pkg/aggregator/go.sum | 4 ++-- pkg/expr/sql/frame_db_conv.go | 2 +- pkg/expr/sql/frame_table.go | 2 +- pkg/promlib/go.mod | 2 +- pkg/promlib/go.sum | 4 ++-- pkg/services/searchV2/bluge.go | 4 ++-- .../loganalytics/azure-response-table-frame.go | 2 +- pkg/tsdb/azuremonitor/metrics/url-builder.go | 8 ++++---- .../azure-resource-graph-datasource.go | 11 +++++++---- pkg/tsdb/azuremonitor/time/time-grain.go | 4 ++-- pkg/tsdb/azuremonitor/utils/utils.go | 15 +++++++++------ pkg/tsdb/elasticsearch/response_parser_test.go | 8 ++++---- .../sqleng/sql_engine.go | 4 ++-- pkg/tsdb/graphite/query.go | 7 +++++-- pkg/tsdb/influxdb/influxql/converter/converter.go | 12 ++++++------ .../influxdb/influxql/converter/converter_test.go | 12 ++++++------ pkg/tsdb/mssql/sqleng/sql_engine.go | 2 +- pkg/tsdb/mysql/sqleng/sql_engine.go | 2 +- 30 files changed, 82 insertions(+), 64 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 6611470fb8f..6e944d56c3b 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -10,7 +10,7 @@ require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/grafana/grafana-plugin-sdk-go v0.283.0 + github.com/grafana/grafana-plugin-sdk-go v0.284.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 79aea07d576..61d3d00b8c2 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -624,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.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +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/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= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index b281b21e4e4..e5a1352e7d8 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -7,7 +7,7 @@ require ( github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana-app-sdk v0.48.2 github.com/grafana/grafana-app-sdk/logging v0.48.1 - github.com/grafana/grafana-plugin-sdk-go v0.283.0 + github.com/grafana/grafana-plugin-sdk-go v0.284.0 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 diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index c0efb792788..135eec59afc 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -89,8 +89,8 @@ github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJH github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana-plugin-sdk-go v0.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +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/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= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 73891acd454..ad6c6039cc5 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -225,7 +225,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.283.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect github.com/grafana/grafana/apps/plugins v0.0.0 // indirect github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index 4cba06c00ec..c91c571fa7d 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -847,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.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +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/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= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index d2b2f2e8045..ef1b58517a3 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -74,7 +74,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.283.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 1c479c59942..06683624e65 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -169,8 +169,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.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +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/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= diff --git a/go.mod b/go.mod index 936cf97655b..e1ed6dddd56 100644 --- a/go.mod +++ b/go.mod @@ -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.283.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.284.0 // @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 diff --git a/go.sum b/go.sum index f7c4c8e5578..971b7b06f74 100644 --- a/go.sum +++ b/go.sum @@ -1647,8 +1647,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.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +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/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= diff --git a/go.work.sum b/go.work.sum index 72f5dd7e97d..c4b732eaadb 100644 --- a/go.work.sum +++ b/go.work.sum @@ -896,6 +896,7 @@ github.com/grafana/grafana-plugin-sdk-go v0.277.0/go.mod h1:mAUWg68w5+1f5TLDqagI github.com/grafana/grafana-plugin-sdk-go v0.278.0/go.mod h1:+8NXT/XUJ/89GV6FxGQ366NZ3nU+cAXDMd0OUESF9H4= github.com/grafana/grafana-plugin-sdk-go v0.279.0/go.mod h1:/7oGN6Z7DGTGaLHhgIYrRr6Wvmdsb3BLw5hL4Kbjy88= github.com/grafana/grafana-plugin-sdk-go v0.280.0/go.mod h1:Z15Wiq3c4I0tzHYrLYpOqrO8u3+2RJ+HN2Q9uiZTILA= +github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/advisor v0.0.0-20250220154326-6e5de80ef295/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A= @@ -1323,6 +1324,7 @@ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkq github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8= github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ= @@ -1862,6 +1864,7 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632 golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= @@ -1890,6 +1893,7 @@ golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= @@ -1912,6 +1916,7 @@ golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -1920,6 +1925,7 @@ golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= @@ -1948,6 +1954,7 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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/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= @@ -1967,6 +1974,7 @@ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -1989,6 +1997,7 @@ golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 32ba2addf5e..24f0d25efae 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -4,7 +4,7 @@ go 1.25.3 require ( github.com/emicklei/go-restful/v3 v3.13.0 - github.com/grafana/grafana-plugin-sdk-go v0.283.0 + github.com/grafana/grafana-plugin-sdk-go v0.284.0 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 diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 2f44039e35c..313c0b7c200 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -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.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +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/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= diff --git a/pkg/expr/sql/frame_db_conv.go b/pkg/expr/sql/frame_db_conv.go index fe94ae79266..451b7c4f9a8 100644 --- a/pkg/expr/sql/frame_db_conv.go +++ b/pkg/expr/sql/frame_db_conv.go @@ -187,7 +187,7 @@ func fieldValFromRowVal(fieldType data.FieldType, val interface{}) (interface{}, case data.FieldTypeBool, data.FieldTypeNullableBool: return parseBoolFromInt8(val, nullable) - case data.FieldTypeJSON, data.FieldTypeNullableJSON: + case data.FieldTypeJSON, data.FieldTypeNullableJSON: //nolint:staticcheck switch v := val.(type) { case types.JSONDocument: raw := json.RawMessage(v.String()) diff --git a/pkg/expr/sql/frame_table.go b/pkg/expr/sql/frame_table.go index 245efc6e655..3e24457ef3b 100644 --- a/pkg/expr/sql/frame_table.go +++ b/pkg/expr/sql/frame_table.go @@ -182,7 +182,7 @@ func convertDataType(fieldType data.FieldType) mysql.Type { return types.Boolean case data.FieldTypeTime, data.FieldTypeNullableTime: return types.Timestamp - case data.FieldTypeJSON, data.FieldTypeNullableJSON: + case data.FieldTypeJSON, data.FieldTypeNullableJSON: //nolint:staticcheck return types.JSON default: fmt.Printf("------- Unsupported field type: %v", fieldType) diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 877b00ae2e4..314d19580a4 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -4,7 +4,7 @@ go 1.25.3 require ( github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 - github.com/grafana/grafana-plugin-sdk-go v0.283.0 + github.com/grafana/grafana-plugin-sdk-go v0.284.0 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 diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 066b92e7746..85d212bc772 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -130,8 +130,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.283.0 h1:G7IHshAr30rLWV9FtX3iLlFTTlBhuOkfe7xVAoIP5rE= -github.com/grafana/grafana-plugin-sdk-go v0.283.0/go.mod h1:20qhoYxIgbZRmwCEO1KMP8q2yq/Kge5+xE/99/hLEk0= +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/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= diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index d1fdeac5424..bc670677fb6 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -513,9 +513,9 @@ func doSearchQuery( fName := data.NewFieldFromFieldType(data.FieldTypeString, 0) fURL := data.NewFieldFromFieldType(data.FieldTypeString, 0) fLocation := data.NewFieldFromFieldType(data.FieldTypeString, 0) - fTags := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + fTags := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) //nolint:staticcheck fDSUIDs := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) - fExplain := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + fExplain := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) //nolint:staticcheck fScore.Name = "score" fUID.Name = "uid" diff --git a/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go index 2181d832dcf..94fcad61819 100644 --- a/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go +++ b/pkg/tsdb/azuremonitor/loganalytics/azure-response-table-frame.go @@ -138,7 +138,7 @@ type KeyValue struct { } var tagsConverter = data.FieldConverter{ - OutputFieldType: data.FieldTypeNullableJSON, + OutputFieldType: data.FieldTypeNullableJSON, //nolint:staticcheck Converter: func(v any) (any, error) { if v == nil { return nil, nil diff --git a/pkg/tsdb/azuremonitor/metrics/url-builder.go b/pkg/tsdb/azuremonitor/metrics/url-builder.go index 32b268eeca5..1f3ef37cabd 100644 --- a/pkg/tsdb/azuremonitor/metrics/url-builder.go +++ b/pkg/tsdb/azuremonitor/metrics/url-builder.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" + "github.com/grafana/grafana-plugin-sdk-go/backend" ) // urlBuilder builds the URL for calling the Azure Monitor API @@ -35,7 +35,7 @@ func (params *urlBuilder) buildResourceURI() (*string, error) { if metricNamespace == nil || *metricNamespace == "" { if params.MetricDefinition == nil || *params.MetricDefinition == "" { - return nil, errorsource.DownstreamError(fmt.Errorf("no metricNamespace or metricDefiniton value provided"), false) + return nil, backend.DownstreamError(fmt.Errorf("no metricNamespace or metricDefiniton value provided")) } metricNamespace = params.MetricDefinition } @@ -47,7 +47,7 @@ func (params *urlBuilder) buildResourceURI() (*string, error) { provider = metricNamespaceArray[0] metricNamespaceArray = metricNamespaceArray[1:] } else { - return nil, errorsource.DownstreamError(fmt.Errorf("metricNamespace is not in the correct format"), false) + return nil, backend.DownstreamError(fmt.Errorf("metricNamespace is not in the correct format")) } var resourceNameArray []string @@ -78,7 +78,7 @@ func (params *urlBuilder) buildResourceURI() (*string, error) { if i < len(resourceNameArray) { urlArray = append(urlArray, namespace, resourceNameArray[i]) } else { - return nil, errorsource.DownstreamError(fmt.Errorf("resourceNameArray does not have enough elements"), false) + return nil, backend.DownstreamError(fmt.Errorf("resourceNameArray does not have enough elements")) } } diff --git a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go index 699ee7055a5..a8d01db97d5 100644 --- a/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go +++ b/pkg/tsdb/azuremonitor/resourcegraph/azure-resource-graph-datasource.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -71,7 +70,7 @@ func (e *AzureResourceGraphDatasource) ExecuteTimeSeriesQuery(ctx context.Contex } res, err := e.executeQuery(ctx, graphQuery, dsInfo, client, url) if err != nil { - errorsource.AddErrorToResponse(query.RefID, result, err) + result.Responses[query.RefID] = backend.ErrorResponseWithErrorSource(err) continue } result.Responses[query.RefID] = *res @@ -158,7 +157,7 @@ func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query * res, err := client.Do(req) if err != nil { - return nil, errorsource.DownstreamError(err, false) + return nil, backend.DownstreamError(err) } defer func() { @@ -218,7 +217,11 @@ func (e *AzureResourceGraphDatasource) unmarshalResponse(res *http.Response) (Az }() if res.StatusCode/100 != 2 { - return AzureResourceGraphResponse{}, errorsource.SourceError(backend.ErrorSourceFromHTTPStatus(res.StatusCode), fmt.Errorf("%s. Azure Resource Graph error: %s", res.Status, string(body)), false) + err := fmt.Errorf("%s. Azure Resource Graph error: %s", res.Status, string(body)) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return AzureResourceGraphResponse{}, backend.DownstreamError(err) + } + return AzureResourceGraphResponse{}, backend.PluginError(err) } var data AzureResourceGraphResponse diff --git a/pkg/tsdb/azuremonitor/time/time-grain.go b/pkg/tsdb/azuremonitor/time/time-grain.go index d89e59bbf96..bc8a02fabef 100644 --- a/pkg/tsdb/azuremonitor/time/time-grain.go +++ b/pkg/tsdb/azuremonitor/time/time-grain.go @@ -6,8 +6,8 @@ import ( "strings" "time" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" ) // TimeGrain handles conversions between @@ -27,7 +27,7 @@ func CreateISO8601DurationFromIntervalMS(it int64) (string, error) { timeValueString := formatted[0 : len(formatted)-1] timeValue, err := strconv.Atoi(timeValueString) if err != nil { - return "", errorsource.DownstreamError(fmt.Errorf("could not parse interval %q to an ISO 8061 duration: %w", it, err), false) + return "", backend.DownstreamError(fmt.Errorf("could not parse interval %q to an ISO 8061 duration: %w", it, err)) } unit := formatted[len(formatted)-1:] diff --git a/pkg/tsdb/azuremonitor/utils/utils.go b/pkg/tsdb/azuremonitor/utils/utils.go index fd36558886d..b87ee1a2e98 100644 --- a/pkg/tsdb/azuremonitor/utils/utils.go +++ b/pkg/tsdb/azuremonitor/utils/utils.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) @@ -28,7 +28,7 @@ func GetFirstSubscriptionOrDefault(ctx context.Context, dsInfo types.DatasourceI res, err := dsInfo.Services["Azure Monitor"].HTTPClient.Do(request) if err != nil { - return "", errorsource.DownstreamError(fmt.Errorf("failed to retrieve subscriptions: %v", err), false) + return "", backend.DownstreamError(fmt.Errorf("failed to retrieve subscriptions: %v", err)) } defer func() { if err := res.Body.Close(); err != nil { @@ -42,7 +42,7 @@ func GetFirstSubscriptionOrDefault(ctx context.Context, dsInfo types.DatasourceI } if len(subscriptions) == 0 { - return "", errorsource.DownstreamError(fmt.Errorf("no subscriptions found: %v", err), false) + return "", backend.DownstreamError(fmt.Errorf("no subscriptions found: %v", err)) } return subscriptions[0], nil @@ -73,9 +73,12 @@ func ParseSubscriptions(res *http.Response, logger log.Logger) ([]string, error) } func ApplySourceFromError(errorMessage error, err error) error { - var sourceError errorsource.Error - if errors.As(err, &sourceError) { - return errorsource.SourceError(sourceError.Source(), errorMessage, false) + var errorWithSource backend.ErrorWithSource + if errors.As(err, &errorWithSource) { + if errorWithSource.ErrorSource() == backend.ErrorSourcePlugin { + return backend.PluginError(errorMessage) + } + return backend.DownstreamError(errorMessage) } return errorMessage } diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index 0032796cadf..fe913953ed4 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -355,7 +355,7 @@ func TestProcessLogsResponse(t *testing.T) { // Correctly detects float64 types require.Equal(t, data.FieldTypeNullableFloat64, frame.Fields[7].Type()) // Correctly detects json types - require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[8].Type()) + require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[8].Type()) //nolint:staticcheck // Correctly flattens fields require.Equal(t, "nested.field.double_nested", frame.Fields[13].Name) require.Equal(t, data.FieldTypeNullableString, frame.Fields[13].Type()) @@ -546,7 +546,7 @@ func TestProcessRawDataResponse(t *testing.T) { // Correctly detects float64 types require.Equal(t, data.FieldTypeNullableFloat64, frame.Fields[5].Type()) // Correctly detects json types - require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[6].Type()) + require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[6].Type()) //nolint:staticcheck // Correctly flattens fields require.Equal(t, "nested.field.double_nested", frame.Fields[11].Name) require.Equal(t, data.FieldTypeNullableString, frame.Fields[11].Type()) @@ -736,7 +736,7 @@ func TestProcessRawDocumentResponse(t *testing.T) { require.Len(t, fields, 1) f := fields[0] - require.Equal(t, data.FieldTypeNullableJSON, f.Type()) + require.Equal(t, data.FieldTypeNullableJSON, f.Type()) //nolint:staticcheck require.Equal(t, 2, f.Len()) v := f.At(0).(*json.RawMessage) @@ -829,7 +829,7 @@ func TestProcessRawDocumentResponse(t *testing.T) { //Fields have the correct length require.Equal(t, 2, frame.Fields[0].Len()) // The only field is the raw document - require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[0].Type()) + require.Equal(t, data.FieldTypeNullableJSON, frame.Fields[0].Type()) //nolint:staticcheck require.Equal(t, "A", frame.Fields[0].Name) }) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go index 5a19ca859f4..61784b36e51 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go +++ b/pkg/tsdb/grafana-postgresql-datasource/sqleng/sql_engine.go @@ -368,7 +368,7 @@ func (e *DataSourceHandler) processFrame(frame *data.Frame, qm *dataQueryModel, } var err error - frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) + frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) //nolint:staticcheck if err != nil { logger.Error("Failed to resample dataframe", "err", err) frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) @@ -748,7 +748,7 @@ func getFieldTypesFromDescriptions(fieldDescriptions []pgconn.FieldDescription, case "timestamptz", "timestamp", "date": fieldTypes[i] = data.FieldTypeNullableTime case "json", "jsonb": - fieldTypes[i] = data.FieldTypeNullableJSON + fieldTypes[i] = data.FieldTypeNullableJSON //nolint:staticcheck default: fieldTypes[i] = data.FieldTypeNullableString } diff --git a/pkg/tsdb/graphite/query.go b/pkg/tsdb/graphite/query.go index 2ab60b21592..c06c6d5af08 100644 --- a/pkg/tsdb/graphite/query.go +++ b/pkg/tsdb/graphite/query.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "golang.org/x/net/html" @@ -250,7 +249,11 @@ func (s *Service) parseResponse(res *http.Response) ([]TargetResponseDTO, error) if res.StatusCode/100 != 2 { graphiteError := parseGraphiteError(res.StatusCode, string(body)) s.logger.Info("Request failed", "status", res.Status, "error", graphiteError, "body", string(body)) - return nil, errorsource.SourceError(backend.ErrorSourceFromHTTPStatus(res.StatusCode), fmt.Errorf("request failed with error: %s", graphiteError), false) + err := fmt.Errorf("request failed with error: %s", graphiteError) + if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream { + return nil, backend.DownstreamError(err) + } + return nil, backend.PluginError(err) } var data []TargetResponseDTO diff --git a/pkg/tsdb/influxdb/influxql/converter/converter.go b/pkg/tsdb/influxdb/influxql/converter/converter.go index b4c366bff51..6b338fb3207 100644 --- a/pkg/tsdb/influxdb/influxql/converter/converter.go +++ b/pkg/tsdb/influxdb/influxql/converter/converter.go @@ -151,7 +151,7 @@ func readSeries(iter *sdkjsoniter.Iterator, query *models.Query) *backend.DataRe } if util.GetVisType(query.ResultFormat) != util.TableVisType { for i, v := range valueFields { - if v.Type() == data.FieldTypeNullableJSON { + if v.Type() == data.FieldTypeNullableJSON { //nolint:staticcheck maybeFixValueFieldType(valueFields, data.FieldTypeNullableFloat64, i) } } @@ -189,7 +189,7 @@ func readSeries(iter *sdkjsoniter.Iterator, query *models.Query) *backend.DataRe // also frontend probably will not interpret the nullableJson value for i, f := range rsp.Frames { for j, v := range f.Fields { - if v.Type() == data.FieldTypeNullableJSON { + if v.Type() == data.FieldTypeNullableJSON { //nolint:staticcheck newField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, 0) newField.Name = v.Name newField.Config = v.Config @@ -301,7 +301,7 @@ func readValues(iter *sdkjsoniter.Iterator, hasTimeColumn bool) (valueFields dat // we don't know the type of the values for this field, yet // so we create a FieldTypeNullableJSON to hold nil values // if that is something else it will be replaced later - unknownField := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + unknownField := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) //nolint:staticcheck unknownField.Name = "Value" valueFields = append(valueFields, unknownField) } @@ -335,7 +335,7 @@ func maybeCreateValueField(valueFields data.Fields, expectedType data.FieldType, // or the type of the field in valueFields is nullableJSON // we change the type of the field as expectedType func maybeFixValueFieldType(valueFields data.Fields, expectedType data.FieldType, colIdx int) { - if valueFields[colIdx].Type() == expectedType || valueFields[colIdx].Type() != data.FieldTypeNullableJSON { + if valueFields[colIdx].Type() == expectedType || valueFields[colIdx].Type() != data.FieldTypeNullableJSON { //nolint:staticcheck return } stringField := data.NewFieldFromFieldType(expectedType, 0) @@ -364,7 +364,7 @@ func typeOf(value interface{}) data.FieldType { return data.FieldTypeNullableBool default: slog.Error("unknown influx value type", "value", v) - return data.FieldTypeNullableJSON + return data.FieldTypeNullableJSON //nolint:staticcheck } } @@ -471,7 +471,7 @@ func handleTableFormatValueFields(rsp *backend.DataResponse, valueFields data.Fi } else { ll := valueFields[i].Len() for vi := 0; vi < ll; vi++ { - if valueFields[i].Type() == data.FieldTypeNullableJSON { + if valueFields[i].Type() == data.FieldTypeNullableJSON { //nolint:staticcheck // add nil explicitly. // we don't know if it is a float pointer nil or string pointer nil or etc rsp.Frames[0].Fields[si].Append(nil) diff --git a/pkg/tsdb/influxdb/influxql/converter/converter_test.go b/pkg/tsdb/influxdb/influxql/converter/converter_test.go index 2c5e1deecd5..ac1561afcc2 100644 --- a/pkg/tsdb/influxdb/influxql/converter/converter_test.go +++ b/pkg/tsdb/influxdb/influxql/converter/converter_test.go @@ -38,28 +38,28 @@ func TestMaybeFixValueFieldType(t *testing.T) { }, { name: "should return nullableJson if both are nullableJson", - valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, - inputType: data.FieldTypeNullableJSON, + valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, //nolint:staticcheck + inputType: data.FieldTypeNullableJSON, //nolint:staticcheck colIdx: 0, - expectedType: data.FieldTypeNullableJSON, + expectedType: data.FieldTypeNullableJSON, //nolint:staticcheck }, { name: "should return nullableString if valueField is nullableJson and input is nullableString", - valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, + valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, //nolint:staticcheck inputType: data.FieldTypeNullableString, colIdx: 0, expectedType: data.FieldTypeNullableString, }, { name: "should return nullableBool if valueField is nullableJson and input is nullableBool", - valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, + valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, //nolint:staticcheck inputType: data.FieldTypeNullableBool, colIdx: 0, expectedType: data.FieldTypeNullableBool, }, { name: "should return nullableFloat64 if valueField is nullableJson and input is nullableFloat64", - valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, + valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)}, //nolint:staticcheck inputType: data.FieldTypeNullableFloat64, colIdx: 0, expectedType: data.FieldTypeNullableFloat64, diff --git a/pkg/tsdb/mssql/sqleng/sql_engine.go b/pkg/tsdb/mssql/sqleng/sql_engine.go index 80e3d29927c..3d27b0a9a01 100644 --- a/pkg/tsdb/mssql/sqleng/sql_engine.go +++ b/pkg/tsdb/mssql/sqleng/sql_engine.go @@ -400,7 +400,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG } var err error - frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) + frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) //nolint:staticcheck if err != nil { logger.Error("Failed to resample dataframe", "err", err) frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) diff --git a/pkg/tsdb/mysql/sqleng/sql_engine.go b/pkg/tsdb/mysql/sqleng/sql_engine.go index 350c038b3c0..2d628b5c7f9 100644 --- a/pkg/tsdb/mysql/sqleng/sql_engine.go +++ b/pkg/tsdb/mysql/sqleng/sql_engine.go @@ -357,7 +357,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG } var err error - frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) + frame, err = sqlutil.ResampleWideFrame(frame, qm.FillMissing, alignedTimeRange, qm.Interval) //nolint:staticcheck if err != nil { logger.Error("Failed to resample dataframe", "err", err) frame.AppendNotices(data.Notice{Text: "Failed to resample dataframe", Severity: data.NoticeSeverityWarning}) From a6497cf91ec0973a8b9ff1244eb00a296eab4787 Mon Sep 17 00:00:00 2001 From: Ihor Yeromin Date: Tue, 25 Nov 2025 17:27:45 +0100 Subject: [PATCH 03/94] Expressions: Fix duplicate SQL expression tracking events (#114239) * Expressions: Fix duplicate SQL expression tracking events * fix(tracking): event firing on type select dropdown * chore(queries): wrap handler with useCallback --- .../PanelDataPane/PanelDataQueriesTab.tsx | 19 +++++++++++++++++-- .../expressions/ExpressionQueryEditor.tsx | 17 ----------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index 490084b24ff..cea1dfc6cb0 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -1,7 +1,9 @@ +import { useCallback } from 'react'; + import { CoreApp, DataSourceApi, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; -import { config, getDataSourceSrv } from '@grafana/runtime'; +import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { SceneObjectBase, SceneComponentProps, @@ -337,11 +339,24 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps { + reportInteraction('dashboards_expression_interaction', { + action: 'add_expression', + expression_type: type, + context: 'panel_query_section', + }); + model.onAddExpressionOfType(type); + }, + [model] + ); + if (!datasource || !dsSettings || !data) { return null; } const showAddButton = !isSharedDashboardQuery(dsSettings.name); + const onSelectQueryFromLibrary = async (query: DataQuery) => { // ensure all queries explicitly define a datasource const enrichedQueries = queries.map((q) => @@ -421,7 +436,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps )} {config.expressionsEnabled && model.isExpressionsSupported(dsSettings) && ( - + diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx index e08cdd5d94c..4e3d89cd005 100644 --- a/public/app/features/expressions/ExpressionQueryEditor.tsx +++ b/public/app/features/expressions/ExpressionQueryEditor.tsx @@ -3,7 +3,6 @@ import { useCallback, useEffect, useRef } from 'react'; import { DataSourceApi, FeatureState, GrafanaTheme2, QueryEditorProps } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; import { Button, FeatureBadge, IconButton, InlineField, PopoverContent, useStyles2 } from '@grafana/ui'; import { ClassicConditions } from './components/ClassicConditions'; @@ -97,22 +96,6 @@ export function ExpressionQueryEditor(props: ExpressionQueryEditorProps) { const styles = useStyles2(getStyles); - const initialExpressionRef = useRef(query.expression); - const hasTrackedAddExpression = useRef(false); - - useEffect(() => { - // Only track if 1) query has a type, and 2) we haven't tracked yet for this component instance, and - // 3) initial expression was empty (indicating a new expression, not editing existing) - if (query.type && !hasTrackedAddExpression.current && !initialExpressionRef.current) { - reportInteraction('dashboards_expression_interaction', { - action: 'add_expression', - expression_type: query.type, - context: 'panel_query_section', - }); - hasTrackedAddExpression.current = true; - } - }, [query.type, query.refId]); - useEffect(() => { setCachedExpression(query.type, query.expression); }, [query.expression, query.type, setCachedExpression]); From ccba9fd70bf9207ca320f4880a850a58eff478e8 Mon Sep 17 00:00:00 2001 From: Renato Costa <103441181+renatolabs@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:33:06 -0500 Subject: [PATCH 04/94] chore: print wire file diff in `Check Wire Changes` workflow (#114417) --- .github/workflows/pr-go-workspace-check.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/pr-go-workspace-check.yml b/.github/workflows/pr-go-workspace-check.yml index c90573d6f4e..fd08206f0b5 100644 --- a/.github/workflows/pr-go-workspace-check.yml +++ b/.github/workflows/pr-go-workspace-check.yml @@ -107,6 +107,7 @@ jobs: CURRENT_OSS_WIRE_CHECKSUM=$(sha256sum pkg/server/wire_gen.go | cut -d' ' -f1) if [ "$CURRENT_OSS_WIRE_CHECKSUM" != "${{ steps.pre_gen_oss.outputs.wire_checksum }}" ]; then OSS_WIRE_CHANGED=true + OSS_DIFF=$(git diff pkg/server/wire_gen.go) echo "Uncomitted changes detected in pkg/server/wire_gen.go" fi @@ -115,6 +116,7 @@ jobs: CURRENT_ENTERPRISE_WIRE_CHECKSUM=$(sha256sum pkg/server/enterprise_wire_gen.go | cut -d' ' -f1) if [ "$CURRENT_ENTERPRISE_WIRE_CHECKSUM" != "${{ steps.pre_gen_enterprise.outputs.wire_checksum }}" ]; then ENTERPRISE_WIRE_CHANGED=true + ENTERPRISE_DIFF=$(git diff pkg/server/enterprise_wire_gen.go) echo "Uncomitted changes detected in pkg/server/enterprise_wire_gen.go" fi fi @@ -124,6 +126,14 @@ jobs: else if [[ "${{ github.event.pull_request.head.repo.fork }}" == "false" ]]; then echo "> !!! Please synchronize the grafana OSS and grafana enterprise code bases as defined in the enterprise readme, then run 'make gen-go' in the OSS folder and commit the changes to both repositories." + if [[ "$OSS_WIRE_CHANGED" == "true" ]]; then + echo "OSS changes:" + echo "$OSS_DIFF" + fi + if [[ "$ENTERPRISE_WIRE_CHANGED" == "true" ]]; then + echo "Enterprise changes:" + echo "$ENTERPRISE_DIFF" + fi else echo "> !!! Please run 'make gen-go' and commit the changes." fi From 6e0093f048a8840c307985db74f37254de8d734d Mon Sep 17 00:00:00 2001 From: Renato Costa <103441181+renatolabs@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:38:26 -0500 Subject: [PATCH 05/94] fix: update search request for existing provisioned dashboards in modes 3+ (#114412) Fix search for existing provisioned dashboards in modes 3+ The search query was not requesting the dashboard's "legacy ID". As a result, the provisioning process would not find existing provisioned dashboards, making copies of these dashboards every time there was a change in the provisioned dashboard's definition. --- .../dashboards/service/dashboard_service.go | 9 ++++++++- .../service/dashboard_service_test.go | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index b63b4a40f96..6ebeb537257 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -2009,7 +2009,14 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.Limit = query.Limit request.Page = query.Page request.Offset = (query.Page - 1) * query.Limit // only relevant when running in modes 3+ - request.Fields = dashboardsearch.IncludeFields + request.Fields = append( + dashboardsearch.IncludeFields, + // Include the dashboard legacy ID in the results, as it is needed when + // determining whether a provisioned dashboard exists or not, see + // `(*DashboardServiceImpl).searchProvisionedDashboardsThroughK8s`. + resource.SEARCH_FIELD_LEGACY_ID, + resource.SEARCH_FIELD_LABELS+"."+resource.SEARCH_FIELD_LEGACY_ID, + ) namespace := dr.k8sclient.GetNamespace(query.OrgId) var err error diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 5725cdf193b..35ac47afd9a 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -2016,6 +2016,26 @@ func TestSearchDashboardsThroughK8sRaw(t *testing.T) { _, err := service.searchDashboardsThroughK8s(ctx, query) require.NoError(t, err) }) + + t.Run("search will request legacy dashboard ID", func(t *testing.T) { + ctx := context.Background() + k8sCliMock := new(client.MockK8sHandler) + service := &DashboardServiceImpl{k8sclient: k8sCliMock} + query := &dashboards.FindPersistedDashboardsQuery{ + ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck + OrgId: 1, + } + k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") + k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool { + return slices.Contains(req.Fields, "grafana.app/deprecatedInternalID") && + slices.Contains(req.Fields, "labels.grafana.app/deprecatedInternalID") + })).Return(&resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{}, + TotalHits: 0, + }, nil) + _, err := service.searchDashboardsThroughK8s(ctx, query) + require.NoError(t, err) + }) } func TestSearchProvisionedDashboardsThroughK8sRaw(t *testing.T) { From da374527f2118154bb347132edac62384fe7f552 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Tue, 25 Nov 2025 16:34:10 -0300 Subject: [PATCH 06/94] ShortURL: K8s Implement custom authorizer (#114192) --- apps/shorturl/go.mod | 2 +- apps/shorturl/pkg/app/authorizer.go | 20 ++++++++++++++++++++ pkg/registry/apps/shorturl/register.go | 5 +++++ pkg/tests/api/shorturl/short_url_test.go | 2 +- pkg/tests/apis/shorturl/shorturl_test.go | 16 ++++++++-------- 5 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 apps/shorturl/pkg/app/authorizer.go diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index b3db27ba205..740aea32a72 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -7,6 +7,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.48.1 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.2 + k8s.io/apiserver v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -90,7 +91,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.2 // indirect k8s.io/apiextensions-apiserver v0.34.2 // indirect - k8s.io/apiserver v0.34.2 // indirect k8s.io/client-go v0.34.2 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/apps/shorturl/pkg/app/authorizer.go b/apps/shorturl/pkg/app/authorizer.go new file mode 100644 index 00000000000..4cde905414d --- /dev/null +++ b/apps/shorturl/pkg/app/authorizer.go @@ -0,0 +1,20 @@ +package app + +import ( + "context" + + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func GetAuthorizer() authorizer.Authorizer { + return 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 + }) +} diff --git a/pkg/registry/apps/shorturl/register.go b/pkg/registry/apps/shorturl/register.go index c5d314ff2bc..e15950dbc56 100644 --- a/pkg/registry/apps/shorturl/register.go +++ b/pkg/registry/apps/shorturl/register.go @@ -5,6 +5,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/rest" restclient "k8s.io/client-go/rest" @@ -55,6 +56,10 @@ func RegisterAppInstaller( return installer, nil } +func (a *ShortURLAppInstaller) GetAuthorizer() authorizer.Authorizer { + return shorturlapp.GetAuthorizer() +} + func (s *ShortURLAppInstaller) GetLegacyStorage(requested schema.GroupVersionResource) grafanarest.Storage { gvr := shorturl.ShortURLKind().GroupVersionResource() if requested.String() != gvr.String() { diff --git a/pkg/tests/api/shorturl/short_url_test.go b/pkg/tests/api/shorturl/short_url_test.go index cc014662457..e3c4d10c675 100644 --- a/pkg/tests/api/shorturl/short_url_test.go +++ b/pkg/tests/api/shorturl/short_url_test.go @@ -42,7 +42,7 @@ func TestShortURL(t *testing.T) { // Test that the endpoint is accessible with authentication. username, password := "viewer", "viewer" createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleEditor), + DefaultOrgRole: string(org.RoleNone), Password: user.Password(password), Login: username, }) diff --git a/pkg/tests/apis/shorturl/shorturl_test.go b/pkg/tests/apis/shorturl/shorturl_test.go index be7023c0c6a..becb0b2b564 100644 --- a/pkg/tests/apis/shorturl/shorturl_test.go +++ b/pkg/tests/apis/shorturl/shorturl_test.go @@ -123,7 +123,7 @@ func TestIntegrationShortURL(t *testing.T) { // Only legacy API should be used, no K8s API interaction func doLegacyOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -189,7 +189,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest t.Run("Legacy API -> K8s API visibility", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -223,7 +223,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest t.Run("K8s API -> Legacy API visibility", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -259,7 +259,7 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest t.Run("Redirect functionality", func(t *testing.T) { t.Skip("Skipping redirect functionality tests for now - flaky test") client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -312,7 +312,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { t.Run("K8s API CRUD (unified storage only)", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -349,7 +349,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { t.Run("K8s API validation - invalid paths", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -402,7 +402,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { t.Run("K8s API validation - valid edge cases", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) @@ -441,7 +441,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) { t.Run("Redirect functionality (unified only)", func(t *testing.T) { client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Editor, + User: helper.Org1.None, GVR: gvr, }) From 235ebbf5c5b4a4b747c3d68d81c9d0c536864733 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 25 Nov 2025 13:27:38 -0700 Subject: [PATCH 07/94] Dashboards: Prevent panic in validation (#114436) --- apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go | 3 +++ apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go index 7c5573f600a..a90eab95126 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/validation.go @@ -66,6 +66,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis } func formatErrorPath(path []string) string { + if len(path) <= 4 { + return strings.Join(path, ".") + } // omitting the "lineage.schemas[0].schema.spec" prefix here. return strings.Join(path[4:], ".") } diff --git a/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go b/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go index b48e38ffd7b..248dc27111c 100644 --- a/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go +++ b/apps/dashboard/pkg/apis/dashboard/v1beta1/validation.go @@ -67,6 +67,9 @@ func ValidateDashboardSpec(obj *Dashboard, forceValidation bool) (field.ErrorLis } func formatErrorPath(path []string) string { + if len(path) <= 4 { + return strings.Join(path, ".") + } // omitting the "lineage.schemas[0].schema.spec" prefix here. return strings.Join(path[4:], ".") } From fee3f12e4eb30ae3b4b91191a91fa7d976a2f501 Mon Sep 17 00:00:00 2001 From: Bryan Huhta <32787160+bryanhuhta@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:19:29 -0700 Subject: [PATCH 08/94] grafana-flamegraph: Add storybook stories for major components (#113375) --- .gitignore | 1 + .../.storybook/copyAssets.ts | 66 +++ .../grafana-flamegraph/.storybook/main.ts | 53 ++ .../grafana-flamegraph/.storybook/preview.ts | 54 ++ packages/grafana-flamegraph/package.json | 9 +- .../src/FlameGraph/FlameGraph.story.tsx | 31 + .../src/FlameGraph/testData/dataBasic.ts | 32 ++ .../src/FlameGraphContainer.story.tsx | 23 + .../FlameGraphTopTableContainer.story.tsx | 41 ++ .../TopTable/FlameGraphTopTableContainer.tsx | 1 + .../src/utils/storybook/withTheme.tsx | 36 ++ .../src/utils/storybook/withTimeZone.tsx | 12 + yarn.lock | 534 ++++++++++++++++-- 13 files changed, 850 insertions(+), 43 deletions(-) create mode 100644 packages/grafana-flamegraph/.storybook/copyAssets.ts create mode 100644 packages/grafana-flamegraph/.storybook/main.ts create mode 100644 packages/grafana-flamegraph/.storybook/preview.ts create mode 100644 packages/grafana-flamegraph/src/FlameGraph/FlameGraph.story.tsx create mode 100644 packages/grafana-flamegraph/src/FlameGraph/testData/dataBasic.ts create mode 100644 packages/grafana-flamegraph/src/FlameGraphContainer.story.tsx create mode 100644 packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.story.tsx create mode 100644 packages/grafana-flamegraph/src/utils/storybook/withTheme.tsx create mode 100644 packages/grafana-flamegraph/src/utils/storybook/withTimeZone.tsx diff --git a/.gitignore b/.gitignore index d0e233d6a42..f1a90d05693 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ pkg/services/quota/quotaimpl/storage/storage.json /packages/grafana-ui/.yarn/.cache /packages/grafana-ui/.storybook/static /packages/grafana-ui/unstable +/packages/grafana-flamegraph/.storybook/static /packages/**/dist /packages/**/compiled /packages/**/.rpt2_cache diff --git a/packages/grafana-flamegraph/.storybook/copyAssets.ts b/packages/grafana-flamegraph/.storybook/copyAssets.ts new file mode 100644 index 00000000000..ab357f9b72c --- /dev/null +++ b/packages/grafana-flamegraph/.storybook/copyAssets.ts @@ -0,0 +1,66 @@ +// This script is used to copy assets from the public folder to a temporary static folder within +// the .storybook directory. +// We selectively limit assets that are uploaded to the Storybook bucket to prevent rate limiting +// when publishing new storybook. +// Note: Storybook has a static copying feature but it copies entire directories which can contain thousands of icons. + +import fs from 'fs-extra'; +import { resolve } from 'node:path'; + +// avoid importing from @grafana/data to prevent error: window is not defined +import { availableIconsIndex, IconName } from '../../grafana-data/src/types/icon.ts'; +import { getIconSubDir } from '../../grafana-ui/src/components/Icon/utils.ts'; + +const __dirname = import.meta.dirname; + +// doesn't require uploading 1000s of unused assets. +const iconPaths = Object.keys(availableIconsIndex) + .filter((iconName) => !iconName.startsWith('fa ')) + .map((iconName) => { + const subDir = getIconSubDir(iconName as IconName, 'default'); + return { + from: `../../../public/img/icons/${subDir}/${iconName}.svg`, + to: `./static/public/build/img/icons/${subDir}/${iconName}.svg`, + }; + }); + +export function copyAssetsSync() { + const assets = [ + { + from: '../../../public/fonts', + to: './static/public/fonts', + }, + { + from: '../../../public/img/grafana_text_logo-dark.svg', + to: './static/public/img/grafana_text_logo-dark.svg', + }, + { + from: '../../../public/img/grafana_text_logo-light.svg', + to: './static/public/img/grafana_text_logo-light.svg', + }, + { + from: '../../../public/img/fav32.png', + to: './static/public/img/fav32.png', + }, + { + from: '../../../public/lib', + to: './static/public/lib', + }, + ...iconPaths, + // copy over the MSW mock service worker so we can mock requests in Storybook + { + from: '../../../public/mockServiceWorker.js', + to: './static/mockServiceWorker.js', + }, + ]; + + for (const asset of assets) { + const fromPath = resolve(__dirname, asset.from); + const toPath = resolve(__dirname, asset.to); + if (!fs.existsSync(toPath)) { + fs.copySync(fromPath, toPath, { + filter: (src) => !fs.lstatSync(src).isSymbolicLink(), + }); + } + } +} diff --git a/packages/grafana-flamegraph/.storybook/main.ts b/packages/grafana-flamegraph/.storybook/main.ts new file mode 100644 index 00000000000..5f950843923 --- /dev/null +++ b/packages/grafana-flamegraph/.storybook/main.ts @@ -0,0 +1,53 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; +import { copyAssetsSync } from './copyAssets.ts'; + +import { createRequire } from 'node:module'; +const require = createRequire(import.meta.url); + +copyAssetsSync(); + +const config: StorybookConfig = { + stories: ['../src/**/*.story.tsx'], + addons: [ + { + name: '@storybook/preset-scss', + options: { + styleLoaderOptions: { + // this is required for theme switching .use() and .unuse() + injectType: 'lazyStyleTag', + }, + cssLoaderOptions: { + url: false, + importLoaders: 2, + }, + sassLoaderOptions: { + sassOptions: { + // silencing these warnings since we're planning to remove sass when angular is gone + silenceDeprecations: ['import', 'global-builtin'], + }, + }, + }, + }, + '@storybook/addon-webpack5-compiler-swc', + ], + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + staticDirs: ['static'], + webpackFinal: async (config) => { + const webpack = await import('webpack'); + + // Define process.env for browser context + config.plugins?.push( + new webpack.default.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.STORYBOOK_THEME': JSON.stringify(process.env.STORYBOOK_THEME || 'system'), + }) + ); + + return config; + }, +}; + +export default config; diff --git a/packages/grafana-flamegraph/.storybook/preview.ts b/packages/grafana-flamegraph/.storybook/preview.ts new file mode 100644 index 00000000000..00d08fccc24 --- /dev/null +++ b/packages/grafana-flamegraph/.storybook/preview.ts @@ -0,0 +1,54 @@ +import { Preview } from '@storybook/react'; + +import { getBuiltInThemes, getTimeZone, getTimeZones, GrafanaTheme2 } from '@grafana/data'; + +import { withTheme } from '../src/utils/storybook/withTheme'; +import { withTimeZone } from '../src/utils/storybook/withTimeZone'; + +const allowedExtraThemes: string[] = []; + +if (process.env.NODE_ENV === 'development') { + allowedExtraThemes.push('debug'); + allowedExtraThemes.push('desertbloom'); + allowedExtraThemes.push('gildedgrove'); + allowedExtraThemes.push('gloom'); + allowedExtraThemes.push('sapphiredusk'); + allowedExtraThemes.push('tron'); +} + +const preview: Preview = { + decorators: [withTheme(), withTimeZone()], + globalTypes: { + theme: { + name: 'Theme', + description: 'Global theme for components', + defaultValue: 'system', + toolbar: { + icon: 'paintbrush', + items: getBuiltInThemes(allowedExtraThemes).map((theme) => ({ + value: theme.id, + title: theme.name, + })), + showName: true, + }, + }, + timeZone: { + description: 'Set the timezone for the storybook preview', + defaultValue: getTimeZone(), + toolbar: { + icon: 'globe', + items: getTimeZones(true) + .filter((timezone) => !!timezone) + .map((timezone) => ({ + title: timezone, + value: timezone, + })), + }, + }, + }, + initialGlobals: { + theme: process.env.STORYBOOK_THEME || 'system', + }, +}; + +export default preview; diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 3e1c2347cd1..f4ec11c7cfa 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -36,7 +36,8 @@ "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-npm-package.js", - "postpack": "mv package.json.bak package.json" + "postpack": "mv package.json.bak package.json", + "storybook": "storybook dev -p 6006" }, "browserslist": [ "defaults", @@ -60,11 +61,15 @@ "@babel/preset-env": "7.28.0", "@babel/preset-react": "7.27.1", "@rollup/plugin-node-resolve": "16.0.1", + "@storybook/addon-webpack5-compiler-swc": "^4.0.2", + "@storybook/react": "^10.0.8", + "@storybook/react-webpack5": "^10.0.8", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", "@types/d3": "^7", + "@types/fs-extra": "^11", "@types/jest": "^29.5.4", "@types/lodash": "4.17.20", "@types/node": "24.10.1", @@ -73,11 +78,13 @@ "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", "esbuild": "0.25.8", + "fs-extra": "^11.3.2", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", "rollup": "^4.22.4", "rollup-plugin-esbuild": "6.2.1", "rollup-plugin-node-externals": "^8.0.0", + "storybook": "^10.0.8", "ts-jest": "29.4.0", "ts-node": "10.9.2", "typescript": "5.9.2" diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.story.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.story.tsx new file mode 100644 index 00000000000..fdd0cd08074 --- /dev/null +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.story.tsx @@ -0,0 +1,31 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { createDataFrame } from '@grafana/data'; + +import { ColorScheme, SelectedView } from '../types'; + +import FlameGraph from './FlameGraph'; +import { CollapsedMap, FlameGraphDataContainer } from './dataTransform'; +import { data } from './testData/dataBasic'; + +const meta: Meta = { + title: 'FlameGraph', + component: FlameGraph, + args: { + rangeMin: 0, + rangeMax: 1, + textAlign: 'left', + colorScheme: ColorScheme.PackageBased, + selectedView: SelectedView.Both, + search: '', + }, +}; + +export default meta; +export const Basic: StoryObj = { + render: (args) => { + const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: false }); + + return ; + }, +}; diff --git a/packages/grafana-flamegraph/src/FlameGraph/testData/dataBasic.ts b/packages/grafana-flamegraph/src/FlameGraph/testData/dataBasic.ts new file mode 100644 index 00000000000..0d582dfa0b7 --- /dev/null +++ b/packages/grafana-flamegraph/src/FlameGraph/testData/dataBasic.ts @@ -0,0 +1,32 @@ +import { DataFrameDTO } from '@grafana/data'; + +export const data: DataFrameDTO = { + name: 'response', + refId: 'A', + // @ts-ignore + meta: { preferredVisualisationType: 'flamegraph' }, + fields: [ + { + name: 'level', + values: [0, 1, 2, 3, 2, 1], + }, + { + name: 'value', + values: [10000, 5000, 4000, 3000, 500, 3000], + config: { + unit: 'ms', + }, + }, + { + name: 'self', + values: [10000, 500, 1000, 3000, 500, 3000], + config: { + unit: 'ms', + }, + }, + { + name: 'label', + values: ['total', 'fn_1', 'fn_2', 'fn_3', 'fn_4', 'fn_5'], + }, + ], +}; diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.story.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.story.tsx new file mode 100644 index 00000000000..67fadcb0cac --- /dev/null +++ b/packages/grafana-flamegraph/src/FlameGraphContainer.story.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { createDataFrame } from '@grafana/data'; +import { useTheme2 } from '@grafana/ui'; + +import { data } from './FlameGraph/testData/dataNestedSet'; +import FlameGraphContainer, { Props } from './FlameGraphContainer'; + +const WrappedFlameGraph = (props: Omit) => { + const theme = useTheme2(); + const df = createDataFrame(data); + return theme} />; +}; + +const meta: Meta = { + title: 'FlameGraphContainer', + render: (args) => { + return ; + }, +}; + +export default meta; +export const Basic: StoryObj = {}; diff --git a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.story.tsx b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.story.tsx new file mode 100644 index 00000000000..37eb7bd4c4e --- /dev/null +++ b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.story.tsx @@ -0,0 +1,41 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { createDataFrame } from '@grafana/data'; + +import { FlameGraphDataContainer } from '../FlameGraph/dataTransform'; +import { data } from '../FlameGraph/testData/dataNestedSet'; +import { ColorScheme } from '../types'; + +import FlameGraphTopTableContainer from './FlameGraphTopTableContainer'; + +const meta: Meta = { + title: 'TopTable', + component: FlameGraphTopTableContainer, + args: { + colorScheme: ColorScheme.ValueBased, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +export const Basic: StoryObj = { + render: (args) => { + const dataContainer = new FlameGraphDataContainer(createDataFrame(data), { collapsing: true }); + + return ( + {}} + onSearch={() => {}} + onSandwich={() => {}} + /> + ); + }, +}; diff --git a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx index b9072f42c29..dc70b7c5690 100644 --- a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx +++ b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.tsx @@ -144,6 +144,7 @@ function buildFilteredTable(data: FlameGraphDataContainer, matchedLabels?: Set) => { + const theme = getThemeById(themeId); + + const css = ` + #storybook-root { + padding: ${theme.spacing(2)}; + } + + body { + background: ${theme.colors.background.primary}; + } + `; + + return ( + + + + + {children} + + ); +}; + +export const withTheme = + (): Decorator => + // eslint-disable-next-line react/display-name + (story, context) => {story()}; diff --git a/packages/grafana-flamegraph/src/utils/storybook/withTimeZone.tsx b/packages/grafana-flamegraph/src/utils/storybook/withTimeZone.tsx new file mode 100644 index 00000000000..27445b3c24e --- /dev/null +++ b/packages/grafana-flamegraph/src/utils/storybook/withTimeZone.tsx @@ -0,0 +1,12 @@ +import { Decorator } from '@storybook/react'; +import { useEffect } from 'react'; + +import { setTimeZoneResolver } from '@grafana/data'; + +export const withTimeZone = (): Decorator => (Story, context) => { + useEffect(() => { + setTimeZoneResolver(() => context.globals.timeZone ?? 'browser'); + }, [context.globals.timeZone]); + + return Story(); +}; diff --git a/yarn.lock b/yarn.lock index 131a46fe1fc..2022568eb8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1473,7 +1473,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": version: 7.28.4 resolution: "@babel/runtime@npm:7.28.4" checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 @@ -2324,15 +2324,15 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.3.4": - version: 2.3.4 - resolution: "@formatjs/ecma402-abstract@npm:2.3.4" +"@formatjs/ecma402-abstract@npm:2.3.6": + version: 2.3.6 + resolution: "@formatjs/ecma402-abstract@npm:2.3.6" dependencies: "@formatjs/fast-memoize": "npm:2.2.7" - "@formatjs/intl-localematcher": "npm:0.6.1" + "@formatjs/intl-localematcher": "npm:0.6.2" decimal.js: "npm:^10.4.3" tslib: "npm:^2.8.0" - checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2 + checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc languageName: node linkType: hard @@ -2376,13 +2376,13 @@ __metadata: linkType: hard "@formatjs/intl-durationformat@npm:^0.7.0": - version: 0.7.4 - resolution: "@formatjs/intl-durationformat@npm:0.7.4" + version: 0.7.6 + resolution: "@formatjs/intl-durationformat@npm:0.7.6" dependencies: - "@formatjs/ecma402-abstract": "npm:2.3.4" - "@formatjs/intl-localematcher": "npm:0.6.1" + "@formatjs/ecma402-abstract": "npm:2.3.6" + "@formatjs/intl-localematcher": "npm:0.6.2" tslib: "npm:^2.8.0" - checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15 + checksum: 10/442236ba85bcd9cb7296c43a708271fa09f110b1ca9d5899066d00812fc2965eaeaec6b5240be421b80daba62860352131088449ba0fcd2061f671cec6240f0b languageName: node linkType: hard @@ -2395,12 +2395,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.6.1": - version: 0.6.1 - resolution: "@formatjs/intl-localematcher@npm:0.6.1" +"@formatjs/intl-localematcher@npm:0.6.2": + version: 0.6.2 + resolution: "@formatjs/intl-localematcher@npm:0.6.2" dependencies: tslib: "npm:^2.8.0" - checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998 + checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064 languageName: node linkType: hard @@ -3260,11 +3260,15 @@ __metadata: "@grafana/ui": "npm:12.4.0-pre" "@leeoniya/ufuzzy": "npm:1.0.19" "@rollup/plugin-node-resolve": "npm:16.0.1" + "@storybook/addon-webpack5-compiler-swc": "npm:^4.0.2" + "@storybook/react": "npm:^10.0.8" + "@storybook/react-webpack5": "npm:^10.0.8" "@testing-library/dom": "npm:10.4.1" "@testing-library/jest-dom": "npm:^6.1.2" "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" "@types/d3": "npm:^7" + "@types/fs-extra": "npm:^11" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.20" "@types/node": "npm:24.10.1" @@ -3274,6 +3278,7 @@ __metadata: babel-jest: "npm:29.7.0" d3: "npm:^7.8.5" esbuild: "npm:0.25.8" + fs-extra: "npm:^11.3.2" jest: "npm:^29.6.4" jest-canvas-mock: "npm:2.5.2" lodash: "npm:4.17.21" @@ -3283,6 +3288,7 @@ __metadata: rollup: "npm:^4.22.4" rollup-plugin-esbuild: "npm:6.2.1" rollup-plugin-node-externals: "npm:^8.0.0" + storybook: "npm:^10.0.8" tinycolor2: "npm:1.6.0" ts-jest: "npm:29.4.0" ts-node: "npm:10.9.2" @@ -4245,7 +4251,7 @@ __metadata: languageName: node linkType: hard -"@inquirer/type@npm:^3.0.10, @inquirer/type@npm:^3.0.9": +"@inquirer/type@npm:^3.0.10": version: 3.0.10 resolution: "@inquirer/type@npm:3.0.10" peerDependencies: @@ -4257,6 +4263,18 @@ __metadata: languageName: node linkType: hard +"@inquirer/type@npm:^3.0.9": + version: 3.0.9 + resolution: "@inquirer/type@npm:3.0.9" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/960ba4737405f70bac17e7cdc4696c60064b06c8dd13a4b3d0783763ba1714bdadbd598b88d537ab9415b7d5d61e011ac042cfbd1438b2a35298e2868724b853 + languageName: node + linkType: hard + "@internationalized/date@npm:^3.10.0": version: 3.10.0 resolution: "@internationalized/date@npm:3.10.0" @@ -5027,6 +5045,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -6096,9 +6121,9 @@ __metadata: linkType: hard "@openfeature/core@npm:^1.9.0": - version: 1.9.0 - resolution: "@openfeature/core@npm:1.9.0" - checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094 + version: 1.9.1 + resolution: "@openfeature/core@npm:1.9.1" + checksum: 10/6099e16b1b4cd6e3c45c05ab4acd44c9cb4ab501b676ab6f3e77f0be1b56abc7506c5629187381333a02975d315d831e0905582435b356d9676255e72a465899 languageName: node linkType: hard @@ -6123,11 +6148,11 @@ __metadata: linkType: hard "@openfeature/web-sdk@npm:^1.6.1": - version: 1.6.1 - resolution: "@openfeature/web-sdk@npm:1.6.1" + version: 1.7.1 + resolution: "@openfeature/web-sdk@npm:1.7.1" peerDependencies: "@openfeature/core": ^1.9.0 - checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d + checksum: 10/358cabfddda8bf67a9bd96ce28f0f84a3218416907f8df4f2a58750ee840d164d86a1c859e8c9aa92d132fa28fcf72a0f17ceece798ff8af297d3e1c1e30cdab languageName: node linkType: hard @@ -7943,6 +7968,18 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-webpack5-compiler-swc@npm:^4.0.2": + version: 4.0.2 + resolution: "@storybook/addon-webpack5-compiler-swc@npm:4.0.2" + dependencies: + "@swc/core": "npm:^1.13.5" + swc-loader: "npm:^0.2.6" + peerDependencies: + storybook: ^9.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 + checksum: 10/1383346b97d9f679ad830751dd1c94c8ab1c6aeb141feda60dcc7ef2ac69e97c3fb3a2a2a3f020e07598dea315760cb0c079f928f29faaf12f143d627487b0e1 + languageName: node + linkType: hard + "@storybook/blocks@npm:8.6.2, @storybook/blocks@npm:^8.6.2": version: 8.6.2 resolution: "@storybook/blocks@npm:8.6.2" @@ -7962,6 +7999,34 @@ __metadata: languageName: node linkType: hard +"@storybook/builder-webpack5@npm:10.0.8": + version: 10.0.8 + resolution: "@storybook/builder-webpack5@npm:10.0.8" + dependencies: + "@storybook/core-webpack": "npm:10.0.8" + case-sensitive-paths-webpack-plugin: "npm:^2.4.0" + cjs-module-lexer: "npm:^1.2.3" + css-loader: "npm:^7.1.2" + es-module-lexer: "npm:^1.5.0" + fork-ts-checker-webpack-plugin: "npm:^9.1.0" + html-webpack-plugin: "npm:^5.5.0" + magic-string: "npm:^0.30.5" + style-loader: "npm:^4.0.0" + terser-webpack-plugin: "npm:^5.3.14" + ts-dedent: "npm:^2.0.0" + webpack: "npm:5" + webpack-dev-middleware: "npm:^6.1.2" + webpack-hot-middleware: "npm:^2.25.1" + webpack-virtual-modules: "npm:^0.6.0" + peerDependencies: + storybook: ^10.0.8 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/136ad58f58a3595d443fa4bd3a5dd9cca30a52a59c3a34e82d1b32b1be82e292b02d3c4b74204b1e5d92c3bab3de86a164ae69f19671a52408da5cc14f550720 + languageName: node + linkType: hard + "@storybook/builder-webpack5@npm:8.6.2": version: 8.6.2 resolution: "@storybook/builder-webpack5@npm:8.6.2" @@ -8017,6 +8082,17 @@ __metadata: languageName: node linkType: hard +"@storybook/core-webpack@npm:10.0.8": + version: 10.0.8 + resolution: "@storybook/core-webpack@npm:10.0.8" + dependencies: + ts-dedent: "npm:^2.0.0" + peerDependencies: + storybook: ^10.0.8 + checksum: 10/b71baea4b0c929d1173c39bf287be94fd2862c91cf0945bed62331a4ed2b6f96dc9273b1d8b229e885cfd260d0cac7d7a8a86582a20d95c5bf27669be198752e + languageName: node + linkType: hard + "@storybook/core-webpack@npm:8.6.2": version: 8.6.2 resolution: "@storybook/core-webpack@npm:8.6.2" @@ -8080,6 +8156,16 @@ __metadata: languageName: node linkType: hard +"@storybook/icons@npm:^1.6.0": + version: 1.6.0 + resolution: "@storybook/icons@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + checksum: 10/f9036ca3b0d2904778eb4e202305f2780b549434380f9760f0bc704fe3ee19d7332f9560a66435ebb2156346cee9a863e40fa5e4b27790bf993b0c1180a3146d + languageName: node + linkType: hard + "@storybook/instrumenter@npm:8.6.2": version: 8.6.2 resolution: "@storybook/instrumenter@npm:8.6.2" @@ -8108,6 +8194,30 @@ __metadata: languageName: node linkType: hard +"@storybook/preset-react-webpack@npm:10.0.8": + version: 10.0.8 + resolution: "@storybook/preset-react-webpack@npm:10.0.8" + dependencies: + "@storybook/core-webpack": "npm:10.0.8" + "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" + "@types/semver": "npm:^7.3.4" + magic-string: "npm:^0.30.5" + react-docgen: "npm:^7.1.1" + resolve: "npm:^1.22.8" + semver: "npm:^7.3.7" + tsconfig-paths: "npm:^4.2.0" + webpack: "npm:5" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.0.8 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b361b27b1550db1f03888fb06596cee35e1b6724aa41eafc23e47b28623d64c5ec1d33d0120f0280e49064a573144ccf8de114014f1d2f23730045be0ac86773 + languageName: node + linkType: hard + "@storybook/preset-react-webpack@npm:8.6.2": version: 8.6.2 resolution: "@storybook/preset-react-webpack@npm:8.6.2" @@ -8172,6 +8282,17 @@ __metadata: languageName: node linkType: hard +"@storybook/react-dom-shim@npm:10.0.8": + version: 10.0.8 + resolution: "@storybook/react-dom-shim@npm:10.0.8" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.0.8 + checksum: 10/99106afb92644ce801c0c24923e40d78d7ed935b9f8f10ceb1a1a827d777d294c37ecbdf793148747dcb4e735a34d876c691b6bac82c7058deef9ad451147dd1 + languageName: node + linkType: hard + "@storybook/react-dom-shim@npm:8.6.2": version: 8.6.2 resolution: "@storybook/react-dom-shim@npm:8.6.2" @@ -8183,6 +8304,25 @@ __metadata: languageName: node linkType: hard +"@storybook/react-webpack5@npm:^10.0.8": + version: 10.0.8 + resolution: "@storybook/react-webpack5@npm:10.0.8" + dependencies: + "@storybook/builder-webpack5": "npm:10.0.8" + "@storybook/preset-react-webpack": "npm:10.0.8" + "@storybook/react": "npm:10.0.8" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.0.8 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/384b1261a9a94c4399109da0121c0070b8138a1a1ce7d1a39bab43fe5c4898144ffacb22308d6d7101677f5c4b70ffe0001b41054cc675ffe8d0fa7c84e9ed24 + languageName: node + linkType: hard + "@storybook/react-webpack5@npm:^8.6.2": version: 8.6.2 resolution: "@storybook/react-webpack5@npm:8.6.2" @@ -8202,6 +8342,24 @@ __metadata: languageName: node linkType: hard +"@storybook/react@npm:10.0.8, @storybook/react@npm:^10.0.8": + version: 10.0.8 + resolution: "@storybook/react@npm:10.0.8" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/react-dom-shim": "npm:10.0.8" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.0.8 + typescript: ">= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/cbb46246958a0d2db0cc641661a14e5ae20b08ee4d2626e6d39ae4332053b3c6d84bdb77e36bc839ae7dd770504049f187466383ee06aac33bc2ef5a1c69b78f + languageName: node + linkType: hard + "@storybook/react@npm:8.6.2, @storybook/react@npm:^8.6.2": version: 8.6.2 resolution: "@storybook/react@npm:8.6.2" @@ -8881,6 +9039,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-darwin-arm64@npm:1.15.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-darwin-x64@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-darwin-x64@npm:1.13.3" @@ -8895,6 +9060,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-x64@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-darwin-x64@npm:1.15.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@swc/core-linux-arm-gnueabihf@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.3" @@ -8909,6 +9081,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm-gnueabihf@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.15.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@swc/core-linux-arm64-gnu@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm64-gnu@npm:1.13.3" @@ -8923,6 +9102,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-gnu@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-arm64-gnu@npm:1.15.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-arm64-musl@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm64-musl@npm:1.13.3" @@ -8937,6 +9123,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-musl@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-arm64-musl@npm:1.15.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-x64-gnu@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-x64-gnu@npm:1.13.3" @@ -8951,6 +9144,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-gnu@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-x64-gnu@npm:1.15.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-x64-musl@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-x64-musl@npm:1.13.3" @@ -8965,6 +9165,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-musl@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-linux-x64-musl@npm:1.15.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@swc/core-win32-arm64-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-arm64-msvc@npm:1.13.3" @@ -8979,6 +9186,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-arm64-msvc@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-win32-arm64-msvc@npm:1.15.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-win32-ia32-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-ia32-msvc@npm:1.13.3" @@ -8993,6 +9207,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-ia32-msvc@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-win32-ia32-msvc@npm:1.15.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@swc/core-win32-x64-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-x64-msvc@npm:1.13.3" @@ -9007,6 +9228,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-x64-msvc@npm:1.15.3": + version: 1.15.3 + resolution: "@swc/core-win32-x64-msvc@npm:1.15.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@swc/core@npm:1.13.3": version: 1.13.3 resolution: "@swc/core@npm:1.13.3" @@ -9053,7 +9281,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.15.2, @swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": +"@swc/core@npm:1.15.2, @swc/core@npm:^1.13.5": version: 1.15.2 resolution: "@swc/core@npm:1.15.2" dependencies: @@ -9099,6 +9327,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": + version: 1.15.3 + resolution: "@swc/core@npm:1.15.3" + dependencies: + "@swc/core-darwin-arm64": "npm:1.15.3" + "@swc/core-darwin-x64": "npm:1.15.3" + "@swc/core-linux-arm-gnueabihf": "npm:1.15.3" + "@swc/core-linux-arm64-gnu": "npm:1.15.3" + "@swc/core-linux-arm64-musl": "npm:1.15.3" + "@swc/core-linux-x64-gnu": "npm:1.15.3" + "@swc/core-linux-x64-musl": "npm:1.15.3" + "@swc/core-win32-arm64-msvc": "npm:1.15.3" + "@swc/core-win32-ia32-msvc": "npm:1.15.3" + "@swc/core-win32-x64-msvc": "npm:1.15.3" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.25" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/280330d82328818138ed64fdcf9ea9abde6b6f16eca65a9d4db27dde06a8dfffd2649f3447d2243387277513c7430fa4142cafcfd64e943d682ce6a713cb8c2d + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -9480,6 +9754,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10/e79947307dc235953622e65f83d2683835212357ca261389116ab90bed369ac862ba28b146b4fed08b503ae1e1a12cb93ce783f24bb8d562950469f4320e1c7c + languageName: node + linkType: hard + "@types/chance@npm:^1.1.7": version: 1.1.7 resolution: "@types/chance@npm:1.1.7" @@ -9844,6 +10128,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c + languageName: node + linkType: hard + "@types/doctrine@npm:^0.0.9": version: 0.0.9 resolution: "@types/doctrine@npm:0.0.9" @@ -9916,7 +10207,7 @@ __metadata: languageName: node linkType: hard -"@types/fs-extra@npm:^11.0.4": +"@types/fs-extra@npm:^11, @types/fs-extra@npm:^11.0.4": version: 11.0.4 resolution: "@types/fs-extra@npm:11.0.4" dependencies: @@ -11134,6 +11425,38 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/expect@npm:3.2.4" + dependencies: + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:3.2.4" + "@vitest/utils": "npm:3.2.4" + chai: "npm:^5.2.0" + tinyrainbow: "npm:^2.0.0" + checksum: 10/dc69ce886c13714dfbbff78f2d2cb7eb536017e82301a73c42d573a9e9d2bf91005ac7abd9b977adf0a3bd431209f45a8ac2418029b68b0a377e092607c843ce + languageName: node + linkType: hard + +"@vitest/mocker@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/mocker@npm:3.2.4" + dependencies: + "@vitest/spy": "npm:3.2.4" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.17" + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/5e92431b6ed9fc1679060e4caef3e4623f4750542a5d7cd944774f8217c4d231e273202e8aea00bab33260a5a9222ecb7005d80da0348c3c829bd37d123071a8 + languageName: node + linkType: hard + "@vitest/pretty-format@npm:2.0.5": version: 2.0.5 resolution: "@vitest/pretty-format@npm:2.0.5" @@ -11152,6 +11475,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/pretty-format@npm:3.2.4" + dependencies: + tinyrainbow: "npm:^2.0.0" + checksum: 10/8dd30cbf956e01fbab042fe651fb5175d9f0cd00b7b569a46cd98df89c4fec47dab12916201ad6e09a4f25f2a2ec8927a4bfdc61118593097f759c90b18a51d4 + languageName: node + linkType: hard + "@vitest/spy@npm:2.0.5": version: 2.0.5 resolution: "@vitest/spy@npm:2.0.5" @@ -11161,6 +11493,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/spy@npm:3.2.4" + dependencies: + tinyspy: "npm:^4.0.3" + checksum: 10/7d38c299f42a8c7e5e41652b203af98ca54e63df69c3b072d0e401d5a57fbbba3e39d8538ac1b3022c26718a6388d0bcc222bc2f07faab75942543b9247c007d + languageName: node + linkType: hard + "@vitest/utils@npm:2.0.5": version: 2.0.5 resolution: "@vitest/utils@npm:2.0.5" @@ -11173,6 +11514,17 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:3.2.4": + version: 3.2.4 + resolution: "@vitest/utils@npm:3.2.4" + dependencies: + "@vitest/pretty-format": "npm:3.2.4" + loupe: "npm:^3.1.4" + tinyrainbow: "npm:^2.0.0" + checksum: 10/7f12ef63bd8ee13957744d1f336b0405f164ade4358bf9dfa531f75bbb58ffac02bf61aba65724311ddbc50b12ba54853a169e59c6b837c16086173b9a480710 + languageName: node + linkType: hard + "@vitest/utils@npm:^2.1.1": version: 2.1.9 resolution: "@vitest/utils@npm:2.1.9" @@ -13201,6 +13553,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.2.0": + version: 5.3.3 + resolution: "chai@npm:5.3.3" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10/0d0ef63106083b05c7ba510697cd9991a02b8df5984a7d010ab4af10205c7a1f27d1c06bfa4679540894295ac4dcc22aa2a281e2e4cfe5133c1db379626689a2 + languageName: node + linkType: hard + "chalk@npm:4.1.0": version: 4.1.0 resolution: "chalk@npm:4.1.0" @@ -14455,7 +14820,7 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:7.1.2": +"css-loader@npm:7.1.2, css-loader@npm:^7.1.2": version: 7.1.2 resolution: "css-loader@npm:7.1.2" dependencies: @@ -17951,7 +18316,7 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:9.1.0": +"fork-ts-checker-webpack-plugin@npm:9.1.0, fork-ts-checker-webpack-plugin@npm:^9.1.0": version: 9.1.0 resolution: "fork-ts-checker-webpack-plugin@npm:9.1.0" dependencies: @@ -18097,6 +18462,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.3.2": + version: 11.3.2 + resolution: "fs-extra@npm:11.3.2" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/d559545c73fda69c75aa786f345c2f738b623b42aea850200b1582e006a35278f63787179e3194ba19413c26a280441758952b0c7e88dd96762d497e365a6c3e + languageName: node + linkType: hard + "fs-extra@npm:^8.0.1, fs-extra@npm:^8.1.0": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" @@ -20037,22 +20413,22 @@ __metadata: linkType: hard "i18next@npm:^23.5.1 || ^24.2.0": - version: 24.2.2 - resolution: "i18next@npm:24.2.2" + version: 24.2.3 + resolution: "i18next@npm:24.2.3" dependencies: - "@babel/runtime": "npm:^7.23.2" + "@babel/runtime": "npm:^7.26.10" peerDependencies: typescript: ^5 peerDependenciesMeta: typescript: optional: true - checksum: 10/f66ed9e56d9412e59502f5df39163631daf9f1264774732fb21edbd66a528ca7a6b67dc2e2aec95683c6c7956e42c651587a54bd8ee082bd12008880ce6cd326 + checksum: 10/6c73d964f2a98b1aa2c2717fe6da66fc265bcbbc5fcd52b2bfff51ff013d30d4f7d7449c4eb7f464d27af43e2e73f2e7f1d46a144d731bd3bdb1385d4c199e4c languageName: node linkType: hard "i18next@npm:^25.0.0, i18next@npm:^25.5.2": - version: 25.5.2 - resolution: "i18next@npm:25.5.2" + version: 25.6.2 + resolution: "i18next@npm:25.6.2" dependencies: "@babel/runtime": "npm:^7.27.6" peerDependencies: @@ -20060,7 +20436,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/8d52e82386722a228f4465aa5cf39d82bd9861ea9cc8b31d7cc4d22d5e70b2740ee006068b09f486d5273cabd227a2ac14f37d68fab26344524200083f679bcd + checksum: 10/bb7f3e200caac72bacaead766287dbad5ed5945430bd9f2be1c14fbccd49cf78ab751f78cf53cd0be3ec063bc4c40837d7d771f7b14d50fcb9022e1ed5d6e72f languageName: node linkType: hard @@ -23161,9 +23537,9 @@ __metadata: linkType: hard "lossless-json@npm:^4.1.1": - version: 4.2.0 - resolution: "lossless-json@npm:4.2.0" - checksum: 10/b29cbf9a90b8f3108219ff410223a90a3a6ad14cf902eed4038571d9421c69ade7077fb59737e1ca20f3f404ddd68fad69be11365b0d79b4b61421b054759551 + version: 4.3.0 + resolution: "lossless-json@npm:4.3.0" + checksum: 10/a984a882c79b6e62a917d0202518472c17587bdee002f1427d7ec61115f9fbdeb5f0baa9f0e9fff8d9dbacd17da6b6c910012b2dcab69dd33d151cb7c13d5a37 languageName: node linkType: hard @@ -23174,6 +23550,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.4": + version: 3.2.1 + resolution: "loupe@npm:3.2.1" + checksum: 10/a4d78ec758aaa04e0e35d5cd1c15e970beb9cdbfd3d0f34f98b9bcda489f896a7190b3b6cc40b7a6dcb8e97e82e96eafaae10096aaa469804acdba6f7c2bde5f + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -23262,6 +23645,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.17": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 + languageName: node + linkType: hard + "magic-string@npm:^0.30.3, magic-string@npm:^0.30.5": version: 0.30.17 resolution: "magic-string@npm:0.30.17" @@ -23638,9 +24030,9 @@ __metadata: linkType: hard "micro-memoize@npm:^4.1.2": - version: 4.1.3 - resolution: "micro-memoize@npm:4.1.3" - checksum: 10/4e9c7767911cc76ae9c9779584ec87844437af9446b295a01774640a732c2c7f91944794027f44625031f7330ab7f9147740d0a9fb612680d1d2d858dad43402 + version: 4.2.0 + resolution: "micro-memoize@npm:4.2.0" + checksum: 10/260e27a5c15809f7dc435a89a424d93e49ccca62b743fa96ee72f254264df58c95edacbd847a2899a6d7f8ad0daef188548a8b12add5ccff83e88869a50a762c languageName: node linkType: hard @@ -27948,6 +28340,24 @@ __metadata: languageName: node linkType: hard +"react-docgen@npm:^7.1.1": + version: 7.1.1 + resolution: "react-docgen@npm:7.1.1" + dependencies: + "@babel/core": "npm:^7.18.9" + "@babel/traverse": "npm:^7.18.9" + "@babel/types": "npm:^7.18.9" + "@types/babel__core": "npm:^7.18.0" + "@types/babel__traverse": "npm:^7.18.0" + "@types/doctrine": "npm:^0.0.9" + "@types/resolve": "npm:^1.20.2" + doctrine: "npm:^3.0.0" + resolve: "npm:^1.22.1" + strip-indent: "npm:^4.0.0" + checksum: 10/501e5fa0d00e32ee27559f44462a34e9531018ccb46c51efbe60b98a4c077f43dbe8999da5bb91d2ab45a83a34099436a3b725fdabd3f218dbb4493c0b1c9f95 + languageName: node + linkType: hard + "react-dom@npm:18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" @@ -30862,6 +31272,32 @@ __metadata: languageName: node linkType: hard +"storybook@npm:^10.0.8": + version: 10.0.8 + resolution: "storybook@npm:10.0.8" + dependencies: + "@storybook/global": "npm:^5.0.0" + "@storybook/icons": "npm:^1.6.0" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/user-event": "npm:^14.6.1" + "@vitest/expect": "npm:3.2.4" + "@vitest/mocker": "npm:3.2.4" + "@vitest/spy": "npm:3.2.4" + esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" + recast: "npm:^0.23.5" + semver: "npm:^7.6.2" + ws: "npm:^8.18.0" + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + bin: + storybook: ./dist/bin/dispatcher.js + checksum: 10/a30b54248d69d7406f78e1871e075092ed9ae366d1c7cec56dc10b31b72a5196ef9e751453c9a7546797228c4fe0f01893c6fa541d24cf51037124d4f0e1566e + languageName: node + linkType: hard + "storybook@npm:^8.6.2": version: 8.6.2 resolution: "storybook@npm:8.6.2" @@ -31210,7 +31646,7 @@ __metadata: languageName: node linkType: hard -"style-loader@npm:4.0.0": +"style-loader@npm:4.0.0, style-loader@npm:^4.0.0": version: 4.0.0 resolution: "style-loader@npm:4.0.0" peerDependencies: @@ -31656,7 +32092,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.14, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.11": +"terser-webpack-plugin@npm:5.3.14, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.11, terser-webpack-plugin@npm:^5.3.14": version: 5.3.14 resolution: "terser-webpack-plugin@npm:5.3.14" dependencies: @@ -31860,6 +32296,13 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^2.0.0": + version: 2.0.0 + resolution: "tinyrainbow@npm:2.0.0" + checksum: 10/94d4e16246972614a5601eeb169ba94f1d49752426312d3cf8cc4f2cc663a2e354ffc653aa4de4eebccbf9eeebdd0caef52d1150271fdfde65d7ae7f3dcb9eb5 + languageName: node + linkType: hard + "tinyspy@npm:^3.0.0": version: 3.0.2 resolution: "tinyspy@npm:3.0.2" @@ -31867,6 +32310,13 @@ __metadata: languageName: node linkType: hard +"tinyspy@npm:^4.0.3": + version: 4.0.4 + resolution: "tinyspy@npm:4.0.4" + checksum: 10/858a99e3ded2fba8fe7c243099d9e58e926d6525af03d19cdf86c1a9a30398161fb830b4f77890d266bcc1c69df08fa6f4baf29d089385e4cdaa98d7b6296e7c + languageName: node + linkType: hard + "title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3" @@ -34037,7 +34487,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.3, ws@npm:^8.2.3, ws@npm:^8.9.0": +"ws@npm:^8.18.0, ws@npm:^8.18.3, ws@npm:^8.2.3, ws@npm:^8.9.0": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: From 2544a3441ee642e73ec93adddaf817e7bf730ca8 Mon Sep 17 00:00:00 2001 From: Johnny Kartheiser <140559259+JohnnyK-Grafana@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:35:38 -0600 Subject: [PATCH 09/94] alerting docs: initialization_timeout addition (#114373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit added docs for initialization_timeout (https://github.com/grafana/grafana/pull/96053) Co-authored-by: Irene Rodríguez --- docs/sources/setup-grafana/configure-grafana/_index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index c81d24d0758..13c2b77fd2f 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1933,6 +1933,10 @@ The initial delay before retrying a failed alert evaluation. Default is `1s`. This value is the starting point for exponential backoff. +#### `initialization_timeout` + +Allows the context deadline for the `AlertNG` service to be configurable. The default timeout is 30s. + #### `max_retry_delay` The maximum delay between retries during exponential backoff. Default is `4s`. From a99946d92150b65a7a9acc4b5b88468d48c8913f Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Tue, 25 Nov 2025 19:58:20 -0700 Subject: [PATCH 10/94] Dashboards: Cleanup includes fields (#114455) --- pkg/services/dashboards/service/dashboard_service.go | 9 +-------- pkg/services/dashboards/service/search/search.go | 3 +++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 6ebeb537257..b63b4a40f96 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -2009,14 +2009,7 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.Limit = query.Limit request.Page = query.Page request.Offset = (query.Page - 1) * query.Limit // only relevant when running in modes 3+ - request.Fields = append( - dashboardsearch.IncludeFields, - // Include the dashboard legacy ID in the results, as it is needed when - // determining whether a provisioned dashboard exists or not, see - // `(*DashboardServiceImpl).searchProvisionedDashboardsThroughK8s`. - resource.SEARCH_FIELD_LEGACY_ID, - resource.SEARCH_FIELD_LABELS+"."+resource.SEARCH_FIELD_LEGACY_ID, - ) + request.Fields = dashboardsearch.IncludeFields namespace := dr.k8sclient.GetNamespace(query.OrgId) var err error diff --git a/pkg/services/dashboards/service/search/search.go b/pkg/services/dashboards/service/search/search.go index 79e61eab99a..e1ce9e9132d 100644 --- a/pkg/services/dashboards/service/search/search.go +++ b/pkg/services/dashboards/service/search/search.go @@ -40,6 +40,9 @@ var ( resource.SEARCH_FIELD_SOURCE_PATH, resource.SEARCH_FIELD_SOURCE_CHECKSUM, resource.SEARCH_FIELD_SOURCE_TIME, + // below is needed to determine whether a provisioned dashboard exists or not + resource.SEARCH_FIELD_LEGACY_ID, + resource.SEARCH_FIELD_LABELS + "." + resource.SEARCH_FIELD_LEGACY_ID, } ) From d226c359042aa648d760316605860066fcba41d5 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:37:57 +0100 Subject: [PATCH 11/94] Alerting: Add first CLAUDE.md in the frontend alerting folder (#114308) Co-authored-by: Gilles De Mey --- .../app/features/alerting/unified/CLAUDE.md | 432 ++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 public/app/features/alerting/unified/CLAUDE.md diff --git a/public/app/features/alerting/unified/CLAUDE.md b/public/app/features/alerting/unified/CLAUDE.md new file mode 100644 index 00000000000..7778bb08da4 --- /dev/null +++ b/public/app/features/alerting/unified/CLAUDE.md @@ -0,0 +1,432 @@ +# Alerting Squad - Claude Code Configuration + +This file provides context for Claude Code when working on the Grafana Alerting codebase. It contains alerting-specific patterns and references to Grafana's coding standards. + +## Project Context + +**Location**: `public/app/features/alerting/unified/` +**Squad**: Alerting +**Focus**: Frontend development for Grafana's unified alerting system +**Tech Stack**: React, TypeScript, Redux Toolkit, RTK Query, Emotion, Jest, React Testing Library, MSW + +## Grafana Coding Standards + +**IMPORTANT**: Always follow Grafana's official style guides. Do not duplicate standards here - reference the source files: + +### Required Reading + +1. **Frontend Style Guide**: [../../../../contribute/style-guides/frontend.md](../../../../contribute/style-guides/frontend.md) + - Naming conventions, component patterns, TypeScript, exports + - Function declarations for components, callback props with "on" prefix + +2. **Testing Guidelines**: [../../../../contribute/style-guides/testing.md](../../../../contribute/style-guides/testing.md) + - React Testing Library, query priorities, user event setup + +3. **Styling Guide**: [../../../../contribute/style-guides/styling.md](../../../../contribute/style-guides/styling.md) + - Emotion usage, `useStyles2` hook patterns + +4. **Redux Framework**: [../../../../contribute/style-guides/redux.md](../../../../contribute/style-guides/redux.md) + - Redux Toolkit patterns, reducer testing + +5. **Alerting Testing Guide**: [./TESTING.md](./TESTING.md) + - MSW API mocking, permission mocking, data source setup + +### Alerting-Specific Conventions + +**Use @grafana/alerting Package**: + +- **Always check @grafana/alerting for shared components/hooks** before creating new ones: + + ```typescript + // Good - Use exported components/hooks from @grafana/alerting + import { AlertLabel, alertingMatchers } from '@grafana/alerting'; + + // Check what's available before reimplementing + ``` + +**Layout Components**: + +- **Prefer @grafana/ui layout components** over styled divs: + + ```typescript + // Good - Use Box for simple layout/spacing + import { Box } from '@grafana/ui'; + Content + + // Good - Use Stack for flex layouts + import { Stack } from '@grafana/ui'; + +
Item 1
+
Item 2
+
+ + // Avoid - Custom styled divs when layout components exist +
Content
+ ``` + +## Alerting Codebase Structure + +### Key Directories + +- `api/` - RTK Query API slices for data fetching +- `components/` - Feature-specific React components organized by domain +- `hooks/` - Reusable custom hooks for logic and data fetching +- `rule-editor/` - Alert rule creation and editing forms +- `rule-list/` - Alert rules list views (v1 and v2) +- `state/` - Redux state management and context providers +- `utils/` - Utility functions and helpers +- `types/` - TypeScript type definitions +- `mocks/` - MSW mock server setup for testing +- `testSetup/` - Test utilities and configuration + +### Component Domains + +- `alert-groups/` - Alert grouping and filtering +- `contact-points/` - Contact point configuration +- `notification-policies/` - Notification routing policies +- `mute-timings/` - Mute timing windows +- `silences/` - Silence management +- `receivers/` - Receiver configuration +- `templates/` - Notification templates +- `permissions/` - Permission management +- `settings/` - Alertmanager settings + +## State Management Patterns + +### RTK Query (Primary - Preferred) + +**IMPORTANT**: Our direction is to use RTK Query for data fetching, NOT Redux. Do not create new RTKQ endpoints, those should be created manually. + +- API slices in `api/` directory +- Custom base query in `api/alertingApi.ts` +- Automatic caching with 2-minute polling: `RULE_LIST_POLL_INTERVAL_MS` +- Key APIs: `alertRuleApi`, `alertmanagerApi`, `prometheusApi`, `receiversApi` + +**For new features**: Always use RTK Query hooks for data fetching: + +```typescript +import { useGetAlertRulesQuery } from '../api/alertRuleApi'; + +const { data, isLoading, error } = useGetAlertRulesQuery(params); +``` + +### Redux Toolkit (Legacy) + +**Avoid for new features** - Use RTK Query instead + +- Legacy reducers exist in `state/reducers/` +- Use state selectors: `useUnifiedAlertingSelector` +- Only modify if maintaining existing Redux code + +### Context Providers + +- `AlertmanagerContext` - Alertmanager selection state, for managing Alertmanager entities for a specific Alertmanager data source. +- `SettingsContext` - Settings state – used in `public/app/features/alerting/unified/components/settings` +- `WorkbenchContext` - Workbench state used in the alert triage feature `public/app/features/alerting/unified/triage` + +### Forms + +- Use `react-hook-form` (v7) for all forms +- See `rule-editor/alert-rule-form/` for patterns + +## Alerting-Specific Testing Patterns + +See [./TESTING.md](./TESTING.md) for comprehensive testing guide. Key points: + +### API Mocking with MSW + +**REQUIRED**: Use MSW for all API mocking (not `jest.fn()`) – though it's fine to use this function for unit testing. + +```typescript +import { mockApi } from '../mockApi'; + +// Mock common endpoints +mockApi.eval(); // for AlertingQueryRunner +// If helper doesn't exist, add it to mockApi.ts +``` + +**Why MSW?** Forces proper loading state handling, discovers UI issues early + +### Permission Mocking + +**Default: RBAC enabled** (most common user scenario) + +```typescript +import { enableRBAC, grantUserPermissions } from '../mocks'; + +enableRBAC(); // Usually not needed, enabled by default +grantUserPermissions([AccessControlAction.AlertingRuleRead]); +``` + +### Mock Data Factories + +Located in `mocks.ts`: + +```typescript +mockDataSource(); +mockPromAlert(); +mockRulerGrafanaRule(); +mockAlertmanagerAlert(); +mockSilence(); +``` + +### Data Source Setup + +Located in `testSetup/datasources.ts` for data source mocking patterns + +### Test Data Factories + +**Use factories for creating test data** - Don't manually create objects. + +For Kubernetes APIs and new schemas – use the `@grafana/alerting` package. + +Mock factories are defined in `packages/grafana-alerting/src/grafana/api/notifications/v0alpha1/mocks/fakes` +MSW handlers in `packages/grafana-alerting/src/grafana/api/notifications/v0alpha1/mocks/handlers` + +And there are "scenarios" that combine the two above. An example of such is `packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.test.scenario.ts` and is used for integration tests. + +Additionally alerting uses **`alertingFactory`** from `mocks/server/db` for building test data: + +```typescript +import { alertingFactory } from './mocks/server/db'; +import { mockFolder } from './mocks'; + +// Build a single alerting rule +const alertingRuleBuilder = alertingFactory.ruler.grafana.alertingRule; +const rule = alertingRuleBuilder.build(); + +// Build multiple rules +const rules = alertingRuleBuilder.buildList(6); + +// Override specific fields +const customRule = alertingRuleBuilder.build({ + grafana_alert: { title: 'CPU Alert' }, + labels: { severity: 'critical' }, +}); +``` + +**Common patterns**: + +```typescript +// Alerting rules +alertingFactory.ruler.grafana.alertingRule.build(); +alertingFactory.ruler.grafana.alertingRule.buildList(n); + +// Folders +mockFolder(); // Simple mock function + +// Override fields when building +alertingRuleBuilder.build({ + grafana_alert: { title: 'Custom Title' }, + labels: { key: 'value' }, +}); +``` + +**Benefits**: + +- Consistent test data across tests +- Easy to generate multiple instances with `buildList(n)` +- Override only the fields you care about +- Automatic sequencing (e.g., "Alerting rule 1", "Alerting rule 2") + +**Other mock functions** (from `mocks.ts`): + +```typescript +mockDataSource(); +mockPromAlert(); +mockRulerGrafanaRule(); +mockAlertmanagerAlert(); +mockSilence(); +mockFolder(); +``` + +## Alerting-Specific Patterns + +### Feature Toggles & settings + +A full list of features can be found in `pkg/services/featuremgmt/toggles_gen.csv` – focus on feature toggles owned by `@grafana/alerting-squad`. + +```typescript +import { config } from '@grafana/runtime'; + +if (config.featureToggles.alertingTriage) { + // Render triage view +} +``` + +A common configuration setting would be `unifiedAlertingEnabled` which allows a user to configure Grafana without any alerting UI or backend enabled at all. + +### Data Source Abstractions + +```typescript +import { isGrafanaRulerRule } from '../utils/rules'; + +if (isGrafanaRulerRule(rule)) { + // Grafana-managed +} else { + // External alertmanager +} +``` + +### Access Control (RBAC) + +```typescript +import { useAbilities } from '../hooks/useAbilities'; + +function Component() { + const [_, { can }] = useAbilities(); + const canCreate = can(AccessControlAction.AlertingRuleCreate); + + return canCreate ? : null; +} +``` + +### Key Routes + +Defined in `routes.tsx`: + +- `/alerting` - Home +- `/alerting/list` - Rules list (v1/v2) +- `/alerting/new/:type?` - Create rule +- `/alerting/:id/edit` - Edit rule +- `/alerting/notifications` - Contact points +- `/alerting/routes` - Notification policies + +### Common Hooks + +```typescript +useCombinedRuleNamespaces(); // Combines Prometheus + Ruler rules +useAlertmanagerConfig(); // Fetch alertmanager config +useFolder(); // Folder operations +useUnifiedAlertingSelector(); // Redux state – avoid using +useAbilities(); // Permission checking +``` + +### Link URLs and Navigation + +**IMPORTANT**: Different navigation components require different URL formats. + +#### When to use `createRelativeUrl` + +Use `createRelativeUrl` **only with LinkButton** (and other components that render HTML `` elements): + +```typescript +import { createRelativeUrl } from '@grafana/data'; +import { LinkButton } from '@grafana/ui'; + +// LinkButton renders tag - needs manual subpath prefix + + View Rules + +``` + +**Why?** LinkButton renders a native HTML anchor element, so it doesn't use React Router. You must manually add the subpath prefix using `createRelativeUrl`. + +#### When NOT to use `createRelativeUrl` + +Do **NOT** use `createRelativeUrl` with: + +1. **locationService** - Automatically adds prefix: + +```typescript +import { locationService } from '@grafana/runtime'; + +// locationService uses react-router history - prefix added automatically +locationService.push('/alerting/list'); // ✅ Correct - no createRelativeUrl +locationService.push(createRelativeUrl('/alerting/list')); // ❌ Wrong - double prefix! +``` + +2. **TextLink component** - Automatically adds prefix: + +```typescript +import { TextLink } from '@grafana/ui'; + +// TextLink uses react-router Link - prefix added automatically +View Rules // ✅ Correct +View Rules // ❌ Wrong +``` + +#### Summary + +| Component/Service | Use `createRelativeUrl`? | Reason | +| -------------------- | ------------------------ | --------------------------------------- | +| `LinkButton` | ✅ YES | Renders `` tag (native HTML) | +| `Button` with `href` | ✅ YES | Renders `` tag when href provided | +| `locationService` | ❌ NO | Uses react-router history (auto-prefix) | +| `TextLink` | ❌ NO | Uses react-router Link (auto-prefix) | +| React Router `Link` | ❌ NO | React Router component (auto-prefix) | + +**Rule of thumb**: If it renders a native HTML `` tag, use `createRelativeUrl`. If it uses React Router, don't. + +## Key Libraries + +### Grafana Internal + +- `@grafana/ui` - UI components (Button, Select, Input, etc.) +- `@grafana/data` - Data models and utilities +- `@grafana/runtime` - Runtime services (config, backendSrv, locationService) +- `@grafana/scenes` - Scene framework (Insights/Triage views) +- `@grafana/e2e-selectors` - Test selectors +- `@grafana/alerting` - Grafana managed alerting specific package (utility functions, API endpoints, mocks, React components, etc) + +### External + +- `react-hook-form` (v7) - Form state +- `@reduxjs/toolkit` - Redux + RTK Query +- `@emotion/css` - Styling +- `lodash` - Utilities +- `msw` - API mocking for tests + +## Quick Reference Checklists + +### Creating a New Component + +1. ✅ Create in appropriate `components/` subdirectory +2. ✅ Use function declaration (not arrow function) +3. ✅ Add TypeScript props interface (no "I" prefix) +4. ✅ Use `useStyles2` for styling (Emotion) +5. ✅ Create colocated test file +6. ✅ Use MSW for API mocking in tests +7. ✅ Query with `*ByRole` queries + +### Adding a New API Endpoint + +1. ✅ Add to relevant API slice in `api/` (RTK Query) +2. ✅ Add helper to `mockApi.ts` for testing +3. ✅ Handle loading/error states in UI +4. ✅ Test with MSW + +### Creating a New Form + +1. ✅ Use `react-hook-form` (v7) +2. ✅ See `rule-editor/alert-rule-form/` for patterns +3. ✅ Add validation (schema if needed) +4. ✅ Handle API submission errors +5. ✅ Test user interactions with `userEvent` + +### Writing Tests + +Check https://testing-library.com/docs/queries/about/ for what selectors to prefer when using React Testing Library + +- [ ] RBAC enabled by default +- [ ] MSW for API mocking (not `jest.fn()`) +- [ ] Loading states tested +- [ ] Error states tested +- [ ] User interactions use `userEvent.setup()` +- [ ] Queries prefer `*ByRole` +- [ ] Async operations use `await` and `findBy*` +- [ ] Permissions tested with `grantUserPermissions` + +## Getting Help + +- Check patterns in existing `components/` code +- Review test examples in `*.test.tsx` files +- Consult `mockApi.ts` for API mocking +- See `mocks.ts` for data factories +- Read [./TESTING.md](./TESTING.md) for testing details +- Review Grafana style guides (linked at top) + +--- + +**Last Updated**: 2025-11-21 +**Maintained By**: Alerting Squad From bfda534825f13ff541c19728206717f8cc6db0cf Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 26 Nov 2025 10:40:35 +0100 Subject: [PATCH 12/94] Zanzana: Implement role bindings write APIs (#114385) --- pkg/services/authz/proto/v1/extention.pb.go | 942 +++++++++++------- pkg/services/authz/proto/v1/extention.proto | 24 + .../authz/zanzana/server/server_mutate.go | 7 + .../server/server_mutate_rolebindings.go | 99 ++ .../server/server_mutate_rolebindings_test.go | 106 ++ .../authz/zanzana/server/server_test.go | 4 + 6 files changed, 818 insertions(+), 364 deletions(-) create mode 100644 pkg/services/authz/zanzana/server/server_mutate_rolebindings.go create mode 100644 pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go diff --git a/pkg/services/authz/proto/v1/extention.pb.go b/pkg/services/authz/proto/v1/extention.pb.go index 03a44978db9..358b6a0df95 100644 --- a/pkg/services/authz/proto/v1/extention.pb.go +++ b/pkg/services/authz/proto/v1/extention.pb.go @@ -123,6 +123,8 @@ type MutateOperation struct { // *MutateOperation_UpdateUserOrgRole // *MutateOperation_DeleteUserOrgRole // *MutateOperation_AddUserOrgRole + // *MutateOperation_CreateRoleBinding + // *MutateOperation_DeleteRoleBinding Operation isMutateOperation_Operation `protobuf_oneof:"operation"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -228,6 +230,24 @@ func (x *MutateOperation) GetAddUserOrgRole() *AddUserOrgRoleOperation { return nil } +func (x *MutateOperation) GetCreateRoleBinding() *CreateRoleBindingOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_CreateRoleBinding); ok { + return x.CreateRoleBinding + } + } + return nil +} + +func (x *MutateOperation) GetDeleteRoleBinding() *DeleteRoleBindingOperation { + if x != nil { + if x, ok := x.Operation.(*MutateOperation_DeleteRoleBinding); ok { + return x.DeleteRoleBinding + } + } + return nil +} + type isMutateOperation_Operation interface { isMutateOperation_Operation() } @@ -260,6 +280,14 @@ type MutateOperation_AddUserOrgRole struct { AddUserOrgRole *AddUserOrgRoleOperation `protobuf:"bytes,7,opt,name=add_user_org_role,json=addUserOrgRole,proto3,oneof"` } +type MutateOperation_CreateRoleBinding struct { + CreateRoleBinding *CreateRoleBindingOperation `protobuf:"bytes,8,opt,name=create_role_binding,json=createRoleBinding,proto3,oneof"` +} + +type MutateOperation_DeleteRoleBinding struct { + DeleteRoleBinding *DeleteRoleBindingOperation `protobuf:"bytes,9,opt,name=delete_role_binding,json=deleteRoleBinding,proto3,oneof"` +} + func (*MutateOperation_SetFolderParent) isMutateOperation_Operation() {} func (*MutateOperation_DeleteFolder) isMutateOperation_Operation() {} @@ -274,6 +302,10 @@ func (*MutateOperation_DeleteUserOrgRole) isMutateOperation_Operation() {} func (*MutateOperation_AddUserOrgRole) isMutateOperation_Operation() {} +func (*MutateOperation_CreateRoleBinding) isMutateOperation_Operation() {} + +func (*MutateOperation_DeleteRoleBinding) isMutateOperation_Operation() {} + type SetFolderParentOperation struct { state protoimpl.MessageState `protogen:"open.v1"` // UID of the folder @@ -667,6 +699,150 @@ func (x *DeleteUserOrgRoleOperation) GetRole() string { return "" } +type CreateRoleBindingOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + SubjectKind string `protobuf:"bytes,1,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"` + // uid of the identity + SubjectName string `protobuf:"bytes,2,opt,name=subject_name,json=subjectName,proto3" json:"subject_name,omitempty"` + // kind of the role (Role/CoreRole/GlobalRole) + RoleKind string `protobuf:"bytes,3,opt,name=role_kind,json=roleKind,proto3" json:"role_kind,omitempty"` + // uid of the role + RoleName string `protobuf:"bytes,4,opt,name=role_name,json=roleName,proto3" json:"role_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRoleBindingOperation) Reset() { + *x = CreateRoleBindingOperation{} + mi := &file_extention_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRoleBindingOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRoleBindingOperation) ProtoMessage() {} + +func (x *CreateRoleBindingOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRoleBindingOperation.ProtoReflect.Descriptor instead. +func (*CreateRoleBindingOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{10} +} + +func (x *CreateRoleBindingOperation) GetSubjectKind() string { + if x != nil { + return x.SubjectKind + } + return "" +} + +func (x *CreateRoleBindingOperation) GetSubjectName() string { + if x != nil { + return x.SubjectName + } + return "" +} + +func (x *CreateRoleBindingOperation) GetRoleKind() string { + if x != nil { + return x.RoleKind + } + return "" +} + +func (x *CreateRoleBindingOperation) GetRoleName() string { + if x != nil { + return x.RoleName + } + return "" +} + +type DeleteRoleBindingOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + SubjectKind string `protobuf:"bytes,1,opt,name=subject_kind,json=subjectKind,proto3" json:"subject_kind,omitempty"` + // uid of the identity + SubjectName string `protobuf:"bytes,2,opt,name=subject_name,json=subjectName,proto3" json:"subject_name,omitempty"` + // kind of the role (Role/CoreRole/GlobalRole) + RoleKind string `protobuf:"bytes,3,opt,name=role_kind,json=roleKind,proto3" json:"role_kind,omitempty"` + // uid of the role + RoleName string `protobuf:"bytes,4,opt,name=role_name,json=roleName,proto3" json:"role_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRoleBindingOperation) Reset() { + *x = DeleteRoleBindingOperation{} + mi := &file_extention_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRoleBindingOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRoleBindingOperation) ProtoMessage() {} + +func (x *DeleteRoleBindingOperation) ProtoReflect() protoreflect.Message { + mi := &file_extention_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRoleBindingOperation.ProtoReflect.Descriptor instead. +func (*DeleteRoleBindingOperation) Descriptor() ([]byte, []int) { + return file_extention_proto_rawDescGZIP(), []int{11} +} + +func (x *DeleteRoleBindingOperation) GetSubjectKind() string { + if x != nil { + return x.SubjectKind + } + return "" +} + +func (x *DeleteRoleBindingOperation) GetSubjectName() string { + if x != nil { + return x.SubjectName + } + return "" +} + +func (x *DeleteRoleBindingOperation) GetRoleKind() string { + if x != nil { + return x.RoleKind + } + return "" +} + +func (x *DeleteRoleBindingOperation) GetRoleName() string { + if x != nil { + return x.RoleName + } + return "" +} + type Resource struct { state protoimpl.MessageState `protogen:"open.v1"` // group of the resource (e.g: "dashboard.grafana.app") @@ -681,7 +857,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} - mi := &file_extention_proto_msgTypes[10] + mi := &file_extention_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -693,7 +869,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[10] + mi := &file_extention_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -706,7 +882,7 @@ func (x *Resource) ProtoReflect() protoreflect.Message { // Deprecated: Use Resource.ProtoReflect.Descriptor instead. func (*Resource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{10} + return file_extention_proto_rawDescGZIP(), []int{12} } func (x *Resource) GetGroup() string { @@ -744,7 +920,7 @@ type Permission struct { func (x *Permission) Reset() { *x = Permission{} - mi := &file_extention_proto_msgTypes[11] + mi := &file_extention_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -756,7 +932,7 @@ func (x *Permission) String() string { func (*Permission) ProtoMessage() {} func (x *Permission) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[11] + mi := &file_extention_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -769,7 +945,7 @@ func (x *Permission) ProtoReflect() protoreflect.Message { // Deprecated: Use Permission.ProtoReflect.Descriptor instead. func (*Permission) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{11} + return file_extention_proto_rawDescGZIP(), []int{13} } func (x *Permission) GetKind() string { @@ -805,7 +981,7 @@ type TupleKey struct { func (x *TupleKey) Reset() { *x = TupleKey{} - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -817,7 +993,7 @@ func (x *TupleKey) String() string { func (*TupleKey) ProtoMessage() {} func (x *TupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[12] + mi := &file_extention_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -830,7 +1006,7 @@ func (x *TupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKey.ProtoReflect.Descriptor instead. func (*TupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{12} + return file_extention_proto_rawDescGZIP(), []int{14} } func (x *TupleKey) GetUser() string { @@ -871,7 +1047,7 @@ type Tuple struct { func (x *Tuple) Reset() { *x = Tuple{} - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -883,7 +1059,7 @@ func (x *Tuple) String() string { func (*Tuple) ProtoMessage() {} func (x *Tuple) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[13] + mi := &file_extention_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -896,7 +1072,7 @@ func (x *Tuple) ProtoReflect() protoreflect.Message { // Deprecated: Use Tuple.ProtoReflect.Descriptor instead. func (*Tuple) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{13} + return file_extention_proto_rawDescGZIP(), []int{15} } func (x *Tuple) GetKey() *TupleKey { @@ -924,7 +1100,7 @@ type TupleKeyWithoutCondition struct { func (x *TupleKeyWithoutCondition) Reset() { *x = TupleKeyWithoutCondition{} - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -936,7 +1112,7 @@ func (x *TupleKeyWithoutCondition) String() string { func (*TupleKeyWithoutCondition) ProtoMessage() {} func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[14] + mi := &file_extention_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -949,7 +1125,7 @@ func (x *TupleKeyWithoutCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleKeyWithoutCondition.ProtoReflect.Descriptor instead. func (*TupleKeyWithoutCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{14} + return file_extention_proto_rawDescGZIP(), []int{16} } func (x *TupleKeyWithoutCondition) GetUser() string { @@ -983,7 +1159,7 @@ type RelationshipCondition struct { func (x *RelationshipCondition) Reset() { *x = RelationshipCondition{} - mi := &file_extention_proto_msgTypes[15] + mi := &file_extention_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -995,7 +1171,7 @@ func (x *RelationshipCondition) String() string { func (*RelationshipCondition) ProtoMessage() {} func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[15] + mi := &file_extention_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1008,7 +1184,7 @@ func (x *RelationshipCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use RelationshipCondition.ProtoReflect.Descriptor instead. func (*RelationshipCondition) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{15} + return file_extention_proto_rawDescGZIP(), []int{17} } func (x *RelationshipCondition) GetName() string { @@ -1037,7 +1213,7 @@ type ReadRequest struct { func (x *ReadRequest) Reset() { *x = ReadRequest{} - mi := &file_extention_proto_msgTypes[16] + mi := &file_extention_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1049,7 +1225,7 @@ func (x *ReadRequest) String() string { func (*ReadRequest) ProtoMessage() {} func (x *ReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[16] + mi := &file_extention_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1062,7 +1238,7 @@ func (x *ReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequest.ProtoReflect.Descriptor instead. func (*ReadRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{16} + return file_extention_proto_rawDescGZIP(), []int{18} } func (x *ReadRequest) GetNamespace() string { @@ -1104,7 +1280,7 @@ type ReadRequestTupleKey struct { func (x *ReadRequestTupleKey) Reset() { *x = ReadRequestTupleKey{} - mi := &file_extention_proto_msgTypes[17] + mi := &file_extention_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1116,7 +1292,7 @@ func (x *ReadRequestTupleKey) String() string { func (*ReadRequestTupleKey) ProtoMessage() {} func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[17] + mi := &file_extention_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1129,7 +1305,7 @@ func (x *ReadRequestTupleKey) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadRequestTupleKey.ProtoReflect.Descriptor instead. func (*ReadRequestTupleKey) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{17} + return file_extention_proto_rawDescGZIP(), []int{19} } func (x *ReadRequestTupleKey) GetUser() string { @@ -1163,7 +1339,7 @@ type ReadResponse struct { func (x *ReadResponse) Reset() { *x = ReadResponse{} - mi := &file_extention_proto_msgTypes[18] + mi := &file_extention_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1175,7 +1351,7 @@ func (x *ReadResponse) String() string { func (*ReadResponse) ProtoMessage() {} func (x *ReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[18] + mi := &file_extention_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1188,7 +1364,7 @@ func (x *ReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResponse.ProtoReflect.Descriptor instead. func (*ReadResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{18} + return file_extention_proto_rawDescGZIP(), []int{20} } func (x *ReadResponse) GetTuples() []*Tuple { @@ -1214,7 +1390,7 @@ type WriteRequestWrites struct { func (x *WriteRequestWrites) Reset() { *x = WriteRequestWrites{} - mi := &file_extention_proto_msgTypes[19] + mi := &file_extention_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1226,7 +1402,7 @@ func (x *WriteRequestWrites) String() string { func (*WriteRequestWrites) ProtoMessage() {} func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[19] + mi := &file_extention_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1239,7 +1415,7 @@ func (x *WriteRequestWrites) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestWrites.ProtoReflect.Descriptor instead. func (*WriteRequestWrites) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{19} + return file_extention_proto_rawDescGZIP(), []int{21} } func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey { @@ -1258,7 +1434,7 @@ type WriteRequestDeletes struct { func (x *WriteRequestDeletes) Reset() { *x = WriteRequestDeletes{} - mi := &file_extention_proto_msgTypes[20] + mi := &file_extention_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1270,7 +1446,7 @@ func (x *WriteRequestDeletes) String() string { func (*WriteRequestDeletes) ProtoMessage() {} func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[20] + mi := &file_extention_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1283,7 +1459,7 @@ func (x *WriteRequestDeletes) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequestDeletes.ProtoReflect.Descriptor instead. func (*WriteRequestDeletes) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{20} + return file_extention_proto_rawDescGZIP(), []int{22} } func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition { @@ -1304,7 +1480,7 @@ type WriteRequest struct { func (x *WriteRequest) Reset() { *x = WriteRequest{} - mi := &file_extention_proto_msgTypes[21] + mi := &file_extention_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1316,7 +1492,7 @@ func (x *WriteRequest) String() string { func (*WriteRequest) ProtoMessage() {} func (x *WriteRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[21] + mi := &file_extention_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1329,7 +1505,7 @@ func (x *WriteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead. func (*WriteRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{21} + return file_extention_proto_rawDescGZIP(), []int{23} } func (x *WriteRequest) GetNamespace() string { @@ -1361,7 +1537,7 @@ type WriteResponse struct { func (x *WriteResponse) Reset() { *x = WriteResponse{} - mi := &file_extention_proto_msgTypes[22] + mi := &file_extention_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1373,7 +1549,7 @@ func (x *WriteResponse) String() string { func (*WriteResponse) ProtoMessage() {} func (x *WriteResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[22] + mi := &file_extention_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1386,7 +1562,7 @@ func (x *WriteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WriteResponse.ProtoReflect.Descriptor instead. func (*WriteResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{22} + return file_extention_proto_rawDescGZIP(), []int{24} } type BatchCheckRequest struct { @@ -1400,7 +1576,7 @@ type BatchCheckRequest struct { func (x *BatchCheckRequest) Reset() { *x = BatchCheckRequest{} - mi := &file_extention_proto_msgTypes[23] + mi := &file_extention_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1412,7 +1588,7 @@ func (x *BatchCheckRequest) String() string { func (*BatchCheckRequest) ProtoMessage() {} func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[23] + mi := &file_extention_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1425,7 +1601,7 @@ func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead. func (*BatchCheckRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{23} + return file_extention_proto_rawDescGZIP(), []int{25} } func (x *BatchCheckRequest) GetSubject() string { @@ -1463,7 +1639,7 @@ type BatchCheckItem struct { func (x *BatchCheckItem) Reset() { *x = BatchCheckItem{} - mi := &file_extention_proto_msgTypes[24] + mi := &file_extention_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1475,7 +1651,7 @@ func (x *BatchCheckItem) String() string { func (*BatchCheckItem) ProtoMessage() {} func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[24] + mi := &file_extention_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1488,7 +1664,7 @@ func (x *BatchCheckItem) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckItem.ProtoReflect.Descriptor instead. func (*BatchCheckItem) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{24} + return file_extention_proto_rawDescGZIP(), []int{26} } func (x *BatchCheckItem) GetVerb() string { @@ -1542,7 +1718,7 @@ type BatchCheckResponse struct { func (x *BatchCheckResponse) Reset() { *x = BatchCheckResponse{} - mi := &file_extention_proto_msgTypes[25] + mi := &file_extention_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1554,7 +1730,7 @@ func (x *BatchCheckResponse) String() string { func (*BatchCheckResponse) ProtoMessage() {} func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[25] + mi := &file_extention_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1567,7 +1743,7 @@ func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead. func (*BatchCheckResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{25} + return file_extention_proto_rawDescGZIP(), []int{27} } func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource { @@ -1586,7 +1762,7 @@ type BatchCheckGroupResource struct { func (x *BatchCheckGroupResource) Reset() { *x = BatchCheckGroupResource{} - mi := &file_extention_proto_msgTypes[26] + mi := &file_extention_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1598,7 +1774,7 @@ func (x *BatchCheckGroupResource) String() string { func (*BatchCheckGroupResource) ProtoMessage() {} func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[26] + mi := &file_extention_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1611,7 +1787,7 @@ func (x *BatchCheckGroupResource) ProtoReflect() protoreflect.Message { // Deprecated: Use BatchCheckGroupResource.ProtoReflect.Descriptor instead. func (*BatchCheckGroupResource) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{26} + return file_extention_proto_rawDescGZIP(), []int{28} } func (x *BatchCheckGroupResource) GetItems() map[string]bool { @@ -1631,7 +1807,7 @@ type QueryRequest struct { func (x *QueryRequest) Reset() { *x = QueryRequest{} - mi := &file_extention_proto_msgTypes[27] + mi := &file_extention_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1643,7 +1819,7 @@ func (x *QueryRequest) String() string { func (*QueryRequest) ProtoMessage() {} func (x *QueryRequest) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[27] + mi := &file_extention_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1656,7 +1832,7 @@ func (x *QueryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. func (*QueryRequest) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{27} + return file_extention_proto_rawDescGZIP(), []int{29} } func (x *QueryRequest) GetNamespace() string { @@ -1685,7 +1861,7 @@ type QueryResponse struct { func (x *QueryResponse) Reset() { *x = QueryResponse{} - mi := &file_extention_proto_msgTypes[28] + mi := &file_extention_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1697,7 +1873,7 @@ func (x *QueryResponse) String() string { func (*QueryResponse) ProtoMessage() {} func (x *QueryResponse) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[28] + mi := &file_extention_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1710,7 +1886,7 @@ func (x *QueryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryResponse.ProtoReflect.Descriptor instead. func (*QueryResponse) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{28} + return file_extention_proto_rawDescGZIP(), []int{30} } func (x *QueryResponse) GetResult() isQueryResponse_Result { @@ -1751,7 +1927,7 @@ type QueryOperation struct { func (x *QueryOperation) Reset() { *x = QueryOperation{} - mi := &file_extention_proto_msgTypes[29] + mi := &file_extention_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1763,7 +1939,7 @@ func (x *QueryOperation) String() string { func (*QueryOperation) ProtoMessage() {} func (x *QueryOperation) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[29] + mi := &file_extention_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1776,7 +1952,7 @@ func (x *QueryOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryOperation.ProtoReflect.Descriptor instead. func (*QueryOperation) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{29} + return file_extention_proto_rawDescGZIP(), []int{31} } func (x *QueryOperation) GetOperation() isQueryOperation_Operation { @@ -1815,7 +1991,7 @@ type GetFolderParentsQuery struct { func (x *GetFolderParentsQuery) Reset() { *x = GetFolderParentsQuery{} - mi := &file_extention_proto_msgTypes[30] + mi := &file_extention_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1827,7 +2003,7 @@ func (x *GetFolderParentsQuery) String() string { func (*GetFolderParentsQuery) ProtoMessage() {} func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[30] + mi := &file_extention_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1840,7 +2016,7 @@ func (x *GetFolderParentsQuery) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsQuery.ProtoReflect.Descriptor instead. func (*GetFolderParentsQuery) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{30} + return file_extention_proto_rawDescGZIP(), []int{32} } func (x *GetFolderParentsQuery) GetFolder() string { @@ -1860,7 +2036,7 @@ type GetFolderParentsResult struct { func (x *GetFolderParentsResult) Reset() { *x = GetFolderParentsResult{} - mi := &file_extention_proto_msgTypes[31] + mi := &file_extention_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1872,7 +2048,7 @@ func (x *GetFolderParentsResult) String() string { func (*GetFolderParentsResult) ProtoMessage() {} func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { - mi := &file_extention_proto_msgTypes[31] + mi := &file_extention_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1885,7 +2061,7 @@ func (x *GetFolderParentsResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GetFolderParentsResult.ProtoReflect.Descriptor instead. func (*GetFolderParentsResult) Descriptor() ([]byte, []int) { - return file_extention_proto_rawDescGZIP(), []int{31} + return file_extention_proto_rawDescGZIP(), []int{33} } func (x *GetFolderParentsResult) GetParentUids() []string { @@ -1914,7 +2090,7 @@ var file_extention_proto_rawDesc = string([]byte{ 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4d, 0x75, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa8, 0x05, 0x0a, 0x0f, 0x4d, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xec, 0x06, 0x0a, 0x0f, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x5a, 0x0a, 0x11, 0x73, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, @@ -1956,245 +2132,277 @@ var file_extention_proto_rawDesc = string([]byte{ 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0e, 0x61, 0x64, 0x64, 0x55, 0x73, 0x65, - 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, - 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, - 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x70, 0x0a, 0x15, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x72, - 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x65, 0x78, - 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x95, 0x01, 0x0a, - 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, + 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x60, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, + 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x60, 0x0a, 0x13, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x62, 0x69, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x11, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x42, 0x0b, 0x0a, 0x09, + 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x73, 0x0a, 0x18, 0x53, 0x65, 0x74, + 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, + 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, + 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, + 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x70, + 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x5f, 0x65, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x45, 0x78, 0x69, 0x73, 0x74, 0x69, 0x6e, 0x67, + 0x22, 0x95, 0x01, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, + 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, + 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x50, - 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x0a, - 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x0a, 0x17, - 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, + 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, + 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x95, 0x01, 0x0a, 0x19, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x12, 0x3e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x22, 0x41, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, + 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, + 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, + 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, + 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, - 0x44, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, - 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x52, 0x6f, 0x6c, 0x65, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x50, 0x0a, 0x08, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x48, 0x0a, - 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, - 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, 0x70, 0x6c, - 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, 0x0a, 0x09, - 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, - 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x12, 0x2e, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, - 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, - 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, 0x0a, 0x15, - 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6e, - 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, - 0x75, 0x63, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, 0x01, 0x0a, - 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x74, 0x75, - 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, + 0x9c, 0x01, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, + 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, + 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x9c, + 0x01, 0x0a, 0x1a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x69, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, + 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, + 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6b, 0x69, 0x6e, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4b, 0x69, 0x6e, 0x64, + 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x50, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, + 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, + 0x48, 0x0a, 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, 0x65, 0x72, 0x62, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x54, 0x75, + 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x47, + 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x68, 0x69, 0x70, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x71, 0x0a, 0x05, 0x54, 0x75, 0x70, 0x6c, 0x65, + 0x12, 0x2e, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, - 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, - 0x12, 0x38, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, - 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, - 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, 0x70, 0x6c, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, - 0x70, 0x6c, 0x65, 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x63, - 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, 0x57, 0x72, - 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, - 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, - 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, - 0x65, 0x79, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, 0x62, 0x0a, - 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, - 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, + 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x62, 0x0a, 0x18, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, - 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x12, 0x3e, 0x0a, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, - 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, - 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, 0x01, 0x0a, - 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x12, - 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x76, - 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x75, 0x62, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x66, - 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, - 0x64, 0x65, 0x72, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, 0x75, 0x74, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x5e, + 0x0a, 0x15, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0xda, + 0x01, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, + 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x44, 0x0a, 0x09, + 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, + 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2d, 0x0a, 0x12, + 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, + 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x5d, 0x0a, 0x13, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, + 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x70, 0x0a, 0x0c, 0x52, 0x65, + 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x75, + 0x70, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa1, - 0x01, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x69, 0x74, - 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, 0x65, 0x6d, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x6e, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x12, 0x40, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, - 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x6f, 0x6c, 0x64, 0x65, - 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x22, 0x78, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6f, 0x6c, 0x64, - 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x48, 0x00, 0x52, 0x10, 0x67, - 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, - 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, 0x0a, 0x15, - 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x39, 0x0a, - 0x16, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, - 0x74, 0x5f, 0x75, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x55, 0x69, 0x64, 0x73, 0x32, 0xac, 0x03, 0x0a, 0x15, 0x41, 0x75, 0x74, - 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, - 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x57, 0x72, - 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, + 0x54, 0x75, 0x70, 0x6c, 0x65, 0x52, 0x06, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, + 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x69, + 0x6e, 0x75, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x51, 0x0a, 0x12, + 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, + 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x75, 0x70, 0x6c, + 0x65, 0x4b, 0x65, 0x79, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x22, + 0x62, 0x0a, 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0a, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x54, 0x75, 0x70, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x57, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x43, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x74, 0x75, 0x70, 0x6c, 0x65, 0x4b, + 0x65, 0x79, 0x73, 0x22, 0xaf, 0x01, 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x77, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x57, 0x72, 0x69, 0x74, 0x65, 0x73, 0x52, 0x06, 0x77, 0x72, 0x69, 0x74, + 0x65, 0x73, 0x12, 0x41, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x52, 0x07, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x11, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0xa4, + 0x01, 0x0a, 0x0e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x49, 0x74, 0x65, + 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x76, 0x65, 0x72, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x76, 0x65, 0x72, 0x62, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, + 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x73, 0x75, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, + 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0xc8, 0x01, 0x0a, 0x12, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x06, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x1a, 0x66, 0x0a, 0x0b, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x41, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, + 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0xa1, 0x01, 0x0a, 0x17, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x05, + 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x49, 0x74, + 0x65, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6e, 0x0a, 0x0c, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x6e, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x0e, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x5f, + 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x22, 0x78, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x59, 0x0a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x66, 0x6f, + 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x48, 0x00, 0x52, + 0x10, 0x67, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, + 0x73, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2f, + 0x0a, 0x15, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, 0x6e, + 0x74, 0x73, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, + 0x39, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x46, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x50, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x5f, 0x75, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, + 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x55, 0x69, 0x64, 0x73, 0x32, 0xac, 0x03, 0x0a, 0x15, 0x41, + 0x75, 0x74, 0x68, 0x7a, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0a, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x12, 0x25, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x49, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1f, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, + 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x51, 0x75, 0x65, - 0x72, 0x79, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x57, 0x72, 0x69, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x06, 0x4d, 0x75, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x75, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x05, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x12, 0x20, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, - 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2e, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, + 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -2209,7 +2417,7 @@ func file_extention_proto_rawDescGZIP() []byte { return file_extention_proto_rawDescData } -var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_extention_proto_msgTypes = make([]protoimpl.MessageInfo, 36) var file_extention_proto_goTypes = []any{ (*MutateRequest)(nil), // 0: authz.extention.v1.MutateRequest (*MutateResponse)(nil), // 1: authz.extention.v1.MutateResponse @@ -2221,33 +2429,35 @@ var file_extention_proto_goTypes = []any{ (*AddUserOrgRoleOperation)(nil), // 7: authz.extention.v1.AddUserOrgRoleOperation (*UpdateUserOrgRoleOperation)(nil), // 8: authz.extention.v1.UpdateUserOrgRoleOperation (*DeleteUserOrgRoleOperation)(nil), // 9: authz.extention.v1.DeleteUserOrgRoleOperation - (*Resource)(nil), // 10: authz.extention.v1.Resource - (*Permission)(nil), // 11: authz.extention.v1.Permission - (*TupleKey)(nil), // 12: authz.extention.v1.TupleKey - (*Tuple)(nil), // 13: authz.extention.v1.Tuple - (*TupleKeyWithoutCondition)(nil), // 14: authz.extention.v1.TupleKeyWithoutCondition - (*RelationshipCondition)(nil), // 15: authz.extention.v1.RelationshipCondition - (*ReadRequest)(nil), // 16: authz.extention.v1.ReadRequest - (*ReadRequestTupleKey)(nil), // 17: authz.extention.v1.ReadRequestTupleKey - (*ReadResponse)(nil), // 18: authz.extention.v1.ReadResponse - (*WriteRequestWrites)(nil), // 19: authz.extention.v1.WriteRequestWrites - (*WriteRequestDeletes)(nil), // 20: authz.extention.v1.WriteRequestDeletes - (*WriteRequest)(nil), // 21: authz.extention.v1.WriteRequest - (*WriteResponse)(nil), // 22: authz.extention.v1.WriteResponse - (*BatchCheckRequest)(nil), // 23: authz.extention.v1.BatchCheckRequest - (*BatchCheckItem)(nil), // 24: authz.extention.v1.BatchCheckItem - (*BatchCheckResponse)(nil), // 25: authz.extention.v1.BatchCheckResponse - (*BatchCheckGroupResource)(nil), // 26: authz.extention.v1.BatchCheckGroupResource - (*QueryRequest)(nil), // 27: authz.extention.v1.QueryRequest - (*QueryResponse)(nil), // 28: authz.extention.v1.QueryResponse - (*QueryOperation)(nil), // 29: authz.extention.v1.QueryOperation - (*GetFolderParentsQuery)(nil), // 30: authz.extention.v1.GetFolderParentsQuery - (*GetFolderParentsResult)(nil), // 31: authz.extention.v1.GetFolderParentsResult - nil, // 32: authz.extention.v1.BatchCheckResponse.GroupsEntry - nil, // 33: authz.extention.v1.BatchCheckGroupResource.ItemsEntry - (*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 35: google.protobuf.Struct - (*wrapperspb.Int32Value)(nil), // 36: google.protobuf.Int32Value + (*CreateRoleBindingOperation)(nil), // 10: authz.extention.v1.CreateRoleBindingOperation + (*DeleteRoleBindingOperation)(nil), // 11: authz.extention.v1.DeleteRoleBindingOperation + (*Resource)(nil), // 12: authz.extention.v1.Resource + (*Permission)(nil), // 13: authz.extention.v1.Permission + (*TupleKey)(nil), // 14: authz.extention.v1.TupleKey + (*Tuple)(nil), // 15: authz.extention.v1.Tuple + (*TupleKeyWithoutCondition)(nil), // 16: authz.extention.v1.TupleKeyWithoutCondition + (*RelationshipCondition)(nil), // 17: authz.extention.v1.RelationshipCondition + (*ReadRequest)(nil), // 18: authz.extention.v1.ReadRequest + (*ReadRequestTupleKey)(nil), // 19: authz.extention.v1.ReadRequestTupleKey + (*ReadResponse)(nil), // 20: authz.extention.v1.ReadResponse + (*WriteRequestWrites)(nil), // 21: authz.extention.v1.WriteRequestWrites + (*WriteRequestDeletes)(nil), // 22: authz.extention.v1.WriteRequestDeletes + (*WriteRequest)(nil), // 23: authz.extention.v1.WriteRequest + (*WriteResponse)(nil), // 24: authz.extention.v1.WriteResponse + (*BatchCheckRequest)(nil), // 25: authz.extention.v1.BatchCheckRequest + (*BatchCheckItem)(nil), // 26: authz.extention.v1.BatchCheckItem + (*BatchCheckResponse)(nil), // 27: authz.extention.v1.BatchCheckResponse + (*BatchCheckGroupResource)(nil), // 28: authz.extention.v1.BatchCheckGroupResource + (*QueryRequest)(nil), // 29: authz.extention.v1.QueryRequest + (*QueryResponse)(nil), // 30: authz.extention.v1.QueryResponse + (*QueryOperation)(nil), // 31: authz.extention.v1.QueryOperation + (*GetFolderParentsQuery)(nil), // 32: authz.extention.v1.GetFolderParentsQuery + (*GetFolderParentsResult)(nil), // 33: authz.extention.v1.GetFolderParentsResult + nil, // 34: authz.extention.v1.BatchCheckResponse.GroupsEntry + nil, // 35: authz.extention.v1.BatchCheckGroupResource.ItemsEntry + (*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 37: google.protobuf.Struct + (*wrapperspb.Int32Value)(nil), // 38: google.protobuf.Int32Value } var file_extention_proto_depIdxs = []int32{ 2, // 0: authz.extention.v1.MutateRequest.operations:type_name -> authz.extention.v1.MutateOperation @@ -2258,43 +2468,45 @@ var file_extention_proto_depIdxs = []int32{ 8, // 5: authz.extention.v1.MutateOperation.update_user_org_role:type_name -> authz.extention.v1.UpdateUserOrgRoleOperation 9, // 6: authz.extention.v1.MutateOperation.delete_user_org_role:type_name -> authz.extention.v1.DeleteUserOrgRoleOperation 7, // 7: authz.extention.v1.MutateOperation.add_user_org_role:type_name -> authz.extention.v1.AddUserOrgRoleOperation - 10, // 8: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource - 11, // 9: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission - 10, // 10: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource - 11, // 11: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission - 15, // 12: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition - 12, // 13: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey - 34, // 14: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp - 35, // 15: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct - 17, // 16: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey - 36, // 17: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value - 13, // 18: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple - 12, // 19: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey - 14, // 20: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition - 19, // 21: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites - 20, // 22: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes - 24, // 23: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem - 32, // 24: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry - 33, // 25: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry - 29, // 26: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation - 31, // 27: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult - 30, // 28: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery - 26, // 29: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource - 23, // 30: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest - 16, // 31: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest - 21, // 32: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest - 0, // 33: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest - 27, // 34: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest - 25, // 35: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse - 18, // 36: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse - 22, // 37: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse - 1, // 38: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse - 28, // 39: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse - 35, // [35:40] is the sub-list for method output_type - 30, // [30:35] is the sub-list for method input_type - 30, // [30:30] is the sub-list for extension type_name - 30, // [30:30] is the sub-list for extension extendee - 0, // [0:30] is the sub-list for field type_name + 10, // 8: authz.extention.v1.MutateOperation.create_role_binding:type_name -> authz.extention.v1.CreateRoleBindingOperation + 11, // 9: authz.extention.v1.MutateOperation.delete_role_binding:type_name -> authz.extention.v1.DeleteRoleBindingOperation + 12, // 10: authz.extention.v1.CreatePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 13, // 11: authz.extention.v1.CreatePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 12, // 12: authz.extention.v1.DeletePermissionOperation.resource:type_name -> authz.extention.v1.Resource + 13, // 13: authz.extention.v1.DeletePermissionOperation.permission:type_name -> authz.extention.v1.Permission + 17, // 14: authz.extention.v1.TupleKey.condition:type_name -> authz.extention.v1.RelationshipCondition + 14, // 15: authz.extention.v1.Tuple.key:type_name -> authz.extention.v1.TupleKey + 36, // 16: authz.extention.v1.Tuple.timestamp:type_name -> google.protobuf.Timestamp + 37, // 17: authz.extention.v1.RelationshipCondition.context:type_name -> google.protobuf.Struct + 19, // 18: authz.extention.v1.ReadRequest.tuple_key:type_name -> authz.extention.v1.ReadRequestTupleKey + 38, // 19: authz.extention.v1.ReadRequest.page_size:type_name -> google.protobuf.Int32Value + 15, // 20: authz.extention.v1.ReadResponse.tuples:type_name -> authz.extention.v1.Tuple + 14, // 21: authz.extention.v1.WriteRequestWrites.tuple_keys:type_name -> authz.extention.v1.TupleKey + 16, // 22: authz.extention.v1.WriteRequestDeletes.tuple_keys:type_name -> authz.extention.v1.TupleKeyWithoutCondition + 21, // 23: authz.extention.v1.WriteRequest.writes:type_name -> authz.extention.v1.WriteRequestWrites + 22, // 24: authz.extention.v1.WriteRequest.deletes:type_name -> authz.extention.v1.WriteRequestDeletes + 26, // 25: authz.extention.v1.BatchCheckRequest.items:type_name -> authz.extention.v1.BatchCheckItem + 34, // 26: authz.extention.v1.BatchCheckResponse.groups:type_name -> authz.extention.v1.BatchCheckResponse.GroupsEntry + 35, // 27: authz.extention.v1.BatchCheckGroupResource.items:type_name -> authz.extention.v1.BatchCheckGroupResource.ItemsEntry + 31, // 28: authz.extention.v1.QueryRequest.operation:type_name -> authz.extention.v1.QueryOperation + 33, // 29: authz.extention.v1.QueryResponse.folder_parents:type_name -> authz.extention.v1.GetFolderParentsResult + 32, // 30: authz.extention.v1.QueryOperation.get_folder_parents:type_name -> authz.extention.v1.GetFolderParentsQuery + 28, // 31: authz.extention.v1.BatchCheckResponse.GroupsEntry.value:type_name -> authz.extention.v1.BatchCheckGroupResource + 25, // 32: authz.extention.v1.AuthzExtentionService.BatchCheck:input_type -> authz.extention.v1.BatchCheckRequest + 18, // 33: authz.extention.v1.AuthzExtentionService.Read:input_type -> authz.extention.v1.ReadRequest + 23, // 34: authz.extention.v1.AuthzExtentionService.Write:input_type -> authz.extention.v1.WriteRequest + 0, // 35: authz.extention.v1.AuthzExtentionService.Mutate:input_type -> authz.extention.v1.MutateRequest + 29, // 36: authz.extention.v1.AuthzExtentionService.Query:input_type -> authz.extention.v1.QueryRequest + 27, // 37: authz.extention.v1.AuthzExtentionService.BatchCheck:output_type -> authz.extention.v1.BatchCheckResponse + 20, // 38: authz.extention.v1.AuthzExtentionService.Read:output_type -> authz.extention.v1.ReadResponse + 24, // 39: authz.extention.v1.AuthzExtentionService.Write:output_type -> authz.extention.v1.WriteResponse + 1, // 40: authz.extention.v1.AuthzExtentionService.Mutate:output_type -> authz.extention.v1.MutateResponse + 30, // 41: authz.extention.v1.AuthzExtentionService.Query:output_type -> authz.extention.v1.QueryResponse + 37, // [37:42] is the sub-list for method output_type + 32, // [32:37] is the sub-list for method input_type + 32, // [32:32] is the sub-list for extension type_name + 32, // [32:32] is the sub-list for extension extendee + 0, // [0:32] is the sub-list for field type_name } func init() { file_extention_proto_init() } @@ -2310,11 +2522,13 @@ func file_extention_proto_init() { (*MutateOperation_UpdateUserOrgRole)(nil), (*MutateOperation_DeleteUserOrgRole)(nil), (*MutateOperation_AddUserOrgRole)(nil), + (*MutateOperation_CreateRoleBinding)(nil), + (*MutateOperation_DeleteRoleBinding)(nil), } - file_extention_proto_msgTypes[28].OneofWrappers = []any{ + file_extention_proto_msgTypes[30].OneofWrappers = []any{ (*QueryResponse_FolderParents)(nil), } - file_extention_proto_msgTypes[29].OneofWrappers = []any{ + file_extention_proto_msgTypes[31].OneofWrappers = []any{ (*QueryOperation_GetFolderParents)(nil), } type x struct{} @@ -2323,7 +2537,7 @@ func file_extention_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_extention_proto_rawDesc), len(file_extention_proto_rawDesc)), NumEnums: 0, - NumMessages: 34, + NumMessages: 36, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/services/authz/proto/v1/extention.proto b/pkg/services/authz/proto/v1/extention.proto index 2a11b8c5b97..80886ae13b3 100644 --- a/pkg/services/authz/proto/v1/extention.proto +++ b/pkg/services/authz/proto/v1/extention.proto @@ -34,6 +34,8 @@ message MutateOperation { UpdateUserOrgRoleOperation update_user_org_role = 5; DeleteUserOrgRoleOperation delete_user_org_role = 6; AddUserOrgRoleOperation add_user_org_role = 7; + CreateRoleBindingOperation create_role_binding = 8; + DeleteRoleBindingOperation delete_role_binding = 9; } } @@ -87,6 +89,28 @@ message DeleteUserOrgRoleOperation { string role = 2; } +message CreateRoleBindingOperation { + // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + string subject_kind = 1; + // uid of the identity + string subject_name = 2; + // kind of the role (Role/CoreRole/GlobalRole) + string role_kind = 3; + // uid of the role + string role_name = 4; +} + +message DeleteRoleBindingOperation { + // kind of the identity getting the permission (User/Team/ServiceAccount/BasicRole) + string subject_kind = 1; + // uid of the identity + string subject_name = 2; + // kind of the role (Role/CoreRole/GlobalRole) + string role_kind = 3; + // uid of the role + string role_name = 4; +} + message Resource { // group of the resource (e.g: "dashboard.grafana.app") string group = 1; diff --git a/pkg/services/authz/zanzana/server/server_mutate.go b/pkg/services/authz/zanzana/server/server_mutate.go index 3a691ed6e12..45d40df8173 100644 --- a/pkg/services/authz/zanzana/server/server_mutate.go +++ b/pkg/services/authz/zanzana/server/server_mutate.go @@ -15,6 +15,7 @@ const ( OperationGroupFolder OperationGroup = "folder" OperationGroupPermission OperationGroup = "permission" OperationGroupUserOrgRole OperationGroup = "user_org_role" + OperationGroupRoleBinding OperationGroup = "role_binding" ) func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*authzextv1.MutateResponse, error) { @@ -63,6 +64,10 @@ func (s *Server) mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au if err := s.mutateOrgRoles(ctx, storeInf, operations); err != nil { return nil, fmt.Errorf("failed to mutate org roles: %w", err) } + case OperationGroupRoleBinding: + if err := s.mutateRoleBindings(ctx, storeInf, operations); err != nil { + return nil, fmt.Errorf("failed to mutate role bindings: %w", err) + } default: s.logger.Warn("unsupported operation group", "operationGroup", operationGroup) } @@ -79,6 +84,8 @@ func getOperationGroup(operation *authzextv1.MutateOperation) (OperationGroup, e return OperationGroupPermission, nil case *authzextv1.MutateOperation_UpdateUserOrgRole, *authzextv1.MutateOperation_DeleteUserOrgRole, *authzextv1.MutateOperation_AddUserOrgRole: return OperationGroupUserOrgRole, nil + case *authzextv1.MutateOperation_CreateRoleBinding, *authzextv1.MutateOperation_DeleteRoleBinding: + return OperationGroupRoleBinding, nil } return OperationGroup(""), errors.New("unsupported mutate operation type") } diff --git a/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go b/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go new file mode 100644 index 00000000000..3b18566bee2 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_rolebindings.go @@ -0,0 +1,99 @@ +package server + +import ( + "context" + "fmt" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + zanzana "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func (s *Server) mutateRoleBindings(ctx context.Context, store *storeInfo, operations []*authzextv1.MutateOperation) error { + ctx, span := s.tracer.Start(ctx, "server.mutateRoleBindings") + defer span.End() + + writeTuples := make([]*openfgav1.TupleKey, 0) + deleteTuples := make([]*openfgav1.TupleKeyWithoutCondition, 0) + + for _, operation := range operations { + switch op := operation.Operation.(type) { + case *authzextv1.MutateOperation_CreateRoleBinding: + r := op.CreateRoleBinding + tuple, err := s.getRoleBindingTuple(ctx, r.SubjectKind, r.SubjectName, r.RoleName) + if err != nil { + return err + } + writeTuples = append(writeTuples, tuple) + case *authzextv1.MutateOperation_DeleteRoleBinding: + r := op.DeleteRoleBinding + tuple, err := s.getRoleBindingTuple(ctx, r.SubjectKind, r.SubjectName, r.RoleName) + if err != nil { + return err + } + writeTuple := &openfgav1.TupleKeyWithoutCondition{ + User: tuple.User, + Relation: tuple.Relation, + Object: tuple.Object, + } + deleteTuples = append(deleteTuples, writeTuple) + default: + s.logger.Debug("unsupported mutate operation", "operation", op) + } + } + + writeReq := &openfgav1.WriteRequest{ + StoreId: store.ID, + AuthorizationModelId: store.ModelID, + } + if len(writeTuples) > 0 { + writeReq.Writes = &openfgav1.WriteRequestWrites{ + TupleKeys: writeTuples, + OnDuplicate: "ignore", + } + } + if len(deleteTuples) > 0 { + writeReq.Deletes = &openfgav1.WriteRequestDeletes{ + TupleKeys: deleteTuples, + OnMissing: "ignore", + } + } + + _, err := s.openfga.Write(ctx, writeReq) + if err != nil { + s.logger.Error("failed to write resource role binding tuples", "error", err) + return err + } + + return nil +} + +func (s *Server) getRoleBindingTuple(ctx context.Context, subjectKind string, subjectName string, roleName string) (*openfgav1.TupleKey, error) { + zanzanaType := "" + subjectRelation := "" + + switch subjectKind { + case string(iamv0.RoleBindingSpecSubjectKindUser): + zanzanaType = zanzana.TypeUser + case string(iamv0.RoleBindingSpecSubjectKindTeam): + zanzanaType = zanzana.TypeTeam + subjectRelation = zanzana.RelationTeamMember + case string(iamv0.RoleBindingSpecSubjectKindServiceAccount): + zanzanaType = zanzana.TypeServiceAccount + case string(iamv0.RoleBindingSpecSubjectKindBasicRole): + zanzanaType = zanzana.TypeRole + subjectRelation = zanzana.RelationAssignee + default: + return nil, fmt.Errorf("invalid subject kind: %s", subjectKind) + } + + tuple := &openfgav1.TupleKey{ + User: zanzana.NewTupleEntry(zanzanaType, subjectName, subjectRelation), + Relation: zanzana.RelationAssignee, + Object: zanzana.NewTupleEntry(zanzana.TypeRole, roleName, ""), + } + + return tuple, nil +} diff --git a/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go b/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go new file mode 100644 index 00000000000..87db285c360 --- /dev/null +++ b/pkg/services/authz/zanzana/server/server_mutate_rolebindings_test.go @@ -0,0 +1,106 @@ +package server + +import ( + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "github.com/stretchr/testify/require" + + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana/common" +) + +func setupMutateRoleBindings(t *testing.T, srv *Server) *Server { + t.Helper() + + // seed tuples + tuples := []*openfgav1.TupleKey{ + common.NewTuple("user:1", common.RelationAssignee, "role:foo_viewer"), + } + + return setupOpenFGADatabase(t, srv, tuples) +} + +func testMutateRoleBindings(t *testing.T, srv *Server) { + setupMutateRoleBindings(t, srv) + + t.Run("should update user role and delete old role", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: "User", + SubjectName: "1", + RoleKind: "Role", + RoleName: "foo_editor", + }, + }, + }, + { + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: "User", + SubjectName: "1", + RoleKind: "Role", + RoleName: "foo_viewer", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:foo_editor", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "user:1", res.Tuples[0].Key.User) + + res, err = srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:foo_viewer", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 0) + }) + + t.Run("should assign role to basic role", func(t *testing.T) { + _, err := srv.Mutate(newContextWithNamespace(), &v1.MutateRequest{ + Namespace: "default", + Operations: []*v1.MutateOperation{ + { + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: "BasicRole", + SubjectName: "basic_viewer", + RoleKind: "Role", + RoleName: "foo_bar", + }, + }, + }, + }, + }) + require.NoError(t, err) + + res, err := srv.Read(newContextWithNamespace(), &v1.ReadRequest{ + Namespace: "default", + TupleKey: &v1.ReadRequestTupleKey{ + Relation: common.RelationAssignee, + Object: "role:foo_bar", + }, + }) + require.NoError(t, err) + require.Len(t, res.Tuples, 1) + require.Equal(t, "role:basic_viewer#assignee", res.Tuples[0].Key.User) + }) +} diff --git a/pkg/services/authz/zanzana/server/server_test.go b/pkg/services/authz/zanzana/server/server_test.go index df4f90cf85f..7275fd411af 100644 --- a/pkg/services/authz/zanzana/server/server_test.go +++ b/pkg/services/authz/zanzana/server/server_test.go @@ -136,6 +136,10 @@ func TestIntegrationServer(t *testing.T) { t.Run("test query folders", func(t *testing.T) { testQueryFolders(t, srv) }) + + t.Run("test mutate role bindings", func(t *testing.T) { + testMutateRoleBindings(t, srv) + }) } func setupOpenFGAServer(t *testing.T, testDB db.DB, cfg *setting.Cfg) *Server { From 136a0eb4d6037ddd3732f530365d5e8ff6cfa9dd Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:51:56 +0000 Subject: [PATCH 13/94] I18n: Download translations from Crowdin (#114456) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 16 +++++++++------- public/locales/de-DE/grafana.json | 16 +++++++++------- public/locales/es-ES/grafana.json | 16 +++++++++------- public/locales/fr-FR/grafana.json | 16 +++++++++------- public/locales/hu-HU/grafana.json | 16 +++++++++------- public/locales/id-ID/grafana.json | 16 +++++++++------- public/locales/it-IT/grafana.json | 16 +++++++++------- public/locales/ja-JP/grafana.json | 16 +++++++++------- public/locales/ko-KR/grafana.json | 16 +++++++++------- public/locales/nl-NL/grafana.json | 16 +++++++++------- public/locales/pl-PL/grafana.json | 16 +++++++++------- public/locales/pt-BR/grafana.json | 16 +++++++++------- public/locales/pt-PT/grafana.json | 16 +++++++++------- public/locales/ru-RU/grafana.json | 16 +++++++++------- public/locales/sv-SE/grafana.json | 16 +++++++++------- public/locales/tr-TR/grafana.json | 16 +++++++++------- public/locales/zh-Hans/grafana.json | 16 +++++++++------- public/locales/zh-Hant/grafana.json | 16 +++++++++------- 18 files changed, 162 insertions(+), 126 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index ac6538d3719..0a61dfbdd25 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -2648,7 +2648,9 @@ "placeholder-search-input": "Hledejte podle názvu nebo zadejte dotaz na filtr…" }, "grafana-folder": "Složka Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Kondice", "label": { "hide": "Skrýt", @@ -2659,6 +2661,7 @@ "placeholder-all-data-sources": "Všechny zdroje dat", "placeholder-contact-point": "Vyberte kontaktní bod", "placeholder-data-sources": "Vyberte zdroje dat", + "placeholder-group-search": "", "placeholder-labels": "Vyberte štítky", "plugin-rules": "Pravidla pluginu", "rule-source": { @@ -6228,8 +6231,8 @@ "placeholder-search-for": "Hledat…", "radio-options": { "label": { - "suggestions": "Návrhy", - "visualizations": "Vizualizace" + "all-visualizations": "", + "suggestions": "Návrhy" } }, "title-close": "Zavřít" @@ -8175,8 +8178,7 @@ "edit-pane": { "go-back": "Přejít zpět" } - }, - "select-group": "Vybrat skupinu" + } }, "grafana-data": { "datetime": { @@ -11144,8 +11146,8 @@ "tooltip-delete": "Odstranit" }, "visualization-suggestions": { - "based-on-current-data": "Na základě současných dat", - "no-results-matched-your-query": "Vašemu dotazu neodpovídají žádné výsledky" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Nenašly jsme žádnou shodu s vaším dotazem" diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 47445cd97ed..8da43ece8a3 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Nach Name suchen oder Filterabfrage eingeben …" }, "grafana-folder": "Grafana-Ordner", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Zustand", "label": { "hide": "Ausblenden", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Alle Datenquellen", "placeholder-contact-point": "Kontaktpunkt auswählen", "placeholder-data-sources": "Datenquellen auswählen", + "placeholder-group-search": "", "placeholder-labels": "Labels auswählen", "plugin-rules": "Plugin-Regeln", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Suche nach ...", "radio-options": { "label": { - "suggestions": "Vorschläge", - "visualizations": "Visualisierungen" + "all-visualizations": "", + "suggestions": "Vorschläge" } }, "title-close": "Schließen" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Zurückgehen" } - }, - "select-group": "Gruppe auswählen" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Löschen" }, "visualization-suggestions": { - "based-on-current-data": "Basierend auf aktuellen Daten", - "no-results-matched-your-query": "Keine passenden Ergebnisse zu Ihrer Abfrage" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Es konnte nichts gefunden werden, was Ihrer Abfrage entspricht" diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 653d7942c02..f25a23bb253 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Buscar por nombre o introducir consulta de filtro..." }, "grafana-folder": "Carpeta de Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Salud", "label": { "hide": "Ocultar", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Todas las fuentes de datos", "placeholder-contact-point": "Seleccionar punto de contacto", "placeholder-data-sources": "Seleccionar fuentes de datos", + "placeholder-group-search": "", "placeholder-labels": "Seleccionar etiquetas", "plugin-rules": "Reglas del plugin", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Buscar...", "radio-options": { "label": { - "suggestions": "Sugerencias", - "visualizations": "Visualizaciones" + "all-visualizations": "", + "suggestions": "Sugerencias" } }, "title-close": "Cerrar" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Volver" } - }, - "select-group": "Seleccionar grupo" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Eliminar" }, "visualization-suggestions": { - "based-on-current-data": "Basado en datos actuales", - "no-results-matched-your-query": "No hay resultados que coincidan con tu consulta" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "No se ha encontrado nada que coincida con tu consulta" diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index fecfc76d3ea..5d716092b57 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Rechercher par nom ou saisir une requête de filtre..." }, "grafana-folder": "Dossier Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Santé", "label": { "hide": "Masquer", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Toutes les sources de données", "placeholder-contact-point": "Sélectionner un point de contact", "placeholder-data-sources": "Sélectionner des sources de données", + "placeholder-group-search": "", "placeholder-labels": "Sélectionner des étiquettes", "plugin-rules": "Règles de plugin", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Rechercher…", "radio-options": { "label": { - "suggestions": "Suggestions", - "visualizations": "Visualisations" + "all-visualizations": "", + "suggestions": "Suggestions" } }, "title-close": "Fermer" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Retour" } - }, - "select-group": "Sélectionner un groupe" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Supprimer" }, "visualization-suggestions": { - "based-on-current-data": "Sur la base des données actuelles", - "no-results-matched-your-query": "Aucun résultat ne correspond à votre requête" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Impossible de trouver quoi que ce soit correspondant à votre requête" diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index e691d36eff2..d0dbf31ba9e 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Keresés név alapján, vagy szűrőlekérdezés megadása..." }, "grafana-folder": "Grafana mappa", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Állapot", "label": { "hide": "Elrejtés", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Összes adatforrás", "placeholder-contact-point": "Kapcsolattartási pont kiválasztása", "placeholder-data-sources": "Adatforrások kiválasztása", + "placeholder-group-search": "", "placeholder-labels": "Címkék kiválasztása", "plugin-rules": "Bővítményszabályok", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Keresés a következőre…", "radio-options": { "label": { - "suggestions": "Javaslatok", - "visualizations": "Vizualizációk" + "all-visualizations": "", + "suggestions": "Javaslatok" } }, "title-close": "Bezárás" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Visszalépés" } - }, - "select-group": "Csoport kijelölése" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Törlés" }, "visualization-suggestions": { - "based-on-current-data": "Aktuális adatok alapján", - "no-results-matched-your-query": "Nincs találat a lekérdezésre" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Nincs találat a lekérdezésre" diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index ff5ecf95800..3880215b56c 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "Cari berdasarkan nama atau masukkan kueri filter..." }, "grafana-folder": "Folder Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Kesehatan", "label": { "hide": "Sembunyikan", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "Semua sumber data", "placeholder-contact-point": "Pilih titik kontak", "placeholder-data-sources": "Pilih sumber data", + "placeholder-group-search": "", "placeholder-labels": "Pilih label", "plugin-rules": "Aturan plugin", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "Cari...", "radio-options": { "label": { - "suggestions": "Saran", - "visualizations": "Visualisasi" + "all-visualizations": "", + "suggestions": "Saran" } }, "title-close": "Tutup" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "Kembali" } - }, - "select-group": "Pilih grup" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "Hapus" }, "visualization-suggestions": { - "based-on-current-data": "Berdasarkan data saat ini", - "no-results-matched-your-query": "Tidak ada hasil yang cocok dengan kueri Anda" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Tidak dapat menemukan sesuatu yang cocok dengan kueri Anda" diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index d70278c2c40..a86adf9b336 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Cerca per nome o inserisci una query di filtro..." }, "grafana-folder": "Cartella Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Stato", "label": { "hide": "Nascondi", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Tutte le fonti dei dati", "placeholder-contact-point": "Seleziona punto di contatto", "placeholder-data-sources": "Seleziona origini dati", + "placeholder-group-search": "", "placeholder-labels": "Seleziona etichette", "plugin-rules": "Regole dei componenti aggiuntivi", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Cerca...", "radio-options": { "label": { - "suggestions": "Suggerimenti", - "visualizations": "Visualizzazioni" + "all-visualizations": "", + "suggestions": "Suggerimenti" } }, "title-close": "Chiudi" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Torna indietro" } - }, - "select-group": "Seleziona gruppo" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Elimina" }, "visualization-suggestions": { - "based-on-current-data": "In base ai dati attuali", - "no-results-matched-your-query": "Nessun risultato corrisponde alla tua query" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Non è stato possibile trovare nulla che corrispondesse alla tua query" diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 9667ae7ea41..42aa76d2642 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "名前で検索するか、フィルタークエリを入力してください…" }, "grafana-folder": "Grafanaフォルダ", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "健康", "label": { "hide": "非表示", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "すべてのデータソース", "placeholder-contact-point": "コンタクトポイントを選択してください", "placeholder-data-sources": "データソースを選択してください", + "placeholder-group-search": "", "placeholder-labels": "ラベルを選択してください", "plugin-rules": "プラグインルール", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "次を検索...", "radio-options": { "label": { - "suggestions": "提案", - "visualizations": "可視化" + "all-visualizations": "", + "suggestions": "提案" } }, "title-close": "閉じる" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "戻る" } - }, - "select-group": "グループを選択" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "削除" }, "visualization-suggestions": { - "based-on-current-data": "現在のデータに基づく", - "no-results-matched-your-query": "クエリに一致する結果がありません" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "クエリに一致するものが見つかりませんでした" diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 115f653a526..437982e410b 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "이름으로 검색하거나 필터 쿼리를 입력하세요..." }, "grafana-folder": "Grafana 폴더", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "상태", "label": { "hide": "숨기기", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "모든 데이터 소스", "placeholder-contact-point": "연락처 선택", "placeholder-data-sources": "데이터 소스 선택", + "placeholder-group-search": "", "placeholder-labels": "레이블 선택", "plugin-rules": "플러그인 규칙", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "검색…", "radio-options": { "label": { - "suggestions": "제안", - "visualizations": "시각화" + "all-visualizations": "", + "suggestions": "제안" } }, "title-close": "닫기" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "뒤로 가기" } - }, - "select-group": "그룹 선택" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "삭제" }, "visualization-suggestions": { - "based-on-current-data": "현재 데이터 기반", - "no-results-matched-your-query": "쿼리와 일치하는 결과가 없습니다" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "쿼리와 일치하는 항목을 찾을 수 없습니다" diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index f1236285a6b..994f52eb9e4 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Zoeken op naam of filterquery invoeren..." }, "grafana-folder": "Grafana-map", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Gezondheid", "label": { "hide": "Verbergen", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Alle gegevensbronnen", "placeholder-contact-point": "Contactpunt selecteren", "placeholder-data-sources": "Gegevensbronnen selecteren", + "placeholder-group-search": "", "placeholder-labels": "Labels selecteren", "plugin-rules": "Plug-inregels", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Zoeken naar...", "radio-options": { "label": { - "suggestions": "Suggesties", - "visualizations": "Visualisaties" + "all-visualizations": "", + "suggestions": "Suggesties" } }, "title-close": "Sluiten" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Ga terug" } - }, - "select-group": "Groep selecteren" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Verwijderen" }, "visualization-suggestions": { - "based-on-current-data": "Op basis van huidige gegevens", - "no-results-matched-your-query": "Geen resultaten gevonden voor je query" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Kon niets vinden dat overeenkomt met je query" diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 1a5897fd7ed..8cafca8a198 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -2648,7 +2648,9 @@ "placeholder-search-input": "Szukaj według nazwy lub wprowadź wyrażenie do filtrowania…" }, "grafana-folder": "Folder Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Zdrowie", "label": { "hide": "Ukryj", @@ -2659,6 +2661,7 @@ "placeholder-all-data-sources": "Wszystkie źródła danych", "placeholder-contact-point": "Wybierz punkt kontaktowy", "placeholder-data-sources": "Wybierz źródła danych", + "placeholder-group-search": "", "placeholder-labels": "Wybierz etykiety", "plugin-rules": "Reguły dotyczące wtyczek", "rule-source": { @@ -6228,8 +6231,8 @@ "placeholder-search-for": "Szukaj…", "radio-options": { "label": { - "suggestions": "Sugestie", - "visualizations": "Wizualizacje" + "all-visualizations": "", + "suggestions": "Sugestie" } }, "title-close": "Zamknij" @@ -8175,8 +8178,7 @@ "edit-pane": { "go-back": "Wróć" } - }, - "select-group": "Wybierz grupę" + } }, "grafana-data": { "datetime": { @@ -11144,8 +11146,8 @@ "tooltip-delete": "Usuń" }, "visualization-suggestions": { - "based-on-current-data": "Na podstawie aktualnych danych", - "no-results-matched-your-query": "Nie znaleziono wyników pasujących do zapytania" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Nie znaleziono wyników pasujących do Twojego zapytania" diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 419d550fe09..1151ca293a1 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Pesquise por nome ou insira uma consulta de filtro…" }, "grafana-folder": "Pasta do Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Integridade", "label": { "hide": "Ocultar", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Todas as fontes de dados", "placeholder-contact-point": "Selecionar ponto de contato", "placeholder-data-sources": "Selecionar fontes de dados", + "placeholder-group-search": "", "placeholder-labels": "Selecionar rótulos", "plugin-rules": "Regras de plug-in", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Pesquisar por…", "radio-options": { "label": { - "suggestions": "Sugestões", - "visualizations": "Visualizações" + "all-visualizations": "", + "suggestions": "Sugestões" } }, "title-close": "Fechar" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Voltar" } - }, - "select-group": "Selecionar grupo" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Excluir" }, "visualization-suggestions": { - "based-on-current-data": "Com base nos dados atuais", - "no-results-matched-your-query": "Nenhum resultado correspondeu à sua consulta" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Não foi possível encontrar nada que corresponda à sua consulta" diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 28b728aee0f..856c8b78a3e 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Pesquise por nome ou introduza uma consulta de filtro..." }, "grafana-folder": "Pasta Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Saúde", "label": { "hide": "Ocultar", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Todas as origens de dados", "placeholder-contact-point": "Selecionar ponto de contacto", "placeholder-data-sources": "Selecionar origens de dados", + "placeholder-group-search": "", "placeholder-labels": "Selecionar etiquetas", "plugin-rules": "Regras de plugin", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Pesquisar por...", "radio-options": { "label": { - "suggestions": "Sugestões", - "visualizations": "Visualizações" + "all-visualizations": "", + "suggestions": "Sugestões" } }, "title-close": "Fechar" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Voltar" } - }, - "select-group": "Selecionar grupo" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Eliminar" }, "visualization-suggestions": { - "based-on-current-data": "Com base nos dados atuais", - "no-results-matched-your-query": "Não existem resultados que correspondam à sua consulta" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Não foi possível encontrar nada que corresponda à sua consulta" diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 1d05879899a..50c67543a33 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -2648,7 +2648,9 @@ "placeholder-search-input": "Выполните поиск по имени или введите запрос фильтра..." }, "grafana-folder": "Папка Grafana", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Работоспособность", "label": { "hide": "Скрыть", @@ -2659,6 +2661,7 @@ "placeholder-all-data-sources": "Все источники данных", "placeholder-contact-point": "Выбрать точку контакта", "placeholder-data-sources": "Выбрать источники данных", + "placeholder-group-search": "", "placeholder-labels": "Выбрать метки", "plugin-rules": "Правила плагинов", "rule-source": { @@ -6228,8 +6231,8 @@ "placeholder-search-for": "Поиск...", "radio-options": { "label": { - "suggestions": "Предложения", - "visualizations": "Визуализации" + "all-visualizations": "", + "suggestions": "Предложения" } }, "title-close": "Закрыть" @@ -8175,8 +8178,7 @@ "edit-pane": { "go-back": "Назад" } - }, - "select-group": "Выбрать группу" + } }, "grafana-data": { "datetime": { @@ -11144,8 +11146,8 @@ "tooltip-delete": "Удалить" }, "visualization-suggestions": { - "based-on-current-data": "На основе текущих данных", - "no-results-matched-your-query": "По вашему запросу ничего не найдено" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Не удалось найти данные, соответствующие вашему запросу" diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 95e2ae6130e..f88845db555 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "Sök efter namn eller ange filterfråga …" }, "grafana-folder": "Grafana-mapp", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Hälsa", "label": { "hide": "Dölj", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Alla datakällor", "placeholder-contact-point": "Välj kontaktpunkt", "placeholder-data-sources": "Välj datakällor", + "placeholder-group-search": "", "placeholder-labels": "Välj etiketter", "plugin-rules": "Tilläggsregler", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Sök efter …", "radio-options": { "label": { - "suggestions": "Förslag", - "visualizations": "Visualiseringar" + "all-visualizations": "", + "suggestions": "Förslag" } }, "title-close": "Stäng" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Tillbaka" } - }, - "select-group": "Markera grupp" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Ta bort" }, "visualization-suggestions": { - "based-on-current-data": "Baserat på aktuella data", - "no-results-matched-your-query": "Inga träffar matchade din sökfråga" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Kunde inte hitta något som matchar din fråga" diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 16e0520c9a7..574351940a4 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -2624,7 +2624,9 @@ "placeholder-search-input": "" }, "grafana-folder": "", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "Sağlık", "label": { "hide": "Gizle", @@ -2635,6 +2637,7 @@ "placeholder-all-data-sources": "Tüm veri kaynakları", "placeholder-contact-point": "", "placeholder-data-sources": "", + "placeholder-group-search": "", "placeholder-labels": "", "plugin-rules": "Eklenti kuralları", "rule-source": { @@ -6182,8 +6185,8 @@ "placeholder-search-for": "Ara...", "radio-options": { "label": { - "suggestions": "Öneriler", - "visualizations": "Görselleştirmeler" + "all-visualizations": "", + "suggestions": "Öneriler" } }, "title-close": "Kapat" @@ -8123,8 +8126,7 @@ "edit-pane": { "go-back": "Geri dön" } - }, - "select-group": "" + } }, "grafana-data": { "datetime": { @@ -11050,8 +11052,8 @@ "tooltip-delete": "Sil" }, "visualization-suggestions": { - "based-on-current-data": "Mevcut verilere göre", - "no-results-matched-your-query": "Sorgunuza uygun sonuç bulunamadı" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "Sorgunuzla eşleşen hiçbir şey bulunamadı" diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 4dccf320255..67b617cc3b8 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "按名称搜索或输入筛选条件查询……" }, "grafana-folder": "Grafana 文件夹", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "健康", "label": { "hide": "隐藏", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "所有数据源", "placeholder-contact-point": "选择联络点", "placeholder-data-sources": "选择数据源", + "placeholder-group-search": "", "placeholder-labels": "选择标签", "plugin-rules": "插件规则", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "搜索...", "radio-options": { "label": { - "suggestions": "建议", - "visualizations": "可视化" + "all-visualizations": "", + "suggestions": "建议" } }, "title-close": "关闭" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "返回" } - }, - "select-group": "选择组" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "删除" }, "visualization-suggestions": { - "based-on-current-data": "基于当前数据", - "no-results-matched-your-query": "没有符合您查询的结果" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "找不到与您的查询匹配的内容" diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index dec7715e1fd..a387547aab8 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -2612,7 +2612,9 @@ "placeholder-search-input": "按名稱搜尋或輸入篩選條件查詢…" }, "grafana-folder": "Grafana 資料夾", - "group-autocomplete-unavailable": "", + "group-no-results": "", + "group-search-error": "", + "group-search-prompt": "", "health": "使用情況", "label": { "hide": "隱藏", @@ -2623,6 +2625,7 @@ "placeholder-all-data-sources": "所有資料來源", "placeholder-contact-point": "選擇聯絡點", "placeholder-data-sources": "選擇資料來源", + "placeholder-group-search": "", "placeholder-labels": "選擇標籤", "plugin-rules": "外掛程式規則", "rule-source": { @@ -6159,8 +6162,8 @@ "placeholder-search-for": "搜尋…", "radio-options": { "label": { - "suggestions": "建議", - "visualizations": "視覺化" + "all-visualizations": "", + "suggestions": "建議" } }, "title-close": "關閉" @@ -8097,8 +8100,7 @@ "edit-pane": { "go-back": "返回" } - }, - "select-group": "選擇群組" + } }, "grafana-data": { "datetime": { @@ -11003,8 +11005,8 @@ "tooltip-delete": "刪除" }, "visualization-suggestions": { - "based-on-current-data": "根據目前的資料", - "no-results-matched-your-query": "沒有符合您查詢的結果" + "apply-suggestion-aria-label": "", + "use-this-suggestion": "" }, "viz-type-picker": { "could-anything-matching-query": "找不到符合您查詢的內容" From 513b81a531b395197c9ddd10f59ad50f2b3592ea Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:15:58 +0100 Subject: [PATCH 14/94] Scopes: Sync navigation scope and apply subScopes (#114083) * Set navigationScope if we have a subScope * Proper URL sync * Update unit test with dashboards service subscription handling * Add a bunch of tests * Update functionality to change scopes when clicking on icon * Add test for TreeFolderItem * Udpate test and remove errors * Fix issues in test * Use ScopeNavgiations by default in the ScopesDashboardService unit tests * Remove misplaced test --- .../app/features/scopes/ScopesService.test.ts | 168 ++++++++-- public/app/features/scopes/ScopesService.ts | 15 + .../ScopesDashboardsService.test.ts | 184 +++++++++-- .../dashboards/ScopesDashboardsService.ts | 13 + .../dashboards/ScopesDashboardsTree.tsx | 4 +- .../ScopesDashboardsTreeFolderItem.test.tsx | 287 ++++++++++++++++++ .../ScopesDashboardsTreeFolderItem.tsx | 10 +- .../ScopesNavigationTreeLink.test.tsx | 175 +++++++++++ .../dashboards/ScopesNavigationTreeLink.tsx | 51 +++- .../selector/ScopesSelectorService.test.ts | 30 ++ .../scopes/selector/ScopesSelectorService.ts | 20 +- 11 files changed, 905 insertions(+), 52 deletions(-) create mode 100644 public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.test.tsx diff --git a/public/app/features/scopes/ScopesService.test.ts b/public/app/features/scopes/ScopesService.test.ts index 61f85257dcf..6b360f853e3 100644 --- a/public/app/features/scopes/ScopesService.test.ts +++ b/public/app/features/scopes/ScopesService.test.ts @@ -14,15 +14,22 @@ describe('ScopesService', () => { let selectorService: jest.Mocked; let dashboardsService: jest.Mocked; let locationService: jest.Mocked; - let stateSubscription: + let selectorStateSubscription: | (( state: { appliedScopes: Array<{ scopeId: string; scopeNodeId?: string; parentNodeId?: string }> }, prevState: { appliedScopes: Array<{ scopeId: string; scopeNodeId?: string; parentNodeId?: string }> } ) => void) | undefined; + let dashboardsStateSubscription: + | (( + state: { navigationScope?: string; drawerOpened: boolean }, + prevState: { navigationScope?: string; drawerOpened: boolean } + ) => void) + | undefined; beforeEach(() => { - stateSubscription = undefined; + selectorStateSubscription = undefined; + dashboardsStateSubscription = undefined; selectorService = { state: { @@ -46,7 +53,7 @@ describe('ScopesService', () => { tree: { scopeNodeId: '', expanded: false, query: '', children: {} }, }), subscribeToState: jest.fn((callback) => { - stateSubscription = callback; + selectorStateSubscription = callback; return { unsubscribe: jest.fn() }; }), changeScopes: jest.fn(), @@ -63,6 +70,7 @@ describe('ScopesService', () => { forScopeNames: [], loading: false, searchQuery: '', + navigationScope: undefined, }, stateObservable: new BehaviorSubject({ drawerOpened: false, @@ -73,7 +81,13 @@ describe('ScopesService', () => { forScopeNames: [], loading: false, searchQuery: '', + navigationScope: undefined, }), + subscribeToState: jest.fn((callback) => { + dashboardsStateSubscription = callback; + return { unsubscribe: jest.fn() }; + }), + setNavigationScope: jest.fn(), } as unknown as jest.Mocked; locationService = { @@ -160,6 +174,40 @@ describe('ScopesService', () => { expect(selectorService.changeScopes).toHaveBeenCalledWith(['scope1', 'scope2'], undefined, 'node1', false); }); + + it('should read navigation_scope from URL on init', () => { + locationService.getLocation = jest.fn().mockReturnValue({ + pathname: '/test', + search: '?scopes=scope1&navigation_scope=navScope1', + }); + + service = new ScopesService(selectorService, dashboardsService, locationService); + + expect(dashboardsService.setNavigationScope).toHaveBeenCalledWith('navScope1'); + }); + + it('should read navigation_scope along with other scope parameters', () => { + locationService.getLocation = jest.fn().mockReturnValue({ + pathname: '/test', + search: '?scopes=scope1&scope_node=node1&navigation_scope=navScope1', + }); + + service = new ScopesService(selectorService, dashboardsService, locationService); + + expect(dashboardsService.setNavigationScope).toHaveBeenCalledWith('navScope1'); + expect(selectorService.changeScopes).toHaveBeenCalledWith(['scope1'], undefined, 'node1', false); + }); + + it('should not call setNavigationScope when navigation_scope is not in URL', () => { + locationService.getLocation = jest.fn().mockReturnValue({ + pathname: '/test', + search: '?scopes=scope1', + }); + + service = new ScopesService(selectorService, dashboardsService, locationService); + + expect(dashboardsService.setNavigationScope).not.toHaveBeenCalled(); + }); }); describe('URL synchronization', () => { @@ -172,11 +220,11 @@ describe('ScopesService', () => { }); it('should write scope_node to URL when scopes change', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1' }], }, @@ -196,11 +244,11 @@ describe('ScopesService', () => { }); it('should reset scope_parent to null when writing URL', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1', parentNodeId: 'parent1' }], }, @@ -218,11 +266,11 @@ describe('ScopesService', () => { }); it('should handle scopeNodeId changes without scope changes', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node2' }], }, @@ -242,11 +290,11 @@ describe('ScopesService', () => { }); it('should handle missing scopeNodeId gracefully', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1' }], }, @@ -266,13 +314,13 @@ describe('ScopesService', () => { }); it('should not update URL when scopes and scopeNodeId have not changed', () => { - if (!stateSubscription) { - throw new Error('stateSubscription not set'); + if (!selectorStateSubscription) { + throw new Error('selectorStateSubscription not set'); } jest.clearAllMocks(); - stateSubscription( + selectorStateSubscription( { appliedScopes: [{ scopeId: 'scope1', scopeNodeId: 'node1' }], }, @@ -283,6 +331,90 @@ describe('ScopesService', () => { expect(locationService.partial).not.toHaveBeenCalled(); }); + + it('should write navigation_scope to URL when navigationScope changes', () => { + if (!dashboardsStateSubscription) { + throw new Error('dashboardsStateSubscription not set'); + } + + dashboardsStateSubscription( + { + navigationScope: 'navScope1', + drawerOpened: true, + }, + { + navigationScope: undefined, + drawerOpened: false, + } + ); + + expect(locationService.partial).toHaveBeenCalledWith({ + navigation_scope: 'navScope1', + }); + }); + + it('should update navigation_scope in URL when navigationScope changes', () => { + if (!dashboardsStateSubscription) { + throw new Error('dashboardsStateSubscription not set'); + } + + dashboardsStateSubscription( + { + navigationScope: 'navScope2', + drawerOpened: true, + }, + { + navigationScope: 'navScope1', + drawerOpened: true, + } + ); + + expect(locationService.partial).toHaveBeenCalledWith({ + navigation_scope: 'navScope2', + }); + }); + + it('should not update URL when navigationScope has not changed', () => { + if (!dashboardsStateSubscription) { + throw new Error('dashboardsStateSubscription not set'); + } + + jest.clearAllMocks(); + + dashboardsStateSubscription( + { + navigationScope: 'navScope1', + drawerOpened: true, + }, + { + navigationScope: 'navScope1', + drawerOpened: false, + } + ); + + expect(locationService.partial).not.toHaveBeenCalled(); + }); + + it('should clear navigation_scope from URL when navigationScope is cleared', () => { + if (!dashboardsStateSubscription) { + throw new Error('dashboardsStateSubscription not set'); + } + + dashboardsStateSubscription( + { + navigationScope: undefined, + drawerOpened: false, + }, + { + navigationScope: 'navScope1', + drawerOpened: true, + } + ); + + expect(locationService.partial).toHaveBeenCalledWith({ + navigation_scope: undefined, + }); + }); }); describe('setEnabled', () => { diff --git a/public/app/features/scopes/ScopesService.ts b/public/app/features/scopes/ScopesService.ts index 2595ff66876..36edcc94b13 100644 --- a/public/app/features/scopes/ScopesService.ts +++ b/public/app/features/scopes/ScopesService.ts @@ -73,6 +73,11 @@ export class ScopesService implements ScopesContextValue { const scopeNodeId = queryParams.get('scope_node'); // TODO: figure out when to remove this. scope_parent is for backward compatibility only const parentNodeId = queryParams.get('scope_parent'); + const navigationScope = queryParams.get('navigation_scope'); + + if (navigationScope) { + this.dashboardsService.setNavigationScope(navigationScope); + } this.changeScopes(queryParams.getAll('scopes'), parentNodeId ?? undefined, scopeNodeId ?? undefined); @@ -133,6 +138,16 @@ export class ScopesService implements ScopesContextValue { } }) ); + // Update the URL based on change in the navigation scope + this.subscriptions.push( + this.dashboardsService.subscribeToState((state, prevState) => { + if (state.navigationScope !== prevState.navigationScope) { + this.locationService.partial({ + navigation_scope: state.navigationScope, + }); + } + }) + ); } /** diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts b/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts index 62b3b71e09d..52368b4577d 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts +++ b/public/app/features/scopes/dashboards/ScopesDashboardsService.test.ts @@ -1,7 +1,6 @@ import { Location } from 'history'; import { Subject } from 'rxjs'; -import { ScopeDashboardBinding } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { ScopesApiClient } from '../ScopesApiClient'; @@ -15,7 +14,7 @@ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), config: { featureToggles: { - useScopesNavigationEndpoint: false, + useScopesNavigationEndpoint: true, }, apps: {}, }, @@ -33,27 +32,32 @@ describe('ScopesDashboardsService', () => { let mockApiClient: jest.Mocked; beforeEach(() => { + const fetchScopeNavigationsMock = jest.fn().mockResolvedValue([]); mockApiClient = { fetchDashboards: jest.fn(), - fetchScopeNavigations: jest.fn(), + fetchScopeNavigations: fetchScopeNavigationsMock, } as unknown as jest.Mocked; service = new ScopesDashboardsService(mockApiClient); }); + afterEach(() => { + config.featureToggles.useScopesNavigationEndpoint = true; + }); + describe('folder expansion based on location', () => { it('should expand folders when current location matches dashboard ID', async () => { // Mock current location to be a dashboard (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/d/dashboard1' } as Location); - const mockDashboards: ScopeDashboardBinding[] = [ + const mockNavigations: ScopeNavigation[] = [ { spec: { scope: 'scope1', - dashboard: 'dashboard1', + url: '/d/dashboard1', }, status: { - dashboardTitle: 'Test Dashboard', + title: 'Test Dashboard', groups: ['group1'], }, metadata: { @@ -62,7 +66,7 @@ describe('ScopesDashboardsService', () => { }, ]; - mockApiClient.fetchDashboards.mockResolvedValue(mockDashboards); + mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations); await service.fetchDashboards(['scope1']); // Verify that the folder is expanded because the current dashboard ID matches @@ -168,14 +172,14 @@ describe('ScopesDashboardsService', () => { // Mock current location to not match any navigation (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/different-path' } as Location); - const mockDashboards: ScopeDashboardBinding[] = [ + const mockNavigations: ScopeNavigation[] = [ { spec: { scope: 'scope1', - dashboard: 'dashboard1', + url: '/d/dashboard1', }, status: { - dashboardTitle: 'Test Dashboard', + title: 'Test Dashboard', groups: ['group1'], }, metadata: { @@ -184,7 +188,7 @@ describe('ScopesDashboardsService', () => { }, ]; - mockApiClient.fetchDashboards.mockResolvedValue(mockDashboards); + mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations); await service.fetchDashboards(['scope1']); // Verify that the folder is not expanded because the current location doesn't match @@ -197,14 +201,14 @@ describe('ScopesDashboardsService', () => { pathname: '/d/dashboard1/very-important', } as Location); - const mockDashboards: ScopeDashboardBinding[] = [ + const mockNavigations: ScopeNavigation[] = [ { spec: { scope: 'scope1', - dashboard: 'dashboard1', + url: '/d/dashboard1', }, status: { - dashboardTitle: 'Test Dashboard', + title: 'Test Dashboard', groups: ['group1'], }, metadata: { @@ -213,7 +217,7 @@ describe('ScopesDashboardsService', () => { }, ]; - mockApiClient.fetchDashboards.mockResolvedValue(mockDashboards); + mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations); await service.fetchDashboards(['scope1']); // Verify that the folder is expanded because the current path starts with the dashboard ID @@ -224,14 +228,14 @@ describe('ScopesDashboardsService', () => { // Mock current location to be a specific dashboard (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/d/dashboard1' } as Location); - const mockDashboards: ScopeDashboardBinding[] = [ + const mockNavigations: ScopeNavigation[] = [ { spec: { scope: 'scope1', - dashboard: 'dashboard1', + url: '/d/dashboard1', }, status: { - dashboardTitle: 'Test Dashboard', + title: 'Test Dashboard', groups: ['group1'], }, metadata: { @@ -241,10 +245,10 @@ describe('ScopesDashboardsService', () => { { spec: { scope: 'scope1', - dashboard: 'dashboard2', + url: '/d/dashboard2', }, status: { - dashboardTitle: 'Another Dashboard', + title: 'Another Dashboard', groups: ['group2'], }, metadata: { @@ -253,7 +257,7 @@ describe('ScopesDashboardsService', () => { }, ]; - mockApiClient.fetchDashboards.mockResolvedValue(mockDashboards); + mockApiClient.fetchScopeNavigations.mockResolvedValue(mockNavigations); await service.fetchDashboards(['scope1']); // Verify that only the folder containing the current dashboard is expanded @@ -697,4 +701,142 @@ describe('ScopesDashboardsService', () => { expect(filteredItems.some((item) => item.metadata.name === 'loki-item-1')).toBe(true); }); }); + + describe('setNavigationScope', () => { + beforeEach(() => { + (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/' } as Location); + // Reset mocks but keep the mock functions + mockApiClient.fetchDashboards.mockClear(); + // Note: fetchScopeNavigations mock is set up in top-level beforeEach + // Individual tests will override it with their own mockResolvedValue calls + }); + + it('should set navigation scope and fetch dashboards', async () => { + // Mock non-empty results so drawerOpened stays true after fetchDashboards completes + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'navScope1', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + + await service.setNavigationScope('navScope1'); + + expect(service.state.navigationScope).toBe('navScope1'); + expect(service.state.drawerOpened).toBe(true); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['navScope1']); + }); + + it('should clear navigation scope and use fallback scope names', async () => { + // Mock non-empty results so drawerOpened stays true after fetchDashboards completes + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'fallbackScope1', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + // Set an initial navigation scope + await service.setNavigationScope('initialScope'); + expect(service.state.navigationScope).toBe('initialScope'); + expect(service.state.drawerOpened).toBe(true); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['initialScope']); + + await service.setNavigationScope(undefined, ['fallbackScope1', 'fallbackScope2']); + + expect(service.state.navigationScope).toBeUndefined(); + expect(service.state.drawerOpened).toBe(true); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['fallbackScope1', 'fallbackScope2']); + }); + + it('should not run if previous and next navigation scopes are undefined and we provide fallback scope names', async () => { + await service.setNavigationScope(undefined, ['fallbackScope1', 'fallbackScope2']); + expect(service.state.navigationScope).toBeUndefined(); + expect(service.state.drawerOpened).toBe(false); + expect(mockApiClient.fetchScopeNavigations).not.toHaveBeenCalled(); + }); + + it('should close drawer when navigation scope is cleared without fallback', async () => { + // When setNavigationScope is called with undefined and no fallback, + // it calls fetchDashboards([]), which returns early without calling the API + await service.setNavigationScope(undefined); + + expect(service.state.navigationScope).toBeUndefined(); + expect(service.state.drawerOpened).toBe(false); + // fetchDashboards([]) returns early, so API client is not called + expect(mockApiClient.fetchScopeNavigations).not.toHaveBeenCalled(); + }); + + it('should not update state if navigation scope has not changed', async () => { + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'navScope1', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + await service.setNavigationScope('navScope1'); + mockApiClient.fetchScopeNavigations.mockClear(); + + await service.setNavigationScope('navScope1'); + + expect(mockApiClient.fetchScopeNavigations).not.toHaveBeenCalled(); + }); + + it('should update navigation scope when changing from one scope to another', async () => { + mockApiClient.fetchScopeNavigations.mockResolvedValue([]); + + await service.setNavigationScope('navScope1'); + mockApiClient.fetchScopeNavigations.mockClear(); + + await service.setNavigationScope('navScope2'); + + expect(service.state.navigationScope).toBe('navScope2'); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['navScope2']); + }); + + it('should update navigation scope when clearing an existing scope', async () => { + mockApiClient.fetchScopeNavigations.mockResolvedValue([]); + + await service.setNavigationScope('navScope1'); + mockApiClient.fetchScopeNavigations.mockClear(); + + await service.setNavigationScope(undefined, ['fallbackScope']); + + expect(service.state.navigationScope).toBeUndefined(); + expect(mockApiClient.fetchScopeNavigations).toHaveBeenCalledWith(['fallbackScope']); + }); + + it('should open drawer when navigation scope is set', async () => { + // Mock to return non-empty results so drawer stays open + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'navScope1', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + + await service.setNavigationScope('navScope1'); + + expect(service.state.drawerOpened).toBe(true); + }); + + it('should open drawer when fallback scopes are provided', async () => { + // Mock non-empty results so drawerOpened stays true after fetchDashboards completes + mockApiClient.fetchScopeNavigations.mockResolvedValue([ + { + spec: { scope: 'fallbackScope', url: '/d/dashboard1' }, + status: { title: 'Test', groups: [] }, + metadata: { name: 'dashboard1' }, + }, + ]); + await service.setNavigationScope('initialScope'); + + await service.setNavigationScope(undefined, ['fallbackScope']); + + expect(service.state.drawerOpened).toBe(true); + }); + }); }); diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts index be6f966623e..55ea5ab737a 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts +++ b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts @@ -23,6 +23,7 @@ interface ScopesDashboardsServiceState { forScopeNames: string[]; loading: boolean; searchQuery: string; + navigationScope?: string; } export class ScopesDashboardsService extends ScopesServiceBase { @@ -56,6 +57,18 @@ export class ScopesDashboardsService extends ScopesServiceBase { + if (this.state.navigationScope === navigationScope) { + return; + } + + const forScopeNames = navigationScope ? [navigationScope] : (fallbackScopeNames ?? []); + this.updateState({ navigationScope, drawerOpened: forScopeNames.length > 0 }); + await this.fetchDashboards(forScopeNames); + }; + // Expand the group that matches the current path, if it is not already expanded private onLocationChange = (pathname: string) => { if (!this.state.drawerOpened) { diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx b/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx index b25a3bfc55b..7e7601f3b06 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx +++ b/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx @@ -9,12 +9,13 @@ import { ScopesNavigationTreeLink } from './ScopesNavigationTreeLink'; import { OnFolderUpdate, SuggestedNavigationsFoldersMap } from './types'; export interface ScopesDashboardsTreeProps { + subScope?: string; folders: SuggestedNavigationsFoldersMap; folderPath: string[]; onFolderUpdate: OnFolderUpdate; } -export function ScopesDashboardsTree({ folders, folderPath, onFolderUpdate }: ScopesDashboardsTreeProps) { +export function ScopesDashboardsTree({ subScope, folders, folderPath, onFolderUpdate }: ScopesDashboardsTreeProps) { const [queryParams] = useQueryParams(); const styles = useStyles2(getStyles); @@ -52,6 +53,7 @@ export function ScopesDashboardsTree({ folders, folderPath, onFolderUpdate }: Sc ))} {regularNavigations.map((navigation) => ( ({ + useQueryParams: jest.fn(() => [{}]), +})); + +// Mock ScopesContextProvider +const mockScopesSelectorService = { + changeScopes: jest.fn(), +}; + +const mockScopesDashboardsService = { + setNavigationScope: jest.fn(), +}; + +jest.mock('../ScopesContextProvider', () => ({ + ...jest.requireActual('../ScopesContextProvider'), + useScopesServices: jest.fn(() => ({ + scopesSelectorService: mockScopesSelectorService, + scopesDashboardsService: mockScopesDashboardsService, + })), +})); + +describe('ScopesDashboardsTreeFolderItem', () => { + const mockOnFolderUpdate = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const createMockFolder = (overrides?: Partial): SuggestedNavigationsFolder => ({ + title: 'Test Folder', + expanded: false, + folders: {}, + suggestedNavigations: {}, + ...overrides, + }); + + const createMockFolders: SuggestedNavigationsFoldersMap = { + '': { + title: '', + expanded: true, + folders: {}, + suggestedNavigations: {}, + }, + }; + + it('renders folder with correct props', () => { + const folder = createMockFolder(); + render( + + ); + + expect(screen.getByText('Test Folder')).toBeInTheDocument(); + expect(screen.getByTestId('scopes-dashboards-Test Folder-expand')).toBeInTheDocument(); + }); + + it('calls onFolderUpdate when expand button is clicked', async () => { + const user = userEvent.setup(); + const folder = createMockFolder({ expanded: false }); + + render( + + ); + + const expandButton = screen.getByTestId('scopes-dashboards-Test Folder-expand'); + await user.click(expandButton); + + expect(mockOnFolderUpdate).toHaveBeenCalledWith([''], true); + }); + + it('shows exchange icon when folder has subScopeName', () => { + const folder = createMockFolder({ subScopeName: 'subScope1' }); + + render( + + ); + + // IconButton with exchange-alt icon should be present + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + expect(exchangeButton).toBeInTheDocument(); + }); + + it('does not show exchange icon when folder does not have subScopeName', () => { + const folder = createMockFolder(); + + render( + + ); + + const exchangeButtons = screen.queryAllByRole('button', { name: /change root scope/i }); + expect(exchangeButtons).toHaveLength(0); + }); + + it('calls setNavigationScope when exchange icon is clicked', async () => { + const user = userEvent.setup(); + const folder = createMockFolder({ subScopeName: 'subScope1' }); + + render( + + ); + + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + await user.click(exchangeButton); + + expect(mockScopesDashboardsService.setNavigationScope).toHaveBeenCalledWith(undefined, ['subScope1']); + }); + + it('calls changeScopes when exchange icon is clicked', async () => { + const user = userEvent.setup(); + const folder = createMockFolder({ subScopeName: 'subScope1' }); + + render( + + ); + + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + await user.click(exchangeButton); + + expect(mockScopesSelectorService.changeScopes).toHaveBeenCalledWith(['subScope1']); + }); + + it('passes subScope prop to ScopesDashboardsTree when folder is expanded', () => { + const folder = createMockFolder({ expanded: true, subScopeName: 'subScope1' }); + const childFolders: SuggestedNavigationsFoldersMap = { + '': { + title: '', + expanded: true, + folders: { + childFolder: { + title: 'Child Folder', + expanded: false, + folders: {}, + suggestedNavigations: {}, + }, + }, + suggestedNavigations: {}, + }, + childFolder: { + title: 'Child Folder', + expanded: false, + folders: {}, + suggestedNavigations: {}, + }, + }; + + render( + + ); + + // ScopesDashboardsTree should be rendered when folder is expanded + // We can verify this by checking that the children container is present + const childrenContainer = screen.getByText('Test Folder').closest('div')?.nextSibling; + expect(childrenContainer).toBeInTheDocument(); + }); + + it('does not render ScopesDashboardsTree when folder is not expanded', () => { + const folder = createMockFolder({ expanded: false, subScopeName: 'subScope1' }); + + render( + + ); + + // When not expanded, the children container should not be visible + // The structure should only show the folder row + expect(screen.getByText('Test Folder')).toBeInTheDocument(); + }); + + it('prevents default and stops propagation when exchange icon is clicked', async () => { + const folder = createMockFolder({ subScopeName: 'subScope1' }); + const preventDefault = jest.fn(); + const stopPropagation = jest.fn(); + + render( + + ); + + const exchangeButton = screen.getByRole('button', { name: /change root scope/i }); + const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); + Object.defineProperty(clickEvent, 'preventDefault', { value: preventDefault }); + Object.defineProperty(clickEvent, 'stopPropagation', { value: stopPropagation }); + + exchangeButton.dispatchEvent(clickEvent); + + // The onClick handler should prevent default and stop propagation + // Note: userEvent.click doesn't trigger preventDefault/stopPropagation directly, + // but the handler should call them. We verify the handler was called correctly. + expect(mockScopesDashboardsService.setNavigationScope).toHaveBeenCalled(); + expect(stopPropagation).toHaveBeenCalled(); + expect(preventDefault).toHaveBeenCalled(); + }); + + it('does not call setNavigationScope when subScopeName is missing', async () => { + const folder = createMockFolder({ subScopeName: undefined }); + + render( + + ); + + // No exchange button should be present, so no click should trigger setNavigationScope + expect(mockScopesDashboardsService.setNavigationScope).not.toHaveBeenCalled(); + // Check precense of exchange button + const exchangeButton = screen.queryByRole('button', { name: /change root scope/i }); + expect(exchangeButton).not.toBeInTheDocument(); + }); + + it('does not call setNavigationScope when scopesSelectorService is not available', async () => { + const user = userEvent.setup(); + const folder = createMockFolder({ subScopeName: 'subScope1' }); + + // Mock useScopesServices to return undefined + jest.spyOn(require('../ScopesContextProvider'), 'useScopesServices').mockReturnValue(undefined); + + render( + + ); + + const exchangeButton = screen.queryByRole('button', { name: /change root scope/i }); + if (exchangeButton) { + await user.click(exchangeButton); + } + + // Should not crash, but also should not call setNavigationScope if service is not available + // The component checks for scopesSelectorService existence before calling setNavigationScope + expect(mockScopesDashboardsService.setNavigationScope).not.toHaveBeenCalled(); + }); +}); diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx b/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx index da1f315c5e2..9d572a4d54c 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx +++ b/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx @@ -26,7 +26,7 @@ export function ScopesDashboardsTreeFolderItem({ // get scopesselector service const scopesSelectorService = useScopesServices()?.scopesSelectorService ?? undefined; - + const scopesDashboardsService = useScopesServices()?.scopesDashboardsService ?? undefined; return (
@@ -57,6 +57,7 @@ export function ScopesDashboardsTreeFolderItem({ e.preventDefault(); e.stopPropagation(); if (folder.subScopeName && scopesSelectorService) { + scopesDashboardsService?.setNavigationScope(undefined, [folder.subScopeName]); scopesSelectorService.changeScopes([folder.subScopeName]); } }} @@ -66,7 +67,12 @@ export function ScopesDashboardsTreeFolderItem({ {folder.expanded && (
- +
)}
diff --git a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.test.tsx b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.test.tsx index 3aba827d4ef..8dafc80c612 100644 --- a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.test.tsx +++ b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.test.tsx @@ -1,6 +1,9 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { MemoryRouter, useLocation } from 'react-router-dom-v5-compat'; +import { locationService } from '@grafana/runtime'; + import { ScopesNavigationTreeLink } from './ScopesNavigationTreeLink'; // Mock react-router-dom's useLocation @@ -9,15 +12,50 @@ jest.mock('react-router-dom-v5-compat', () => ({ useLocation: jest.fn(), })); +// Mock @grafana/runtime +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + locationService: { + push: jest.fn(), + }, +})); + +// Mock ScopesContextProvider +const mockScopesSelectorService = { + state: { + appliedScopes: [{ scopeId: 'currentScope' }], + }, + changeScopes: jest.fn(), +}; + +const mockScopesDashboardsService = { + state: { + navigationScope: undefined as string | undefined, + }, + setNavigationScope: jest.fn(), +}; + +jest.mock('../ScopesContextProvider', () => ({ + ...jest.requireActual('../ScopesContextProvider'), + useScopesServices: jest.fn(() => ({ + scopesSelectorService: mockScopesSelectorService, + scopesDashboardsService: mockScopesDashboardsService, + })), +})); + const renderWithRouter = (ui: React.ReactElement) => { return render({ui}); }; describe('ScopesNavigationTreeLink', () => { const mockUseLocation = useLocation as jest.Mock; + const mockLocationServicePush = locationService.push as jest.Mock; beforeEach(() => { mockUseLocation.mockReturnValue({ pathname: '/current-path' }); + jest.clearAllMocks(); + mockScopesSelectorService.state.appliedScopes = [{ scopeId: 'currentScope' }]; + mockScopesDashboardsService.state.navigationScope = undefined; }); afterEach(() => { @@ -133,4 +171,141 @@ describe('ScopesNavigationTreeLink', () => { expect(icon).toBeInTheDocument(); expect(link).toHaveTextContent('Metrics Drilldown'); }); + + describe('click handler with subScope', () => { + beforeEach(() => { + // Mock window.location.origin + Object.defineProperty(window, 'location', { + value: { + origin: 'http://localhost', + }, + writable: true, + }); + }); + + it('should prevent default navigation when subScope is provided', async () => { + const user = userEvent.setup(); + + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await user.click(link); + + // Should call changeScopes instead of navigating normally + expect(mockScopesSelectorService.changeScopes).toHaveBeenCalled(); + expect(mockLocationServicePush).toHaveBeenCalled(); + }); + + it('should set navigation scope from current scope when not already set', async () => { + mockScopesDashboardsService.state.navigationScope = undefined; + mockScopesSelectorService.state.appliedScopes = [{ scopeId: 'currentScope' }]; + + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + expect(mockScopesDashboardsService.setNavigationScope).toHaveBeenCalledWith('currentScope'); + }); + + it('should not set navigation scope when already set', async () => { + mockScopesDashboardsService.state.navigationScope = 'existingNavScope'; + mockScopesSelectorService.state.appliedScopes = [{ scopeId: 'currentScope' }]; + + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + // Should not call setNavigationScope with currentScope since it's already set + expect(mockScopesDashboardsService.setNavigationScope).not.toHaveBeenCalledWith('currentScope'); + }); + + it('should call changeScopes with subScope', async () => { + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + expect(mockScopesSelectorService.changeScopes).toHaveBeenCalledWith(['subScope1'], undefined, undefined, false); + }); + + it('should navigate to URL with updated query params', async () => { + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + expect(mockLocationServicePush).toHaveBeenCalled(); + const pushedUrl = mockLocationServicePush.mock.calls[0][0]; + expect(pushedUrl).toContain('/test-path'); + expect(pushedUrl).toContain('scopes=subScope1'); + expect(pushedUrl).toContain('navigation_scope=currentScope'); + }); + + it('should remove scope_node and scope_parent from URL when navigating with subScope', async () => { + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await userEvent.click(link); + + expect(mockLocationServicePush).toHaveBeenCalled(); + const pushedUrl = mockLocationServicePush.mock.calls[0][0]; + expect(pushedUrl).not.toContain('scope_node'); + expect(pushedUrl).not.toContain('scope_parent'); + }); + + it('should allow normal navigation when subScope is not provided', async () => { + const user = userEvent.setup(); + + renderWithRouter(); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await user.click(link); + + // Should not call changeScopes or locationService.push when subScope is not provided + expect(mockScopesSelectorService.changeScopes).not.toHaveBeenCalled(); + expect(mockLocationServicePush).not.toHaveBeenCalled(); + }); + + it('should handle URL with existing query params correctly', async () => { + const user = userEvent.setup(); + mockScopesDashboardsService.state.navigationScope = undefined; + mockScopesSelectorService.state.appliedScopes = [{ scopeId: 'currentScope' }]; + + renderWithRouter( + + ); + + const link = screen.getByTestId('scopes-dashboards-test-id'); + await user.click(link); + + expect(mockLocationServicePush).toHaveBeenCalled(); + const pushedUrl = mockLocationServicePush.mock.calls[0][0]; + expect(pushedUrl).toContain('scopes=subScope1'); + expect(pushedUrl).toContain('navigation_scope=currentScope'); + }); + }); }); diff --git a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx index 859b6059059..b7722edcf8c 100644 --- a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx +++ b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx @@ -2,31 +2,76 @@ import { css, cx } from '@emotion/css'; import { useMemo } from 'react'; import { Link, useLocation } from 'react-router-dom-v5-compat'; -import { GrafanaTheme2, IconName, locationUtil } from '@grafana/data'; +import { GrafanaTheme2, IconName, locationUtil, UrlQueryMap, urlUtil } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; import { Icon, useStyles2 } from '@grafana/ui'; +import { useScopesServices } from '../ScopesContextProvider'; + import { isCurrentPath, normalizePath } from './scopeNavgiationUtils'; export interface ScopesNavigationTreeLinkProps { + subScope?: string; to: string; title: string; id: string; } -export function ScopesNavigationTreeLink({ to, title, id }: ScopesNavigationTreeLinkProps) { +export function ScopesNavigationTreeLink({ subScope, to, title, id }: ScopesNavigationTreeLinkProps) { const styles = useStyles2(getStyles); const linkIcon = useMemo(() => getLinkIcon(to), [to]); const locPathname = useLocation().pathname; - + const services = useScopesServices(); // Ignore query params const isCurrent = isCurrentPath(locPathname, to); + const handleClick = (e: React.MouseEvent) => { + if (subScope) { + e.preventDefault(); // Prevent default Link navigation + + // Set current scope to navigation scope and subScope to scope + const currentScope = services?.scopesSelectorService?.state.appliedScopes[0]?.scopeId; + const currentNavigationScope = services?.scopesDashboardsService?.state.navigationScope; + + // Parse the URL to extract path and existing query params + const url = new URL(to, window.location.origin); + const pathname = url.pathname; + const searchParams = new URLSearchParams(url.search); + if (!currentNavigationScope && currentScope) { + searchParams.set('navigation_scope', currentScope); + services?.scopesDashboardsService?.setNavigationScope(currentScope); + } + + // Update query params with the new subScope + searchParams.set('scopes', subScope); + // Remove scope_node and scope_parent since we're changing to a subScope + searchParams.delete('scope_node'); + searchParams.delete('scope_parent'); + + // Convert URLSearchParams to query map object for urlUtil.renderUrl + const queryMap: UrlQueryMap = {}; + searchParams.forEach((value, key) => { + queryMap[key] = value; + }); + + // Build the new URL safely using urlUtil.renderUrl + const newUrl = urlUtil.renderUrl(pathname, queryMap); + + // Change scopes first (this updates the state) + services?.scopesSelectorService?.changeScopes([subScope], undefined, undefined, false); + + // Then navigate to the URL with updated query params + locationService.push(newUrl); + } + }; + return ( diff --git a/public/app/features/scopes/selector/ScopesSelectorService.test.ts b/public/app/features/scopes/selector/ScopesSelectorService.test.ts index eef594d2335..57ff735f019 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.test.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.test.ts @@ -73,6 +73,7 @@ describe('ScopesSelectorService', () => { dashboardsService = { fetchDashboards: jest.fn().mockResolvedValue(undefined), + setNavigationScope: jest.fn(), state: { scopeNavigations: [], dashboards: [], @@ -82,6 +83,7 @@ describe('ScopesSelectorService', () => { forScopeNames: [], loading: false, searchQuery: '', + navigationScope: undefined, }, } as unknown as jest.Mocked; @@ -395,6 +397,34 @@ describe('ScopesSelectorService', () => { await service.removeAllScopes(); expect(service.state.appliedScopes).toEqual([]); }); + + it('should clear navigation scope when removing all scopes', async () => { + await service.updateNode('', true, ''); + await service.selectScope('test-scope-node'); + await service.apply(); + await service.removeAllScopes(); + expect(dashboardsService.setNavigationScope).toHaveBeenCalledWith(undefined); + }); + }); + + describe('navigation scope interaction', () => { + it('should skip fetchDashboards when navigationScope is set', async () => { + dashboardsService.state.navigationScope = 'navScope1'; + jest.clearAllMocks(); + + await service.changeScopes(['test-scope']); + + expect(dashboardsService.fetchDashboards).not.toHaveBeenCalled(); + }); + + it('should call fetchDashboards when navigationScope is not set', async () => { + dashboardsService.state.navigationScope = undefined; + jest.clearAllMocks(); + + await service.changeScopes(['test-scope']); + + expect(dashboardsService.fetchDashboards).toHaveBeenCalledWith(['test-scope']); + }); }); describe('getRecentScopes', () => { diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index 742a77ad5d4..14a16be69ac 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -371,12 +371,15 @@ export class ScopesSelectorService extends ScopesServiceBase s.scopeId)).then(() => { - const selectedScopeNode = scopes[0]?.scopeNodeId ? this.state.nodes[scopes[0]?.scopeNodeId] : undefined; - if (redirectOnApply) { - this.redirectAfterApply(selectedScopeNode); - } - }); + // Only fetch dashboards based on the scopes if we don't have a navigation scope set. + if (!this.dashboardsService.state.navigationScope) { + this.dashboardsService.fetchDashboards(scopes.map((s) => s.scopeId)).then(() => { + const selectedScopeNode = scopes[0]?.scopeNodeId ? this.state.nodes[scopes[0]?.scopeNodeId] : undefined; + if (redirectOnApply) { + this.redirectAfterApply(selectedScopeNode); + } + }); + } if (scopes.length > 0) { const fetchedScopes = await this.apiClient.fetchMultipleScopes(scopes.map((s) => s.scopeId)); @@ -431,7 +434,10 @@ export class ScopesSelectorService extends ScopesServiceBase this.applyScopes([], false); + public removeAllScopes = () => { + this.applyScopes([], false); + this.dashboardsService.setNavigationScope(undefined); + }; private addRecentScopes = (scopes: Scope[], parentNode?: ScopeNode) => { if (scopes.length === 0) { From 5538dfe73dfdfa51947c62cf7f6df0f338057e12 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:17:51 +0100 Subject: [PATCH 15/94] Alerting: Add RBAC for enrichment (#113296) * wip * wip 2 * prettier * fix tests * address pr feedback * address pr feedback 2 * address review comments * update useEnrichmentAbilities changing AlwaysSupported with onfig.featureToggles.alertEnrichmentfix --- .../components/rule-viewer/AlertRuleMenu.tsx | 7 +- .../rule-viewer/RuleViewer.test.tsx | 44 ++++++ .../components/rules/RulesTable.test.tsx | 3 + .../rule-view-page/extensions.tsx | 41 +++--- .../rule-view-page/navigation.test.ts | 10 +- .../unified/hooks/useAbilities.test.tsx | 126 ++++++++++++++++++ .../alerting/unified/hooks/useAbilities.ts | 31 ++++- public/app/types/accessControl.ts | 4 + 8 files changed, 244 insertions(+), 22 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx index a36a1fada67..8834fd72580 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx @@ -13,7 +13,9 @@ import { PromAlertingRuleState, RulerRuleDTO } from 'app/types/unified-alerting- import { AlertRuleAction, + EnrichmentAction, skipToken, + useEnrichmentAbility, useGrafanaPromRuleAbilities, useRulerRuleAbilities, } from '../../hooks/useAbilities'; @@ -112,6 +114,8 @@ const AlertRuleMenu = ({ const extensionsAvailable = ruleExtensionLinks.length > 0; + const [enrichmentReadSupported, enrichmentReadAllowed] = useEnrichmentAbility(EnrichmentAction.Read); + /** * Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana. * We should show it in development mode @@ -139,7 +143,8 @@ const AlertRuleMenu = ({ ruleUid && handleManageEnrichments && config.featureToggles.alertingEnrichmentPerRule && - config.featureToggles.alertEnrichment; + enrichmentReadSupported && + enrichmentReadAllowed; const menuItems = ( <> diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx index 253418c3135..9e91bd34178 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx @@ -8,6 +8,10 @@ import { AlertManagerDataSourceJsonData } from 'app/plugins/datasource/alertmana import { AccessControlAction } from 'app/types/accessControl'; import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting'; +import { + __clearRuleViewTabsForTests, + addEnrichmentSection, +} from '../../enterprise-components/rule-view-page/extensions'; import { getCloudRule, getGrafanaRule, @@ -86,6 +90,18 @@ const openSilenceDrawer = async () => { await screen.findByText(/Configure silences/i); }; +beforeAll(() => { + // Register the enrichment tab for all tests + addEnrichmentSection(); +}); + +afterEach(() => { + // Clear tabs after each test to prevent interference + __clearRuleViewTabsForTests(); + // Re-register for next test + addEnrichmentSection(); +}); + beforeEach(() => { grantPermissionsHelper([ AccessControlAction.AlertingRuleCreate, @@ -484,6 +500,34 @@ describe('RuleViewer', () => { ); expect(screen.getByTestId('enrichment-section')).toBeInTheDocument(); }); + + it('should show enrichment tab when user has enrichments:read permission', async () => { + grantPermissionsHelper([AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingEnrichmentsRead]); + + await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Query); + + // Check if enrichment tab exists in the navigation + expect(screen.getByText('Alert enrichment')).toBeInTheDocument(); + }); + + it('should hide enrichment tab when user lacks enrichments:read permission', async () => { + grantPermissionsHelper([AccessControlAction.AlertingRuleRead]); + + await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Query); + + // Check that enrichment tab does not exist in the navigation + expect(screen.queryByText('Alert enrichment')).not.toBeInTheDocument(); + }); + + it('should show enrichment tab for admin users', async () => { + grantPermissionsHelper([AccessControlAction.AlertingRuleRead]); + jest.spyOn(require('../../utils/misc'), 'isAdmin').mockReturnValue(true); + + await renderRuleViewer(mockRule, mockRuleIdentifier, ActiveTab.Query); + + // Admin should see the tab even without explicit permission + expect(screen.getByText('Alert enrichment')).toBeInTheDocument(); + }); }); }); diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx index a4be47bbf81..61c143ab843 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx @@ -7,6 +7,7 @@ import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { AlertRuleAction, useAlertRuleAbility, + useEnrichmentAbility, useGrafanaPromRuleAbilities, useGrafanaPromRuleAbility, useRulerRuleAbilities, @@ -29,6 +30,7 @@ const mocks = { useGrafanaPromRuleAbility: jest.mocked(useGrafanaPromRuleAbility), useRulerRuleAbilities: jest.mocked(useRulerRuleAbilities), useGrafanaPromRuleAbilities: jest.mocked(useGrafanaPromRuleAbilities), + useEnrichmentAbility: jest.mocked(useEnrichmentAbility), }; setPluginLinksHook(() => ({ @@ -63,6 +65,7 @@ describe('RulesTable RBAC', () => { mocks.useAlertRuleAbility.mockReturnValue([false, false]); mocks.useRulerRuleAbility.mockReturnValue([false, false]); mocks.useGrafanaPromRuleAbility.mockReturnValue([false, false]); + mocks.useEnrichmentAbility.mockReturnValue([false, false]); // Plural hooks (used by AlertRuleMenu) - need to return arrays based on input actions mocks.useRulerRuleAbilities.mockImplementation((_rule, _groupIdentifier, actions) => { diff --git a/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx b/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx index a4d5034e103..bc366ec1cfe 100644 --- a/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx +++ b/public/app/features/alerting/unified/enterprise-components/rule-view-page/extensions.tsx @@ -5,6 +5,7 @@ import { t } from '@grafana/i18n'; import { FeatureBadge, useStyles2 } from '@grafana/ui'; import { useAlertRule } from '../../components/rule-viewer/RuleContext'; +import { EnrichmentAction, useEnrichmentAbility } from '../../hooks/useAbilities'; import { rulerRuleType } from '../../utils/rules'; type SetActiveTab = (tab: string) => void; @@ -14,7 +15,7 @@ type RuleViewTabBuilderArgs = { setActiveTab: SetActiveTab; }; -type RuleViewTabBuilder = (args: RuleViewTabBuilderArgs) => NavModelItem; +type RuleViewTabBuilder = (args: RuleViewTabBuilderArgs) => NavModelItem | null; type RuleViewTabBuilderConfig = { filterOnlyGrafanaAlertRules: boolean; ruleViewTabBuilder: RuleViewTabBuilder; @@ -29,21 +30,35 @@ function registerRuleViewTab(builder: RuleViewTabBuilder) { }); } -export function useRuleViewExtensionTabs(args: RuleViewTabBuilderArgs): NavModelItem[] { - const { rule } = useAlertRule(); - const isGrafanaAlertRule = rulerRuleType.grafana.alertingRule(rule.rulerRule); +export function getRuleViewExtensionTabs(args: RuleViewTabBuilderArgs, isGrafanaAlertRule: boolean): NavModelItem[] { return ruleViewTabBuilders .filter((config) => { - if (config.filterOnlyGrafanaAlertRules) { - return isGrafanaAlertRule; + // Check if rule type matches requirement + if (config.filterOnlyGrafanaAlertRules && !isGrafanaAlertRule) { + return false; } return true; }) - .map((config) => config.ruleViewTabBuilder(args)); + .map((config) => config.ruleViewTabBuilder(args)) + .filter((item): item is NavModelItem => item !== null); +} + +export function useRuleViewExtensionTabs(args: RuleViewTabBuilderArgs): NavModelItem[] { + const { rule } = useAlertRule(); + const isGrafanaAlertRule = rulerRuleType.grafana.alertingRule(rule.rulerRule); + + return getRuleViewExtensionTabs(args, isGrafanaAlertRule); } export function addEnrichmentSection() { registerRuleViewTab(({ activeTab, setActiveTab }) => { + const [, canReadEnrichments] = useEnrichmentAbility(EnrichmentAction.Read); + + // Return null if user doesn't have permission (will be filtered out) + if (!canReadEnrichments) { + return null; + } + const tabId = 'enrichment'; return { text: t('alerting.use-page-nav.page-nav.text.enrichment', 'Alert enrichment'), @@ -59,18 +74,6 @@ export function __clearRuleViewTabsForTests() { ruleViewTabBuilders.splice(0, ruleViewTabBuilders.length); } -// ONLY FOR TESTS: non-hook version for testing -export function getRuleViewExtensionTabs(args: RuleViewTabBuilderArgs, isGrafanaAlertRule: boolean): NavModelItem[] { - return ruleViewTabBuilders - .filter((config) => { - if (config.filterOnlyGrafanaAlertRules) { - return isGrafanaAlertRule; - } - return true; - }) - .map((config) => config.ruleViewTabBuilder(args)); -} - function getStyles() { return { tabSuffix: css({ diff --git a/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts b/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts index 9fdaa2a74d9..b4ea7312d86 100644 --- a/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts +++ b/public/app/features/alerting/unified/enterprise-components/rule-view-page/navigation.test.ts @@ -1,12 +1,20 @@ -import { beforeEach, describe, expect, it } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import { addRulePageEnrichmentSection } from '../../components/rule-viewer/tabs/extensions/RuleViewerExtension'; +import { useEnrichmentAbility } from '../../hooks/useAbilities'; import { __clearRuleViewTabsForTests, addEnrichmentSection, getRuleViewExtensionTabs } from './extensions'; +jest.mock('../../hooks/useAbilities'); + +const mocks = { + useEnrichmentAbility: jest.mocked(useEnrichmentAbility), +}; + describe('rule-view-page navigation', () => { beforeEach(() => { __clearRuleViewTabsForTests(); + mocks.useEnrichmentAbility.mockReturnValue([false, true]); }); it('does not include Alert enrichment tab when not registered', () => { diff --git a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx index 3fdd05e6c63..b08d31a631c 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.test.tsx +++ b/public/app/features/alerting/unified/hooks/useAbilities.test.tsx @@ -1,6 +1,7 @@ import { PropsWithChildren } from 'react'; import { getWrapper, render, renderHook, screen, waitFor } from 'test/test-utils'; +import { config } from '@grafana/runtime'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setFolderAccessControl } from 'app/features/alerting/unified/mocks/server/configure'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; @@ -10,16 +11,21 @@ import { CombinedRule } from 'app/types/unified-alerting'; import { getCloudRule, getGrafanaRule, grantUserPermissions, mockDataSource } from '../mocks'; import { AlertmanagerProvider } from '../state/AlertmanagerContext'; +import { grantPermissionsHelper } from '../test/test-utils'; import { setupDataSources } from '../testSetup/datasources'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import * as misc from '../utils/misc'; import { AlertRuleAction, AlertmanagerAction, + EnrichmentAction, useAlertmanagerAbilities, useAlertmanagerAbility, useAllAlertRuleAbilities, useAllAlertmanagerAbilities, + useEnrichmentAbilities, + useEnrichmentAbility, } from './useAbilities'; /** @@ -213,6 +219,126 @@ describe('AlertRule abilities', () => { }); }); +describe('enrichment abilities', () => { + setupMswServer(); + + const originalFeatureToggle = config.featureToggles.alertEnrichment; + + beforeEach(() => { + // Default to feature toggle enabled + config.featureToggles.alertEnrichment = true; + }); + + afterEach(() => { + config.featureToggles.alertEnrichment = originalFeatureToggle; + }); + + it('should grant read and write permissions to admin users when feature is enabled', () => { + grantPermissionsHelper([]); + jest.spyOn(misc, 'isAdmin').mockReturnValue(true); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(true); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(true); + }); + + it('should grant read permission when user has enrichments:read permission', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(true); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(false); + }); + + it('should grant write permission when user has enrichments:write permission', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsWrite]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(false); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(true); + }); + + it('should grant both read and write permissions when user has both permissions', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead, AccessControlAction.AlertingEnrichmentsWrite]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(true); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(true); + }); + + it('should deny all permissions when user is not admin and has no permissions', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + expect(readSupported).toBe(true); + expect(readAllowed).toBe(false); + expect(writeSupported).toBe(true); + expect(writeAllowed).toBe(false); + }); + + it('should return correct ability for specific action using useEnrichmentAbility', () => { + jest.spyOn(misc, 'isAdmin').mockReturnValue(false); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead]); + + const { result } = renderHook(() => useEnrichmentAbility(EnrichmentAction.Read), { wrapper: wrapper() }); + + const [supported, allowed] = result.current; + + expect(supported).toBe(true); + expect(allowed).toBe(true); + }); + + it('should report enrichments as not supported when feature toggle is disabled', () => { + config.featureToggles.alertEnrichment = false; + jest.spyOn(misc, 'isAdmin').mockReturnValue(true); + grantPermissionsHelper([AccessControlAction.AlertingEnrichmentsRead, AccessControlAction.AlertingEnrichmentsWrite]); + + const { result } = renderHook(() => useEnrichmentAbilities(), { wrapper: wrapper() }); + + const [readSupported, readAllowed] = result.current[EnrichmentAction.Read]; + const [writeSupported, writeAllowed] = result.current[EnrichmentAction.Write]; + + // Enrichments not supported when feature toggle is off + expect(readSupported).toBe(false); + expect(writeSupported).toBe(false); + // Permissions would be granted if it were supported + expect(readAllowed).toBe(true); + expect(writeAllowed).toBe(true); + }); +}); + function createAlertmanagerWrapper(alertmanagerSourceName: string) { const ProviderWrapper = getWrapper({ renderWithRouter: true }); const wrapperComponent = (props: PropsWithChildren) => ( diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 5635a3d666a..535d99d0bc0 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; +import { config } from '@grafana/runtime'; import { contextSrv as ctx } from 'app/core/services/context_srv'; import { PERMISSIONS_CONTACT_POINTS_READ } from 'app/features/alerting/unified/components/contact-points/permissions'; import { @@ -96,6 +97,12 @@ export enum AlertRuleAction { DeletePermanently = 'delete-alert-rule-permanently', } +// this enum lists all of the available actions we can perform with enrichments +export enum EnrichmentAction { + Read = 'read-enrichment', + Write = 'write-enrichment', +} + // this enum list all of the bulk actions we can perform on a folder export enum FolderBulkAction { Pause = 'pause-folder', // unpause permissions are the same as pause @@ -124,7 +131,7 @@ export enum AlertingAction { const AlwaysSupported = true; const NotSupported = false; -export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction | FolderBulkAction; +export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction | FolderBulkAction | EnrichmentAction; /** * Represents the ability to perform an action, with two distinct checks: @@ -189,6 +196,28 @@ export const useAlertingAbility = (action: AlertingAction): Ability => { return allAbilities[action]; }; +/** + * This one will check for enrichment abilities + */ +export const useEnrichmentAbilities = (): Abilities => { + const userIsAdmin = isAdmin(); + const hasReadPermission = ctx.hasPermission(AccessControlAction.AlertingEnrichmentsRead); + const hasWritePermission = ctx.hasPermission(AccessControlAction.AlertingEnrichmentsWrite); + + // Enrichments are only available when the feature toggle is enabled + const enrichmentsSupported = Boolean(config.featureToggles.alertEnrichment); + + return { + [EnrichmentAction.Read]: [enrichmentsSupported, userIsAdmin || hasReadPermission], + [EnrichmentAction.Write]: [enrichmentsSupported, userIsAdmin || hasWritePermission], + }; +}; + +export const useEnrichmentAbility = (action: EnrichmentAction): Ability => { + const allAbilities = useEnrichmentAbilities(); + return allAbilities[action]; +}; + /** * This hook will check if we support the action and have sufficient permissions for it on a single alert rule */ diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index abbf8969b05..d09459b9f9b 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -155,6 +155,10 @@ export enum AccessControlAction { AlertingTemplatesWrite = 'alert.notifications.templates:write', AlertingTemplatesDelete = 'alert.notifications.templates:delete', + // Alerting enrichments actions + AlertingEnrichmentsRead = 'alert.enrichments:read', + AlertingEnrichmentsWrite = 'alert.enrichments:write', + PluginsInstall = 'plugins:install', PluginsWrite = 'plugins:write', From 291e3ea9cfe2ca0826c57c3ebe9e93149106ae13 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Wed, 26 Nov 2025 12:00:15 +0100 Subject: [PATCH 16/94] Logs: Persist sort order in the Explore URL (#114350) * Logs: store sort order in the URL * ToolbarExtensionPoint: pass sort order to extension * Logs: send sort order in links * ToolbarExtensionPoint: pass panelState instead of sortOrder * Update test * Remove condition * Logs: initialize sort order and remove unnecessary check --- packages/grafana-data/src/types/explore.ts | 3 +- public/app/features/explore/Logs/Logs.tsx | 59 ++++++++++++------- .../extensions/ToolbarExtensionPoint.test.tsx | 25 +++++++- .../extensions/ToolbarExtensionPoint.tsx | 21 +++++-- 4 files changed, 77 insertions(+), 31 deletions(-) diff --git a/packages/grafana-data/src/types/explore.ts b/packages/grafana-data/src/types/explore.ts index c3755fa954f..71fd7eae35d 100644 --- a/packages/grafana-data/src/types/explore.ts +++ b/packages/grafana-data/src/types/explore.ts @@ -1,4 +1,4 @@ -import { DataQuery } from '@grafana/schema'; +import { DataQuery, LogsSortOrder } from '@grafana/schema'; import { PreferredVisualisationType } from './data'; import { SelectableValue } from './select'; @@ -84,6 +84,7 @@ export interface ExploreLogsPanelState { // Used for logs table visualisation, contains the refId of the dataFrame that is currently visualized refId?: string; displayedFields?: string[]; + sortOrder?: LogsSortOrder; } export interface SplitOpenOptions { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 02b63224e76..b958aebcc62 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -197,7 +197,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const [dedupStrategy, setDedupStrategy] = useState(LogsDedupStrategy.none); const [logsSortOrder, setLogsSortOrder] = useState( - store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending + panelState?.logs?.sortOrder ?? store.get(SETTINGS_KEYS.logsSortOrder) ?? LogsSortOrder.Descending ); const [isFlipping, setIsFlipping] = useState(false); const [displayedFields, setDisplayedFields] = useState(panelState?.logs?.displayedFields ?? []); @@ -269,6 +269,18 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { } }, [dispatch, exploreId, loading, panelState, previousLoading]); + useEffect(() => { + // Initialize URL sort order + if (!panelState?.logs?.sortOrder) { + dispatch( + changePanelState(exploreId, 'logs', { + ...panelState, + sortOrder: logsSortOrder, + }) + ); + } + }, [dispatch, exploreId, logsSortOrder, panelState]); + useEffect(() => { const visualisationType = panelState?.logs?.visualisationType ?? getDefaultVisualisationType(); setVisualisationType(visualisationType); @@ -287,23 +299,17 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { useUnmount(() => { // If we're unmounting logs (e.g. switching to another datasource), we need to remove the logs specific panel state, otherwise it will persist in the explore url - if ( - panelState?.logs?.columns || - panelState?.logs?.refId || - panelState?.logs?.labelFieldName || - panelState?.logs?.displayedFields - ) { - dispatch( - changePanelState(exploreId, 'logs', { - ...panelState?.logs, - columns: undefined, - visualisationType: visualisationType, - labelFieldName: undefined, - refId: undefined, - displayedFields: undefined, - }) - ); - } + dispatch( + changePanelState(exploreId, 'logs', { + ...panelState?.logs, + columns: undefined, + visualisationType: visualisationType, + labelFieldName: undefined, + refId: undefined, + displayedFields: undefined, + sortOrder: undefined, + }) + ); }); const updatePanelState = useCallback( @@ -398,8 +404,14 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { dispatch(changeQueries({ exploreId, queries: newQueries })); dispatch(runQueries({ exploreId })); } + dispatch( + changePanelState(exploreId, 'logs', { + ...panelState?.logs, + sortOrder: newSortOrder, + }) + ); }, - [dispatch, exploreId, logsQueries] + [dispatch, exploreId, logsQueries, panelState?.logs] ); const onChangeLogsSortOrder = useCallback( @@ -588,7 +600,12 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { const urlState = getUrlStateFromPaneState(getState().explore.panes[exploreId]!); urlState.panelsState = { ...panelState, - logs: { id: row.uid, visualisationType: visualisationType ?? getDefaultVisualisationType(), displayedFields }, + logs: { + id: row.uid, + visualisationType: visualisationType ?? getDefaultVisualisationType(), + displayedFields, + sortOrder: logsSortOrder, + }, }; urlState.range = getLogsPermalinkRange(row, logRows, absoluteRange); @@ -604,7 +621,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { logRowLevel: row.logLevel, }); }, - [absoluteRange, displayedFields, exploreId, logRows, panelState, visualisationType] + [absoluteRange, displayedFields, exploreId, logRows, logsSortOrder, panelState, visualisationType] ); const scrollToTopLogs = useCallback(() => { diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx index d3988d68e95..6fd93785099 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx @@ -3,9 +3,9 @@ import userEvent from '@testing-library/user-event'; import { ReactNode } from 'react'; import { Provider } from 'react-redux'; -import { PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data'; +import { ExplorePanelsState, PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data'; import { usePluginLinks } from '@grafana/runtime'; -import { DataQuery } from '@grafana/schema'; +import { DataQuery, LogsSortOrder } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; import { ExplorePanelData, ExploreState } from 'app/types/explore'; @@ -27,13 +27,14 @@ const usePluginLinksMock = jest.mocked(usePluginLinks); type storeOptions = { targets: DataQuery[]; data: ExplorePanelData; + panelsState?: ExplorePanelsState; }; function renderWithExploreStore( children: ReactNode, options: storeOptions = { targets: [{ refId: 'A' }], data: createEmptyQueryResponse() } ) { - const { targets, data } = options; + const { targets, data, panelsState } = options; const store = configureStore({ explore: { panes: { @@ -43,6 +44,7 @@ function renderWithExploreStore( range: { raw: { from: 'now-1h', to: 'now' }, }, + panelsState, }, }, } as unknown as ExploreState, @@ -90,6 +92,9 @@ describe('ToolbarExtensionPoint', () => { isLoading: false, }); }); + beforeEach(() => { + jest.mocked(usePluginLinksMock).mockClear(); + }); it('should render "Add" extension point menu button', () => { renderWithExploreStore(setupToolbarExtensionPoint()); @@ -180,6 +185,20 @@ describe('ToolbarExtensionPoint', () => { expect(extensionPointId).toBe(PluginExtensionPoints.ExploreToolbarAction); }); + + it('should pass panelsState to the extensions', async () => { + const panelsState: ExplorePanelsState = { + logs: { sortOrder: LogsSortOrder.Ascending, displayedFields: ['time', 'body'] }, + }; + const targets = [{ refId: 'A' }]; + const data = createEmptyQueryResponse(); + renderWithExploreStore(setupToolbarExtensionPoint(), { targets, data, panelsState }); + + const [options] = usePluginLinksMock.mock.calls[0]; + const { context } = options; + + expect(context).toHaveProperty('panelsState', panelsState); + }); }); describe('with extension points without categories', () => { diff --git a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx index 070e48caa47..f9d6abe3df2 100644 --- a/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx +++ b/public/app/features/explore/extensions/ToolbarExtensionPoint.tsx @@ -1,6 +1,12 @@ import { ReactElement, useMemo, useState } from 'react'; -import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange, getTimeZone } from '@grafana/data'; +import { + type ExplorePanelsState, + type PluginExtensionLink, + PluginExtensionPoints, + RawTimeRange, + getTimeZone, +} from '@grafana/data'; import { reportInteraction, usePluginLinks } from '@grafana/runtime'; import { DataQuery, TimeZone } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; @@ -89,13 +95,14 @@ export type PluginExtensionExploreContext = { timeRange: RawTimeRange; timeZone: TimeZone; shouldShowAddCorrelation: boolean; + panelsSate?: ExplorePanelsState; }; function useExtensionPointContext(props: Props): PluginExtensionExploreContext { const { exploreId, timeZone } = props; const isCorrelationDetails = useSelector(selectCorrelationDetails); const isCorrelationsEditorMode = isCorrelationDetails?.editorMode || false; - const { queries, queryResponse, range } = useSelector(getExploreItemSelector(exploreId))!; + const { queries, queryResponse, range, panelsState } = useSelector(getExploreItemSelector(exploreId))!; const isLeftPane = useSelector(isLeftPaneSelector(exploreId)); const datasourceUids = queries.map((query) => query?.datasource?.uid).filter((uid) => uid !== undefined); @@ -110,16 +117,18 @@ function useExtensionPointContext(props: Props): PluginExtensionExploreContext { timeRange: range.raw, timeZone: getTimeZone({ timeZone }), shouldShowAddCorrelation: canWriteCorrelations && !isCorrelationsEditorMode && isLeftPane && numUniqueIds === 1, + panelsState, }; }, [ + canWriteCorrelations, exploreId, + isCorrelationsEditorMode, + isLeftPane, + numUniqueIds, + panelsState, queries, queryResponse, range.raw, timeZone, - canWriteCorrelations, - isCorrelationsEditorMode, - isLeftPane, - numUniqueIds, ]); } From a3dacabedf479fab9e66448b28b15e6e41fd0ded Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Wed, 26 Nov 2025 12:10:54 +0100 Subject: [PATCH 17/94] MSSQL: Current-user authentication (#113977) * Moving things around * Update frontend to support CUA * Add CUA support to backend * Copy parseURL function to where it's used * Update test * Remove experimental-strip-types * Docs * A bit more of a refactor to reduce complexity * Revert "Remove experimental-strip-types" This reverts commit 70fbc1c0cd46aee3acddaf36e9c50e6cb76d0147. * Review * Docs updates * Another docs fix --- .../datasources/mssql/configure/index.md | 153 +++++++++++++++++- pkg/tsdb/mssql/azure/connection.go | 15 +- pkg/tsdb/mssql/mssql.go | 9 +- pkg/tsdb/mssql/sqleng/connection.go | 16 +- pkg/tsdb/mssql/sqleng/handler_checkhealth.go | 10 +- pkg/tsdb/mssql/sqleng/sql_engine.go | 110 ++++++++++--- pkg/tsdb/mssql/sqleng/sql_engine_test.go | 2 +- .../mssql/azureauth/AzureAuthSettings.tsx | 2 + .../mssql/azureauth/AzureCredentialsForm.tsx | 22 ++- 9 files changed, 290 insertions(+), 49 deletions(-) diff --git a/docs/sources/datasources/mssql/configure/index.md b/docs/sources/datasources/mssql/configure/index.md index 3329d585159..4d3eb60ee81 100644 --- a/docs/sources/datasources/mssql/configure/index.md +++ b/docs/sources/datasources/mssql/configure/index.md @@ -74,6 +74,21 @@ refs: destination: /docs/grafana//datasources/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//datasources/ + configure-grafana-azure-auth: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/ + configure-grafana-azure: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-grafana/#azure + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-grafana/#azure + configure-grafana-azure-auth-scopes: + - pattern: /docs/grafana/ + destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana//setup-grafana/configure-access/configure-authentication/azuread/#enable-azure-ad-oauth-in-grafana --- # Configure the Microsoft SQL Server data source @@ -138,14 +153,19 @@ If you're using an older version of Microsoft SQL Server like 2008 and 2008R2, y **Authentication:** -| Authentication Type | Description | Credentials / Fields | -| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username
- **Password**: SQL Server password | -| **Windows Authentication**
(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials | -| **Windows AD**
(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`
- **Password**: Active Directory password | -| **Windows AD**
(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`
- **Keytab file path**: Path to your keytab file | -| **Windows AD**
(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) | -| **Windows AD**
(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`
- **Credential cache file path**: e.g., `/home/grot/cache.json` | +{{< admonition type="note" >}} +In order to use Azure AD Authentication the toggle `auth.azure_auth_enabled` must be set to `true` in the Grafana configuration file. +{{< /admonition >}} + +| Authentication Type | Description | Credentials / Fields | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **SQL Server Authentication** | Default method to connect to MSSQL. Use a SQL Server or Windows login in `DOMAIN\User` format. | - **Username**: SQL Server username
- **Password**: SQL Server password | +| **Windows Authentication**
(Integrated Security) | Uses the logged-in Windows user's credentials via single sign-on. Available only when SQL Server allows Windows Authentication. | No input required; uses the logged-in Windows user's credentials | +| **Windows AD**
(Username/Password) | Authenticates a domain user with their Active Directory username and password. | - **Username**: `user@example.com`
- **Password**: Active Directory password | +| **Windows AD**
(Keytab) | Authenticates a domain user using a keytab file. | - **Username**: `user@example.com`
- **Keytab file path**: Path to your keytab file | +| **Windows AD**
(Credential Cache) | Uses a Kerberos credential cache already loaded in memory (e.g., from a prior `kinit` command). No file needed. | - **Credential cache path**: Path to in-memory credential (e.g., `/tmp/krb5cc_1000`) | +| **Windows AD**
(Credential Cache File) | Authenticates a domain user using a credential cache file (`.ccache`). | - **Username**: `user@example.com`
- **Credential cache file path**: e.g., `/home/grot/cache.json` | +| **Azure Entra ID (formerly Azure AD) Authentication** | Authenticates the data source using Azure authentication methods. | Details on the supported authentication methods and how to configure them can be found in the [Azure authentication section](./index.md#azure-entra-id-formerly-azure-ad-authentication). | **Additional settings:** @@ -185,6 +205,123 @@ After configuring your MSSQL data source options, click **Save & test** at the b **Database Connection OK** +### Azure Entra ID (formerly Azure AD) Authentication + +The following Azure authentication methods are supported: + +- Current User authentication +- App Registration +- Managed Identity +- Azure Entra Password + +The Azure SQL Server that you are connecting to should support Azure Entra authentication to support adding the App Registration as a user in the database. For configuration details, refer to the [Azure SQL documentation](https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-configure?view=azuresql&tabs=azure-portal). + +#### Current User authentication + +This is the recommended authentication mechanism when working with SQL Server instances that are hosted in Azure. It allows users to be authenticated to and query the database using their own credentials rather than long-lived credentials. + +This authentication method requires your Grafana instance to be configured with Azure Entra ID (formerly Active Directory) authentication for login. With Azure Entra ID login, this method can be used to forward the currently logged in user’s credentials to the data source. The users credentials will then be used when requesting data from the data source. For details on how to configure your Grafana instance using Azure Entra refer to the [documentation](ref:configure-grafana-azure-auth). + +{{< admonition type="note" >}} +Additional configuration is required to ensure that the App Registration used to login a user via Azure provides an access token with the permissions required by the data source. + +The App Registration must be configured to issue both **Access Tokens** and **ID Tokens**. + +1. In the Azure Portal, open the App Registration that requires configuration. +2. Select **Authentication** in the side menu. +3. Under **Implicit grant and hybrid flows** check both the **Access tokens** and **ID tokens** boxes. +4. Save the changes to ensure the App Registration is updated. + +The App Registration must also be configured with additional **API Permissions** to provide authenticated users with access to the APIs utilised by the data source. + +1. In the Azure Portal, open the App Registration that requires configuration. +1. Select **API Permissions** in the side menu. +1. Ensure the `openid`, `profile`, `email`, and `offline_access` permissions are present under the **Microsoft Graph** section. If not, they must be added. +1. Select **Add a permission** and choose the following permissions. They must be added individually. Refer to the [Azure documentation](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-access-web-apis) for more information. + - Select **APIs my organization uses** > Search for **Azure SQL** and select it > **Delegated permissions** > `user_impersonation` > **Add permissions** + +After all permissions have been added, the Azure authentication section in Grafana must be updated. The `scopes` section must be updated to include the `.default` scope to ensure that a token with access to all APIs declared on the App Registration is requested by Grafana. After updated the scopes value should equal: `.default openid email profile`. +{{< /admonition >}} + +This method of authentication doesn't inherently support all backend functionality as a user's credentials won't be in scope. Affected functionality includes alerting, reporting, and recorded queries. Also, note that query and resource caching is disabled by default for data sources using current user authentication. + +**To enable current user authentication for Grafana:** + +1. Set the `user_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). + + ```ini + [azure] + user_identity_enabled = true + ``` + +2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Current User**. + +### App Registration + +You must create an app registration and service principal in Azure Entra to authenticate the data source. +For configuration details, refer to the [Azure documentation for service principals](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal#get-tenant-and-app-id-values-for-signing-in). + +After the app registration has been created, make note of the tenant ID, client ID, and client secret. Take the following steps to add the app registration as a SQL user: + +1. Connect to your Azure SQL database as a user with administrative permissions (the user used here must have the ability to read your Azure Entra directory e.g. by possessing the `Directory Readers` role). +2. Run `CREATE USER [$IDENTITY_NAME] FROM EXTERNAL PROVIDER;`, substituting `IDENTITY_NAME` with the app registration name. +3. Grant the created user the appropriate level of permissions for your use-case. It is recommended that users configured for data sources only have reader permissions. + +After the appropriate permissions have been granted, configure the SQL Server data source to use the app registration: + +1. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **App Registration**. +2. Set the **Azure Cloud** value to the correct value. If you are using the Azure public cloud this will be **Azure**. +3. Set the **Directory (tenant) ID**, **Application (client) ID**, and **Client Secret** values to those for your app registration. + +### Managed Identity + +{{< admonition type="note" >}} +Managed Identity is available only in [Azure Managed Grafana](https://azure.microsoft.com/en-us/products/managed-grafana) or Grafana OSS/Enterprise when deployed in Azure. It is not available in Grafana Cloud. +{{< /admonition >}} + +You can use managed identity to configure SQL Server in Grafana if you host Grafana in Azure (such as an App Service or with Azure Virtual Machines) and have managed identity enabled on your VM. +This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations. +For details on Azure managed identities, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview). + +**To enable managed identity for Grafana:** + +1. Set the `managed_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). + + ```ini + [azure] + managed_identity_enabled = true + ``` + +2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Managed Identity**. + + This hides the directory ID, application ID, and client secret fields, and the data source uses managed identity to authenticate to SQL Server. + +3. You can set the `managed_identity_client_id` field in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure) to allow a user-assigned managed identity to be used instead of the default system-assigned identity. + +Ensure that the managed identity used is added to your Azure SQL instance as a user. + +### Azure Entra Password + +{{< admonition type="warning" >}} +Azure Entra Password is not a recommended authentication mechanism as it requires configuration using a single users password. Consider an alternative authentication method such as current user authentication or app registration. +{{< /admonition >}} + +You can connect to an Azure SQL database using the username and password of a user that has permissions in the desired database. This also requires an app registration to be configured with access to the database. + +**To enable Azure Entra password for Grafana:** + +1. Set the `azure_entra_password_credentials_enabled` flag in the `[azure]` section of the [Grafana server configuration](ref:configure-grafana-azure). + + ```ini + [azure] + azure_entra_password_credentials_enabled = true + ``` + +2. In the SQL Server data source configuration, set **Authentication** to **Azure AD Authentication** and the Azure Authentication type to **Azure Entra Password**. +3. Set the **User ID** value to the username of the user in the Azure SQL database. +4. Set the **Application Client ID** to the client ID of the app registration that has been added to the Azure SQL database +5. Set the **Password** value to the password of the user in the Azure SQL database. + ### Min time interval The **Min time interval** setting defines a lower limit for the [`$__interval`](ref:add-template-variables-interval) and [`$__interval_ms`][add-template-variables-interval_ms] variables. diff --git a/pkg/tsdb/mssql/azure/connection.go b/pkg/tsdb/mssql/azure/connection.go index 40978229b39..1d3c2b46cc2 100644 --- a/pkg/tsdb/mssql/azure/connection.go +++ b/pkg/tsdb/mssql/azure/connection.go @@ -4,14 +4,15 @@ import ( "fmt" "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" ) -func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool) (string, error) { +func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentials, azureSettings *azsettings.AzureSettings, userAssertion string) (string, error) { connStr := "" switch c := azureCredentials.(type) { case *azcredentials.AzureManagedIdentityCredentials: - if azureManagedIdentityClientId != "" { - connStr += fmt.Sprintf("user id=%s;", azureManagedIdentityClientId) + if azureSettings.ManagedIdentityClientId != "" { + connStr += fmt.Sprintf("user id=%s;", azureSettings.ManagedIdentityClientId) } connStr += fmt.Sprintf("fedauth=%s;", "ActiveDirectoryManagedIdentity") @@ -23,7 +24,7 @@ func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia "ActiveDirectoryApplication", ) case *azcredentials.AzureEntraPasswordCredentials: - if azureEntraPasswordCredentialsEnabled { + if azureSettings.AzureEntraPasswordCredentialsEnabled { connStr += fmt.Sprintf("user id=%s;password=%s;applicationclientid=%s;fedauth=%s;", c.UserId, c.Password, @@ -33,6 +34,12 @@ func GetAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia } else { return "", fmt.Errorf("azure entra password authentication is not enabled") } + case *azcredentials.AadCurrentUserCredentials: + if userAssertion == "" { + return "", fmt.Errorf("user ID token is empty but required for current user authentication") + } + connStr += fmt.Sprintf("user id=%s;userassertion=%s;password=%s;fedauth=%s;", + azureSettings.UserIdentityTokenEndpoint.ClientId, userAssertion, azureSettings.UserIdentityTokenEndpoint.ClientSecret, "ActiveDirectoryOnBehalfOf") default: return "", fmt.Errorf("unsupported azure authentication type") } diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index ca8595edd2e..77de0b82603 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" + "github.com/grafana/grafana-azure-sdk-go/v2/azusercontext" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" @@ -43,7 +44,8 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) if err != nil { return nil, err } - return dsHandler.QueryData(ctx, req) + + return dsHandler.QueryData(azusercontext.WithUserFromQueryReq(ctx, req), req) } func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.InstanceFactoryFunc { @@ -53,6 +55,8 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc if err != nil { return nil, err } + pluginCfg := backend.PluginConfigFromContext(ctx) + jsonData := sqleng.JsonData{ MaxOpenConns: sqlCfg.DefaultMaxOpenConns, MaxIdleConns: sqlCfg.DefaultMaxIdleConns, @@ -87,6 +91,7 @@ func NewInstanceSettings(cfg *setting.Cfg, logger log.Logger) datasource.Instanc Updated: settings.Updated, UID: settings.UID, DecryptedSecureJSONData: settings.DecryptedSecureJSONData, + OrgID: pluginCfg.OrgID, } userFacingDefaultError, err := grafCfg.UserFacingDefaultError() @@ -117,5 +122,5 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque return nil, err } - return dsHandler.CheckHealth(ctx, req) + return dsHandler.CheckHealth(azusercontext.WithUserFromHealthCheckReq(ctx, req), req) } diff --git a/pkg/tsdb/mssql/sqleng/connection.go b/pkg/tsdb/mssql/sqleng/connection.go index 58a6d014219..7b779b547c8 100644 --- a/pkg/tsdb/mssql/sqleng/connection.go +++ b/pkg/tsdb/mssql/sqleng/connection.go @@ -1,14 +1,14 @@ package sqleng import ( - "context" "database/sql" "fmt" "time" "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" - "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana/pkg/tsdb/mssql/azure" "github.com/grafana/grafana/pkg/tsdb/mssql/kerberos" "github.com/grafana/grafana/pkg/tsdb/mssql/utils" @@ -17,7 +17,7 @@ import ( "github.com/microsoft/go-mssqldb/azuread" ) -func newMSSQL(ctx context.Context, driverName string, rowLimit int64, dsInfo DataSourceInfo, cnnstr string, logger log.Logger, settings backend.DataSourceInstanceSettings) (*sql.DB, error) { +func newMSSQL(driverName string, rowLimit int64, dsInfo DataSourceInfo, cnnstr string, logger log.Logger, proxyClient proxy.Client) (*sql.DB, error) { var connector *mssql.Connector var err error if driverName == "azuresql" { @@ -31,12 +31,6 @@ func newMSSQL(ctx context.Context, driverName string, rowLimit int64, dsInfo Dat return nil, fmt.Errorf("mssql connector creation failed") } - proxyClient, err := settings.ProxyClient(ctx) - if err != nil { - logger.Error("mssql proxy creation failed", "error", err) - return nil, fmt.Errorf("mssql proxy creation failed") - } - if proxyClient.SecureSocksProxyEnabled() { dialer, err := proxyClient.NewSecureSocksProxyContextDialer() if err != nil { @@ -81,7 +75,7 @@ const ( kerberosCredentialCacheFile = "Windows AD: Credential cache file" // #nosec G101 ) -func generateConnectionString(dsInfo DataSourceInfo, azureManagedIdentityClientId string, azureEntraPasswordCredentialsEnabled bool, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger) (string, error) { +func generateConnectionString(dsInfo DataSourceInfo, azureCredentials azcredentials.AzureCredentials, kerberosAuth kerberos.KerberosAuth, logger log.Logger, azureSettings *azsettings.AzureSettings, userAssertion string) (string, error) { const dfltPort = "0" var addr util.NetworkAddress if dsInfo.URL != "" { @@ -119,7 +113,7 @@ func generateConnectionString(dsInfo DataSourceInfo, azureManagedIdentityClientI switch dsInfo.JsonData.AuthenticationType { case azureAuthentication: - azureCredentialDSNFragment, err := azure.GetAzureCredentialDSNFragment(azureCredentials, azureManagedIdentityClientId, azureEntraPasswordCredentialsEnabled) + azureCredentialDSNFragment, err := azure.GetAzureCredentialDSNFragment(azureCredentials, azureSettings, userAssertion) if err != nil { return "", err } diff --git a/pkg/tsdb/mssql/sqleng/handler_checkhealth.go b/pkg/tsdb/mssql/sqleng/handler_checkhealth.go index 133be66a70a..9a4bddeb686 100644 --- a/pkg/tsdb/mssql/sqleng/handler_checkhealth.go +++ b/pkg/tsdb/mssql/sqleng/handler_checkhealth.go @@ -13,7 +13,15 @@ import ( ) func (e *DataSourceHandler) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if err := e.db.Ping(); err != nil { + db, err := e.getDB(ctx) + if err != nil { + logCheckHealthError(ctx, e.dsInfo, err) + if strings.EqualFold(req.PluginContext.User.Role, "Admin") { + return ErrToHealthCheckResult(err) + } + return &backend.CheckHealthResult{Status: backend.HealthStatusError, Message: e.TransformQueryError(e.log, err).Error()}, nil + } + if err := db.Ping(); err != nil { logCheckHealthError(ctx, e.dsInfo, err) if strings.EqualFold(req.PluginContext.User.Role, "Admin") { return ErrToHealthCheckResult(err) diff --git a/pkg/tsdb/mssql/sqleng/sql_engine.go b/pkg/tsdb/mssql/sqleng/sql_engine.go index 3d27b0a9a01..f6997a720c5 100644 --- a/pkg/tsdb/mssql/sqleng/sql_engine.go +++ b/pkg/tsdb/mssql/sqleng/sql_engine.go @@ -2,6 +2,7 @@ package sqleng import ( "context" + "crypto/sha256" "database/sql" "encoding/json" "errors" @@ -16,9 +17,11 @@ import ( "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" + "github.com/grafana/grafana-azure-sdk-go/v2/azusercontext" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" @@ -74,6 +77,7 @@ type DataSourceInfo struct { Updated time.Time UID string DecryptedSecureJSONData map[string]string + OrgID int64 } type DataPluginConfiguration struct { @@ -97,6 +101,8 @@ type DataSourceHandler struct { azureCredentials azcredentials.AzureCredentials kerberosAuth kerberos.KerberosAuth driverName string + proxyClient proxy.Client + dbConnections sync.Map } type QueryJson struct { @@ -142,6 +148,12 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc return nil, fmt.Errorf("error getting kerberos settings: %w", err) } + proxyClient, err := settings.ProxyClient(ctx) + if err != nil { + logger.Error("mssql proxy creation failed", "error", err) + return nil, fmt.Errorf("mssql proxy creation failed") + } + queryDataHandler := DataSourceHandler{ queryResultTransformer: &queryResultTransformer, macroEngine: newMssqlMacroEngine(), @@ -154,6 +166,7 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc azureCredentials: azureCredentials, kerberosAuth: kerberosAuth, driverName: driverName, + proxyClient: proxyClient, } if len(config.TimeColumnNames) > 0 { @@ -164,18 +177,21 @@ func NewQueryDataHandler(ctx context.Context, settings backend.DataSourceInstanc queryDataHandler.metricColumnTypes = config.MetricColumnTypes } - cnnstr, err := generateConnectionString(config.DSInfo, azureSettings.ManagedIdentityClientId, azureSettings.AzureEntraPasswordCredentialsEnabled, azureCredentials, kerberosAuth, log) - if err != nil { - return nil, err - } + // Every auth method besides Azure AD Current User Identity can use a persistent DB connection + if config.DSInfo.JsonData.AuthenticationType != azureAuthentication || azureCredentials.AzureAuthType() != azcredentials.AzureAuthCurrentUserIdentity { + cnnstr, err := generateConnectionString(config.DSInfo, azureCredentials, kerberosAuth, log, azureSettings, "") + if err != nil { + return nil, err + } - db, err := newMSSQL(ctx, driverName, config.RowLimit, config.DSInfo, cnnstr, log, settings) - if err != nil { - logger.Error("Failed connecting to MSSQL", "err", err) - return nil, err - } + db, err := newMSSQL(driverName, config.RowLimit, config.DSInfo, cnnstr, log, proxyClient) + if err != nil { + logger.Error("Failed connecting to MSSQL", "err", err) + return nil, err + } - queryDataHandler.db = db + queryDataHandler.db = db + } return &queryDataHandler, nil } @@ -192,9 +208,52 @@ func (e *DataSourceHandler) Dispose() { e.log.Error("Failed to dispose db", "error", err) } } + + // Clear any cached user-specific connections + e.dbConnections.Range(func(_, conn interface{}) bool { + _ = conn.(*sql.DB).Close() + return true + }) + e.dbConnections.Clear() + e.log.Debug("DB disposed") } +func (e *DataSourceHandler) getDB(ctx context.Context) (*sql.DB, error) { + e.log.Debug("Getting DB...") + if e.dsInfo.JsonData.AuthenticationType != azureAuthentication || e.azureCredentials.AzureAuthType() != azcredentials.AzureAuthCurrentUserIdentity { + if e.db == nil { + return nil, fmt.Errorf("database connection is not initialized") + } + return e.db, nil + } + + userCtx, ok := azusercontext.GetCurrentUser(ctx) + if !ok { + return nil, fmt.Errorf("failed to get user from context for Azure Current User authentication") + } + cacheKey := fmt.Sprintf("mssql-%d-%x-%x-%s", e.dsInfo.OrgID, sha256.Sum256([]byte(userCtx.User.Email)), sha256.Sum256([]byte(userCtx.IdToken)), e.dsInfo.UID) + + conn, ok := e.dbConnections.Load(cacheKey) + if ok { + return conn.(*sql.DB), nil + } + + cnnstr, err := generateConnectionString(e.dsInfo, e.azureCredentials, e.kerberosAuth, e.log, e.azureSettings, userCtx.IdToken) + if err != nil { + return nil, err + } + + db, err := newMSSQL(e.driverName, e.rowLimit, e.dsInfo, cnnstr, e.log, e.proxyClient) + if err != nil { + logger.Error("Failed connecting to MSSQL", "err", err) + return nil, err + } + e.dbConnections.Store(cacheKey, db) + + return db, nil +} + func (e *DataSourceHandler) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() ch := make(chan DBDataResponse, len(req.Queries)) @@ -293,7 +352,12 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG return } - rows, err := e.db.QueryContext(queryContext, interpolatedQuery) + db, err := e.getDB(queryContext) + if err != nil { + errAppendDebug("retrieving database connection failed", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourcePlugin) + return + } + rows, err := db.QueryContext(queryContext, interpolatedQuery) if err != nil { errAppendDebug("db query error", e.TransformQueryError(logger, err), interpolatedQuery, backend.ErrorSourceDownstream) return @@ -310,12 +374,19 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG return } + frame := e.processResponse(qm, rows, interpolatedQuery, errAppendDebug) + + queryResult.dataResponse.Frames = data.Frames{frame} + ch <- queryResult +} + +func (e *DataSourceHandler) processResponse(qm *dataQueryModel, rows *sql.Rows, interpolatedQuery string, errAppendDebug func(string, error, string, backend.ErrorSource)) *data.Frame { // Convert row.Rows to dataframe stringConverters := e.queryResultTransformer.GetConverterList() frame, err := sqlutil.FrameFromRows(rows, e.rowLimit, sqlutil.ToConverters(stringConverters...)...) if err != nil { errAppendDebug("convert frame from rows error", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + return nil } if frame.Meta == nil { @@ -330,21 +401,19 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG // additionally-needed frame data stays intact and is correctly passed to our visulization. if frame.Rows() == 0 { frame.Fields = []*data.Field{} - queryResult.dataResponse.Frames = data.Frames{frame} - ch <- queryResult - return + return frame } if err := convertSQLTimeColumnsToEpochMS(frame, qm); err != nil { errAppendDebug("converting time columns failed", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + return nil } if qm.Format == dataQueryFormatSeries { // time series has to have time column if qm.timeIndex == -1 { errAppendDebug("db has no time column", errors.New("time column is missing; make sure your data includes a time column for time series format or switch to a table format that doesn't require it"), interpolatedQuery, backend.ErrorSourceDownstream) - return + return nil } // Make sure to name the time field 'Time' to be backward compatible with Grafana pre-v8. @@ -362,7 +431,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG var err error if frame, err = convertSQLValueColumnToFloat(frame, i); err != nil { errAppendDebug("convert value to float failed", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + return nil } } @@ -373,7 +442,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG frame, err = data.LongToWide(frame, qm.FillMissing) if err != nil { errAppendDebug("failed to convert long to wide series when converting from dataframe", err, interpolatedQuery, backend.ErrorSourcePlugin) - return + return nil } // Before 8x, a special metric column was used to name time series. The LongToWide transforms that into a metric label on the value field. @@ -408,8 +477,7 @@ func (e *DataSourceHandler) executeQuery(query backend.DataQuery, wg *sync.WaitG } } - queryResult.dataResponse.Frames = data.Frames{frame} - ch <- queryResult + return frame } // Interpolate provides global macros/substitutions for all sql datasources. diff --git a/pkg/tsdb/mssql/sqleng/sql_engine_test.go b/pkg/tsdb/mssql/sqleng/sql_engine_test.go index c4b21b2b849..8a354fadc2f 100644 --- a/pkg/tsdb/mssql/sqleng/sql_engine_test.go +++ b/pkg/tsdb/mssql/sqleng/sql_engine_test.go @@ -697,7 +697,7 @@ func TestGenerateConnectionString(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - connStr, err := generateConnectionString(tc.dataSource, "", false, nil, tc.kerberosCfg, logger) + connStr, err := generateConnectionString(tc.dataSource, nil, tc.kerberosCfg, logger, nil, "") require.NoError(t, err) assert.Equal(t, tc.expConnStr, connStr) }) diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx b/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx index 0d53f030095..f38a417466e 100644 --- a/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx +++ b/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx @@ -15,6 +15,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => { const { dataSourceConfig: dsSettings, onChange } = props; const managedIdentityEnabled = config.azure.managedIdentityEnabled; const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled; + const userIdentityEnabled = config.azure.userIdentityEnabled; const credentials = useMemo(() => getCredentials(dsSettings), [dsSettings]); @@ -33,6 +34,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => { void; @@ -22,13 +23,25 @@ export const AzureCredentialsForm = (props: Props) => { azureCloudOptions, onCredentialsChange, disabled, + userIdentityEnabled, } = props; const onAuthTypeChange = (selected: SelectableValue) => { + const defaultAuthType = (() => { + if (managedIdentityEnabled) { + return 'msi'; + } + + if (userIdentityEnabled) { + return 'currentuser'; + } + + return 'clientsecret'; + })(); if (onCredentialsChange) { const updated: AzureCredentials = { ...credentials, - authType: selected.value || 'msi', + authType: selected.value || defaultAuthType, }; onCredentialsChange(updated); } @@ -131,6 +144,13 @@ export const AzureCredentialsForm = (props: Props) => { value: 'ad-password', label: t('azureauth.azure-credentials-form.auth-options-azure-entra', 'Azure Entra Password'), }); + + if (userIdentityEnabled) { + authTypeOptions.unshift({ + value: 'currentuser', + label: 'Current User', + }); + } } return ( From cef4449f14b2c9ebb92614d6ee32013cc08d08ff Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Wed, 26 Nov 2025 11:16:47 +0000 Subject: [PATCH 18/94] Folders: Send permissions query param with app platform for folder picker (#114158) --- .../rtkq/dashboard/v0alpha1/endpoints.gen.ts | 4 +-- pkg/registry/apis/dashboard/search.go | 4 +-- pkg/tests/apis/dashboard/search_test.go | 28 +++++++++---------- .../dashboard.grafana.app-v0alpha1.json | 8 +++--- .../NestedFolderPicker/useFoldersQuery.ts | 8 +++++- .../useFoldersQueryAppPlatform.ts | 8 +++--- 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts index 264ac389e42..d2cc21d31e6 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts @@ -612,8 +612,8 @@ export type GetSearchApiArg = { tags?: string[]; /** find dashboards that reference a given libraryPanel */ libraryPanel?: string; - /** permission needed for the resource (View, Edit, Admin) */ - permission?: 'View' | 'Edit' | 'Admin'; + /** permission needed for the resource (view, edit, admin) */ + permission?: 'view' | 'edit' | 'admin'; /** sortable field */ sort?: string; /** number of results to return */ diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 6d85a66b344..19e5b71e0d9 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -136,9 +136,9 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) * ParameterProps: spec3.ParameterProps{ Name: "permission", In: "query", - Description: "permission needed for the resource (View, Edit, Admin)", + Description: "permission needed for the resource (view, edit, admin)", Required: false, - Schema: spec.StringProperty().WithEnum("View", "Edit", "Admin"), + Schema: spec.StringProperty().WithEnum("view", "edit", "admin"), }, }, { diff --git a/pkg/tests/apis/dashboard/search_test.go b/pkg/tests/apis/dashboard/search_test.go index 129a44aee21..09d23398c3f 100644 --- a/pkg/tests/apis/dashboard/search_test.go +++ b/pkg/tests/apis/dashboard/search_test.go @@ -126,7 +126,7 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { // 2. Viewer searching with permission=View should find it { - res := callSearch(helper.Org1.Viewer, "permission=View") + res := callSearch(helper.Org1.Viewer, "permission=view") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -134,12 +134,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Viewer should find folder with permission=View") + require.True(t, found, "Viewer should find folder with permission=view") } // 3. Viewer searching with permission=Edit should NOT find it { - res := callSearch(helper.Org1.Viewer, "permission=Edit") + res := callSearch(helper.Org1.Viewer, "permission=edit") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -147,12 +147,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.False(t, found, "Viewer should NOT find folder with permission=Edit") + require.False(t, found, "Viewer should NOT find folder with permission=edit") } // 4. Editor searching with permission=Edit should find it { - res := callSearch(helper.Org1.Editor, "permission=Edit") + res := callSearch(helper.Org1.Editor, "permission=edit") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -160,12 +160,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Editor should find folder with permission=Edit") + require.True(t, found, "Editor should find folder with permission=edit") } // 5. Editor searching with permission=View should find it (Edit permission includes View) { - res := callSearch(helper.Org1.Editor, "permission=View") + res := callSearch(helper.Org1.Editor, "permission=view") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -173,7 +173,7 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Editor should find folder with permission=View (Edit includes View)") + require.True(t, found, "Editor should find folder with permission=view (Edit includes View)") } // 6. Editor searching without permission parameter should find it (has Edit access) @@ -191,7 +191,7 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { // 7. Admin searching with permission=View should find it (Admin has full access) { - res := callSearch(helper.Org1.Admin, "permission=View") + res := callSearch(helper.Org1.Admin, "permission=view") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -199,12 +199,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Admin should find folder with permission=View") + require.True(t, found, "Admin should find folder with permission=view") } // 8. Admin searching with permission=Edit should find it (Admin has full access) { - res := callSearch(helper.Org1.Admin, "permission=Edit") + res := callSearch(helper.Org1.Admin, "permission=edit") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -212,12 +212,12 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Admin should find folder with permission=Edit") + require.True(t, found, "Admin should find folder with permission=edit") } // 9. Admin searching with permission=Admin should find it (Admin has full access) { - res := callSearch(helper.Org1.Admin, "permission=Admin") + res := callSearch(helper.Org1.Admin, "permission=admin") found := false for _, h := range res.Hits { if h.Name == folderUID { // Verify it's our folder @@ -225,7 +225,7 @@ func runSearchPermissionTest(t *testing.T, mode rest.DualWriterMode) { break } } - require.True(t, found, "Admin should find folder with permission=Admin") + require.True(t, found, "Admin should find folder with permission=admin") } // 10. Admin searching without permission parameter should find it (has Admin access) diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index 06bad0ba004..238db87aff4 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -1823,13 +1823,13 @@ { "name": "permission", "in": "query", - "description": "permission needed for the resource (View, Edit, Admin)", + "description": "permission needed for the resource (view, edit, admin)", "schema": { "type": "string", "enum": [ - "View", - "Edit", - "Admin" + "view", + "edit", + "admin" ] } }, diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts index d852d9c241f..19ceab28b77 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQuery.ts @@ -22,7 +22,13 @@ export function useFoldersQuery({ rootFolderItem, }: UseFoldersQueryProps) { const resultLegacy = useFoldersQueryLegacy({ isBrowsing, openFolders, permission, rootFolderUID, rootFolderItem }); - const resultAppPlatform = useFoldersQueryAppPlatform({ isBrowsing, openFolders, rootFolderUID, rootFolderItem }); + const resultAppPlatform = useFoldersQueryAppPlatform({ + isBrowsing, + openFolders, + permission, + rootFolderUID, + rootFolderItem, + }); // Running the hooks themselves don't have any side effects, so we can just conditionally use one or the other // requestNextPage function from the result diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts b/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts index 34d068589c9..ae6c8574adb 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts @@ -27,14 +27,14 @@ const collator = new Intl.Collator(); * does not have pagination at the moment. */ -type Props = Omit; export function useFoldersQueryAppPlatform({ isBrowsing, openFolders, /* rootFolderUID: configure which folder to start browsing from */ rootFolderUID, rootFolderItem, -}: Props) { + permission, +}: UseFoldersQueryProps) { const dispatch = useDispatch(); // Keep a list of all request subscriptions so we can unsubscribe from them when the component is unmounted @@ -89,7 +89,7 @@ export function useFoldersQueryAppPlatform({ return; } - const args = { folder: finalParentUid, type: 'folder' } as const; + const args = { folder: finalParentUid, type: 'folder', permission } as const; // Make a request const subscription = dispatch(dashboardAPIv0alpha1.endpoints.getSearch.initiate(args)); @@ -101,7 +101,7 @@ export function useFoldersQueryAppPlatform({ // the subscriptions are saved in a ref so they can be unsubscribed on unmount requestsRef.current = requestsRef.current.concat([subscription]); }, - [state, dispatch] + [state, dispatch, permission] ); // Unsubscribe from all requests when the component is unmounted From 8c7170727b24e33255ea6bcf613ffc724799bf7f Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 26 Nov 2025 12:54:50 +0100 Subject: [PATCH 19/94] `grafana-iam`: Prevent crashloops of the standalone IAM server (#114473) * `grafana-iam`: Prevent crashloops of the standalone IAM server --- pkg/registry/apis/iam/register.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index a464568c12f..99c9dda7d8d 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -219,7 +219,9 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge } storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) - storage[teamResource.StoragePath("groups")] = b.teamGroupsHandler + if b.teamGroupsHandler != nil { + storage[teamResource.StoragePath("groups")] = b.teamGroupsHandler + } teamBindingResource := iamv0.TeamBindingResourceInfo teamBindingUniStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, teamBindingResource, opts.OptsGetter) From 21c1d9aedd0c5e2fdf2ce66f269e31a5e38f08b5 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Wed, 26 Nov 2025 12:58:00 +0100 Subject: [PATCH 20/94] Secrets: Remove unused methods and dependencies from secure value service (#114467) --- pkg/registry/apis/secret/contracts/secure_value.go | 3 +-- pkg/registry/apis/secret/service/secure_value.go | 10 ---------- pkg/registry/apis/secret/testutils/testutils.go | 2 +- pkg/server/wire_gen.go | 8 ++++---- pkg/storage/secret/metadata/secure_value_test.go | 4 ++-- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/pkg/registry/apis/secret/contracts/secure_value.go b/pkg/registry/apis/secret/contracts/secure_value.go index bfb763be3f6..09702fa7f5f 100644 --- a/pkg/registry/apis/secret/contracts/secure_value.go +++ b/pkg/registry/apis/secret/contracts/secure_value.go @@ -11,7 +11,7 @@ import ( ) // The maximum size of a secure value in bytes when written as raw input. -const SecureValueRawInputMaxSizeBytes = 24576 // 24 KiB +const SecureValueRawInputMaxSizeBytes = 24 << 10 // 24 KiB type DecryptSecureValue struct { Keeper *string @@ -47,7 +47,6 @@ type SecureValueService interface { List(ctx context.Context, namespace xkube.Namespace) (*secretv1beta1.SecureValueList, error) Update(ctx context.Context, newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv1beta1.SecureValue, error) - SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, keeperName string) error } type SecureValueClient interface { diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 91f6a2e73ff..30b7f4a625d 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -27,7 +27,6 @@ var _ contracts.SecureValueService = (*SecureValueService)(nil) type SecureValueService struct { tracer trace.Tracer accessClient claims.AccessClient - database contracts.Database secureValueMetadataStorage contracts.SecureValueMetadataStorage secureValueValidator contracts.SecureValueValidator secureValueMutator contracts.SecureValueMutator @@ -39,7 +38,6 @@ type SecureValueService struct { func ProvideSecureValueService( tracer trace.Tracer, accessClient claims.AccessClient, - database contracts.Database, secureValueMetadataStorage contracts.SecureValueMetadataStorage, secureValueValidator contracts.SecureValueValidator, secureValueMutator contracts.SecureValueMutator, @@ -50,7 +48,6 @@ func ProvideSecureValueService( return &SecureValueService{ tracer: tracer, accessClient: accessClient, - database: database, secureValueMetadataStorage: secureValueMetadataStorage, secureValueValidator: secureValueValidator, secureValueMutator: secureValueMutator, @@ -369,10 +366,3 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa return sv, nil } - -func (s *SecureValueService) SetKeeperAsActive(ctx context.Context, namespace xkube.Namespace, name string) error { - if err := s.keeperMetadataStorage.SetAsActive(ctx, namespace, name); err != nil { - return fmt.Errorf("calling keeper metadata storage to set keeper as active: %w", err) - } - return nil -} diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 6a5f1e40d30..37394905b79 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -152,7 +152,7 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { secureValueValidator := validator.ProvideSecureValueValidator() secureValueMutator := mutator.ProvideSecureValueMutator() - secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, keeperService, nil) + secureValueService := service.ProvideSecureValueService(tracer, accessClient, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, keeperService, nil) decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, nil) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 279a67f4134..64ffd48ca9f 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -459,8 +459,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } ossDashboardStats := builders.ProvideDashboardStats() documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) - databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) clockClock := clock.ProvideClock() + databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(clockClock, databaseDatabase, tracer, registerer) if err != nil { return nil, err @@ -508,7 +508,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - secureValueService := service5.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, ossKeeperService, registerer) + secureValueService := service5.ProvideSecureValueService(tracer, accessClient, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, ossKeeperService, registerer) inlineSecureValueSupport, err := inline.ProvideInlineSecureValueService(cfg, tracer, secureValueService, accessClient) if err != nil { return nil, err @@ -1107,8 +1107,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } ossDashboardStats := builders.ProvideDashboardStats() documentBuilderSupplier := search.ProvideDocumentBuilders(sqlStore, ossDashboardStats) - databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) clockClock := clock.ProvideClock() + databaseDatabase := database4.ProvideDatabase(sqlStore, tracer) secureValueMetadataStorage, err := metadata.ProvideSecureValueMetadataStorage(clockClock, databaseDatabase, tracer, registerer) if err != nil { return nil, err @@ -1156,7 +1156,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - secureValueService := service5.ProvideSecureValueService(tracer, accessClient, databaseDatabase, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, ossKeeperService, registerer) + secureValueService := service5.ProvideSecureValueService(tracer, accessClient, secureValueMetadataStorage, secureValueValidator, secureValueMutator, keeperMetadataStorage, ossKeeperService, registerer) inlineSecureValueSupport, err := inline.ProvideInlineSecureValueService(cfg, tracer, secureValueService, accessClient) if err != nil { return nil, err diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go index 0eba9bbf9ce..dafd96fc3ad 100644 --- a/pkg/storage/secret/metadata/secure_value_test.go +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -618,7 +618,7 @@ func TestSecureValueServiceExampleBased(t *testing.T) { }, "actor-uid") require.NoError(t, err) - require.NoError(t, sut.SecureValueService.SetKeeperAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name)) + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k1.Namespace), k1.Name)) value := secretv1beta1.NewExposedSecureValue("v1") sv1, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(&secretv1beta1.SecureValue{ @@ -643,7 +643,7 @@ func TestSecureValueServiceExampleBased(t *testing.T) { }, }, "actor-uid") require.NoError(t, err) - require.NoError(t, sut.SecureValueService.SetKeeperAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name)) + require.NoError(t, sut.KeeperMetadataStorage.SetAsActive(t.Context(), xkube.Namespace(k2.Namespace), k2.Name)) // - Read secure value created with inactive keeper readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name) From ae2e5f0df77cd1afe2a04e22ad1f7c29201a450c Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Nov 2025 13:11:44 +0100 Subject: [PATCH 21/94] NPM: Fix e2e-selectors change detection (#114471) fix git cmd --- scripts/publish-npm-packages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/publish-npm-packages.sh b/scripts/publish-npm-packages.sh index eaa2e595824..34dc06fc009 100755 --- a/scripts/publish-npm-packages.sh +++ b/scripts/publish-npm-packages.sh @@ -60,7 +60,7 @@ if (( ${#failed_packages[@]} > 0 )); then fi # Check if any files in packages/grafana-e2e-selectors were changed. If so, add a 'modified' tag to the package -CHANGES_COUNT=$(git diff HEAD~1..HEAD --name-only -- packages/grafana-e2e-selectors | awk 'END{print NR}') +CHANGES_COUNT=$(git show --name-only --format= HEAD -- packages/grafana-e2e-selectors | awk 'END{print NR}') if (( CHANGES_COUNT > 0 )); then # Wait a little bit to allow the package to be published to the registry sleep 5s From e1a2f178e7459986d8c10bb04d5fffc67d5652fc Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 26 Nov 2025 13:41:06 +0100 Subject: [PATCH 22/94] App Plugins: Allow to define experimental pages (#114232) --- pkg/middleware/auth.go | 29 ++++++ pkg/middleware/auth_test.go | 96 ++++++++++++++++++++ pkg/services/navtree/navtreeimpl/applinks.go | 5 + 3 files changed, 130 insertions(+) diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 719d2ab5cb5..f013d9d2bfa 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "errors" "net/http" "net/url" @@ -21,6 +22,13 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" + "github.com/open-feature/go-sdk/openfeature" +) + +var openfeatureClient = openfeature.NewDefaultClient() + +const ( + pluginPageFeatureFlagPrefix = "plugin-page-visible." ) type AuthOptions struct { @@ -146,6 +154,12 @@ func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, log return } + if !PageIsFeatureToggleEnabled(c.Req.Context(), c.Req.URL.Path) { + logger.Debug("Forbidden experimental plugin page", "plugin", pluginID, "path", c.Req.URL.Path) + accessForbidden(c) + return + } + permitted := true path := normalizeIncludePath(c.Req.URL.Path) hasAccess := ac.HasAccess(accessControl, c) @@ -294,3 +308,18 @@ func shouldForceLogin(c *contextmodel.ReqContext) bool { return forceLogin } + +// PageIsFeatureToggleEnabled checks if a page is enabled via OpenFeature feature flags. +// It returns false if the feature flag is set and set to false. +// The feature flag key format is: "plugin-page-visible." +func PageIsFeatureToggleEnabled(ctx context.Context, path string) bool { + flagKey := pluginPageFeatureFlagPrefix + filepath.Clean(path) + enabled := openfeatureClient.Boolean( + ctx, + flagKey, + true, + openfeature.TransactionContext(ctx), + ) + + return enabled +} diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index fdca1d04ee3..19a7d68559e 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -1,12 +1,17 @@ package middleware import ( + "context" "errors" "fmt" "net/http" "net/http/httptest" + "sync" "testing" + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/memprovider" + oftesting "github.com/open-feature/go-sdk/openfeature/testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,6 +33,8 @@ import ( "github.com/grafana/grafana/pkg/web" ) +var openfeatureTestMutex sync.Mutex + func setupAuthMiddlewareTest(t *testing.T, identity *authn.Identity, authErr error) *contexthandler.ContextHandler { return contexthandler.ProvideService(setting.NewCfg(), &authntest.FakeService{ ExpectedErr: authErr, @@ -422,6 +429,60 @@ func TestCanAdminPlugin(t *testing.T) { } } +func TestPageIsFeatureToggleEnabled(t *testing.T) { + type testCase struct { + desc string + path string + flags map[string]bool + expectedResult bool + } + + tests := []testCase{ + { + desc: "returns true when feature flag is enabled", + path: "/a/my-plugin/settings", + flags: map[string]bool{ + pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": true, + }, + expectedResult: true, + }, + { + desc: "returns false when feature flag is disabled", + path: "/a/my-plugin/settings", + flags: map[string]bool{ + pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": false, + }, + expectedResult: false, + }, + { + desc: "returns false when feature flag is disabled with trailing slash", + path: "/a/my-plugin/settings/", + flags: map[string]bool{ + pluginPageFeatureFlagPrefix + "/a/my-plugin/settings": false, + }, + expectedResult: false, + }, + { + desc: "returns true when feature flag does not exist", + path: "/a/my-plugin/settings", + flags: map[string]bool{}, + expectedResult: true, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + + setupTestProvider(t, tt.flags) + + result := PageIsFeatureToggleEnabled(ctx, tt.path) + + assert.Equal(t, tt.expectedResult, result) + }) + } +} + func contextProvider(modifiers ...func(c *contextmodel.ReqContext)) web.Handler { return func(c *web.Context) { reqCtx := &contextmodel.ReqContext{ @@ -437,3 +498,38 @@ func contextProvider(modifiers ...func(c *contextmodel.ReqContext)) web.Handler c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx)) } } + +// setupTestProvider creates a test OpenFeature provider with the given flags. +// Uses a global lock to prevent concurrent provider changes across tests. +func setupTestProvider(t *testing.T, flags map[string]bool) oftesting.TestProvider { + t.Helper() + + // Lock to prevent concurrent provider changes + openfeatureTestMutex.Lock() + + testProvider := oftesting.NewTestProvider() + flagsMap := map[string]memprovider.InMemoryFlag{} + + for key, value := range flags { + flagsMap[key] = memprovider.InMemoryFlag{ + DefaultVariant: "defaultVariant", + Variants: map[string]any{ + "defaultVariant": value, + }, + } + } + + testProvider.UsingFlags(t, flagsMap) + + err := openfeature.SetProviderAndWait(testProvider) + require.NoError(t, err) + + t.Cleanup(func() { + testProvider.Cleanup() + _ = openfeature.SetProviderAndWait(openfeature.NoopProvider{}) + // Unlock after cleanup to allow other tests to run + openfeatureTestMutex.Unlock() + }) + + return testProvider +} diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 4f8015f5687..b9b0edac970 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/plugins" ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" @@ -128,6 +129,10 @@ func (s *ServiceImpl) processAppPlugin(plugin pluginstore.Plugin, c *contextmode } if include.Type == "page" { + if !middleware.PageIsFeatureToggleEnabled(c.Req.Context(), include.Path) { + s.log.Debug("Skipping page", "plugin", plugin.ID, "path", include.Path) + continue + } link := &navtree.NavLink{ Text: include.Name, Icon: include.Icon, From 5ba3139d4a055ca5f0c54a4e6f743412c92ca4e3 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 26 Nov 2025 13:51:16 +0100 Subject: [PATCH 23/94] E2E Selectors: Fix readme typo (#114480) fix typo --- packages/grafana-e2e-selectors/src/selectors/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/README.md b/packages/grafana-e2e-selectors/src/selectors/README.md index ef419dac1fa..134197e6a6a 100644 --- a/packages/grafana-e2e-selectors/src/selectors/README.md +++ b/packages/grafana-e2e-selectors/src/selectors/README.md @@ -16,7 +16,7 @@ const components = { A few things to keep in mind: -- Strive to use e2e selector for all components in grafana/ui. +- Strive to use e2e selectors for all components in grafana/ui. - Don't ever delete selectors. Even though a selector may not be used in the Grafana repository, it can still be used in external plugins. - Only create new selector in case you're creating a new piece of UI. If you're changing an existing piece of UI that already has a selector defined, you need to keep using that selector. Otherwise you might break plugin end-to-end tests. - Prefer using string selectors in favour of function selectors. The purpose of the selectors is to provide a canonical way to select elements. From c94bf34d0bb2ff3be8beee61e5dc9f2e164a9bbb Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 26 Nov 2025 14:27:31 +0100 Subject: [PATCH 24/94] Alerting: Patch missing expression model refIds (#114477) --- .../components/expressions/Expression.tsx | 2 - .../rule-editor/ExpressionsEditor.tsx | 11 +-- .../QueryAndExpressionsStep.tsx | 4 -- .../__snapshots__/reducer.test.tsx.snap | 52 -------------- .../reducer.test.tsx | 17 ----- .../query-and-alert-condition/reducer.ts | 17 ----- .../alerting/unified/utils/rule-form.test.ts | 69 +++++++++++++++++++ .../alerting/unified/utils/rule-form.ts | 59 +++++++++++----- 8 files changed, 115 insertions(+), 116 deletions(-) diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index 79f805d52ad..0e5d5fd4b79 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -48,7 +48,6 @@ interface ExpressionProps { onSetCondition: (refId: string) => void; onUpdateRefId: (oldRefId: string, newRefId: string) => void; onRemoveExpression: (refId: string) => void; - onUpdateExpressionType: (refId: string, type: ExpressionQueryType) => void; onChangeQuery: (query: ExpressionQuery) => void; } @@ -62,7 +61,6 @@ export const Expression: FC = ({ onSetCondition, onUpdateRefId, onRemoveExpression, - onUpdateExpressionType, // this method is not used? maybe we should remove it onChangeQuery, }) => { const styles = useStyles2(getStyles); diff --git a/public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx index 87952c4b19f..bf1a9985ec2 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ExpressionsEditor.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { GrafanaTheme2, PanelData } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { isExpressionQuery } from 'app/features/expressions/guards'; -import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; +import { ExpressionQuery } from 'app/features/expressions/types'; import { AlertQuery } from 'app/types/unified-alerting-dto'; import { Expression } from '../expressions/Expression'; @@ -18,7 +18,6 @@ interface Props { queries: AlertQuery[]; onRemoveExpression: (refId: string) => void; onUpdateRefId: (oldRefId: string, newRefId: string) => void; - onUpdateExpressionType: (refId: string, type: ExpressionQueryType) => void; onUpdateQueryExpression: (query: ExpressionQuery) => void; } @@ -29,12 +28,15 @@ export const ExpressionsEditor = ({ panelData, onUpdateRefId, onRemoveExpression, - onUpdateExpressionType, onUpdateQueryExpression, }: Props) => { const expressionQueries = useMemo(() => { return queries.reduce((acc: ExpressionQuery[], query) => { - return isExpressionQuery(query.model) ? acc.concat(query.model) : acc; + if (isExpressionQuery(query.model)) { + acc.push(query.model); + } + + return acc; }, []); }, [queries]); const styles = useStyles2(getStyles); @@ -64,7 +66,6 @@ export const ExpressionsEditor = ({ onSetCondition={onSetCondition} onRemoveExpression={onRemoveExpression} onUpdateRefId={onUpdateRefId} - onUpdateExpressionType={onUpdateExpressionType} onChangeQuery={onUpdateQueryExpression} /> ); diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 844443a4dff..d19ac4bb619 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -73,7 +73,6 @@ import { updateExpression, updateExpressionRefId, updateExpressionTimeRange, - updateExpressionType, } from './reducer'; import { useAdvancedMode } from './useAdvancedMode'; import { useAlertQueryRunner } from './useAlertQueryRunner'; @@ -591,9 +590,6 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange, mod dispatch(removeExpression(refId)); }} onUpdateRefId={onUpdateRefId} - onUpdateExpressionType={(refId, type) => { - dispatch(updateExpressionType({ refId, type })); - }} onUpdateQueryExpression={(model) => { dispatch(updateExpression(model)); }} diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap index 292a6904f80..33f3b0ea4e8 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/__snapshots__/reducer.test.tsx.snap @@ -442,55 +442,3 @@ exports[`Query and expressions reducer should update an expression refId and rew ], } `; - -exports[`Query and expressions reducer should update expression type 1`] = ` -{ - "queries": [ - { - "datasourceUid": "abc123", - "model": { - "refId": "A", - }, - "queryType": "query", - "refId": "A", - }, - { - "datasourceUid": "__expr__", - "model": { - "conditions": [ - { - "evaluator": { - "params": [ - 0, - 0, - ], - "type": "gt", - }, - "operator": { - "type": "and", - }, - "query": { - "params": [], - }, - "reducer": { - "params": [], - "type": "avg", - }, - "type": "query", - }, - ], - "datasource": { - "name": "Expression", - "type": "__expr__", - "uid": "__expr__", - }, - "expression": "", - "refId": "B", - "type": "reduce", - }, - "queryType": "", - "refId": "B", - }, - ], -} -`; diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx index 31858408509..a4a214b8348 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.test.tsx @@ -22,7 +22,6 @@ import { updateExpression, updateExpressionRefId, updateExpressionTimeRange, - updateExpressionType, } from './reducer'; const reduceExpression: AlertQuery = { @@ -388,22 +387,6 @@ describe('Query and expressions reducer', () => { expect(newState).toMatchSnapshot(); }); - - it('should update expression type', () => { - const initialState: QueriesAndExpressionsState = { - queries: [alertQuery, expressionQuery], - }; - - const newState = queriesAndExpressionsReducer( - initialState, - updateExpressionType({ - refId: 'B', - type: ExpressionQueryType.reduce, - }) - ); - - expect(newState).toMatchSnapshot(); - }); it('should remove first reducer', () => { const initialState: QueriesAndExpressionsState = { queries: [alertQuery, reduceExpression, thresholdExpression], diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts index 372e028bbcc..1514533cce0 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts @@ -283,23 +283,6 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder queryType: 'expression', }); } - }) - .addCase(updateExpressionType, (state, action) => { - state.queries = state.queries.map((query) => { - return query.refId === action.payload.refId - ? { - ...query, - model: { - ...expressionDatasource.newQuery({ - type: action.payload.type, - conditions: [{ ...defaultCondition, query: { params: [] } }], - expression: '', - }), - refId: action.payload.refId, - }, - } - : query; - }); }); }); diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index 2e9b7a5730b..a5410e6ebd6 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -7,6 +7,7 @@ import { GrafanaAlertStateDecision, GrafanaRuleDefinition, RulerAlertingRuleDTO, + RulerGrafanaRuleDTO, } from 'app/types/unified-alerting-dto'; import { EvalFunction } from '../../state/alertDef'; @@ -20,6 +21,7 @@ import { alertingRulerRuleToRuleForm, cleanAnnotations, cleanLabels, + fixMissingRefIdsInExpressionModel, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, getContactPointsFromDTO, @@ -520,3 +522,70 @@ describe('getDefaultExpressions', () => { expect(thresholdModel.expression).toBe('X'); }); }); + +describe('fixMissingRefIdsInExpressionModel', () => { + it('should return non-Grafana managed rules unchanged', () => { + const cloudAlertingRule: RulerAlertingRuleDTO = { + alert: 'CloudAlert', + expr: 'up == 0', + for: '5m', + labels: { severity: 'critical' }, + annotations: { summary: 'Instance down' }, + }; + + const result = fixMissingRefIdsInExpressionModel(cloudAlertingRule); + + expect(result).toEqual(cloudAlertingRule); + expect(result).toBe(cloudAlertingRule); // should be the exact same reference + }); + + it('should copy refId from query to model when model.refId is missing in Grafana managed rules', () => { + const ruleWithMissingRefId: RulerGrafanaRuleDTO = { + grafana_alert: { + uid: 'test-uid', + title: 'Test Alert', + namespace_uid: 'namespace-uid', + rule_group: 'test-group', + condition: 'B', + no_data_state: GrafanaAlertStateDecision.NoData, + exec_err_state: GrafanaAlertStateDecision.Alerting, + is_paused: false, + data: [ + { + refId: 'A', + datasourceUid: 'datasource-uid', + queryType: '', + relativeTimeRange: { from: 600, to: 0 }, + // @ts-ignore + model: { + // refId is missing here + datasource: { + type: 'grafana-testdata-datasource', + uid: 'PD8C576611E62080A', + }, + }, + }, + { + refId: 'B', + datasourceUid: ExpressionDatasourceUID, + queryType: '', + // @ts-ignore + model: { + // refId is missing here + type: ExpressionQueryType.reduce, + expression: 'A', + }, + }, + ], + }, + for: '5m', + labels: {}, + annotations: {}, + }; + + const result = fixMissingRefIdsInExpressionModel(ruleWithMissingRefId); + + expect(result.grafana_alert.data[0].model.refId).toBe('A'); + expect(result.grafana_alert.data[1].model.refId).toBe('B'); + }); +}); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index 077934e21e1..0e83a276851 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -1,3 +1,5 @@ +import { produce } from 'immer'; + import { DataSourceInstanceSettings, IntervalValues, @@ -278,14 +280,16 @@ function getEditorSettingsFromDTO(ga: GrafanaRuleDefinition) { export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues { const { ruleSourceName, namespace, group, rule } = ruleWithLocation; - const isGrafanaRecordingRule = rulerRuleType.grafana.recordingRule(rule); + const normalizedRule = fixMissingRefIdsInExpressionModel(rule); + + const isGrafanaRecordingRule = rulerRuleType.grafana.recordingRule(normalizedRule); const defaultFormValues = getDefaultFormValues(isGrafanaRecordingRule ? RuleFormType.grafanaRecording : undefined); if (isGrafanaRulesSource(ruleSourceName)) { // GRAFANA-MANAGED RULES if (isGrafanaRecordingRule) { // grafana recording rule - const ga = rule.grafana_alert; + const ga = normalizedRule.grafana_alert; return { ...defaultFormValues, name: ga.title, @@ -294,16 +298,16 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF evaluateEvery: group.interval || defaultFormValues.evaluateEvery, queries: ga.data, condition: ga.condition, - annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)), - labels: listifyLabelsOrAnnotations(rule.labels, true), + annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(normalizedRule.annotations, false)), + labels: listifyLabelsOrAnnotations(normalizedRule.labels, true), folder: { title: namespace, uid: ga.namespace_uid }, isPaused: ga.is_paused, metric: ga.record?.metric, targetDatasourceUid: ga.record?.target_datasource_uid || defaultFormValues.targetDatasourceUid, }; - } else if (rulerRuleType.grafana.rule(rule)) { + } else if (rulerRuleType.grafana.rule(normalizedRule)) { // grafana alerting rule - const ga = rule.grafana_alert; + const ga = normalizedRule.grafana_alert; const routingSettings: AlertManagerManualRouting | undefined = getContactPointsFromDTO(ga); if (ga.no_data_state !== undefined && ga.exec_err_state !== undefined) { return { @@ -312,14 +316,14 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF type: RuleFormType.grafana, group: group.name, evaluateEvery: group.interval || defaultFormValues.evaluateEvery, - evaluateFor: rule.for || '0', - keepFiringFor: rule.keep_firing_for || '0', + evaluateFor: normalizedRule.for || '0', + keepFiringFor: normalizedRule.keep_firing_for || '0', noDataState: ga.no_data_state, execErrState: ga.exec_err_state, queries: ga.data, condition: ga.condition, - annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)), - labels: listifyLabelsOrAnnotations(rule.labels, true), + annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(normalizedRule.annotations, false)), + labels: listifyLabelsOrAnnotations(normalizedRule.labels, true), folder: { title: namespace, uid: ga.namespace_uid }, isPaused: ga.is_paused, @@ -338,7 +342,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF } } else { // DATASOURCE-MANAGED RULES - if (rulerRuleType.dataSource.alertingRule(rule)) { + if (rulerRuleType.dataSource.alertingRule(normalizedRule)) { const datasourceUid = getDataSourceSrv().getInstanceSettings(ruleSourceName)?.uid ?? ''; const defaultQuery = { @@ -346,27 +350,27 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF datasourceUid, queryType: '', relativeTimeRange: getDefaultRelativeTimeRange(), - expr: rule.expr, + expr: normalizedRule.expr, model: { refId: 'A', hide: false, - expr: rule.expr, + expr: normalizedRule.expr, }, }; - const alertingRuleValues = alertingRulerRuleToRuleForm(rule); + const alertingRuleValues = alertingRulerRuleToRuleForm(normalizedRule); return { ...defaultFormValues, ...alertingRuleValues, queries: [defaultQuery], - annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)), + annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(normalizedRule.annotations, false)), type: RuleFormType.cloudAlerting, dataSourceName: ruleSourceName, namespace, group: group.name, }; - } else if (rulerRuleType.dataSource.recordingRule(rule)) { + } else if (rulerRuleType.dataSource.recordingRule(normalizedRule)) { const datasourceUid = getDataSourceSrv().getInstanceSettings(ruleSourceName)?.uid ?? ''; const defaultQuery = { @@ -374,15 +378,15 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF datasourceUid, queryType: '', relativeTimeRange: getDefaultRelativeTimeRange(), - expr: rule.expr, + expr: normalizedRule.expr, model: { refId: 'A', hide: false, - expr: rule.expr, + expr: normalizedRule.expr, }, }; - const recordingRuleValues = recordingRulerRuleToRuleForm(rule); + const recordingRuleValues = recordingRulerRuleToRuleForm(normalizedRule); return { ...defaultFormValues, @@ -399,6 +403,23 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF } } +/** + * This function isn't supposed to be needed, but we've noticed some customers are creating rules via Provisioning or + * other interfaces where they aren't including the RefId in the "model" of the expression so copy the refId from the query definition. + */ +export function fixMissingRefIdsInExpressionModel(rule: T): T { + // non-Grafana managed rules don't use expression nodes so we return the rule as-is + if (!rulerRuleType.grafana.rule(rule)) { + return rule; + } + + return produce(rule, (draft) => { + draft.grafana_alert.data.forEach((query) => { + query.model.refId = query.model.refId ?? query.refId; + }); + }); +} + export function grafanaRuleDtoToFormValues(rule: RulerGrafanaRuleDTO, namespace: string): RuleFormValues { const isGrafanaRecordingRule = rulerRuleType.grafana.recordingRule(rule); const defaultFormValues = getDefaultFormValues(isGrafanaRecordingRule ? RuleFormType.grafanaRecording : undefined); From f0a394e67b02eaade338bdf50ae7b272d63b01e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 26 Nov 2025 14:30:18 +0100 Subject: [PATCH 25/94] Dashboards: Use new sidebar in dynamic dashboards (#114245) * Dynamic dashboards sidebar wip * Progress * Outline in view mode cannot change name * Only one pane at a time * Adding starbutton and custom precence * undo / redo working * Progress * Update * Progress * Update * Public badge, provisining badge * playlist stop * Update * some initial unit tests * Fix export tooltip * Close pane when leaving edit mode if pane is an element * Update * e2e fixes * fixing e2e * Fix lint suppressions * e2e fixes * fixing more e2e * fixing e2e * fix e2e * e2e fixes * Fixinfg e2e * fixing e2e --- .../dashboard-group-panels.spec.ts | 24 +- .../dashboard-outline.spec.ts | 3 + .../dashboards-add-panel.spec.ts | 3 + .../dashboards-edit-custom-variables.spec.ts | 1 + .../dashboards-panel-layouts.spec.ts | 37 ++- .../dashboards-repeats-auto-grid.spec.ts | 36 +- .../dashboards-repeats-custom-grid.spec.ts | 4 +- .../dashboards-repeats-tabs-layout.spec.ts | 8 +- .../dashboards-title-description.spec.ts | 6 +- e2e-playwright/dashboard-new-layouts/utils.ts | 1 + eslint-suppressions.json | 5 - .../src/selectors/components.ts | 8 +- .../src/selectors/pages.ts | 8 + .../src/components/Sidebar/Sidebar.tsx | 3 +- .../src/components/Sidebar/SidebarButton.tsx | 60 ++-- .../components/Sidebar/SidebarPaneHeader.tsx | 2 + .../src/components/Sidebar/useSidebar.tsx | 6 +- .../edit-pane/DashboardEditPane.tsx | 51 ++- .../DashboardEditPaneRenderer.test.tsx | 69 ++-- .../edit-pane/DashboardEditPaneRenderer.tsx | 307 ++++++++---------- .../edit-pane/DashboardEditPaneSplitter.tsx | 178 +++++----- .../edit-pane/DashboardEditableElement.tsx | 30 +- .../edit-pane/DashboardExportButton.tsx | 57 ++++ .../edit-pane/DashboardOutline.test.tsx | 2 +- .../edit-pane/DashboardOutline.tsx | 25 +- .../edit-pane/EditPaneHeader.tsx | 38 +-- .../dashboard-scene/edit-pane/shared.ts | 5 - .../edit-pane/useEditableElement.ts | 21 -- .../edit-pane/useOutlineRename.tsx | 6 +- .../scene/DashboardControls.tsx | 42 ++- .../dashboard-scene/scene/DashboardScene.tsx | 10 +- .../scene/GoToSnapshotOriginButton.tsx | 2 +- .../scene/NavToolbarActions.tsx | 4 +- .../scene/new-toolbar/RightActions.tsx | 96 +----- .../actions/ExportDashboardButton.tsx | 39 --- .../actions/OpenSnapshotOriginButton.tsx | 2 +- .../new-toolbar/actions/SaveDashboard.tsx | 12 +- .../actions/ShareExportDashboardButton.tsx | 4 +- .../new-toolbar/actions/ToolbarSwitch.tsx | 14 +- .../variables/VariableEditableElement.tsx | 8 +- public/locales/en-US/grafana.json | 47 ++- 41 files changed, 634 insertions(+), 650 deletions(-) create mode 100644 public/app/features/dashboard-scene/edit-pane/DashboardExportButton.tsx delete mode 100644 public/app/features/dashboard-scene/edit-pane/useEditableElement.ts delete mode 100644 public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts index ffdd52e24f6..f077f48a7e0 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -414,13 +414,13 @@ test.describe( ).toBeVisible(); // Go back to dashboard options - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); + 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('Tabs').click(); + await page.getByLabel('layout-selection-option-Tabs').click(); await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible(); await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible(); @@ -518,14 +518,14 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) .fill('Test row 1'); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // clear the title input to simulate no title and click away to trigger onBlur await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 1')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) .fill(''); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // title should be set to a default name await expect( @@ -543,14 +543,14 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) .fill('Test row 2'); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // clear the title input to simulate no title and click away to trigger onBlur await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 2')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) .fill(''); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // title should be set to a default name + 1 to avoid duplicates await expect( @@ -755,13 +755,13 @@ test.describe( await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible(); // Go back to dashboard options - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); + 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('Rows').click(); + await page.getByLabel('layout-selection-option-Rows').click(); await dashboardPage .getByGrafanaSelector(selectors.components.DashboardRow.wrapper('New tab 1')) @@ -903,14 +903,14 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) .fill('Test tab 1'); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // clear the title input to simulate no title and click away to trigger onBlur await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 1')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) .fill(''); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // title should be set to a default name await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); @@ -923,14 +923,14 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) .fill('Test tab 2'); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // clear the title input to simulate no title and click away to trigger onBlur await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 2')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) .fill(''); - await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.closePane).click(); // title should be set to a default name + 1 to avoid duplicates await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts index ef38379f006..00a84a188a5 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts @@ -21,6 +21,7 @@ test.describe( const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); // Should be able to click Variables item in outline to see add variable button await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); @@ -28,6 +29,8 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) ).toBeVisible(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); + // Clicking a panel should scroll that panel in view await expect(page.getByText('Dashboard panel 48')).toBeHidden(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Panel #48')).click(); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts index ffea8243a2f..c2af96349cf 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts @@ -22,6 +22,9 @@ test.describe( const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await page.evaluate(() => { diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts index 24c61e5e960..4715dfc7128 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-custom-variables.spec.ts @@ -199,6 +199,7 @@ test.describe( .click(); // Open the modal editor in the side pane + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.node('Variables')).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('foo')).click(); await openModal(dashboardPage, selectors); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts index 087551a55cd..66d634eefa7 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -32,9 +32,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); await expect( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) @@ -50,6 +50,7 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await checkAutoGridLayoutInputs(dashboardPage, selectors); }); @@ -63,9 +64,10 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); // Get initial positions - standard width should have panels on different rows const firstPanelTop = await getPanelTop(dashboardPage, selectors); @@ -98,6 +100,7 @@ test.describe( await page.reload(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector( @@ -123,9 +126,10 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) @@ -134,7 +138,7 @@ test.describe( await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth) - .fill('900'); + .fill('1100'); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth) .blur(); @@ -148,12 +152,13 @@ test.describe( await verifyPanelsStackedVertically(dashboardPage, selectors); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector( selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth ) - ).toHaveValue('900'); + ).toHaveValue('1100'); await verifyPanelsStackedVertically(dashboardPage, selectors); @@ -180,9 +185,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) @@ -198,6 +203,7 @@ test.describe( await verifyPanelsStackedVertically(dashboardPage, selectors); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) @@ -215,9 +221,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); const regularRowHeight = await getPanelHeight(dashboardPage, selectors); @@ -250,6 +256,7 @@ test.describe( }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) @@ -270,9 +277,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); const regularRowHeight = await getPanelHeight(dashboardPage, selectors); @@ -303,6 +310,7 @@ test.describe( }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector( @@ -327,9 +335,9 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toHaveCount(3); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await page.getByLabel('Expand Panel layout category').click(); - - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); // Set narrow column width first to ensure panels fit horizontally await dashboardPage @@ -357,6 +365,7 @@ test.describe( }).toPass(); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await expect( dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts index 28ca51e3b28..7cf22108e1e 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts @@ -37,9 +37,10 @@ test.describe( }, () => { test('can enable repeats', async ({ dashboardPage, selectors, page }) => { - await importTestDashboard(page, selectors, 'Auto grid repeats - add repeats'); + await importTestDashboard(page, selectors, 'Auto-grid repeats - add repeats'); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await switchToAutoGrid(page); @@ -70,11 +71,12 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - update on variable change', + 'Auto-grid repeats - update on variable change', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); @@ -113,6 +115,7 @@ test.describe( ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await switchToAutoGrid(page); @@ -138,11 +141,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - update through panel editor', + 'Auto-grid repeats - update through panel editor', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -202,11 +207,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - update through directly loaded panel editor', + 'Auto-grid repeats - update through directly loaded panel editor', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); @@ -257,11 +264,12 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - move repeated panels', + 'Auto-grid repeats - move repeated panels', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); await switchToAutoGrid(page); @@ -304,11 +312,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - move repeated panels', + 'Auto-grid repeats - move repeated panels 2', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -332,9 +342,7 @@ test.describe( const repeatedPanelUrl = page.url(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); await dashboardPage .getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) @@ -367,11 +375,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - view embedded repeated panel', + 'Auto-grid repeats - view embedded repeated panel', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -393,11 +403,13 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto grid repeats - remove repeats', + 'Auto-grid repeats - remove repeats', JSON.stringify(testV2DashWithRepeats) ); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); + await switchToAutoGrid(page); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -453,5 +465,5 @@ test.describe( async function switchToAutoGrid(page: Page) { await page.getByLabel('Expand Panel layout category').click(); - await page.getByLabel('Auto grid').click(); + await page.getByLabel('layout-selection-option-Auto grid').click(); } diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts index 2d235571f97..8cc1f552377 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts @@ -303,9 +303,7 @@ test.describe( const repeatedPanelUrl = page.url(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); await dashboardPage .getByGrafanaSelector(selectors.components.Panels.Panel.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts index 100ec864b0d..f78bbc5e08e 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts @@ -316,9 +316,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) ).toBeVisible(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); // repeated panel in original tab repeat await dashboardPage @@ -341,9 +339,7 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) ).toBeVisible(); - await dashboardPage - .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) - .click(); + await page.keyboard.press('Escape'); // repeated panel in repeated tab await dashboardPage diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts index 67dc17f648b..d9a82364a70 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts @@ -21,11 +21,7 @@ test.describe( const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); - - // Check that current dashboard title is visible in breadcrumb - await expect( - dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('Annotation filtering')) - ).toBeVisible(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); const titleInput = page.locator('[aria-label="dashboard-options Title field property editor"] input'); await expect(titleInput).toHaveValue('Annotation filtering'); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index da629ac8141..c1e00e1a67b 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -48,6 +48,7 @@ export const flows = { }, async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 2450d3721d7..fde5e53587d 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1907,11 +1907,6 @@ "count": 2 } }, - "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx": { "react-hooks/rules-of-hooks": { "count": 4 diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index e56318477c0..98fa6053d11 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -57,6 +57,11 @@ export const versionedComponents = { '12.1.0': 'data-testid DashboardEditPaneSplitter primary body', }, }, + Sidebar: { + closePane: { + '12.4.0': 'data-testid Sidebar close pane', + }, + }, EditPaneHeader: { deleteButton: { '12.1.0': 'data-testid EditPaneHeader delete panel', @@ -70,9 +75,6 @@ export const versionedComponents = { duplicate: { '12.1.0': 'data-testid EditPaneHeader duplicate', }, - backButton: { - '12.1.0': 'data-testid EditPaneHeader back', - }, }, TimePicker: { openButton: { diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 96d033d5b14..79422ee8db9 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -183,6 +183,14 @@ export const versionedPages = { url: { [MIN_GRAFANA_VERSION]: (uid: string) => `/d/${uid}`, }, + Sidebar: { + optionsButton: { + '12.4.0': 'data-testid Dashboard Sidebar options button', + }, + outlineButton: { + '12.4.0': 'data-testid Dashboard Sidebar outline button', + }, + }, DashNav: { nav: { [MIN_GRAFANA_VERSION]: 'Dashboard navigation', diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx index 2d9b407a1ad..8c1bf784e53 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx @@ -59,8 +59,9 @@ export function SiderbarToolbar({ children }: SiderbarToolbarProps) { {context.hasOpenPane && ( )}
diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx index f436d9630f6..9f5e0f20d71 100644 --- a/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx +++ b/packages/grafana-ui/src/components/Sidebar/SidebarButton.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useContext } from 'react'; +import React, { ButtonHTMLAttributes, useContext } from 'react'; import { GrafanaTheme2, IconName, isIconName } from '@grafana/data'; @@ -11,38 +11,48 @@ import { Tooltip } from '../Tooltip/Tooltip'; import { SidebarContext } from './useSidebar'; -export interface Props { +export interface Props extends ButtonHTMLAttributes { icon: IconName; active?: boolean; - onClick?: () => void; - title: string; tooltip?: string; + title: string; } -export function SidebarButton({ icon, active, onClick, title, tooltip }: Props) { - const styles = useStyles2(getStyles); - const context = useContext(SidebarContext); +export const SidebarButton = React.forwardRef( + ({ icon, active, onClick, title, tooltip, ...restProps }, ref) => { + const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); - if (!context) { - throw new Error('Sidebar.Button must be used within a Sidebar component'); + if (!context) { + throw new Error('Sidebar.Button must be used within a Sidebar component'); + } + + const buttonClass = cx( + styles.button, + context.compact && styles.compact, + active && styles.active, + context.position === 'left' && styles.leftButton + ); + + return ( + + + + ); } +); - const buttonClass = cx( - styles.button, - context.compact && styles.compact, - active && styles.active, - context.position === 'left' && styles.leftButton - ); - - return ( - - - - ); -} +SidebarButton.displayName = 'SidebarButton'; function renderIcon(icon: IconName | React.ReactNode, compact?: boolean) { if (!icon) { diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx index 75393777879..42fe30b083c 100644 --- a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx +++ b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { ReactNode } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { useStyles2 } from '../../themes/ThemeContext'; @@ -27,6 +28,7 @@ export function SidebarPaneHeader({ children, onClose, title }: Props) { onClick={onClose} aria-label={t('grafana-ui.sidebar.close', 'Close')} tooltip={t('grafana-ui.sidebar.close', 'Close')} + data-testid={selectors.components.Sidebar.closePane} /> )} diff --git a/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx index 03d04b2f81a..1d51b3fdd79 100644 --- a/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx +++ b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx @@ -16,7 +16,7 @@ export interface SidebarContextValue { bottomMargin: number; edgeMargin: number; contentMargin: number; - onDockChange: () => void; + onToggleDock: () => void; onResize: (diff: number) => void; } @@ -56,7 +56,7 @@ export function useSidebar({ // Used to accumulate drag distance to know when to change compact mode const [_, setCompactDrag] = React.useState(0); - const onDockChange = useCallback(() => setIsDocked((prev) => !prev), []); + const onToggleDock = useCallback(() => setIsDocked((prev) => !prev), []); const prop = position === 'right' ? 'paddingRight' : 'paddingLeft'; const toolbarWidth = @@ -98,7 +98,7 @@ export function useSidebar({ return { isDocked, - onDockChange, + onToggleDock, onResize, outerWrapperProps, position, diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index 5a1b3624a2b..64f0c774ea8 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -26,8 +26,12 @@ export interface DashboardEditPaneState extends SceneObjectState { undoStack: DashboardEditActionEventPayload[]; redoStack: DashboardEditActionEventPayload[]; + openPane?: DashboardSidebarPaneName; + isDocked?: boolean; } +export type DashboardSidebarPaneName = 'element' | 'outline' | 'filters'; + export class DashboardEditPane extends SceneObjectBase { public constructor() { super({ @@ -192,6 +196,7 @@ export class DashboardEditPane extends SceneObjectBase { this.setState({ selectionContext: { ...this.state.selectionContext, selected: [], enabled: false }, selection: undefined, + openPane: this.state.openPane === 'element' ? undefined : this.state.openPane, }); } @@ -227,7 +232,6 @@ export class DashboardEditPane extends SceneObjectBase { } const elementSelection = this.state.selection ?? new ElementSelection([[id, obj.getRef()]]); - const { selection, contextItems: selected } = elementSelection.getStateWithValue(id, obj, !!multi); this.updateSelection(new ElementSelection(selection), selected); @@ -255,17 +259,58 @@ export class DashboardEditPane extends SceneObjectBase { document.activeElement.blur(); } - this.setState({ selection, selectionContext: { ...this.state.selectionContext, selected } }); + this.setState({ + selection, + selectionContext: { ...this.state.selectionContext, selected }, + openPane: selection ? 'element' : undefined, + }); } - public clearSelection() { + /** + * @param force If force = true it will clear selection even when docked + * @returns + */ + public clearSelection(force = false) { if (!this.state.selection) { return; } + // If we are docked then clearing selection should select dashboard itself + // Unless the user explicitly closes pane + if (this.state.isDocked && !force) { + const obj = this.state.selection?.getFirstObject(); + const dashboard = getDashboardSceneFor(this); + if (obj !== dashboard) { + this.selectObject(dashboard, dashboard.state.key!); + } + return; + } + this.updateSelection(undefined, []); } + public openPane(openPane: DashboardSidebarPaneName) { + if (this.state.selection) { + this.clearSelection(true); + } + + if (openPane === this.state.openPane) { + this.setState({ openPane: undefined }); + } else { + this.setState({ openPane }); + } + } + + public closePane() { + if (this.state.selection) { + this.clearSelection(true); + } + + if (this.state.openPane) { + this.setState({ openPane: undefined }); + } + } + private newObjectAddedToCanvas(obj: SceneObject) { this.selectObject(obj, obj.state.key!); this.state.selection?.markAsNewElement(); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx index 78192a0c8fe..41c81222661 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx @@ -1,18 +1,17 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { act, screen } from '@testing-library/react'; +import { render } from 'test/test-utils'; import { getPanelPlugin } from '@grafana/data/test'; import { selectors } from '@grafana/e2e-selectors'; -import { setPluginImportUtils } from '@grafana/runtime'; +import { setPluginImportUtils, config } from '@grafana/runtime'; import { SceneGridLayout, SceneTimeRange, SceneVariableSet, VizPanel } from '@grafana/scenes'; import { DashboardScene } from '../scene/DashboardScene'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; -import { DashboardInteractions } from '../utils/interactions'; import { activateFullSceneTree } from '../utils/test-utils'; -import { DashboardEditPaneRenderer } from './DashboardEditPaneRenderer'; +import { DashboardEditPaneSplitter } from './DashboardEditPaneSplitter'; setPluginImportUtils({ importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), @@ -26,14 +25,9 @@ jest.mock('../utils/interactions', () => ({ }, })); -jest.mock('react-router-dom-v5-compat', () => ({ - ...jest.requireActual('react-router-dom-v5-compat'), - useLocation: () => ({ - pathname: '/dashboard/test', - search: '', - hash: '', - state: null, - }), +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + useChromeHeaderHeight: jest.fn().mockReturnValue(80), })); export function buildTestScene() { @@ -47,26 +41,43 @@ export function buildTestScene() { }), }), }); - activateFullSceneTree(testScene); return testScene; } describe('DashboardEditPaneRenderer', () => { - describe('outline interactions tracking', () => { - it('should call DashboardInteractions.outlineClicked when clicking on dashboard outline', async () => { - const user = userEvent.setup(); - const scene = buildTestScene(); - render( - {}} - /> - ); - const outlineButton = screen.getByTestId(selectors.components.PanelEditor.Outline.section); - await user.click(outlineButton); + config.featureToggles.dashboardNewLayouts = true; - expect(DashboardInteractions.dashboardOutlineClicked).toHaveBeenCalled(); - }); + it('Should render sidebar', async () => { + const scene = buildTestScene(); + + act(() => activateFullSceneTree(scene)); + + render(); + + expect(await screen.findByTestId(selectors.pages.Dashboard.Sidebar.outlineButton)).toBeInTheDocument(); }); + + it('Should sync sidebar docked state with edit pane state', async () => { + const scene = buildTestScene(); + render(); + + act(() => screen.getByLabelText('Outline').click()); + + expect(await screen.findByTestId('sidebar-dock-toggle')).toBeInTheDocument(); + + act(() => screen.getByTestId('sidebar-dock-toggle').click()); + + expect(scene.state.editPane.state.isDocked).toBe(true); + }); + + // describe('outline interactions tracking', () => { + // it('should call DashboardInteractions.outlineClicked when clicking on dashboard outline', async () => { + // const user = userEvent.setup(); + // const scene = buildTestScene(); + // render(); + // const outlineButton = screen.getByTestId(selectors.components.PanelEditor.Outline.section); + // await user.click(outlineButton); + // expect(DashboardInteractions.dashboardOutlineClicked).toHaveBeenCalled(); + // }); + // }); }); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx index 9ca7fbab86e..80ec361476a 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.tsx @@ -1,211 +1,160 @@ -import { css, cx } from '@emotion/css'; -import { Resizable } from 're-resizable'; -import { useLocalStorage } from 'react-use'; +import { useMemo } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Trans, t } from '@grafana/i18n'; +import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { useSceneObjectState } from '@grafana/scenes'; -import { useStyles2, useSplitter, ToolbarButton, ScrollContainer, Text, Icon, clearButtonStyles } from '@grafana/ui'; +import { Sidebar } from '@grafana/ui'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { DashboardInteractions } from '../utils/interactions'; +import { DashboardScene } from '../scene/DashboardScene'; +import { onOpenSnapshotOriginalDashboard } from '../scene/GoToSnapshotOriginButton'; +import { ManagedDashboardNavBarBadge } from '../scene/ManagedDashboardNavBarBadge'; +import { ToolbarActionProps } from '../scene/new-toolbar/types'; +import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; import { DashboardEditPane } from './DashboardEditPane'; +import { ShareExportDashboardButton } from './DashboardExportButton'; import { DashboardOutline } from './DashboardOutline'; import { ElementEditPane } from './ElementEditPane'; -import { useEditableElement } from './useEditableElement'; export interface Props { editPane: DashboardEditPane; - isEditPaneCollapsed: boolean; - openOverlay?: boolean; - onToggleCollapse: () => void; + dashboard: DashboardScene; + isDocked?: boolean; } /** * Making the EditPane rendering completely standalone (not using editPane.Component) in order to pass custom react props */ -export function DashboardEditPaneRenderer({ editPane, isEditPaneCollapsed, onToggleCollapse, openOverlay }: Props) { - const { selection } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); - const styles = useStyles2(getStyles); - const clearButton = useStyles2(clearButtonStyles); - const editableElement = useEditableElement(selection, editPane); +export function DashboardEditPaneRenderer({ editPane, dashboard, isDocked }: Props) { + const { selection, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); + const { isEditing, meta, uid } = dashboard.useState(); + const hasUid = Boolean(uid); const selectedObject = selection?.getFirstObject(); - const isNewElement = selection?.isNewElement() ?? false; - const [outlineCollapsed, setOutlineCollapsed] = useLocalStorage( - 'grafana.dashboard.edit-pane.outline.collapsed', - false - ); - const [outlinePaneSize = 0.4, setOutlinePaneSize] = useLocalStorage('grafana.dashboard.edit-pane.outline.size', 0.4); - // splitter for template and payload editor - const splitter = useSplitter({ - direction: 'column', - handleSize: 'sm', - // if Grafana Alertmanager, split 50/50, otherwise 100/0 because there is no payload editor - initialSize: 1 - outlinePaneSize, - dragPosition: 'middle', - onSizeChanged: (size) => { - setOutlinePaneSize(1 - size); - }, - }); + const editableElement = useMemo(() => { + if (selection) { + return selection.createSelectionElement(); + } - if (!editableElement) { - return null; - } - - if (isEditPaneCollapsed) { - return ( - <> -
- -
- - {openOverlay && ( - - - - )} - - ); - } - - if (outlineCollapsed) { - splitter.primaryProps.style.flexGrow = 1; - splitter.primaryProps.style.minHeight = 'unset'; - splitter.secondaryProps.style.flexGrow = 0; - splitter.secondaryProps.style.minHeight = 'min-content'; - } else { - splitter.primaryProps.style.minHeight = 'unset'; - splitter.secondaryProps.style.minHeight = 'unset'; - } + return undefined; + }, [selection]); return ( -
-
-
+ <> + {editableElement && ( + -
-
-
- - {!outlineCollapsed && ( -
- - - -
- )} -
-
-
+ + )} + {openPane === 'outline' && ( + + + + )} + + {isEditing && ( + <> + {config.featureToggles.dashboardUndoRedo && ( + <> + + + + )} + editPane.selectObject(dashboard, dashboard.state.key!)} + title={t('dashboard.sidebar.dashboard-options.title', 'Options')} + tooltip={t('dashboard.sidebar.dashboard-options.tooltip', 'Dashboard options')} + data-testid={selectors.pages.Dashboard.Sidebar.optionsButton} + active={selectedObject === dashboard ? true : false} + /> + dashboard.openV2SchemaEditor()} + /> + + + )} + {hasUid && } + editPane.openPane('outline')} + title={t('dashboard.sidebar.outline.title', 'Outline')} + tooltip={t('dashboard.sidebar.outline.tooltip', 'Content outline')} + data-testid={selectors.pages.Dashboard.Sidebar.outlineButton} + active={openPane === 'outline'} + > + {dashboard.isManaged() && Boolean(meta.canEdit) && } + {renderEnterpriseItems()} + {Boolean(meta.isSnapshot) && ( + onOpenSnapshotOriginalDashboard(dashboard.getSnapshotUrl())} + /> + )} + + ); } -function getStyles(theme: GrafanaTheme2) { - return { - wrapper: css({ - display: 'flex', - flexDirection: 'column', - flex: '1 1 0', - marginTop: theme.spacing(2), - borderLeft: `1px solid ${theme.colors.border.weak}`, - borderTop: `1px solid ${theme.colors.border.weak}`, - background: theme.colors.background.primary, - borderTopLeftRadius: theme.shape.radius.default, - }), - overlayWrapper: css({ - right: 0, - bottom: 0, - top: theme.spacing(2), - position: 'absolute !important' as 'absolute', - background: theme.colors.background.primary, - borderLeft: `1px solid ${theme.colors.border.weak}`, - borderTop: `1px solid ${theme.colors.border.weak}`, - boxShadow: theme.shadows.z3, - zIndex: theme.zIndex.navbarFixed, - flexGrow: 1, - }), - paneContent: css({ - overflow: 'hidden', - display: 'flex', - flexDirection: 'column', - }), - rotate180: css({ - rotate: '180deg', - }), - tabsbar: css({ - padding: theme.spacing(0, 1), - margin: theme.spacing(0.5, 0), - }), - expandOptionsWrapper: css({ - display: 'flex', - flexDirection: 'column', - padding: theme.spacing(2, 1, 2, 0), - }), - splitter: css({ - '&::after': { - background: 'transparent', - transform: 'unset', - width: '100%', - height: '1px', - top: '100%', - left: '0', - }, - }), - outlineCollapseButton: css({ - display: 'flex', - padding: theme.spacing(0.5, 2), - gap: theme.spacing(1), - justifyContent: 'space-between', - alignItems: 'center', - background: theme.colors.background.secondary, +function renderEnterpriseItems() { + const dashboard = getDashboardSrv().getCurrent()!; + const showProps = { dashboard }; - '&:hover': { - background: theme.colors.action.hover, - }, - }), - outlineContainer: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - overflow: 'hidden', - }), - }; + return dynamicDashNavActions.right.map((action, index) => { + if (action.show(showProps)) { + const ActionComponent = action.component; + return ; + } + return null; + }); +} + +function UndoButton({ dashboard }: ToolbarActionProps) { + const editPane = dashboard.state.editPane; + const { undoStack } = editPane.useState(); + const undoAction = undoStack[undoStack.length - 1]; + const undoWord = t('dashboard.sidebar.undo', 'Undo'); + const tooltip = `${undoWord}${undoAction?.description ? ` ${undoAction.description}` : ''}`; + + return ( + editPane.undoAction()} + title={undoWord} + tooltip={tooltip} + /> + ); +} + +function RedoButton({ dashboard }: ToolbarActionProps) { + const editPane = dashboard.state.editPane; + const { redoStack } = editPane.useState(); + const redoAction = redoStack[redoStack.length - 1]; + const redoWord = t('dashboard.sidebar.redo', 'Redo'); + const tooltip = `${redoWord}${redoAction?.description ? ` ${redoAction.description}` : ''}`; + + return ( + editPane.redoAction()} + /> + ); } diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index 9ede14fe11d..ce7df7a460b 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -1,19 +1,22 @@ import { css, cx } from '@emotion/css'; -import React, { CSSProperties, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, useChromeHeaderHeight } from '@grafana/runtime'; import { useSceneObjectState } from '@grafana/scenes'; -import { ElementSelectionContext, useStyles2 } from '@grafana/ui'; +import { ElementSelectionContext, useSidebar, useStyles2, Sidebar } from '@grafana/ui'; +import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import NativeScrollbar, { DivScrollElement } from 'app/core/components/NativeScrollbar'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { useSnappingSplitter } from '../panel-edit/splitter/useSnappingSplitter'; import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { PublicDashboardBadge } from '../scene/new-toolbar/actions/PublicDashboardBadge'; +import { StarButton } from '../scene/new-toolbar/actions/StarButton'; +import { dynamicDashNavActions } from '../utils/registerDynamicDashNavAction'; import { DashboardEditPaneRenderer } from './DashboardEditPaneRenderer'; -import { useEditPaneCollapsed } from './shared'; interface Props { dashboard: DashboardScene; @@ -26,7 +29,10 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls const headerHeight = useChromeHeaderHeight(); const { editPane } = dashboard.state; const styles = useStyles2(getStyles, headerHeight ?? 0); - const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed(); + const hasUid = Boolean(dashboard.state.uid); + const canStar = Boolean(dashboard.state.meta.canStar); + + //const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed(); if (!config.featureToggles.dashboardNewLayouts) { return ( @@ -40,21 +46,6 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls ); } - const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = - useSnappingSplitter({ - direction: 'row', - dragPosition: 'end', - initialSize: 330, - handleSize: 'sm', - usePixels: true, - collapseBelowPixels: 250, - collapsed: isCollapsed, - }); - - useEffect(() => { - setIsCollapsed(splitterState.collapsed); - }, [splitterState.collapsed, setIsCollapsed]); - /** * Enable / disable selection based on dashboard isEditing state */ @@ -66,15 +57,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls } }, [isEditing, editPane]); - const { selectionContext } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); - const containerStyle: CSSProperties = {}; - - if (!isEditing) { - primaryProps.style.flexGrow = 1; - primaryProps.style.width = '100%'; - primaryProps.style.minWidth = 'unset'; - containerStyle.overflow = 'unset'; - } + const { selectionContext, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true }); const onBodyRef = (ref: HTMLDivElement | null) => { if (ref) { @@ -82,54 +65,73 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls } }; - return ( -
- -
{ - if (evt.shiftKey) { - return; - } + const sidebarContext = useSidebar({ + hasOpenPane: Boolean(openPane), + contentMargin: 1, + position: 'right', + }); - editPane.clearSelection(); - }} - > - -
{controls}
-
-
- {body} -
+ /** + * Sync docked state to editPane state + */ + useEffect(() => { + editPane.setState({ isDocked: sidebarContext.isDocked }); + }, [sidebarContext.isDocked, editPane]); + + const onClearSelection: React.PointerEventHandler = (evt) => { + if (evt.shiftKey) { + return; + } + + editPane.clearSelection(); + }; + + return ( +
+ + + {hasUid && canStar && } + {hasUid && canStar && } + {renderDynamicNavActions()} + + } + /> +
+ {controls} +
+
+
+ {body}
+ + +
- {isEditing && ( - <> -
-
- 0} - /> -
- - )}
); } +function renderDynamicNavActions() { + const dashboard = getDashboardSrv().getCurrent()!; + const showProps = { dashboard }; + + return dynamicDashNavActions.left.map((action, index) => { + if (action.show(showProps)) { + const ActionComponent = action.component; + return ; + } + return null; + }); +} + function getStyles(theme: GrafanaTheme2, headerHeight: number) { return { canvasWrappperOld: css({ @@ -138,22 +140,33 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) { flexDirection: 'column', flexGrow: 1, }), - canvasWithSplitter: css({ - overflow: 'unset', - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - }), - canvasWithSplitterEditing: css({ - overflow: 'unset', - }), - bodyWrapper: css({ - label: 'body-wrapper', + container: css({ + label: 'container', display: 'flex', flexDirection: 'column', flexGrow: 1, position: 'relative', }), + bodyWrapper: css({ + label: 'body-wrapper', + display: 'flex', + flexDirection: 'row', + flexGrow: 1, + position: 'relative', + flex: '1 1 0', + overflow: 'hidden', + }), + bodyWithToolbar: css({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + minHeight: 0, + overflow: 'auto', + scrollbarWidth: 'thin', + scrollbarGutter: 'stable', + // without top padding the fixed controls headers is rendered over the selection outline. + padding: theme.spacing(0.125, 1, 2, 2), + }), body: css({ label: 'body', display: 'flex', @@ -181,11 +194,6 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) { // borderLeft: `1px solid ${theme.colors.border.weak}`, // background: theme.colors.background.primary, }), - splitter: css({ - '&:after': { - display: 'none', - }, - }), controlsWrapperSticky: css({ [theme.breakpoints.up('md')]: { position: 'sticky', diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 8c81d88b345..c68696e439d 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -8,10 +8,9 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { DashboardScene } from '../scene/DashboardScene'; import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; -import { EditSchemaV2Button } from '../scene/new-toolbar/actions/EditSchemaV2Button'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; -import { dashboardEditActions, undoRedoWasClicked } from './shared'; +import { dashboardEditActions } from './shared'; function useEditPaneOptions( this: DashboardEditableElement, @@ -69,19 +68,16 @@ export class DashboardEditableElement implements EditableDashboardElement { public renderActions(): ReactNode { return ( - <> - - - + ); } } @@ -104,7 +100,7 @@ export function DashboardTitleInput({ dashboard, id }: { dashboard: DashboardSce }} onBlur={(e) => { const titleUnchanged = valueBeforeEdit.current === e.currentTarget.value; - const shouldSkip = titleUnchanged || undoRedoWasClicked(e); + const shouldSkip = titleUnchanged; if (shouldSkip) { return; } @@ -135,7 +131,7 @@ export function DashboardDescriptionInput({ dashboard, id }: { dashboard: Dashbo }} onBlur={(e) => { const descriptionUnchanged = valueBeforeEdit.current === e.currentTarget.value; - const shouldSkip = descriptionUnchanged || undoRedoWasClicked(e); + const shouldSkip = descriptionUnchanged; if (shouldSkip) { return; } diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardExportButton.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardExportButton.tsx new file mode 100644 index 00000000000..35d8afbdb4d --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/DashboardExportButton.tsx @@ -0,0 +1,57 @@ +import { selectors } from '@grafana/e2e-selectors'; +import { t } from '@grafana/i18n'; +import { locationService } from '@grafana/runtime'; +import { Dropdown, Sidebar } from '@grafana/ui'; +import { appEvents } from 'app/core/app_events'; +import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; +import { ShowConfirmModalEvent } from 'app/types/events'; + +import { DashboardScene } from '../scene/DashboardScene'; +import ExportMenu from '../sharing/ExportButton/ExportMenu'; +import { DashboardInteractions } from '../utils/interactions'; + +interface Props { + dashboard: DashboardScene; +} + +const newExportButtonSelector = selectors.pages.Dashboard.DashNav.NewExportButton; + +export function ShareExportDashboardButton({ dashboard }: Props) { + return ( + } placement="left-end"> + { + if (dashboard.state.isEditing && dashboard.state.isDirty) { + evt.preventDefault(); + evt.stopPropagation(); + + appEvents.publish( + new ShowConfirmModalEvent({ + title: t('dashboard.sidebar.export.unsaved-modal.title', 'Save changes to dashboard?'), + text: t( + 'dashboard.sidebar.export.unsaved-modal.text', + 'You have unsaved changes to this dashboard. You need to save them before you can share it.' + ), + icon: 'exclamation-triangle', + noText: t('common.discard', 'Discard'), + yesText: t('common.save', 'Save'), + yesButtonVariant: 'primary', + onConfirm: () => dashboard.openSaveDrawer({}), + }) + ); + } else { + locationService.partial({ shareView: shareDashboardType.export }); + + DashboardInteractions.sharingCategoryClicked({ + item: shareDashboardType.export, + shareResource: getTrackingSource(), + }); + } + }} + /> + + ); +} diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx index 81d56cab57d..7b461551773 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx @@ -101,7 +101,7 @@ describe('DashboardOutline', () => { render( - + ); // select Row lvl 1 diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index 48a9c738856..4ce6536ac5c 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { SceneObject } from '@grafana/scenes'; -import { Box, Icon, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; +import { Box, Icon, Sidebar, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; import { isRepeatCloneOrChildOf } from '../utils/clone'; import { DashboardInteractions } from '../utils/interactions'; @@ -17,28 +17,36 @@ import { useOutlineRename } from './useOutlineRename'; export interface Props { editPane: DashboardEditPane; + isEditing: boolean | undefined; } -export function DashboardOutline({ editPane }: Props) { +export function DashboardOutline({ editPane, isEditing }: Props) { const dashboard = getDashboardSceneFor(editPane); return ( - - - + <> + editPane.closePane()} + /> + + + + ); } interface DashboardOutlineNodeProps { sceneObject: SceneObject; editPane: DashboardEditPane; + isEditing: boolean | undefined; depth: number; index: number; } -function DashboardOutlineNode({ sceneObject, editPane, depth, index }: DashboardOutlineNodeProps) { +function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index }: DashboardOutlineNodeProps) { const styles = useStyles2(getStyles); - const { key } = sceneObject.useState(); + const key = sceneObject.state.key; const [isCollapsed, setIsCollapsed] = useState(depth > 0); const { isSelected, onSelect } = useElementSelection(key); const isCloned = useMemo(() => isRepeatCloneOrChildOf(sceneObject), [sceneObject]); @@ -49,7 +57,7 @@ function DashboardOutlineNode({ sceneObject, editPane, depth, index }: Dashboard const children = editableElement.getOutlineChildren?.() ?? []; const elementInfo = editableElement.getEditableElementInfo(); const instanceName = elementInfo.instanceName === '' ? noTitleText : elementInfo.instanceName; - const outlineRename = useOutlineRename(editableElement); + const outlineRename = useOutlineRename(editableElement, isEditing); const isContainer = editableElement.getOutlineChildren ? true : false; const onNodeClicked = (e: React.MouseEvent) => { @@ -131,6 +139,7 @@ function DashboardOutlineNode({ sceneObject, editPane, depth, index }: Dashboard sceneObject={child} editPane={editPane} depth={depth + 1} + isEditing={isEditing} index={i} /> )) diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx index b2a7a149715..ea14464fc64 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx @@ -1,9 +1,6 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; -import { Button, Menu, Stack, Text, useStyles2, Dropdown, Icon, IconButton } from '@grafana/ui'; +import { Button, Menu, Stack, Dropdown, Icon, Sidebar } from '@grafana/ui'; import { trackDeleteDashboardElement } from 'app/features/dashboard-scene/utils/tracking'; import { EditableDashboardElement } from '../scene/types/EditableDashboardElement'; @@ -17,15 +14,11 @@ interface EditPaneHeaderProps { export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { const elementInfo = element.getEditableElementInfo(); - const styles = useStyles2(getStyles); const onCopy = element.onCopy?.bind(element); const onDuplicate = element.onDuplicate?.bind(element); const onDelete = element.onDelete?.bind(element); const onConfirmDelete = element.onConfirmDelete?.bind(element); - // temporary simple solution, should select parent element - const onGoBack = () => editPane.clearSelection(); - const canGoBack = editPane.state.selection; const onDeleteElement = () => { if (onConfirmDelete) { @@ -37,20 +30,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { }; return ( -
- - {canGoBack && ( - - )} - {elementInfo.typeName} - + editPane.closePane()}> {element.renderActions && element.renderActions()} {(onCopy || onDuplicate) && ( @@ -95,18 +75,6 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { /> )} -
+
); } - -function getStyles(theme: GrafanaTheme2) { - return { - wrapper: css({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: theme.spacing(1, 2), - borderBottom: `1px solid ${theme.colors.border.weak}`, - }), - }; -} diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index 6d9a2279563..39fd5fec9a7 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -14,7 +14,6 @@ import { import { DashboardScene } from '../scene/DashboardScene'; import { SceneGridRowEditableElement } from '../scene/layout-default/SceneGridRowEditableElement'; -import { redoButtonId, undoButtonID } from '../scene/new-toolbar/RightActions'; import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement'; import { LocalVariableEditableElement } from '../settings/variables/LocalVariableEditableElement'; import { VariableAdd, VariableAddEditableElement } from '../settings/variables/VariableAddEditableElement'; @@ -299,7 +298,3 @@ function makeEditAction { - if (!selection) { - const dashboard = getDashboardSceneFor(editPane); - return new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]).createSelectionElement(); - } - - return selection.createSelectionElement(); - }, [selection, editPane]); -} diff --git a/public/app/features/dashboard-scene/edit-pane/useOutlineRename.tsx b/public/app/features/dashboard-scene/edit-pane/useOutlineRename.tsx index 81f3a4556f0..d87d83994eb 100644 --- a/public/app/features/dashboard-scene/edit-pane/useOutlineRename.tsx +++ b/public/app/features/dashboard-scene/edit-pane/useOutlineRename.tsx @@ -8,10 +8,14 @@ export interface OutlineRenameState { error?: string; } -export function useOutlineRename(editableElement: EditableDashboardElement) { +export function useOutlineRename(editableElement: EditableDashboardElement, isEditing: boolean | undefined) { const [state, setState] = useState({}); const onNameDoubleClicked = (evt: React.MouseEvent) => { + if (!isEditing) { + return; + } + if (!editableElement.onChangeName) { return; } diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 5268ea4d865..da18ca17dd4 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -2,6 +2,8 @@ import { css, cx } from '@emotion/css'; import { GrafanaTheme2, VariableHide } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { Trans } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { SceneObjectState, SceneObjectBase, @@ -15,7 +17,8 @@ import { SceneObjectUrlValues, CancelActivationHandler, } from '@grafana/scenes'; -import { Box, useStyles2 } from '@grafana/ui'; +import { Box, Button, useStyles2 } from '@grafana/ui'; +import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { PanelEditControls } from '../panel-edit/PanelEditControls'; import { getDashboardSceneFor } from '../utils/utils'; @@ -25,6 +28,9 @@ import { DashboardDataLayerControls } from './DashboardDataLayerControls'; import { DashboardLinksControls } from './DashboardLinksControls'; import { DashboardScene } from './DashboardScene'; import { VariableControls } from './VariableControls'; +import { EditDashboardSwitch } from './new-toolbar/actions/EditDashboardSwitch'; +import { SaveDashboard } from './new-toolbar/actions/SaveDashboard'; +import { ShareDashboardButton } from './new-toolbar/actions/ShareDashboardButton'; export interface DashboardControlsState extends SceneObjectState { timePicker: SceneTimePicker; @@ -32,7 +38,7 @@ export interface DashboardControlsState extends SceneObjectState { hideTimeControls?: boolean; hideVariableControls?: boolean; hideLinksControls?: boolean; - // Hides the dashbaord-controls dropdown menu + // Hides the dashboard-controls dropdown menu hideDashboardControls?: boolean; } @@ -171,6 +177,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps )} {!hideDashboardControls && model.hasDashboardControls() && } + {config.featureToggles.dashboardNewLayouts && }
{!hideVariableControls && ( <> @@ -185,6 +192,37 @@ function DashboardControlsRenderer({ model }: SceneComponentProps + {showShareButton && } + {isEditing && } + {!isPlaying && canEditDashboard && } + {isPlaying && ( + + )} + + ); +} + function renderHiddenVariables(dashboard: DashboardScene) { const { variables } = sceneGraph.getVariables(dashboard).useState(); const renderAsHiddenVariables = variables.filter((v) => v.UNSAFE_renderAsHidden); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 1c75360a7b5..a542dd92826 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -275,7 +275,7 @@ export class DashboardScene extends SceneObjectBase impleme this._initialUrlState = locationService.getLocation(); // Switch to edit mode - this.setState({ isEditing: true }); + this.setState({ isEditing: true, editable: true }); // Propagate change edit mode change to children this.state.body.editModeChanged?.(true); @@ -692,16 +692,16 @@ export class DashboardScene extends SceneObjectBase impleme canEditDashboard() { const { meta } = this.state; - return Boolean(meta.canEdit || meta.canMakeEditable || config.viewersCanEdit); + return !meta.isSnapshot && Boolean(meta.canEdit || meta.canMakeEditable || config.viewersCanEdit); } public getInitialSaveModel() { return this.serializer.initialSaveModel; } - public getSnapshotUrl = () => { - return this.serializer.getSnapshotUrl(); - }; + public getSnapshotUrl() { + return this.serializer.getSnapshotUrl() ?? ''; + } /** Hacky temp function until we refactor transformSaveModelToScene a bit */ setInitialSaveModel(model?: Dashboard, meta?: DashboardMeta, apiVersion?: string): void; diff --git a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx index 2efcad5a256..80c8487f1c5 100644 --- a/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx +++ b/public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx @@ -20,7 +20,7 @@ export function GoToSnapshotOriginButton(props: { originalURL: string }) { ); } -const onOpenSnapshotOriginalDashboard = (originalUrl: string) => { +export const onOpenSnapshotOriginalDashboard = (originalUrl: string) => { const relativeURL = originalUrl ?? ''; const sanitizedRelativeURL = textUtil.sanitizeUrl(relativeURL); try { diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 68ccadaba13..959913c8f8f 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -153,9 +153,7 @@ export function ToolbarActions({ dashboard }: Props) { toolbarActions.push({ group: 'icon-actions', condition: meta.isSnapshot && !isEditing, - render: () => ( - - ), + render: () => , }); if (!isEditingPanel && !isEditing) { diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx index 59dfc714669..2e2f979d51d 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx @@ -1,8 +1,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui'; +import { ToolbarButtonRow, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; @@ -11,45 +10,35 @@ import { isLibraryPanel } from '../../utils/utils'; import { DashboardScene } from '../DashboardScene'; import { BackToDashboardButton } from './actions/BackToDashboardButton'; -import { DashboardSettingsButton } from './actions/DashboardSettingsButton'; import { DiscardLibraryPanelButton } from './actions/DiscardLibraryPanelButton'; import { DiscardPanelButton } from './actions/DiscardPanelButton'; -import { EditDashboardSwitch } from './actions/EditDashboardSwitch'; -import { ExportDashboardButton } from './actions/ExportDashboardButton'; import { MakeDashboardEditableButton } from './actions/MakeDashboardEditableButton'; import { PlayListNextButton } from './actions/PlayListNextButton'; import { PlayListPreviousButton } from './actions/PlayListPreviousButton'; import { PlayListStopButton } from './actions/PlayListStopButton'; import { SaveDashboard } from './actions/SaveDashboard'; import { SaveLibraryPanelButton } from './actions/SaveLibraryPanelButton'; -import { ShareDashboardButton } from './actions/ShareDashboardButton'; import { UnlinkLibraryPanelButton } from './actions/UnlinkLibraryPanelButton'; -import { ToolbarActionProps } from './types'; import { getDynamicActions, renderActionElements } from './utils'; export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { - const { editPanel, editable, editview, isEditing, uid, meta, viewPanel } = dashboard.useState(); + const { editPanel, editable, editview, isEditing, meta, viewPanel } = dashboard.useState(); const { isPlaying } = playlistSrv.useState(); const styles = useStyles2(getStyles); const isEditable = Boolean(editable); const canSave = Boolean(meta.canSave); - const hasUid = Boolean(uid); const isEditingDashboard = Boolean(isEditing); const hasEditView = Boolean(editview); const isEditingPanel = Boolean(editPanel); const isViewingPanel = Boolean(viewPanel); const isEditingLibraryPanel = isEditingPanel && isLibraryPanel(editPanel!.state.panelRef.resolve()); const isShowingDashboard = !hasEditView && !isViewingPanel && !isEditingPanel; - const isEditingAndShowingDashboard = isEditingDashboard && isShowingDashboard; - const isSnapshot = Boolean(meta.isSnapshot); const canSaveInFolder = contextSrv.hasEditPermissionInFolders; const canEditDashboard = dashboard.canEditDashboard(); const showPanelButtons = isEditingPanel && !hasEditView && !isViewingPanel; const showPlayButtons = isPlaying && isShowingDashboard && !isEditingDashboard; - const showShareButton = hasUid && !isSnapshot && !isPlaying && !isEditingPanel; - const showUndoRedoButtons = isEditingAndShowingDashboard && !!config.featureToggles.dashboardUndoRedo; return ( @@ -106,28 +95,10 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { group: 'panel', condition: showPanelButtons && isEditingLibraryPanel, }, - { - key: 'dashboard-undo', - component: UndoButton, - group: 'dashboard', - condition: showUndoRedoButtons, - }, - { - key: 'dashboard-redo', - component: RedoButton, - group: 'dashboard', - condition: showUndoRedoButtons, - }, - { - key: 'dashboard-settings', - component: DashboardSettingsButton, - group: 'dashboard', - condition: isEditingAndShowingDashboard && canEditDashboard, - }, { key: 'save-dashboard', component: SaveDashboard, - group: 'save-edit', + group: 'panel', condition: isEditingDashboard && !isEditingLibraryPanel && (canSave || canSaveInFolder), }, { @@ -136,31 +107,6 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { group: 'save-edit', condition: !isEditing && canEditDashboard && !isViewingPanel && !isEditable && !isPlaying, }, - { - key: 'edit-dashboard-switch', - component: EditDashboardSwitch, - group: 'save-edit', - condition: - canEditDashboard && - !isEditingPanel && - !isEditingLibraryPanel && - !isViewingPanel && - isEditable && - !isPlaying && - !isEditingPanel, - }, - { - key: 'new-export-dashboard-button', - component: ExportDashboardButton, - group: 'export-share', - condition: showShareButton, - }, - { - key: 'new-share-dashboard-button', - component: ShareDashboardButton, - group: 'export-share', - condition: showShareButton, - }, ], dashboard )} @@ -168,42 +114,6 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => { ); }; -export const undoButtonID = 'undo-button'; -function UndoButton({ dashboard }: ToolbarActionProps) { - const editPane = dashboard.state.editPane; - const { undoStack } = editPane.useState(); - const undoAction = undoStack[undoStack.length - 1]; - const tooltip = `Undo${undoAction?.description ? ` '${undoAction.description}'` : ''}`; - - return ( - editPane.undoAction()} - tooltip={tooltip} - /> - ); -} - -export const redoButtonId = 'redo-button'; -function RedoButton({ dashboard }: ToolbarActionProps) { - const editPane = dashboard.state.editPane; - const { redoStack } = editPane.useState(); - const redoAction = redoStack[redoStack.length - 1]; - const tooltip = `Redo${redoAction?.description ? ` '${redoAction.description}'` : ''}`; - - return ( - editPane.redoAction()} - /> - ); -} - const getStyles = (theme: GrafanaTheme2) => ({ container: css({ paddingLeft: theme.spacing(0.5) }), }); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx deleted file mode 100644 index d8004d00b13..00000000000 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ExportDashboardButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; -import { t } from '@grafana/i18n'; -import { config, locationService } from '@grafana/runtime'; -import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils'; - -import ExportMenu from '../../../sharing/ExportButton/ExportMenu'; -import { DashboardInteractions } from '../../../utils/interactions'; -import { ToolbarActionProps } from '../types'; - -import { ShareExportDashboardButton } from './ShareExportDashboardButton'; - -const newExportButtonSelector = e2eSelectors.pages.Dashboard.DashNav.NewExportButton; - -export const ExportDashboardButton = ({ dashboard }: ToolbarActionProps) => { - const buttonTooltip = config.featureToggles.kubernetesDashboards - ? t('dashboard.toolbar.new.export.tooltip.as-code', 'Export as code') - : t('dashboard.toolbar.new.export.tooltip.json', 'Export as JSON'); - - return ( - } - groupTestId={newExportButtonSelector.container} - buttonLabel={t('dashboard.toolbar.new.export.title', 'Export')} - buttonTooltip={buttonTooltip} - buttonTestId={newExportButtonSelector.container} - onButtonClick={() => { - locationService.partial({ shareView: shareDashboardType.export }); - - DashboardInteractions.sharingCategoryClicked({ - item: shareDashboardType.export, - shareResource: getTrackingSource(), - }); - }} - arrowLabel={t('dashboard.toolbar.new.export.arrow', 'Export')} - arrowTestId={newExportButtonSelector.arrowMenu} - dashboard={dashboard} - /> - ); -}; diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx index c59d06a74ae..51d04d2272a 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/OpenSnapshotOriginButton.tsx @@ -2,5 +2,5 @@ import { GoToSnapshotOriginButton } from '../../GoToSnapshotOriginButton'; import { ToolbarActionProps } from '../types'; export const OpenSnapshotOriginButton = ({ dashboard }: ToolbarActionProps) => ( - + ); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx index 0c33cf19b68..f2819e44c1c 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/SaveDashboard.tsx @@ -6,10 +6,12 @@ import { contextSrv } from 'app/core/services/context_srv'; import { ToolbarActionProps } from '../types'; export const SaveDashboard = ({ dashboard }: ToolbarActionProps) => { - const { meta, isDirty, uid } = dashboard.state; + const { meta, isDirty, uid, editview, editPanel } = dashboard.state; const isNew = !Boolean(uid || dashboard.isManaged()); const isManaged = dashboard.isManaged(); + // In dashboard settings we still use the nav toolbar for a short while + const buttonSize = Boolean(editview) || editPanel ? 'sm' : 'md'; // if we only can save if (isNew) { @@ -17,7 +19,7 @@ export const SaveDashboard = ({ dashboard }: ToolbarActionProps) => { diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx index 59262fc152e..0ec0232e985 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/ToolbarSwitch.tsx @@ -43,7 +43,7 @@ export const ToolbarSwitch = ({ onClick={disabled ? undefined : onClick} >
- +
@@ -53,11 +53,11 @@ export const ToolbarSwitch = ({ const getStyles = (theme: GrafanaTheme2) => ({ container: css({ border: `1px solid ${theme.components.input.borderColor}`, - padding: theme.spacing(0.25), + padding: theme.spacing(0.5), backgroundColor: theme.components.input.background, borderRadius: theme.shape.radius.default, - width: theme.spacing(5.5), - height: theme.spacing(3), + width: theme.spacing(6.5), + height: theme.spacing(theme.components.height.md), cursor: 'pointer', display: 'flex', flexDirection: 'row', @@ -90,19 +90,19 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', alignItems: 'center', justifyContent: 'center', - width: theme.spacing(2.5), + width: theme.spacing(3.5), height: '100%', transform: 'translateX(0)', position: 'relative', borderRadius: styleMixins.getInternalRadius(theme, 2), - border: `1px solid ${theme.colors.secondary.border}`, + border: `1px solid ${theme.colors.border.weak}`, [theme.transitions.handleMotion('no-preference', 'reduce')]: { transition: 'all 0.2s ease-in-out', }, }), boxChecked: css({ - transform: `translateX(calc(100% - ${theme.spacing(0.25)}))`, + transform: `translateX(calc(100% - 14px))`, borderColor: 'transparent', }), }); diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx index 78876f4ee55..00e072e4d6f 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -9,7 +9,7 @@ import { Input, TextArea, Button, Field, Box, Stack } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { dashboardEditActions, undoRedoWasClicked } from '../../edit-pane/shared'; +import { dashboardEditActions } from '../../edit-pane/shared'; import { useEditPaneInputAutoFocus } from '../../scene/layouts-shared/utils'; import { BulkActionElement } from '../../scene/types/BulkActionElement'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement'; @@ -161,7 +161,7 @@ function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable onChange={onChange} onBlur={(e) => { const labelUnchanged = oldName.current === name; - const shouldSkip = labelUnchanged || undoRedoWasClicked(e); + const shouldSkip = labelUnchanged; if (nameError) { setNameError(undefined); @@ -200,7 +200,7 @@ function VariableLabelInput({ variable, id }: VariableInputProps) { onChange={(e) => variable.setState({ label: e.currentTarget.value })} onBlur={(e) => { const labelUnchanged = oldLabel.current === e.currentTarget.value; - const shouldSkip = labelUnchanged || undoRedoWasClicked(e); + const shouldSkip = labelUnchanged; if (shouldSkip) { return; @@ -232,7 +232,7 @@ function VariableDescriptionTextArea({ variable, id }: VariableInputProps) { onChange={(e) => variable.setState({ description: e.currentTarget.value })} onBlur={(e) => { const labelUnchanged = oldDescription.current === e.currentTarget.value; - const shouldSkip = labelUnchanged || undoRedoWasClicked(e); + const shouldSkip = labelUnchanged; if (shouldSkip) { return; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e8d4b15d05f..dad607c0de0 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4172,6 +4172,7 @@ "clear": "Clear", "collapse": "Collapse", "disabled": "Disabled", + "discard": "Discard", "edit": "Edit", "help": "Help", "loading": "Loading...", @@ -4789,7 +4790,6 @@ "variable": "{{type}} variable", "variable-set": "Variables" }, - "open": "Open options pane", "row": { "header": { "hide": "Hide", @@ -5140,6 +5140,7 @@ "title-matched_other": "Matched {{count}}/{{totalCount}} options" }, "outline": { + "pane-header": "Content outline", "repeated-item": "Repeat", "tree-item": { "empty": "(empty)", @@ -5351,6 +5352,32 @@ "share-public-dashboard-loader": { "loading-configuration": "Loading configuration" }, + "sidebar": { + "dashboard-options": { + "title": "Options", + "tooltip": "Dashboard options" + }, + "edit-schema": { + "title": "Code", + "tooltip": "Edit as code" + }, + "export": { + "title": "Export", + "unsaved-modal": { + "text": "You have unsaved changes to this dashboard. You need to save them before you can share it.", + "title": "Save changes to dashboard?" + } + }, + "outline": { + "title": "Outline", + "tooltip": "Content outline" + }, + "redo": "Redo", + "snapshot": { + "tooltip": "Open original dashboard" + }, + "undo": "Undo" + }, "solo-panel": { "loading-initializing-dashboard": "Loading & initializing dashboard", "title-not-found": "Panel with id {{panelId}} not found" @@ -5454,11 +5481,8 @@ "tooltip": "This dashboard was marked as read only" }, "export": { - "arrow": "Export", - "title": "Export", "tooltip": { - "as-code": "Export as code", - "json": "Export as JSON" + "as-code": "Export as code" } }, "more-save-options": "More save options", @@ -5510,6 +5534,9 @@ "save-library-panel": "Save library panel", "settings": "Dashboard settings", "share-button": "Share", + "snapshot": { + "title": "Source" + }, "star-add-error": "Failed to add to starred", "star-added": "Added to starred", "star-remove-error": "Failed to remove from starred", @@ -5839,9 +5866,6 @@ "name-values-separated-comma": "Values separated by comma", "selection-options": "Selection options" }, - "dashboard-edit-pane-renderer": { - "outline": "Outline" - }, "dashboard-link-form": { "back-to-list": "Back to list", "label-icon": "Icon", @@ -8121,13 +8145,6 @@ } } }, - "grafana": { - "dashboard": { - "edit-pane": { - "go-back": "Go back" - } - } - }, "grafana-data": { "datetime": { "rangeutils": { From d49993ddab08bf0d8daa544ec110ad710c7c27c0 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 26 Nov 2025 15:45:36 +0200 Subject: [PATCH 26/94] Provisioning: Disable imports for new dashboard (#114419) * Provisioning: Disable imports for new dashboard * Refactor --- .../DashboardEmpty/DashboardEmpty.tsx | 10 +++++----- .../DashboardEmpty/DashboardEmptyHooks.ts | 20 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx b/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx index bef4318e339..8c623c2c7ca 100644 --- a/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardEmpty/DashboardEmpty.tsx @@ -15,7 +15,7 @@ import { SuggestedDashboards } from '../DashboardLibrary/SuggestedDashboards'; import { DashboardEmptyExtensionPoint } from './DashboardEmptyExtensionPoint'; import { - useIsReadOnlyRepo, + useRepositoryStatus, useOnAddVisualization, useOnAddLibraryPanel, useOnImportDashboard, @@ -149,10 +149,10 @@ export interface Props { // We pass the default empty UI through to the extension point so that the extension can conditionally render it if needed. // For example, an extension might want to render custom UI for a specific experiment cohort, and the default UI for everyone else. const DashboardEmpty = (props: Props) => { - const isReadOnlyRepo = useIsReadOnlyRepo(props); - const onAddVisualization = useOnAddVisualization({ ...props, isReadOnlyRepo }); - const onAddLibraryPanel = useOnAddLibraryPanel({ ...props, isReadOnlyRepo }); - const onImportDashboard = useOnImportDashboard({ ...props, isReadOnlyRepo }); + const { isReadOnlyRepo, isProvisioned } = useRepositoryStatus(props); + const onAddVisualization = useOnAddVisualization({ ...props, isReadOnlyRepo, isProvisioned }); + const onAddLibraryPanel = useOnAddLibraryPanel({ ...props, isReadOnlyRepo, isProvisioned }); + const onImportDashboard = useOnImportDashboard({ ...props, isReadOnlyRepo, isProvisioned }); return ( { - const { isReadOnlyRepo } = useGetResourceRepositoryView({ +export const useRepositoryStatus = ({ dashboard }: Props) => { + const { isReadOnlyRepo, repository } = useGetResourceRepositoryView({ folderName: dashboard instanceof DashboardScene ? dashboard.state.meta.folderUid : dashboard.meta.folderUid, }); - return isReadOnlyRepo; + const isFolderProvisioned = Boolean(repository); + const isProvisioned = isFolderProvisioned || (dashboard instanceof DashboardScene && dashboard.isManagedRepository()); + + return { + isReadOnlyRepo, + isProvisioned, + }; }; interface HookProps extends Props { isReadOnlyRepo: boolean; + isProvisioned: boolean; } export const useOnAddVisualization = ({ dashboard, canCreate, isReadOnlyRepo }: HookProps) => { @@ -53,9 +60,7 @@ export const useOnAddVisualization = ({ dashboard, canCreate, isReadOnlyRepo }: }, [canCreate, isReadOnlyRepo, dashboard, dispatch, initialDatasource]); }; -export const useOnAddLibraryPanel = ({ dashboard, canCreate, isReadOnlyRepo }: HookProps) => { - const isProvisioned = dashboard instanceof DashboardScene && dashboard.isManagedRepository(); - +export const useOnAddLibraryPanel = ({ dashboard, canCreate, isReadOnlyRepo, isProvisioned }: HookProps) => { return useMemo(() => { if (!canCreate || isProvisioned || isReadOnlyRepo) { return undefined; @@ -72,8 +77,7 @@ export const useOnAddLibraryPanel = ({ dashboard, canCreate, isReadOnlyRepo }: H }, [canCreate, isProvisioned, isReadOnlyRepo, dashboard]); }; -export const useOnImportDashboard = ({ dashboard, canCreate, isReadOnlyRepo }: HookProps) => { - const isProvisioned = dashboard instanceof DashboardScene && dashboard.isManagedRepository(); +export const useOnImportDashboard = ({ canCreate, isReadOnlyRepo, isProvisioned }: HookProps) => { return useMemo(() => { if (!canCreate || isProvisioned || isReadOnlyRepo) { return undefined; From 9606e9c51cb70d096df60d903e9c1de44514912f Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Wed, 26 Nov 2025 07:48:18 -0600 Subject: [PATCH 27/94] Docs: Clarify the uid is metadata.name (#114432) --- .../developer-resources/api-reference/http-api/dashboard.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/developer-resources/api-reference/http-api/dashboard.md b/docs/sources/developer-resources/api-reference/http-api/dashboard.md index 5f28d4c54b1..2cc7aa84bb3 100644 --- a/docs/sources/developer-resources/api-reference/http-api/dashboard.md +++ b/docs/sources/developer-resources/api-reference/http-api/dashboard.md @@ -767,12 +767,12 @@ Status Codes: Deletes a dashboard via the dashboard uid. -- namespace: to read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana//developers/http_api/apis/). -- uid: the unique identifier of the dashboard to update. this will be the _name_ in the dashboard response +- **`namespace`**: To read more about the namespace to use, see the [API overview](https://grafana.com/docs/grafana//developers/http_api/apis/). +- **`uid`**: The unique identifier of the dashboard to update. This is the `metadata.name` field in the dashboard response and _not_ the `metadata.uid` field. **Required permissions** -See note in the [introduction]({{< ref "#dashboard-api" >}}) for an explanation. +See note in the [introduction](#new-dashboard-apis) for an explanation. | Action | Scope | From 84fbe6bc7ba6e9562d941551ff907797650e7dcd Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:51:31 +0100 Subject: [PATCH 28/94] Alerting: Analyze an alert rule with Grafana Assistant (#114420) * fix * rename to analyze * Enable Analyze rule for GMA recording rules * Fix declare incident button condition --------- Co-authored-by: Konrad Lalik --- .../unified/PanelAlertTabContent.test.tsx | 3 + .../alerting/unified/RuleList.test.tsx | 3 + .../assistant/AnalizeRuleButton.tsx | 139 ++++++++++++++++++ .../components/rule-viewer/AlertRuleMenu.tsx | 6 + .../rule-viewer/RuleViewer.test.tsx | 4 + .../rules/RuleActionsButtons.test.tsx | 4 + .../rules/RuleListGroupView.test.tsx | 4 + .../rules/RuleListStateView.test.tsx | 4 + .../components/rules/RulesTable.test.tsx | 4 + .../group-details/GroupDetailsPage.test.tsx | 4 + .../rule-list/DataSourceGroupLoader.test.tsx | 4 + .../unified/rule-list/FilterView.test.tsx | 4 + .../rule-list/GrafanaGroupLoader.test.tsx | 76 ++++++++++ .../PanelDataAlertingTab.test.tsx | 9 +- public/locales/en-US/grafana.json | 1 + 15 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index dfe3549eeee..dac15642d7b 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -28,6 +28,9 @@ import { Annotation } from './utils/constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; jest.mock('./api/ruler'); +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); jest.spyOn(alertingAbilities, 'useAlertRuleAbility'); const prometheusModuleSettings = { alerting: true, module: 'core:plugin/prometheus' }; diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index f58aadb9afd..3849c4708aa 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -45,6 +45,9 @@ jest.mock('@grafana/runtime', () => ({ jest.mock('./api/buildInfo'); jest.mock('./api/prometheus'); jest.mock('./api/ruler'); +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]); jest.spyOn(apiRuler, 'rulerUrlBuilder'); diff --git a/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx b/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx new file mode 100644 index 00000000000..d3aa6db4ec0 --- /dev/null +++ b/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx @@ -0,0 +1,139 @@ +import { useMemo } from 'react'; + +import { OpenAssistantProps, createAssistantContextItem, useAssistant } from '@grafana/assistant'; +import { t } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; +import { Menu } from '@grafana/ui'; +import { GrafanaAlertingRule, GrafanaRecordingRule, GrafanaRule } from 'app/types/unified-alerting'; + +import { prometheusRuleType } from '../../utils/rules'; + +interface AnalyzeRuleButtonProps { + /** Alert rule to analyze */ + rule: GrafanaRule; +} + +/** + * A menu item component that analyze an alert rule. + * Automatically creates context from alert data and opens the assistant in assistant mode. + */ +export function AnalyzeRuleButton(props: AnalyzeRuleButtonProps) { + const { isAvailable, openAssistant } = useAssistant(); + + if (!isAvailable || !openAssistant) { + return null; + } + + return ; +} + +function AnalyzeRuleButtonView({ + rule, + openAssistant, +}: AnalyzeRuleButtonProps & { + openAssistant: (props: OpenAssistantProps) => void; +}) { + // Create alert rule context from alert rule data + const alertContext = useMemo(() => { + return createAssistantContextItem('structured', { + title: `Alert: ${rule.name}`, + data: { + rule: { + name: rule.name, + uid: rule.uid, + labels: rule.labels, + query: rule.query, + }, + }, + }); + }, [rule]); + + // Generate default prompt + const analyzeRulePrompt = useMemo(() => buildAnalyzeRulePrompt(rule), [rule]); + + const handleClick = () => { + reportInteraction('grafana_assistant_app_analyze_rule_button_clicked', { + origin: 'alerting', + alertName: rule.name, + alertState: prometheusRuleType.grafana.alertingRule(rule) ? rule.state : undefined, + }); + + openAssistant({ + origin: 'alerting', + mode: 'assistant', + prompt: analyzeRulePrompt, + context: [alertContext], + autoSend: true, + }); + }; + + return ( + + ); +} + +/** + * Builds a prompt for analyzing a rule (alerting or recording). + * Automatically detects the rule type and uses the appropriate prompt builder. + */ +function buildAnalyzeRulePrompt(rule: GrafanaRule): string { + if (prometheusRuleType.grafana.alertingRule(rule)) { + return buildAnalyzeAlertingRulePrompt(rule); + } else if (prometheusRuleType.grafana.recordingRule(rule)) { + return buildAnalyzeRecordingRulePrompt(rule); + } + // Fallback (should not happen for GrafanaRule, but TypeScript requires it) + return `Analyze the rule "${rule.name}".`; +} + +/** + * Builds a prompt for analyzing an alerting rule. + * Includes state, activeAt timestamp, annotations, and labels. + */ +function buildAnalyzeAlertingRulePrompt(rule: GrafanaAlertingRule): string { + const state = rule.state || 'firing'; + const timeInfo = rule.activeAt ? ` starting at ${new Date(rule.activeAt).toISOString()}` : ''; + + let prompt = `Analyze the ${state} alert "${rule.name}"${timeInfo}.`; + + const description = rule.annotations?.description || rule.annotations?.summary || ''; + if (description) { + prompt += ` ${description}`; + } + + const labelsStr = rule.labels + ? Object.entries(rule.labels) + .map(([k, v]) => `${k}="${v}"`) + .join(', ') + : ''; + if (labelsStr) { + prompt += ` Labels: ${labelsStr}.`; + } + + return prompt; +} + +/** + * Builds a prompt for analyzing a recording rule. + * Includes name, query, and labels (no state or activeAt). + */ +function buildAnalyzeRecordingRulePrompt(rule: GrafanaRecordingRule): string { + const labelsStr = rule.labels + ? Object.entries(rule.labels) + .map(([k, v]) => `${k}="${v}"`) + .join(', ') + : ''; + + let prompt = `Analyze the recording rule "${rule.name}".`; + + if (labelsStr) { + prompt += ` Labels: ${labelsStr}.`; + } + + return prompt; +} diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx index 8834fd72580..a473eae60a1 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx @@ -1,5 +1,6 @@ import { PropsOf } from '@emotion/react'; +import { useAssistant } from '@grafana/assistant'; import { AppEvents } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; @@ -29,6 +30,7 @@ import { rulerRuleType, } from '../../utils/rules'; import { createRelativeUrl } from '../../utils/url'; +import { AnalyzeRuleButton } from '../assistant/AnalizeRuleButton'; import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton'; interface Props { @@ -126,6 +128,9 @@ const AlertRuleMenu = ({ prometheusRuleType.alertingRule(promRule) && promRule.state === PromAlertingRuleState.Firing; + const { isAvailable: isAssistantAvailable } = useAssistant(); + const shouldShowAnalyzeRuleButton = isAssistantAvailable && prometheusRuleType.grafana.rule(promRule); + const shareUrl = createShareLink(identifier); const showDivider = @@ -172,6 +177,7 @@ const AlertRuleMenu = ({ )} {/* TODO Migrate Declare Incident to plugin links extensions */} {shouldShowDeclareIncidentButton && } + {shouldShowAnalyzeRuleButton && } {canDuplicate && ( ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + // metadata and interactive elements const ELEMENTS = { loading: byText(/Loading rule/i), diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx index 5de673ccb14..a88d1bef83f 100644 --- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx @@ -21,6 +21,10 @@ import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; import { setupDataSources } from '../../testSetup/datasources'; import { fromCombinedRule, stringifyIdentifier } from '../../utils/rule-id'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + setupMswServer(); jest.mock('app/core/services/context_srv'); const mockContextSrv = jest.mocked(contextSrv); diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx index 19100a72600..42a7594027b 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx @@ -15,6 +15,10 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { RuleListGroupView } from './RuleListGroupView'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + jest.spyOn(analytics, 'logInfo'); const ui = { diff --git a/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx index 260c7436a18..51739c6ae4c 100644 --- a/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx @@ -10,6 +10,10 @@ import { } from 'app/features/alerting/unified/mocks'; import { PromAlertingRuleState } from 'app/types/unified-alerting-dto'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + setPluginLinksHook(() => ({ links: [], isLoading: false, diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx index 61c143ab843..a263b4711ca 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx @@ -18,6 +18,10 @@ import { mimirDataSource } from '../../mocks/server/configure'; import { RulesTable } from './RulesTable'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + jest.mock('../../hooks/useAbilities'); const mocks = { diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx index 90485290325..4e1bb3056fb 100644 --- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx +++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx @@ -23,6 +23,10 @@ import { alertingFactory } from '../mocks/server/db'; import GroupDetailsPage from './GroupDetailsPage'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + jest.mock('react-virtualized-auto-sizer', () => { return ({ children }: Props) => children({ diff --git a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx index 53b80f1ebaf..a057b5030a3 100644 --- a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx @@ -17,6 +17,10 @@ import { fromRulerRuleAndGroupIdentifierV2 } from '../utils/rule-id'; import { DataSourceGroupLoader } from './DataSourceGroupLoader'; import { createViewLinkFromIdentifier } from './DataSourceRuleListItem'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx index 3a7348048da..5c8768f58ec 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -12,6 +12,10 @@ import { RulesFilter } from '../search/rulesSearchParser'; import { FilterView } from './FilterView'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx index 3105c3efd6b..7385215637a 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx @@ -1,6 +1,7 @@ import { render } from 'test/test-utils'; import { byLabelText, byRole } from 'testing-library-selector'; +import { useAssistant } from '@grafana/assistant'; import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; import { AccessControlAction } from 'app/types/accessControl'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; @@ -22,6 +23,12 @@ import { intervalToSeconds } from '../utils/time'; import { GrafanaGroupLoader } from './GrafanaGroupLoader'; +jest.mock('@grafana/assistant', () => ({ + useAssistant: jest.fn(), + createAssistantContextItem: jest.fn((type, data) => ({ type, ...data })), +})); +const mockUseAssistant = jest.mocked(useAssistant); + setPluginLinksHook(() => ({ links: [], isLoading: false })); setPluginComponentsHook(() => ({ components: [], isLoading: false })); @@ -41,11 +48,18 @@ const ui = { export: () => byRole('menuitem', { name: /export/i }), delete: () => byRole('menuitem', { name: /delete/i }), pause: () => byRole('menuitem', { name: /pause/i }), + analyzeRule: () => byRole('menuitem', { name: /analyze rule/i }), }, }; describe('GrafanaGroupLoader', () => { beforeEach(() => { + mockUseAssistant.mockReturnValue({ + isAvailable: false, + openAssistant: jest.fn(), + closeAssistant: jest.fn(), + toggleAssistant: jest.fn(), + }); grantUserPermissions([ AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleDelete, @@ -213,6 +227,68 @@ describe('GrafanaGroupLoader', () => { const menuItems = byRole('menuitem').getAll(); expect(menuItems.length).toBe(6); }); + + it('should render Analyze rule menu item when assistant is available', async () => { + mockUseAssistant.mockReturnValue({ + isAvailable: true, + openAssistant: jest.fn(), + closeAssistant: jest.fn(), + toggleAssistant: jest.fn(), + }); + + setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]); + + const groupIdentifier = getGroupIdentifier(grafanaRulerGroup); + + const { user } = render( + + ); + + const [rule1] = grafanaRulerGroup.rules; + const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find(); + + // Click the More button to open the menu + const moreButton = ui.moreButton().get(ruleListItem); + await user.click(moreButton); + + // Check that Analyze rule menu item is present + expect(ui.menuItems.analyzeRule().get()).toBeInTheDocument(); + + // With assistant enabled, there should be 7 menu items (6 + Analyze rule) + const menuItems = byRole('menuitem').getAll(); + expect(menuItems.length).toBe(7); + }); + + it('should not render Analyze rule menu item when assistant is not available', async () => { + mockUseAssistant.mockReturnValue({ + isAvailable: false, + openAssistant: jest.fn(), + closeAssistant: jest.fn(), + toggleAssistant: jest.fn(), + }); + + setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]); + + const groupIdentifier = getGroupIdentifier(grafanaRulerGroup); + + const { user } = render( + + ); + + const [rule1] = grafanaRulerGroup.rules; + const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find(); + + // Click the More button to open the menu + const moreButton = ui.moreButton().get(ruleListItem); + await user.click(moreButton); + + // Check that Analyze rule menu item is NOT present + expect(ui.menuItems.analyzeRule().query()).not.toBeInTheDocument(); + + // Without assistant, there should be 6 menu items + const menuItems = byRole('menuitem').getAll(); + expect(menuItems.length).toBe(6); + }); }); function rulerGroupToPromGroup(group: RulerRuleGroupDTO): GrafanaPromRuleGroupDTO { diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index 15e323aa8a9..fd15ec40350 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -40,14 +40,15 @@ import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataA jest.mock('app/features/alerting/unified/api/prometheus'); jest.mock('app/features/alerting/unified/api/ruler'); +jest.mock('@grafana/assistant', () => ({ + useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }), +})); + jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false); jest.spyOn(ruler, 'rulerUrlBuilder'); jest.spyOn(alertingAbilities, 'useAlertRuleAbility'); -setPluginLinksHook(() => ({ - links: [], - isLoading: false, -})); +setPluginLinksHook(() => ({ links: [], isLoading: false })); const dataSources = { prometheus: mockDataSource( diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index dad607c0de0..9f8b19218de 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -477,6 +477,7 @@ "noOptionsMessage-no-datasources-found": "No datasources found" }, "alert-menu": { + "analyze-rule": "Analyze rule", "copy-link": "Copy link", "duplicate": "Duplicate", "export": "Export", From 759d49a1df0b30bde07289c0aecf7e042ba7914c Mon Sep 17 00:00:00 2001 From: Andres Torres Date: Wed, 26 Nov 2025 09:58:49 -0500 Subject: [PATCH 29/94] feat(setting): Adding setting service client (#114428) --- .github/CODEOWNERS | 1 + pkg/services/setting/service.go | 384 +++++++++++++++++++ pkg/services/setting/service_test.go | 542 +++++++++++++++++++++++++++ 3 files changed, 927 insertions(+) create mode 100644 pkg/services/setting/service.go create mode 100644 pkg/services/setting/service_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 61ab3a10b1f..465c49ee713 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -185,6 +185,7 @@ /pkg/services/search/ @grafana/grafana-search-and-storage /pkg/services/searchusers/ @grafana/grafana-search-and-storage /pkg/services/secrets/ @grafana/grafana-operator-experience-squad +/pkg/services/setting/ @grafana/grafana-backend-services-squad /pkg/services/shorturls/ @grafana/sharing-squad /pkg/services/sqlstore/ @grafana/grafana-search-and-storage /pkg/services/ssosettings/ @grafana/identity-squad diff --git a/pkg/services/setting/service.go b/pkg/services/setting/service.go new file mode 100644 index 00000000000..5afc9e22159 --- /dev/null +++ b/pkg/services/setting/service.go @@ -0,0 +1,384 @@ +package setting + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" + "gopkg.in/ini.v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/dynamic" + clientrest "k8s.io/client-go/rest" + "k8s.io/client-go/transport" + + authlib "github.com/grafana/authlib/authn" + logging "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/semconv" +) + +var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/setting") + +const LogPrefix = "setting.service" + +const DefaultPageSize = int64(500) +const DefaultQPS = float32(10) +const DefaultBurst = 25 + +const ( + ApiGroup = "setting.grafana.app" + apiVersion = "v0alpha1" + resource = "settings" + kind = "Setting" + listKind = "SettingList" +) + +var settingGroupVersion = schema.GroupVersionResource{ + Group: ApiGroup, + Version: apiVersion, + Resource: resource, +} + +var settingGroupListKind = map[schema.GroupVersionResource]string{ + settingGroupVersion: listKind, +} + +type remoteSettingServiceMetrics struct { + listDuration *prometheus.HistogramVec + listResultSize *prometheus.HistogramVec +} + +// Service retrieves configuration settings from a remote settings service. +// +// The service uses label selectors to filter settings. Settings are labeled with +// "section" and "key" labels matching their spec fields. +// +// Example - Select all settings: +// +// ctx := request.WithNamespace(context.Background(), "my-namespace") +// ini, err := service.ListAsIni(ctx, metav1.LabelSelector{}) +// +// Example - Select settings from specific sections: +// +// selector := metav1.LabelSelector{ +// MatchExpressions: []metav1.LabelSelectorRequirement{ +// { +// Key: "section", +// Operator: metav1.LabelSelectorOpIn, +// Values: []string{"database", "server"}, +// }, +// }, +// } +// ini, err := service.ListAsIni(ctx, selector) +// +// Example - Select settings from a single section with specific labels: +// +// selector := metav1.LabelSelector{ +// MatchLabels: map[string]string{ +// "section": "database", +// }, +// } +// settings, err := service.List(ctx, selector) +type Service interface { + prometheus.Collector + // ListAsIni retrieves settings filtered by a label selector from the namespace in context + // and returns them as an ini.File. + // + // The namespace must be present in the context, ie: via request.WithNamespace. + // An empty selector returns all settings in the namespace. + ListAsIni(ctx context.Context, selector metav1.LabelSelector) (*ini.File, error) + + // List retrieves settings filtered by a label selector from the namespace in context + // and returns them as a slice of Setting structs. + // + // The namespace must be present in the context, ie: via request.WithNamespace. + // An empty selector returns all settings in the namespace. + List(ctx context.Context, selector metav1.LabelSelector) ([]*Setting, error) +} + +type remoteSettingService struct { + dynamicClient dynamic.Interface + log logging.Logger + pageSize int64 + metrics remoteSettingServiceMetrics +} + +var _ Service = (*remoteSettingService)(nil) +var _ prometheus.Collector = (*remoteSettingService)(nil) + +// Config configures a Service. +type Config struct { + // URL is the base URL for the remote settings service (required). + URL string + // TokenExchangeClient authenticates requests (required if WrapTransport is not set). + TokenExchangeClient *authlib.TokenExchangeClient + // WrapTransport wraps the HTTP transport for authentication. + // Takes precedence over TokenExchangeClient when both are set. + // At least one of WrapTransport or TokenExchangeClient is required. + WrapTransport transport.WrapperFunc + // TLSClientConfig configures TLS for the client connection. + TLSClientConfig clientrest.TLSClientConfig + // QPS limits requests per second (defaults to DefaultQPS). + QPS float32 + // Burst allows request bursts above QPS (defaults to DefaultBurst). + Burst int + // PageSize sets the number of items per API page (defaults to DefaultPageSize). + PageSize int64 +} + +// Setting represents the parsed spec of a Setting resource. +type Setting struct { + // Setting section + Section string `json:"section"` + // Setting key + Key string `json:"key"` + // Setting value + Value string `json:"value"` +} + +// New creates a Service from the provided configuration. +func New(config Config) (Service, error) { + log := logging.New(LogPrefix) + dynamicClient, err := getDynamicClient(config, log) + if err != nil { + return nil, err + } + pageSize := DefaultPageSize + if config.PageSize > 0 { + pageSize = config.PageSize + } + + metrics := initMetrics() + + return &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: pageSize, + log: log, + metrics: metrics, + }, nil +} + +func (m *remoteSettingService) ListAsIni(ctx context.Context, labelSelector metav1.LabelSelector) (*ini.File, error) { + namespace, ok := request.NamespaceFrom(ctx) + ns := semconv.GrafanaNamespaceName(namespace) + ctx, span := tracer.Start(ctx, "remoteSettingService.ListAsIni", + trace.WithAttributes(ns)) + defer span.End() + + if !ok || namespace == "" { + return nil, tracing.Errorf(span, "missing namespace in context") + } + + settings, err := m.List(ctx, labelSelector) + if err != nil { + return nil, err + } + iniFile, err := m.toIni(settings) + if err != nil { + return nil, tracing.Error(span, err) + } + return iniFile, nil +} + +func (m *remoteSettingService) List(ctx context.Context, labelSelector metav1.LabelSelector) ([]*Setting, error) { + namespace, ok := request.NamespaceFrom(ctx) + ns := semconv.GrafanaNamespaceName(namespace) + ctx, span := tracer.Start(ctx, "remoteSettingService.List", + trace.WithAttributes(ns)) + defer span.End() + if !ok || namespace == "" { + return nil, tracing.Errorf(span, "missing namespace in context") + } + log := m.log.FromContext(ctx).New(ns.Key, ns.Value, "function", "remoteSettingService.List", "traceId", span.SpanContext().TraceID()) + + startTime := time.Now() + var status string + defer func() { + duration := time.Since(startTime).Seconds() + m.metrics.listDuration.WithLabelValues(status).Observe(duration) + }() + + selector, err := metav1.LabelSelectorAsSelector(&labelSelector) + if err != nil { + status = "error" + return nil, tracing.Error(span, err) + } + if selector.Empty() { + log.Debug("empty selector. Fetching all settings") + } + + var allSettings []*Setting + var continueToken string + hasNext := true + totalPages := 0 + // Using an upper limit to prevent infinite loops + for hasNext && totalPages < 1000 { + totalPages++ + opts := metav1.ListOptions{ + Limit: m.pageSize, + Continue: continueToken, + } + if !selector.Empty() { + opts.LabelSelector = selector.String() + } + + settingsList, lErr := m.dynamicClient.Resource(settingGroupVersion).Namespace(namespace).List(ctx, opts) + if lErr != nil { + status = "error" + return nil, tracing.Error(span, lErr) + } + for i := range settingsList.Items { + setting, pErr := parseSettingResource(&settingsList.Items[i]) + if pErr != nil { + status = "error" + return nil, tracing.Error(span, pErr) + } + allSettings = append(allSettings, setting) + } + continueToken = settingsList.GetContinue() + if continueToken == "" { + hasNext = false + } + } + + status = "success" + m.metrics.listResultSize.WithLabelValues(status).Observe(float64(len(allSettings))) + + return allSettings, nil +} + +func parseSettingResource(setting *unstructured.Unstructured) (*Setting, error) { + spec, found, err := unstructured.NestedMap(setting.Object, "spec") + if err != nil { + return nil, fmt.Errorf("failed to get spec from setting: %w", err) + } + if !found { + return nil, fmt.Errorf("spec not found in setting %s", setting.GetName()) + } + + var result Setting + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(spec, &result); err != nil { + return nil, fmt.Errorf("failed to convert spec to Setting: %w", err) + } + + return &result, nil +} + +func (m *remoteSettingService) toIni(settings []*Setting) (*ini.File, error) { + conf := ini.Empty() + for _, setting := range settings { + if !conf.HasSection(setting.Section) { + _, _ = conf.NewSection(setting.Section) + } + _, err := conf.Section(setting.Section).NewKey(setting.Key, setting.Value) + if err != nil { + return nil, err + } + } + return conf, nil +} + +func getDynamicClient(config Config, log logging.Logger) (dynamic.Interface, error) { + if config.URL == "" { + return nil, fmt.Errorf("URL cannot be empty") + } + if config.WrapTransport == nil && config.TokenExchangeClient == nil { + return nil, fmt.Errorf("must set either TokenExchangeClient or WrapTransport") + } + + wrapTransport := config.WrapTransport + if config.WrapTransport == nil { + log.Debug("using default wrapTransport with TokenExchangeClient") + wrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return &authRoundTripper{ + tokenClient: config.TokenExchangeClient, + transport: rt, + } + } + } + + qps := DefaultQPS + if config.QPS > 0 { + qps = config.QPS + } + + burst := DefaultBurst + if config.Burst > 0 { + burst = config.Burst + } + + return dynamic.NewForConfig(&clientrest.Config{ + Host: config.URL, + WrapTransport: wrapTransport, + TLSClientConfig: config.TLSClientConfig, + QPS: qps, + Burst: burst, + }) +} + +// authRoundTripper wraps an HTTP transport with token-based authentication. +type authRoundTripper struct { + tokenClient *authlib.TokenExchangeClient + transport http.RoundTripper +} + +var _ http.RoundTripper = (*authRoundTripper)(nil) + +func (a *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + token, err := a.tokenClient.Exchange(req.Context(), authlib.TokenExchangeRequest{ + Audiences: []string{ApiGroup}, + Namespace: "*", + }) + if err != nil { + return nil, fmt.Errorf("failed to exchange token: %w", err) + } + req = utilnet.CloneRequest(req) + + req.Header.Set("X-Access-Token", fmt.Sprintf("Bearer %s", token.Token)) + return a.transport.RoundTrip(req) +} + +func initMetrics() remoteSettingServiceMetrics { + metrics := remoteSettingServiceMetrics{ + listDuration: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "settings", + Subsystem: "service", + Name: "list_settings_duration_seconds", + Help: "Duration of remote settings service List operations", + NativeHistogramBucketFactor: 1.1, + }, + []string{"status"}, // status: "success" or "error" + ), + listResultSize: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "settings", + Subsystem: "service", + Name: "list_settings_result_size", + Help: "Number of settings returned by remote settings service List operations", + NativeHistogramBucketFactor: 1.1, + }, + []string{"status"}, // status: "success" or "error" + ), + } + return metrics +} + +func (m *remoteSettingService) Describe(descs chan<- *prometheus.Desc) { + m.metrics.listDuration.Describe(descs) + m.metrics.listResultSize.Describe(descs) +} + +func (m *remoteSettingService) Collect(metrics chan<- prometheus.Metric) { + m.metrics.listDuration.Collect(metrics) + m.metrics.listResultSize.Collect(metrics) +} diff --git a/pkg/services/setting/service_test.go b/pkg/services/setting/service_test.go new file mode 100644 index 00000000000..949ccc73a86 --- /dev/null +++ b/pkg/services/setting/service_test.go @@ -0,0 +1,542 @@ +package setting + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/dynamic/fake" + k8testing "k8s.io/client-go/testing" + + authlib "github.com/grafana/authlib/authn" + "github.com/grafana/grafana/pkg/infra/log" +) + +func TestRemoteSettingService_ListAsIni(t *testing.T) { + t.Run("should filter settings by label selector", func(t *testing.T) { + // Create multiple settings, only some matching the selector + setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"}) + setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) + setting3 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "host", Value: "localhost"}) + + client := newTestClient(500, setting1, setting2, setting3) + + // Create a selector that should match only database settings + selector := metav1.LabelSelector{ + MatchLabels: map[string]string{ + "section": "database", + }, + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.ListAsIni(ctx, selector) + + require.NoError(t, err) + assert.NotNil(t, result) + // Should only have database settings, not server settings + assert.True(t, result.HasSection("database")) + assert.Equal(t, "postgres", result.Section("database").Key("type").String()) + assert.Equal(t, "localhost", result.Section("database").Key("host").String()) + // Should NOT have server settings + assert.False(t, result.HasSection("server")) + }) + + t.Run("should return all settings with empty selector", func(t *testing.T) { + // Create multiple settings across different sections + setting1 := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) + setting2 := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "mysql"}) + + client := newTestClient(500, setting1, setting2) + + // Empty selector should select everything + selector := metav1.LabelSelector{} + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.ListAsIni(ctx, selector) + + require.NoError(t, err) + assert.NotNil(t, result) + // Should have all settings from all sections + assert.True(t, result.HasSection("server")) + assert.Equal(t, "3000", result.Section("server").Key("port").String()) + assert.True(t, result.HasSection("database")) + assert.Equal(t, "mysql", result.Section("database").Key("type").String()) + }) +} + +func TestRemoteSettingService_List(t *testing.T) { + t.Run("should handle single page response", func(t *testing.T) { + setting := newUnstructuredSetting("test-namespace", Setting{Section: "server", Key: "port", Value: "3000"}) + + client := newTestClient(500, setting) + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.NoError(t, err) + assert.Len(t, result, 1) + + spec := result[0] + assert.Equal(t, "server", spec.Section) + assert.Equal(t, "port", spec.Key) + assert.Equal(t, "3000", spec.Value) + }) + + t.Run("should handle multiple pages", func(t *testing.T) { + totalPages := 3 + pageSize := 5 + + pages := make([][]*unstructured.Unstructured, totalPages) + for pageNum := 0; pageNum < totalPages; pageNum++ { + for idx := 0; idx < pageSize; idx++ { + item := newUnstructuredSetting( + "test-namespace", + Setting{ + Section: fmt.Sprintf("section-%d", pageNum), + Key: fmt.Sprintf("key-%d", idx), + Value: fmt.Sprintf("val-%d-%d", pageNum, idx), + }, + ) + pages[pageNum] = append(pages[pageNum], item) + } + } + + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) + listCallCount := 0 + dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { + listCallCount++ + + continueToken := fmt.Sprintf("continue-%d", listCallCount) + if listCallCount == totalPages { + continueToken = "" + } + + if listCallCount <= totalPages { + list := &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": listKind, + }, + } + list.SetContinue(continueToken) + for _, item := range pages[listCallCount-1] { + list.Items = append(list.Items, *item) + } + return true, list, nil + } + + return false, nil, nil + }) + + client := &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: int64(pageSize), + log: log.NewNopLogger(), + metrics: initMetrics(), + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.NoError(t, err) + assert.Len(t, result, totalPages*pageSize) + assert.Equal(t, totalPages, listCallCount) + }) + + t.Run("should pass label selector when provided", func(t *testing.T) { + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) + dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { + listAction := action.(k8testing.ListActionImpl) + assert.Equal(t, "app=grafana", listAction.ListOptions.LabelSelector) + return true, &unstructured.UnstructuredList{}, nil + }) + + client := &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: 500, + log: log.NewNopLogger(), + metrics: initMetrics(), + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + _, err := client.List(ctx, metav1.LabelSelector{MatchLabels: map[string]string{"app": "grafana"}}) + + require.NoError(t, err) + }) + + t.Run("should stop pagination at 1000 pages", func(t *testing.T) { + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) + listCallCount := 0 + dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { + listCallCount++ + // Always return a continue token to simulate infinite pagination + list := &unstructured.UnstructuredList{} + list.SetContinue("continue-forever") + return true, list, nil + }) + + client := &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: 10, + log: log.NewNopLogger(), + metrics: initMetrics(), + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + _, err := client.List(ctx, metav1.LabelSelector{}) + + require.NoError(t, err) + assert.Equal(t, 1000, listCallCount, "Should stop at 1000 pages to prevent infinite loops") + }) + + t.Run("should return error when parsing setting fails", func(t *testing.T) { + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind) + dynamicClient.PrependReactor("list", "settings", func(action k8testing.Action) (handled bool, ret runtime.Object, err error) { + // Return a malformed setting without spec + list := &unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": listKind, + }, + } + malformedSetting := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": "malformed", + "namespace": "test-namespace", + }, + // Missing spec + }, + } + list.Items = append(list.Items, *malformedSetting) + return true, list, nil + }) + + client := &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: 500, + log: log.NewNopLogger(), + metrics: initMetrics(), + } + + ctx := request.WithNamespace(context.Background(), "test-namespace") + result, err := client.List(ctx, metav1.LabelSelector{}) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "spec not found") + }) +} + +func TestParseSettingResource(t *testing.T) { + t.Run("should parse valid setting resource", func(t *testing.T) { + setting := newUnstructuredSetting("test-namespace", Setting{Section: "database", Key: "type", Value: "postgres"}) + + result, err := parseSettingResource(setting) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "database", result.Section) + assert.Equal(t, "type", result.Key) + assert.Equal(t, "postgres", result.Value) + }) + + t.Run("should return error when spec is missing", func(t *testing.T) { + setting := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": "test-setting", + "namespace": "test-namespace", + }, + // No spec + }, + } + + result, err := parseSettingResource(setting) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "spec not found") + }) +} + +func TestRemoteSettingService_ToIni(t *testing.T) { + t.Run("should convert settings to ini format", func(t *testing.T) { + settings := []*Setting{ + {Section: "database", Key: "type", Value: "postgres"}, + {Section: "database", Key: "host", Value: "localhost"}, + {Section: "server", Key: "http_port", Value: "3000"}, + } + + client := &remoteSettingService{ + pageSize: 500, + log: log.NewNopLogger(), + } + + result, err := client.toIni(settings) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.HasSection("database")) + assert.True(t, result.HasSection("server")) + assert.Equal(t, "postgres", result.Section("database").Key("type").String()) + assert.Equal(t, "localhost", result.Section("database").Key("host").String()) + assert.Equal(t, "3000", result.Section("server").Key("http_port").String()) + }) + + t.Run("should handle empty settings list", func(t *testing.T) { + var settings []*Setting + + client := &remoteSettingService{ + pageSize: 500, + log: log.NewNopLogger(), + } + + result, err := client.toIni(settings) + + require.NoError(t, err) + assert.NotNil(t, result) + sections := result.Sections() + assert.Len(t, sections, 1) // Only default section + }) + + t.Run("should create section if it does not exist", func(t *testing.T) { + settings := []*Setting{ + {Section: "new_section", Key: "new_key", Value: "new_value"}, + } + + client := &remoteSettingService{ + pageSize: 500, + log: log.NewNopLogger(), + } + + result, err := client.toIni(settings) + + require.NoError(t, err) + assert.True(t, result.HasSection("new_section")) + assert.Equal(t, "new_value", result.Section("new_section").Key("new_key").String()) + }) + + t.Run("should handle multiple keys in same section", func(t *testing.T) { + settings := []*Setting{ + {Section: "auth", Key: "disable_login_form", Value: "false"}, + {Section: "auth", Key: "disable_signout_menu", Value: "true"}, + } + + client := &remoteSettingService{ + pageSize: 500, + log: log.NewNopLogger(), + } + + result, err := client.toIni(settings) + + require.NoError(t, err) + assert.True(t, result.HasSection("auth")) + authSection := result.Section("auth") + assert.Equal(t, "false", authSection.Key("disable_login_form").String()) + assert.Equal(t, "true", authSection.Key("disable_signout_menu").String()) + }) +} + +func TestNew(t *testing.T) { + t.Run("should create client with default page size", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + } + + client, err := New(config) + + require.NoError(t, err) + assert.NotNil(t, client) + remoteClient := client.(*remoteSettingService) + assert.Equal(t, DefaultPageSize, remoteClient.pageSize) + }) + + t.Run("should create client with custom page size", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + PageSize: 100, + } + + client, err := New(config) + + require.NoError(t, err) + assert.NotNil(t, client) + remoteClient := client.(*remoteSettingService) + assert.Equal(t, int64(100), remoteClient.pageSize) + }) + + t.Run("should use default page size when zero is provided", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + PageSize: 0, + } + + client, err := New(config) + + require.NoError(t, err) + assert.NotNil(t, client) + remoteClient := client.(*remoteSettingService) + assert.Equal(t, DefaultPageSize, remoteClient.pageSize) + }) + + t.Run("should return error when config is invalid", func(t *testing.T) { + config := Config{ + URL: "", // Invalid: empty URL + } + + client, err := New(config) + + require.Error(t, err) + assert.Nil(t, client) + assert.Contains(t, err.Error(), "URL cannot be empty") + }) +} + +func TestGetDynamicClient(t *testing.T) { + logger := log.NewNopLogger() + + t.Run("should return error when SettingServiceURL is empty", func(t *testing.T) { + config := Config{ + URL: "", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + } + + client, err := getDynamicClient(config, logger) + + require.Error(t, err) + assert.Nil(t, client) + assert.Contains(t, err.Error(), "URL cannot be empty") + }) + + t.Run("should return error when both TokenExchangeClient and WrapTransport are nil", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + TokenExchangeClient: nil, + WrapTransport: nil, + } + + client, err := getDynamicClient(config, logger) + + require.Error(t, err) + assert.Nil(t, client) + assert.Contains(t, err.Error(), "must set either TokenExchangeClient or WrapTransport") + }) + + t.Run("should create client with WrapTransport", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + } + + client, err := getDynamicClient(config, logger) + + require.NoError(t, err) + assert.NotNil(t, client) + }) + + t.Run("should not fail when QPS and Burst are not provided", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + } + + client, err := getDynamicClient(config, logger) + + require.NoError(t, err) + assert.NotNil(t, client) + }) + + t.Run("should not fail when custom QPS and Burst are provided", func(t *testing.T) { + config := Config{ + URL: "https://example.com", + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { return rt }, + QPS: 10.0, + Burst: 20, + } + + client, err := getDynamicClient(config, logger) + + require.NoError(t, err) + assert.NotNil(t, client) + }) + + t.Run("should use WrapTransport when both WrapTransport and TokenExchangeClient are provided", func(t *testing.T) { + wrapTransportCalled := false + tokenExchangeClient := &authlib.TokenExchangeClient{} + + config := Config{ + URL: "https://example.com", + TokenExchangeClient: tokenExchangeClient, + WrapTransport: func(rt http.RoundTripper) http.RoundTripper { + wrapTransportCalled = true + return rt + }, + } + + client, err := getDynamicClient(config, logger) + + require.NoError(t, err) + assert.NotNil(t, client) + assert.True(t, wrapTransportCalled, "WrapTransport should be called and take precedence over TokenExchangeClient") + }) +} + +// Helper function to create an unstructured Setting object for tests +func newUnstructuredSetting(namespace string, spec Setting) *unstructured.Unstructured { + // Generate resource name in the format {section}--{key} + name := fmt.Sprintf("%s--%s", spec.Section, spec.Key) + + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": ApiGroup + "/" + apiVersion, + "kind": kind, + "metadata": map[string]interface{}{ + "name": name, + "namespace": namespace, + }, + "spec": map[string]interface{}{ + "section": spec.Section, + "key": spec.Key, + "value": spec.Value, + }, + }, + } + // Always set section and key labels + obj.SetLabels(map[string]string{ + "section": spec.Section, + "key": spec.Key, + }) + return obj +} + +// Helper function to create a test client with the dynamic fake client +func newTestClient(pageSize int64, objects ...runtime.Object) *remoteSettingService { + scheme := runtime.NewScheme() + dynamicClient := fake.NewSimpleDynamicClientWithCustomListKinds(scheme, settingGroupListKind, objects...) + + return &remoteSettingService{ + dynamicClient: dynamicClient, + pageSize: pageSize, + log: log.NewNopLogger(), + metrics: initMetrics(), + } +} From 4130bd9cd300ec7ce0fb492b3a7229968c9167b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:15:24 +0100 Subject: [PATCH 30/94] Revert "K8s: read resource configs from API Enablement for API Builders" (#114475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "K8s: read resource configs from API Enablement for API Builders (#114…" This reverts commit 0c2707bbc41b60124d2217547f607e08a2a3f013. --- pkg/registry/apis/ofrep/register.go | 4 + pkg/services/apiserver/builder/helper.go | 106 +++++------------- pkg/services/apiserver/builder/openapi.go | 19 +--- .../apiserver/builder/request_handler.go | 10 +- pkg/services/apiserver/service.go | 8 +- 5 files changed, 36 insertions(+), 111 deletions(-) diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index d6a443cfee7..cd6f8f0eb29 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -64,6 +64,10 @@ func NewAPIBuilder(providerType string, url *url.URL, insecure bool, caFile stri } func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg) (*APIBuilder, error) { + if !cfg.OpenFeature.APIEnabled { + return nil, nil + } + var staticEvaluator featuremgmt.StaticFlagEvaluator // No static evaluator needed for non-static provider var err error if cfg.OpenFeature.ProviderType == setting.StaticProviderType { diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index ae90295c01d..603c49f3cb2 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -22,7 +22,6 @@ import ( k8srequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" genericapiserver "k8s.io/apiserver/pkg/server" - serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/apiserver/pkg/util/openapi" k8sscheme "k8s.io/client-go/kubernetes/scheme" k8stracing "k8s.io/component-base/tracing" @@ -73,36 +72,13 @@ var PathRewriters = []filters.PathRewriter{ }, } -// GetDefaultBuildHandlerChainFuncForAggregator is a replica of GetDefaultBuildHandlerChainFunc except it skips custom routes handling -func GetDefaultBuildHandlerChainFuncForAggregator() BuildHandlerChainFunc { - return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler { - // filters.WithRequester needs to be after the K8s chain because it depends on the K8s user in context - handler := filters.WithRequester(delegateHandler) - - // Call DefaultBuildHandlerChain on the main entrypoint http.Handler - // See https://github.com/kubernetes/apiserver/blob/v0.28.0/pkg/server/config.go#L906 - // DefaultBuildHandlerChain provides many things, notably CORS, HSTS, cache-control, authz and latency tracking - handler = genericapiserver.DefaultBuildHandlerChain(handler, c) - - handler = filters.WithAcceptHeader(handler) - handler = filters.WithPathRewriters(handler, PathRewriters) - handler = k8stracing.WithTracing(handler, c.TracerProvider, "KubernetesAPI") - handler = filters.WithExtractJaegerTrace(handler) - // Configure filters.WithPanicRecovery to not crash on panic - utilruntime.ReallyCrash = false - - return handler - } -} func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus.Registerer) BuildHandlerChainFunc { return func(delegateHandler http.Handler, c *genericapiserver.Config) http.Handler { requestHandler, err := GetCustomRoutesHandler( delegateHandler, c.LoopbackClientConfig, builders, - reg, - c.MergedResourceConfig, - ) + reg) if err != nil { panic(fmt.Sprintf("could not build the request handler for specified API builders: %s", err.Error())) } @@ -129,8 +105,6 @@ func GetDefaultBuildHandlerChainFunc(builders []APIGroupBuilder, reg prometheus. } } -// SetupConfig sets up the server config for the API server -// specify isAggregator=true, if the chain is being constructed for kube-aggregator func SetupConfig( scheme *runtime.Scheme, serverConfig *genericapiserver.RecommendedConfig, @@ -140,7 +114,6 @@ func SetupConfig( gvs []schema.GroupVersion, additionalOpenAPIDefGetters []common.GetOpenAPIDefinitions, reg prometheus.Registerer, - apiResourceConfig *serverstorage.ResourceConfig, ) error { serverConfig.AdmissionControl = NewAdmissionFromBuilders(builders) defsGetter := GetOpenAPIDefinitions(builders, additionalOpenAPIDefGetters...) @@ -153,7 +126,7 @@ func SetupConfig( openapinamer.NewDefinitionNamer(scheme, k8sscheme.Scheme)) // Add the custom routes to service discovery - serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs, apiResourceConfig) + serverConfig.OpenAPIV3Config.PostProcessSpec = getOpenAPIPostProcessor(buildVersion, builders, gvs) serverConfig.OpenAPIV3Config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) { meta := r.Metadata() kind := "" @@ -314,7 +287,6 @@ func InstallAPIs( features featuremgmt.FeatureToggles, dualWriterMetrics *grafanarest.DualWriterMetrics, builderMetrics *BuilderMetrics, - apiResourceConfig *serverstorage.ResourceConfig, ) error { // dual writing is only enabled when the storage type is not legacy. // this is needed to support setting a default RESTOptionsGetter for new APIs that don't @@ -429,9 +401,34 @@ func InstallAPIs( for group, buildersForGroup := range buildersGroupMap { g := genericapiserver.NewDefaultAPIGroupInfo(group, scheme, metav1.ParameterCodec, codecs) for _, b := range buildersForGroup { - if err := installAPIGroupsForBuilder(&g, group, b, apiResourceConfig, scheme, optsGetter, dualWrite, reg, optsregister, storageOpts, features); err != nil { + if err := b.UpdateAPIGroupInfo(&g, APIGroupOptions{ + Scheme: scheme, + OptsGetter: optsGetter, + DualWriteBuilder: dualWrite, + MetricsRegister: reg, + StorageOptsRegister: optsregister, + StorageOpts: storageOpts, + }); err != nil { return err } + if len(g.PrioritizedVersions) < 1 { + continue + } + + // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed + //nolint:staticcheck // not yet migrated to OpenFeature + if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { + for name := range resources { + if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { + delete(resources, name) + } + } + if len(resources) == 0 { + delete(g.VersionedResourcesStorageMap, "v0alpha1") + } + } + } } // skip installing the group if there are no resources left after filtering @@ -448,53 +445,6 @@ func InstallAPIs( return nil } -func installAPIGroupsForBuilder(g *genericapiserver.APIGroupInfo, group string, b APIGroupBuilder, apiResourceConfig *serverstorage.ResourceConfig, scheme *runtime.Scheme, - optsGetter generic.RESTOptionsGetter, dualWrite grafanarest.DualWriteBuilder, reg prometheus.Registerer, optsregister apistore.StorageOptionsRegister, - storageOpts *options.StorageOptions, features featuremgmt.FeatureToggles) error { - if err := b.UpdateAPIGroupInfo(g, APIGroupOptions{ - Scheme: scheme, - OptsGetter: optsGetter, - DualWriteBuilder: dualWrite, - MetricsRegister: reg, - StorageOptsRegister: optsregister, - StorageOpts: storageOpts, - }); err != nil { - return err - } - if len(g.PrioritizedVersions) < 1 { - return nil - } - - // filter out api groups that are disabled in APIEnablementOptions - for version := range g.VersionedResourcesStorageMap { - gvr := schema.GroupVersionResource{ - Group: group, - Version: version, - } - if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { - klog.InfoS("Skipping storage for disabled resource", "gvr", gvr.String()) - delete(g.VersionedResourcesStorageMap, version) - } - } - - // if grafanaAPIServerWithExperimentalAPIs is not enabled, remove v0alpha1 resources unless explicitly allowed - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - if resources, ok := g.VersionedResourcesStorageMap["v0alpha1"]; ok { - for name := range resources { - if !allowRegisteringResourceByInfo(b.AllowedV0Alpha1Resources(), name) { - delete(resources, name) - } - } - if len(resources) == 0 { - delete(g.VersionedResourcesStorageMap, "v0alpha1") - } - } - } - - return nil -} - // AddPostStartHooks adds post start hooks to a generic API server config func AddPostStartHooks( config *genericapiserver.RecommendedConfig, diff --git a/pkg/services/apiserver/builder/openapi.go b/pkg/services/apiserver/builder/openapi.go index 6d3b33f4baf..9cb7cc17f7c 100644 --- a/pkg/services/apiserver/builder/openapi.go +++ b/pkg/services/apiserver/builder/openapi.go @@ -9,8 +9,6 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime/schema" - serverstorage "k8s.io/apiserver/pkg/server/storage" - "k8s.io/klog/v2" openapi "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" spec "k8s.io/kube-openapi/pkg/validation/spec" @@ -78,7 +76,6 @@ func addBuilderRoutes( targetGroupVersion schema.GroupVersion, openAPISpec *spec3.OpenAPI, apiGroupBuilders []APIGroupBuilder, - apiResourceConfig *serverstorage.ResourceConfig, ) (*spec3.OpenAPI, error) { for _, apiGroupBuilder := range apiGroupBuilders { // Optionally include raw http handlers for all builders @@ -110,24 +107,12 @@ func addBuilderRoutes( } } } - - // filter out api groups that are disabled in APIEnablementOptions - for path := range openAPISpec.Paths.Paths { - if strings.HasPrefix(path, "/apis/"+targetGroupVersion.String()+"/") { - gv := targetGroupVersion.WithResource("") - if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gv) { - klog.InfoS("removing openapi routes for disabled resource", "gv", gv.String()) - delete(openAPISpec.Paths.Paths, path) - } - } - } - return openAPISpec, nil } // Modify the OpenAPI spec to include the additional routes. // nolint:gocyclo -func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion, apiResourceConfig *serverstorage.ResourceConfig) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { +func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []schema.GroupVersion) func(*spec3.OpenAPI) (*spec3.OpenAPI, error) { return func(s *spec3.OpenAPI) (*spec3.OpenAPI, error) { if s.Paths == nil { return s, nil @@ -242,7 +227,7 @@ func getOpenAPIPostProcessor(version string, builders []APIGroupBuilder, gvs []s } } } - return addBuilderRoutes(gv, ©, builders, apiResourceConfig) + return addBuilderRoutes(gv, ©, builders) } } return s, nil diff --git a/pkg/services/apiserver/builder/request_handler.go b/pkg/services/apiserver/builder/request_handler.go index 50761f1d42c..80fbf6d0eaa 100644 --- a/pkg/services/apiserver/builder/request_handler.go +++ b/pkg/services/apiserver/builder/request_handler.go @@ -6,9 +6,7 @@ import ( "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" - serverstorage "k8s.io/apiserver/pkg/server/storage" restclient "k8s.io/client-go/rest" - klog "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/spec3" ) @@ -16,7 +14,7 @@ type requestHandler struct { router *mux.Router } -func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer, apiResourceConfig *serverstorage.ResourceConfig) (http.Handler, error) { +func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient.Config, builders []APIGroupBuilder, metricsRegistry prometheus.Registerer) (http.Handler, error) { useful := false // only true if any routes exist anywhere router := mux.NewRouter() @@ -29,12 +27,6 @@ func GetCustomRoutesHandler(delegateHandler http.Handler, restConfig *restclient } for _, gv := range GetGroupVersions(builder) { - // filter out api groups that are disabled in APIEnablementOptions - gvr := gv.WithResource("") - if apiResourceConfig != nil && !apiResourceConfig.ResourceEnabled(gvr) { - klog.InfoS("Skipping custom route handler for disabled group version", "gv", gv.String()) - continue - } routes := provider.GetAPIRoutes(gv) if routes == nil { continue diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 605016c112d..28da416536c 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -316,11 +316,7 @@ func (s *service) start(ctx context.Context) error { s.cfg.BuildBranch, ) - apiResourceConfig := appinstaller.NewAPIResourceConfig(s.appInstallers) - // add the builder group versions to the api resource config - apiResourceConfig.EnableVersions(groupVersions...) - - if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, apiResourceConfig, s.scheme); err != nil { + if err := o.APIEnablementOptions.ApplyTo(&serverConfig.Config, appinstaller.NewAPIResourceConfig(s.appInstallers), s.scheme); err != nil { return err } @@ -363,7 +359,6 @@ func (s *service) start(ctx context.Context) error { groupVersions, defGetters, s.metrics, - apiResourceConfig, ) if err != nil { return err @@ -405,7 +400,6 @@ func (s *service) start(ctx context.Context) error { s.features, s.dualWriterMetrics, s.builderMetrics, - apiResourceConfig, ) if err != nil { return err From 51d562eb813503bcb4327728fe358d20daa8df45 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Wed, 26 Nov 2025 09:21:12 -0600 Subject: [PATCH 31/94] Docs: Clarify some language in migration assistant docs (#114440) Co-authored-by: Irene Rodriguez --- .../migration-guide/cloud-migration-assistant.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/administration/migration-guide/cloud-migration-assistant.md b/docs/sources/administration/migration-guide/cloud-migration-assistant.md index 439e04c282d..b4a1e5b89fb 100644 --- a/docs/sources/administration/migration-guide/cloud-migration-assistant.md +++ b/docs/sources/administration/migration-guide/cloud-migration-assistant.md @@ -14,10 +14,10 @@ weight: 400 The Grafana Cloud Migration Assistant, generally available from Grafana v12.0, automatically migrates resources from your Grafana OSS/Enterprise instance to Grafana Cloud. It provides the following functionality: - Securely connect your self-managed instance to a Grafana Cloud instance. -- Seamlessly migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps. +- Migrate resources such as dashboards, data sources, and folders to your cloud instance in a few easy steps. - View the migration status of your resources in real-time. -Some of the benefits of the migration assistant are: +Some benefits of the migration assistant are: Ease of use : Follow the steps provided by the UI to easily migrate all your resources to Grafana Cloud without using Grafana APIs or scripts. @@ -44,7 +44,7 @@ The following resources are supported by the migration assistant: To use the Grafana migration assistant, you need: -- Grafana v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles). +- A self-managed Grafana instance version v11.2 or above with the `onPremToCloudMigrations` feature toggle enabled. In Grafana 11.5, this is enabled by default. For more information on how to enable a feature toggle, refer to [Configure feature toggles](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/feature-toggles/#configure-feature-toggles). - A [Grafana Cloud Stack](https://grafana.com/docs/grafana-cloud/get-started/) you intend to migrate your resources to. - [`Admin`](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/cloud-roles/) access to the Grafana Cloud Stack. To check your access level, go to `https://grafana.com/orgs//members`. - [Grafana server administrator](https://grafana.com/docs/grafana//administration/roles-and-permissions/#grafana-server-administrators) access to your existing Grafana OSS/Enterprise instance. To check your access level, go to `https:///admin/users`. @@ -64,7 +64,7 @@ In Grafana Enterprise, the server administrator has access to the migration assi ### Grant access in Grafana Enterprise -{{< admonition type="important">}} +{{< admonition type="note" >}} You must [configure RBAC](https://grafana.com/docs/grafana//administration/roles-and-permissions/access-control/configure-rbac/) before you can grant other administrators access to the Grafana Migration Assistant. {{< /admonition >}} From ca8cad68c8764a8f6653900de21dc39024b6d40f Mon Sep 17 00:00:00 2001 From: beejeebus Date: Tue, 25 Nov 2025 16:47:04 +0000 Subject: [PATCH 32/94] Add a metric to track usage of datasource configuration CRUD This PR adds `ds_config_handler_requests_duration_seconds` metric to help us track the release of the new datasource configuration CRUD api. Fixes https://github.com/grafana/grafana-enterprise/issues/10309 --- pkg/api/datasources.go | 21 +++++++++ pkg/api/datasources_test.go | 23 +++++++++ pkg/api/http_server.go | 49 +++++++++++--------- pkg/registry/apis/datasource/legacy_store.go | 28 ++++++++++- pkg/registry/apis/datasource/register.go | 6 +++ 5 files changed, 104 insertions(+), 23 deletions(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index cba7f5bc202..d7dc89d248f 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -9,6 +9,7 @@ import ( "sort" "strconv" "strings" + "time" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -18,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/setting" @@ -200,6 +202,11 @@ func (hs *HTTPServer) DeleteDataSourceById(c *contextmodel.ReqContext) response. // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Response { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "GetDataSourceByUID"), time.Since(start).Seconds()) + }() + ds, err := hs.getRawDataSourceByUID(c.Req.Context(), web.Params(c.Req)[":uid"], c.GetOrgID()) if err != nil { @@ -231,6 +238,11 @@ func (hs *HTTPServer) GetDataSourceByUID(c *contextmodel.ReqContext) response.Re // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) DeleteDataSourceByUID(c *contextmodel.ReqContext) response.Response { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "DeleteDataSourceByUID"), time.Since(start).Seconds()) + }() + uid := web.Params(c.Req)[":uid"] if uid == "" { @@ -361,6 +373,11 @@ func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error { // 409: conflictError // 500: internalServerError func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Response { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "AddDataSource"), time.Since(start).Seconds()) + }() + cmd := datasources.AddDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -478,6 +495,10 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response. // 409: conflictError // 500: internalServerError func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(c.Req.Context(), hs.dsConfigHandlerRequestsDuration.WithLabelValues("legacy", "UpdateDataSourceByUID"), time.Since(start).Seconds()) + }() cmd := datasources.UpdateDataSourceCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 517dc6e9048..e2f951b0a37 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -81,6 +83,19 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) { }, mockSQLStore) } +// setupDsConfigMetrics creates and registers the prometheus metrics needed for HTTPServer tests +// that call methods using dsConfigHandlerRequestsDuration. +func setupDsConfigHandlerMetrics() (prometheus.Registerer, *prometheus.HistogramVec) { + promRegister := prometheus.NewRegistry() + dsConfigHandlerRequestsDuration := metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "ds_config_handler_requests_duration_seconds", + Help: "Duration of requests handled by datasource configuration handlers", + }, []string{"code_path", "handler"}) + promRegister.MustRegister(dsConfigHandlerRequestsDuration) + return promRegister, dsConfigHandlerRequestsDuration +} + // Adding data sources with invalid URLs should lead to an error. func TestAddDataSource_InvalidURL(t *testing.T) { sc := setupScenarioContext(t, "/api/datasources") @@ -88,6 +103,7 @@ func TestAddDataSource_InvalidURL(t *testing.T) { DataSourcesService: &dataSourcesServiceMock{}, Cfg: setting.NewCfg(), } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ @@ -118,6 +134,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) { AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), accesscontrolService: actest.FakeService{}, } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources") @@ -143,6 +160,7 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) { DataSourcesService: &dataSourcesServiceMock{}, Cfg: setting.NewCfg(), } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources") @@ -175,6 +193,7 @@ func TestUpdateDataSource_InvalidURL(t *testing.T) { DataSourcesService: &dataSourcesServiceMock{}, Cfg: setting.NewCfg(), } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources/1234") sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { @@ -199,6 +218,7 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) { DataSourcesService: &dataSourcesServiceMock{}, Cfg: setting.NewCfg(), } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources/1234") hs.Cfg.AuthProxy.Enabled = true @@ -236,6 +256,7 @@ func TestAddDataSourceTeamHTTPHeaders(t *testing.T) { ExpectedErr: nil, }, } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID)) hs.Cfg.AuthProxy.Enabled = true @@ -289,6 +310,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) { AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), accesscontrolService: actest.FakeService{}, } + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() sc := setupScenarioContext(t, "/api/datasources/1234") @@ -429,6 +451,7 @@ func TestAPI_datasources_AccessControl(t *testing.T) { hs.DataSourcesService = &dataSourcesServiceMock{expectedDatasource: &datasources.DataSource{}} hs.accesscontrolService = actest.FakeService{} hs.Live = newTestLive(t, hs.SQLStore) + hs.promRegister, hs.dsConfigHandlerRequestsDuration = setupDsConfigHandlerMetrics() }) for _, url := range tt.urls { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index fa0b407aa2b..f2ac32a80c6 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -203,27 +203,28 @@ type HTTPServer struct { pluginsCDNService *pluginscdn.Service managedPluginsService managedplugins.Manager - userService user.Service - tempUserService tempUser.Service - loginAttemptService loginAttempt.Service - orgService org.Service - orgDeletionService org.DeletionService - TeamService team.Service - accesscontrolService accesscontrol.Service - annotationsRepo annotations.Repository - tagService tag.Service - oauthTokenService oauthtoken.OAuthTokenService - statsService stats.Service - authnService authn.Service - starApi *starApi.API - promRegister prometheus.Registerer - promGatherer prometheus.Gatherer - clientConfigProvider grafanaapiserver.DirectRestConfigProvider - namespacer request.NamespaceMapper - anonService anonymous.Service - userVerifier user.Verifier - tlsCerts TLSCerts - htmlHandlerRequestsDuration *prometheus.HistogramVec + userService user.Service + tempUserService tempUser.Service + loginAttemptService loginAttempt.Service + orgService org.Service + orgDeletionService org.DeletionService + TeamService team.Service + accesscontrolService accesscontrol.Service + annotationsRepo annotations.Repository + tagService tag.Service + oauthTokenService oauthtoken.OAuthTokenService + statsService stats.Service + authnService authn.Service + starApi *starApi.API + promRegister prometheus.Registerer + promGatherer prometheus.Gatherer + clientConfigProvider grafanaapiserver.DirectRestConfigProvider + namespacer request.NamespaceMapper + anonService anonymous.Service + userVerifier user.Verifier + tlsCerts TLSCerts + htmlHandlerRequestsDuration *prometheus.HistogramVec + dsConfigHandlerRequestsDuration *prometheus.HistogramVec } type TLSCerts struct { @@ -382,9 +383,15 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi Name: "html_handler_requests_duration_seconds", Help: "Duration of requests handled by the index.go HTML handler", }, []string{"handler"}), + dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "ds_config_handler_requests_duration_seconds", + Help: "Duration of requests handled by datasource configuration handlers", + }, []string{"code_path", "handler"}), } promRegister.MustRegister(hs.htmlHandlerRequestsDuration) + promRegister.MustRegister(hs.dsConfigHandlerRequestsDuration) if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/registry/apis/datasource/legacy_store.go b/pkg/registry/apis/datasource/legacy_store.go index 8786dc8d86e..aea7ed1daa3 100644 --- a/pkg/registry/apis/datasource/legacy_store.go +++ b/pkg/registry/apis/datasource/legacy_store.go @@ -3,7 +3,9 @@ package datasource import ( "context" "fmt" + "time" + "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -11,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" ) var ( @@ -26,8 +29,9 @@ var ( ) type legacyStorage struct { - datasources PluginDatasourceProvider - resourceInfo *utils.ResourceInfo + datasources PluginDatasourceProvider + resourceInfo *utils.ResourceInfo + dsConfigHandlerRequestsDuration *prometheus.HistogramVec } func (s *legacyStorage) New() runtime.Object { @@ -57,11 +61,21 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO } func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Get"), time.Since(start).Seconds()) + }() + return s.datasources.GetDataSource(ctx, name) } // Create implements rest.Creater. func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + ds, ok := obj.(*v0alpha1.DataSource) if !ok { return nil, fmt.Errorf("expected a datasource object") @@ -71,6 +85,11 @@ func (s *legacyStorage) Create(ctx context.Context, obj runtime.Object, createVa // Update implements rest.Updater. func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + old, err := s.Get(ctx, name, &metav1.GetOptions{}) if err != nil { return nil, false, err @@ -107,6 +126,11 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up // Delete implements rest.GracefulDeleter. func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + start := time.Now() + defer func() { + metricutil.ObserveWithExemplar(ctx, s.dsConfigHandlerRequestsDuration.WithLabelValues("new", "Create"), time.Since(start).Seconds()) + }() + err := s.datasources.DeleteDataSource(ctx, name) return nil, false, err } diff --git a/pkg/registry/apis/datasource/register.go b/pkg/registry/apis/datasource/register.go index 9feb46a33d4..bec8fdaec5b 100644 --- a/pkg/registry/apis/datasource/register.go +++ b/pkg/registry/apis/datasource/register.go @@ -20,6 +20,7 @@ import ( datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" + "github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/promlib/models" @@ -218,6 +219,11 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver legacyStore := &legacyStorage{ datasources: b.datasources, resourceInfo: &ds, + dsConfigHandlerRequestsDuration: metricutil.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Name: "ds_config_handler_requests_duration_seconds", + Help: "Duration of requests handled by datasource configuration handlers", + }, []string{"code_path", "handler"}), } unified, err := grafanaregistry.NewRegistryStore(opts.Scheme, ds, opts.OptsGetter) if err != nil { From ebaccc781b62ac664b010ec2f7c3e91c337891c7 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 26 Nov 2025 08:30:38 -0800 Subject: [PATCH 33/94] Suggestions: Update all suggestions suppliers to be functions (#113986) * Suggestions: Convert panels to use function supplier * rework deaggregation * BarGauge * cleanup and make consistent the deaggregation in suggestions * Candlestick * Implement timeseries and clean up some things that can already be deleted * spotted some typos in self-review * restore PanelDataSummary deprecated fields, we wont delete till Grafana 13 * change deprecation message * remove some unused imports * run prettier * update radialbar defaults logic * update tests and logic to DRY up the reduceOptions a bit and more thoroughly test the output * Trend: Improve suggestions * updates from review * add unique DataFrameType list to PanelDataSummary * add histogram suggestions * rework panelDataSummary to be a class, change some things * further boil down PanelDataSummary * Improve FlameGgraph suggestions * geomap and other defaults * reorder the single frame with string and number test --- .../grafana-data/src/panel/PanelPlugin.ts | 5 + .../panel/suggestions/getPanelDataSummary.ts | 162 +++++++--- .../transformers/calculateField.ts | 6 +- .../grafana-data/src/types/suggestions.ts | 2 + public/app/features/geo/utils/location.ts | 10 + .../panel/components/PanelDataErrorView.tsx | 7 +- .../suggestions/getAllSuggestions.test.ts | 255 ++++++++++----- .../panel/suggestions/getAllSuggestions.ts | 33 +- .../app/features/panel/suggestions/utils.ts | 45 ++- public/app/plugins/panel/barchart/module.tsx | 4 +- .../app/plugins/panel/barchart/suggestions.ts | 161 +++++----- public/app/plugins/panel/bargauge/module.tsx | 4 +- .../app/plugins/panel/bargauge/suggestions.ts | 157 +++------ .../app/plugins/panel/candlestick/module.tsx | 4 +- .../plugins/panel/candlestick/suggestions.ts | 71 ++--- .../panel/flamegraph/FlameGraphPanel.tsx | 4 +- .../app/plugins/panel/flamegraph/module.tsx | 23 +- .../plugins/panel/flamegraph/suggestions.ts | 31 -- public/app/plugins/panel/flamegraph/types.ts | 3 + public/app/plugins/panel/gauge/module.tsx | 4 +- public/app/plugins/panel/gauge/suggestions.ts | 136 ++++---- public/app/plugins/panel/geomap/module.tsx | 4 +- .../app/plugins/panel/geomap/suggestions.ts | 46 +++ public/app/plugins/panel/heatmap/module.tsx | 4 +- .../plugins/panel/heatmap/suggestions.test.ts | 226 +++++++++++++ .../app/plugins/panel/heatmap/suggestions.ts | 102 +++--- public/app/plugins/panel/histogram/module.tsx | 15 + public/app/plugins/panel/logs/module.tsx | 8 +- public/app/plugins/panel/logs/suggestions.ts | 33 -- public/app/plugins/panel/nodeGraph/module.tsx | 4 +- .../plugins/panel/nodeGraph/suggestions.ts | 119 ++++--- public/app/plugins/panel/piechart/module.tsx | 4 +- .../app/plugins/panel/piechart/suggestions.ts | 133 ++++---- public/app/plugins/panel/radialbar/module.tsx | 4 +- .../panel/radialbar/suggestions.test.ts | 18 +- .../plugins/panel/radialbar/suggestions.ts | 112 +++---- public/app/plugins/panel/stat/module.tsx | 4 +- public/app/plugins/panel/stat/suggestions.ts | 127 ++++---- .../plugins/panel/state-timeline/module.tsx | 2 +- .../plugins/panel/status-history/module.tsx | 42 ++- .../panel/status-history/suggestions.ts | 56 ---- public/app/plugins/panel/table/module.tsx | 4 +- public/app/plugins/panel/table/suggestions.ts | 61 ++-- .../app/plugins/panel/timeseries/module.tsx | 4 +- .../plugins/panel/timeseries/suggestions.ts | 297 +++++++----------- public/app/plugins/panel/traces/module.tsx | 4 +- .../app/plugins/panel/traces/suggestions.ts | 29 -- public/app/plugins/panel/trend/TrendPanel.tsx | 50 +-- public/app/plugins/panel/trend/module.tsx | 51 ++- public/app/plugins/panel/trend/suggestions.ts | 43 --- public/app/plugins/panel/trend/utils.ts | 48 +++ public/app/types/suggestions.ts | 34 -- public/locales/en-US/grafana.json | 33 ++ 53 files changed, 1573 insertions(+), 1275 deletions(-) delete mode 100644 public/app/plugins/panel/flamegraph/suggestions.ts create mode 100644 public/app/plugins/panel/flamegraph/types.ts create mode 100644 public/app/plugins/panel/geomap/suggestions.ts create mode 100644 public/app/plugins/panel/heatmap/suggestions.test.ts delete mode 100644 public/app/plugins/panel/logs/suggestions.ts delete mode 100644 public/app/plugins/panel/status-history/suggestions.ts delete mode 100644 public/app/plugins/panel/traces/suggestions.ts delete mode 100644 public/app/plugins/panel/trend/suggestions.ts create mode 100644 public/app/plugins/panel/trend/utils.ts delete mode 100644 public/app/types/suggestions.ts diff --git a/packages/grafana-data/src/panel/PanelPlugin.ts b/packages/grafana-data/src/panel/PanelPlugin.ts index fdae270fd48..7b766227ba1 100644 --- a/packages/grafana-data/src/panel/PanelPlugin.ts +++ b/packages/grafana-data/src/panel/PanelPlugin.ts @@ -381,6 +381,11 @@ export class PanelPlugin< const appender = builder.getListAppender({ pluginId: this.meta.id, name: this.meta.name, + options: {}, + fieldConfig: { + defaults: {}, + overrides: [], + }, }); const result = supplier(builder.dataSummary); diff --git a/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts b/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts index ea97e1705d7..021d1eb48c6 100644 --- a/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts +++ b/packages/grafana-data/src/panel/suggestions/getPanelDataSummary.ts @@ -1,19 +1,27 @@ import { PreferredVisualisationType } from '../../types/data'; import { DataFrame, FieldType } from '../../types/dataFrame'; +import { DataFrameType } from '../../types/dataFrameTypes'; -/** - * @alpha - */ export interface PanelDataSummary { hasData?: boolean; rowCountTotal: number; + /** max number of rows in any given dataframe in the panel data */ rowCountMax: number; frameCount: number; fieldCount: number; + /** max number of fields in any given dataframe in the panel data */ + fieldCountMax: number; + /** given a field type, return the number of fields across all dataframes which match this type */ fieldCountByType: (type: FieldType) => number; + /** returns true if any fields in any frames match the field type */ hasFieldType: (type: FieldType) => boolean; - /** The first frame that set's this value */ - preferredVisualisationType?: PreferredVisualisationType; + /* returns true if any of the frames in this panel data summary have the type */ + hasDataFrameType: (type: DataFrameType) => boolean; + /* returns true if any of the frames in this panel data summary have the type */ + hasPreferredVisualisationType: (type: PreferredVisualisationType) => boolean; + + /** pass along a reference to the DataFrame array in case it's needed by the plugin */ + rawFrames?: DataFrame[]; /* --- DEPRECATED FIELDS BELOW --- */ /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */ @@ -23,60 +31,114 @@ export interface PanelDataSummary { /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */ stringFieldCount: number; /** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */ - hasNumberField?: boolean; - /** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */ hasTimeField?: boolean; + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */ + hasNumberField?: boolean; /** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */ hasStringField?: boolean; } +/** + * @alpha + */ +class PanelDataSummaryImpl implements PanelDataSummary { + public rowCountTotal = 0; + /** max number of rows in any single dataframe in the panel data */ + public rowCountMax = 0; + public fieldCount = 0; + /** max number of fields in any single dataframe in the panel data */ + public fieldCountMax = 0; + + private countByType: Partial> = {}; + private preferredVisualisationTypes: Set = new Set(); + private dataFrameTypes: Set = new Set(); + + public get hasData(): boolean { + return this.rowCountTotal > 0; + } + + public get frameCount(): number { + return this.rawFrames?.length ?? 0; + } + + constructor(public rawFrames?: DataFrame[]) { + this._processFrames(); + } + + private _processFrames() { + for (const frame of this.rawFrames ?? []) { + this.rowCountTotal += frame.length; + + if (frame.meta?.preferredVisualisationType) { + this.preferredVisualisationTypes.add(frame.meta.preferredVisualisationType); + } + if (frame.meta?.type) { + this.dataFrameTypes.add(frame.meta.type); + } + + for (const field of frame.fields) { + this.fieldCount++; + this.countByType[field.type] = (this.countByType[field.type] || 0) + 1; + } + + if (frame.length > this.rowCountMax) { + this.rowCountMax = frame.length; + } + if (frame.fields.length > this.fieldCountMax) { + this.fieldCountMax = frame.fields.length; + } + } + } + + public fieldCountByType(type: FieldType): number { + return this.countByType[type] ?? 0; + } + + public hasFieldType(type: FieldType): boolean { + return this.fieldCountByType(type) > 0; + } + + public hasPreferredVisualisationType(type: PreferredVisualisationType): boolean { + return this.preferredVisualisationTypes.has(type); + } + + public hasDataFrameType(type: DataFrameType): boolean { + return this.dataFrameTypes.has(type); + } + + /**** DEPRECATED ****/ + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */ + public get numberFieldCount(): number { + return this.fieldCountByType(FieldType.number); + } + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.time) */ + public get timeFieldCount(): number { + return this.fieldCountByType(FieldType.time); + } + /** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */ + public get stringFieldCount() { + return this.fieldCountByType(FieldType.string); + } + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */ + public get hasTimeField() { + return this.fieldCountByType(FieldType.time) > 0; + } + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */ + public get hasNumberField() { + return this.fieldCountByType(FieldType.number) > 0; + } + /** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */ + public get hasStringField() { + return this.fieldCountByType(FieldType.string) > 0; + } +} + /** * @alpha * given a list of dataframes, summarize attributes of those frames for features like suggestions. * @param frames - dataframes to summarize * @returns summary of the dataframes */ -export function getPanelDataSummary(frames: DataFrame[] = []): PanelDataSummary { - let rowCountTotal = 0; - let rowCountMax = 0; - let fieldCount = 0; - const countByType: Partial> = {}; - let preferredVisualisationType: PreferredVisualisationType | undefined; - - for (const frame of frames) { - rowCountTotal += frame.length; - - if (frame.meta?.preferredVisualisationType) { - preferredVisualisationType = frame.meta.preferredVisualisationType; - } - - for (const field of frame.fields) { - fieldCount++; - countByType[field.type] = (countByType[field.type] || 0) + 1; - } - - if (frame.length > rowCountMax) { - rowCountMax = frame.length; - } - } - - const fieldCountByType = (f: FieldType) => countByType[f] ?? 0; - - return { - rowCountTotal, - rowCountMax, - fieldCount, - preferredVisualisationType, - frameCount: frames.length, - hasData: rowCountTotal > 0, - hasFieldType: (f: FieldType) => fieldCountByType(f) > 0, - fieldCountByType, - // deprecated - numberFieldCount: fieldCountByType(FieldType.number), - timeFieldCount: fieldCountByType(FieldType.time), - stringFieldCount: fieldCountByType(FieldType.string), - hasTimeField: fieldCountByType(FieldType.time) > 0, - hasNumberField: fieldCountByType(FieldType.number) > 0, - hasStringField: fieldCountByType(FieldType.string) > 0, - }; +export function getPanelDataSummary(frames?: DataFrame[]): PanelDataSummary { + return new PanelDataSummaryImpl(frames); } diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 733155a3f10..f1023b49502 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -72,7 +72,7 @@ interface IndexOptions { asPercentile: boolean; } -const defaultReduceOptions: ReduceOptions = { +const defaultNumericVizOptions: ReduceOptions = { reducer: ReducerID.sum, }; @@ -149,10 +149,10 @@ export const calculateFieldTransformer: DataTransformerInfo) => void; + /** @deprecated this will no longer be supported in the new Suggestions UI. */ icon?: string; + /** @deprecated this will no longer be supported in the new Suggestions UI. */ imgSrc?: string; }; } diff --git a/public/app/features/geo/utils/location.ts b/public/app/features/geo/utils/location.ts index 96e92929cdd..90aaa0f9d1e 100644 --- a/public/app/features/geo/utils/location.ts +++ b/public/app/features/geo/utils/location.ts @@ -68,6 +68,16 @@ const defaultMatchers: LocationFieldMatchers = { geo: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.geo), }; +/** + * suggestions needs to run sync, and we just want to use the default matchers in that situation. + */ +export function getDefaultLocationMatchers(): LocationFieldMatchers { + return { + ...defaultMatchers, + mode: FrameGeometrySourceMode.Auto, + }; +} + export async function getLocationMatchers(src?: FrameGeometrySource): Promise { const info: LocationFieldMatchers = { ...defaultMatchers, diff --git a/public/app/features/panel/components/PanelDataErrorView.tsx b/public/app/features/panel/components/PanelDataErrorView.tsx index 4c72a3c879f..93723b3bff1 100644 --- a/public/app/features/panel/components/PanelDataErrorView.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { CoreApp, + FieldType, getPanelDataSummary, GrafanaTheme2, PanelDataSummary, @@ -134,15 +135,15 @@ function getMessageFor( return fieldConfig?.defaults.noValue ?? t('panel.panel-data-error-view.no-value.default', 'No data'); } - if (needsStringField && !dataSummary.hasStringField) { + if (needsStringField && !dataSummary.hasFieldType(FieldType.string)) { return t('panel.panel-data-error-view.missing-value.string', 'Data is missing a string field'); } - if (needsNumberField && !dataSummary.hasNumberField) { + if (needsNumberField && !dataSummary.hasFieldType(FieldType.number)) { return t('panel.panel-data-error-view.missing-value.number', 'Data is missing a number field'); } - if (needsTimeField && !dataSummary.hasTimeField) { + if (needsTimeField && !dataSummary.hasFieldType(FieldType.time)) { return t('panel.panel-data-error-view.missing-value.time', 'Data is missing a time field'); } diff --git a/public/app/features/panel/suggestions/getAllSuggestions.test.ts b/public/app/features/panel/suggestions/getAllSuggestions.test.ts index e4a0c72c5d9..1cb10605ab0 100644 --- a/public/app/features/panel/suggestions/getAllSuggestions.test.ts +++ b/public/app/features/panel/suggestions/getAllSuggestions.test.ts @@ -5,12 +5,18 @@ import { LoadingState, PanelData, PanelPluginMeta, - toDataFrame, PanelPluginVisualizationSuggestion, + toDataFrame, } from '@grafana/data'; -import { GraphFieldConfig, ReduceDataOptions } from '@grafana/schema'; +import { + BarGaugeDisplayMode, + BigValueColorMode, + GraphFieldConfig, + ReduceDataOptions, + StackingMode, + VizOrientation, +} from '@grafana/schema'; import { config } from 'app/core/config'; -import { SuggestionName } from 'app/types/suggestions'; import { getAllSuggestions, panelsToCheckFirst } from './getAllSuggestions'; @@ -21,6 +27,8 @@ for (const pluginId of panelsToCheckFirst) { } as PanelPluginMeta; } +const SCALAR_PLUGINS = ['gauge', 'stat', 'bargauge', 'piechart', 'radialbar']; + config.panels['text'] = { id: 'text', name: 'Text', @@ -69,7 +77,10 @@ scenario('No series', (ctx) => { ctx.setData([]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([SuggestionName.Table, SuggestionName.TextPanel]); + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'table' }), + expect.objectContaining({ pluginId: 'text' }), + ]); }); }); @@ -84,7 +95,7 @@ scenario('No rows', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([SuggestionName.Table]); + expect(ctx.suggestions).toEqual([expect.objectContaining({ pluginId: 'table' })]); }); }); @@ -100,33 +111,46 @@ scenario('Single frame with time and number field', (ctx) => { it('should return correct suggestions', () => { expect(ctx.suggestions).toEqual([ - expect.objectContaining({ name: SuggestionName.LineChart }), - expect.objectContaining({ name: SuggestionName.LineChartSmooth }), - expect.objectContaining({ name: SuggestionName.AreaChart }), - expect.objectContaining({ name: SuggestionName.LineChartGradientColorScheme }), - expect.objectContaining({ name: SuggestionName.BarChart }), - expect.objectContaining({ name: SuggestionName.BarChartGradientColorScheme }), - expect.objectContaining({ name: SuggestionName.Gauge }), - expect.objectContaining({ name: SuggestionName.GaugeNoThresholds }), - expect.objectContaining({ name: SuggestionName.Stat }), - expect.objectContaining({ name: SuggestionName.StatColoredBackground }), - expect.objectContaining({ name: SuggestionName.BarGaugeBasic }), - expect.objectContaining({ name: SuggestionName.BarGaugeLCD }), - expect.objectContaining({ name: SuggestionName.Table }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart - smooth' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart' }), + expect.objectContaining({ pluginId: 'gauge' }), + expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }), + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ + pluginId: 'stat', + options: expect.objectContaining({ colorMode: BigValueColorMode.Background }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }), + }), + expect.objectContaining({ pluginId: 'table' }), expect.objectContaining({ pluginId: 'state-timeline' }), - expect.objectContaining({ name: SuggestionName.StatusHistory }), + expect.objectContaining({ pluginId: 'status-history' }), + expect.objectContaining({ pluginId: 'heatmap' }), + expect.objectContaining({ pluginId: 'histogram' }), ]); }); it('Bar chart suggestion should be using timeseries panel', () => { - expect(ctx.suggestions.find((x) => x.name === SuggestionName.BarChart)?.pluginId).toBe('timeseries'); + expect(ctx.suggestions.find((x) => x.name === 'Bar chart')?.pluginId).toBe('timeseries'); }); - it('Stat panels have reduce values disabled', () => { - for (const suggestion of ctx.suggestions) { - if (suggestion.options?.reduceOptions?.values) { - throw new Error(`Suggestion ${suggestion.name} reduce.values set to true when it should be false`); - } + it('Scalar panels should use calcs', () => { + for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) { + expect(suggestion).toEqual( + expect.objectContaining({ + options: expect.objectContaining({ + reduceOptions: expect.objectContaining({ values: false, calcs: ['lastNotNull'] }), + }), + }) + ); } }); }); @@ -144,31 +168,46 @@ scenario('Single frame with time 2 number fields', (ctx) => { it('should return correct suggestions', () => { expect(ctx.suggestions).toEqual([ - expect.objectContaining({ name: SuggestionName.LineChart }), - expect.objectContaining({ name: SuggestionName.LineChartSmooth }), - expect.objectContaining({ name: SuggestionName.AreaChartStacked }), - expect.objectContaining({ name: SuggestionName.AreaChartStackedPercent }), - expect.objectContaining({ name: SuggestionName.BarChartStacked }), - expect.objectContaining({ name: SuggestionName.BarChartStackedPercent }), - expect.objectContaining({ name: SuggestionName.Gauge }), - expect.objectContaining({ name: SuggestionName.GaugeNoThresholds }), - expect.objectContaining({ name: SuggestionName.Stat }), - expect.objectContaining({ name: SuggestionName.StatColoredBackground }), - expect.objectContaining({ name: SuggestionName.PieChart }), - expect.objectContaining({ name: SuggestionName.PieChartDonut }), - expect.objectContaining({ name: SuggestionName.BarGaugeBasic }), - expect.objectContaining({ name: SuggestionName.BarGaugeLCD }), - expect.objectContaining({ name: SuggestionName.Table }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Line chart - smooth' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart - stacked' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Area chart - stacked by percentage' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart - stacked' }), + expect.objectContaining({ pluginId: 'timeseries', name: 'Bar chart - stacked by percentage' }), + expect.objectContaining({ pluginId: 'gauge' }), + expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }), + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ + pluginId: 'stat', + options: expect.objectContaining({ colorMode: BigValueColorMode.Background }), + }), + expect.objectContaining({ pluginId: 'piechart' }), + expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }), + }), + expect.objectContaining({ pluginId: 'table' }), expect.objectContaining({ pluginId: 'state-timeline' }), - expect.objectContaining({ name: SuggestionName.StatusHistory }), + expect.objectContaining({ pluginId: 'status-history' }), + expect.objectContaining({ pluginId: 'heatmap' }), + expect.objectContaining({ pluginId: 'histogram' }), ]); }); - it('Stat panels have reduceOptions.values disabled', () => { - for (const suggestion of ctx.suggestions) { - if (suggestion.options?.reduceOptions?.values) { - throw new Error(`Suggestion ${suggestion.name} reduce.values set to true when it should be false`); - } + it('Scalar panels should use calcs', () => { + for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) { + expect(suggestion).toEqual( + expect.objectContaining({ + options: expect.objectContaining({ + reduceOptions: expect.objectContaining({ values: false, calcs: ['lastNotNull'] }), + }), + }) + ); } }); }); @@ -184,7 +223,7 @@ scenario('Single time series with 100 data points', (ctx) => { ]); it('should not suggest bar chart', () => { - expect(ctx.suggestions.find((x) => x.name === SuggestionName.BarChart)).toBe(undefined); + expect(ctx.suggestions.find((x) => x.name === 'Bar chart')).toBe(undefined); }); }); @@ -235,26 +274,42 @@ scenario('Single frame with string and number field', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([ - SuggestionName.BarChart, - SuggestionName.BarChartHorizontal, - SuggestionName.Gauge, - SuggestionName.GaugeNoThresholds, - SuggestionName.Stat, - SuggestionName.StatColoredBackground, - SuggestionName.PieChart, - SuggestionName.PieChartDonut, - SuggestionName.BarGaugeBasic, - SuggestionName.BarGaugeLCD, - SuggestionName.Table, + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'piechart' }), + expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }), + expect.objectContaining({ pluginId: 'barchart' }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ orientation: VizOrientation.Horizontal }), + }), + expect.objectContaining({ pluginId: 'gauge' }), + expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }), + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ + pluginId: 'stat', + options: expect.objectContaining({ colorMode: BigValueColorMode.Background }), + }), + + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }), + }), + expect.objectContaining({ pluginId: 'table' }), + expect.objectContaining({ pluginId: 'histogram' }), ]); }); - it('Stat/Gauge/BarGauge/PieChart panels to have reduceOptions.values enabled', () => { - for (const suggestion of ctx.suggestions) { - if (suggestion.options?.reduceOptions && !suggestion.options?.reduceOptions?.values) { - throw new Error(`Suggestion ${suggestion.name} reduce.values set to false when it should be true`); - } + it('Scalar panels should contain raw values', () => { + for (const suggestion of ctx.suggestions.filter((s) => SCALAR_PLUGINS.includes(s.pluginId))) { + expect(suggestion).toEqual( + expect.objectContaining({ + options: expect.objectContaining({ reduceOptions: expect.objectContaining({ values: true, calcs: [] }) }), + }) + ); } }); }); @@ -271,22 +326,48 @@ scenario('Single frame with string and 2 number field', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([ - SuggestionName.BarChart, - SuggestionName.BarChartStacked, - SuggestionName.BarChartStackedPercent, - SuggestionName.BarChartHorizontal, - SuggestionName.BarChartHorizontalStacked, - SuggestionName.BarChartHorizontalStackedPercent, - SuggestionName.Gauge, - SuggestionName.GaugeNoThresholds, - SuggestionName.Stat, - SuggestionName.StatColoredBackground, - SuggestionName.PieChart, - SuggestionName.PieChartDonut, - SuggestionName.BarGaugeBasic, - SuggestionName.BarGaugeLCD, - SuggestionName.Table, + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'barchart' }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ stacking: StackingMode.Normal }), + }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ stacking: StackingMode.Percent }), + }), + + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ orientation: VizOrientation.Horizontal }), + }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ orientation: VizOrientation.Horizontal, stacking: StackingMode.Normal }), + }), + expect.objectContaining({ + pluginId: 'barchart', + options: expect.objectContaining({ orientation: VizOrientation.Horizontal, stacking: StackingMode.Percent }), + }), + expect.objectContaining({ pluginId: 'gauge' }), + expect.objectContaining({ pluginId: 'gauge', options: expect.objectContaining({ showThresholdMarkers: false }) }), + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ + pluginId: 'stat', + options: expect.objectContaining({ colorMode: BigValueColorMode.Background }), + }), + expect.objectContaining({ pluginId: 'piechart' }), + expect.objectContaining({ pluginId: 'piechart', options: expect.objectContaining({ pieType: 'donut' }) }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Basic }), + }), + expect.objectContaining({ + pluginId: 'bargauge', + options: expect.objectContaining({ displayMode: BarGaugeDisplayMode.Lcd }), + }), + expect.objectContaining({ pluginId: 'table' }), + expect.objectContaining({ pluginId: 'histogram' }), ]); }); }); @@ -299,11 +380,14 @@ scenario('Single frame with only string field', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([SuggestionName.Stat, SuggestionName.Table]); + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'stat' }), + expect.objectContaining({ pluginId: 'table' }), + ]); }); it('Stat panels have reduceOptions.fields set to show all fields', () => { - for (const suggestion of ctx.suggestions) { + for (const suggestion of ctx.suggestions.filter((s) => s.pluginId === 'stat')) { if (suggestion.options?.reduceOptions) { expect(suggestion.options.reduceOptions.fields).toBe('/.*/'); } @@ -333,7 +417,10 @@ scenario('Given default loki logs data', (ctx) => { ]); it('should return correct suggestions', () => { - expect(ctx.names()).toEqual([SuggestionName.Logs, SuggestionName.Table]); + expect(ctx.suggestions).toEqual([ + expect.objectContaining({ pluginId: 'logs' }), + expect.objectContaining({ pluginId: 'table' }), + ]); }); }); @@ -356,7 +443,7 @@ scenario('Given a preferredVisualisationType', (ctx) => { ]); it('should return the preferred visualization first', () => { - expect(ctx.names()[0]).toEqual(SuggestionName.Table); + expect(ctx.suggestions[0]).toEqual(expect.objectContaining({ pluginId: 'table' })); }); }); diff --git a/public/app/features/panel/suggestions/getAllSuggestions.ts b/public/app/features/panel/suggestions/getAllSuggestions.ts index ff6fbef50e8..a7c9764d2a0 100644 --- a/public/app/features/panel/suggestions/getAllSuggestions.ts +++ b/public/app/features/panel/suggestions/getAllSuggestions.ts @@ -4,6 +4,7 @@ import { VisualizationSuggestionsBuilder, PanelModel, VisualizationSuggestionScore, + PreferredVisualisationType, } from '@grafana/data'; import { config } from '@grafana/runtime'; import { importPanelPlugin } from 'app/features/plugins/importPanelPlugin'; @@ -23,8 +24,26 @@ export const panelsToCheckFirst = [ 'flamegraph', 'traces', 'nodeGraph', + 'heatmap', + 'histogram', + 'geomap', ]; +/** + * some of the PreferredVisualisationTypes do not match the panel plugin ids, so we have to map them. d'oh. + */ +const PLUGIN_ID_TO_PREFERRED_VIZ_TYPE: Record = { + traces: 'trace', + timeseries: 'graph', + table: 'table', + logs: 'logs', + nodeGraph: 'nodeGraph', + flamegraph: 'flamegraph', +}; +const mapPreferredVisualisationTypeToPlugin = (type: string): PreferredVisualisationType | undefined => { + return PLUGIN_ID_TO_PREFERRED_VIZ_TYPE[type]; +}; + export async function getAllSuggestions( data?: PanelData, panel?: PanelModel @@ -61,13 +80,13 @@ export async function getAllSuggestions( } return list.sort((a, b) => { - if (builder.dataSummary.preferredVisualisationType) { - if (a.pluginId === builder.dataSummary.preferredVisualisationType) { - return -1; - } - if (b.pluginId === builder.dataSummary.preferredVisualisationType) { - return 1; - } + const mappedA = mapPreferredVisualisationTypeToPlugin(a.pluginId); + if (mappedA && builder.dataSummary.hasPreferredVisualisationType(mappedA)) { + return -1; + } + const mappedB = mapPreferredVisualisationTypeToPlugin(a.pluginId); + if (mappedB && builder.dataSummary.hasPreferredVisualisationType(mappedB)) { + return 1; } return (b.score ?? VisualizationSuggestionScore.OK) - (a.score ?? VisualizationSuggestionScore.OK); }); diff --git a/public/app/features/panel/suggestions/utils.ts b/public/app/features/panel/suggestions/utils.ts index ca5cc74bd77..07820587be5 100644 --- a/public/app/features/panel/suggestions/utils.ts +++ b/public/app/features/panel/suggestions/utils.ts @@ -1,4 +1,11 @@ -import { PanelData, PanelDataSummary } from '@grafana/data'; +import { + DataFrameType, + PanelData, + PanelDataSummary, + VisualizationSuggestion, + VisualizationSuggestionScore, +} from '@grafana/data'; +import { ReduceDataOptions } from '@grafana/schema'; /** * @internal @@ -10,6 +17,42 @@ export function showDefaultSuggestion(fn: (panelDataSummary: PanelDataSummary) = return (panelDataSummary: PanelDataSummary) => (fn(panelDataSummary) ? [{}] : undefined); } +/** + * @internal + * for panel plugins which render "scalar" data (stat, gauge, etc), this helper provides default reduce options + * depending on whether deaggregation is likely needed. + * @param suggestion the suggestion to modify + * @param panelDataSummary the panel data summary to use for scoring + * @param shouldUseRawValues if true, reduceOptions will be set to use raw values, + * otherwise a calcs will be used with the default value of `lastNotNull`. + */ +export function defaultNumericVizOptions( + suggestion: VisualizationSuggestion<{ reduceOptions?: ReduceDataOptions }>, + panelDataSummary: PanelDataSummary, + shouldUseRawValues: boolean +): VisualizationSuggestion { + suggestion.score = + (suggestion.score ?? + (panelDataSummary.hasDataFrameType(DataFrameType.NumericLong) || + panelDataSummary.hasDataFrameType(DataFrameType.NumericWide) || + panelDataSummary.hasDataFrameType(DataFrameType.NumericMulti))) + ? VisualizationSuggestionScore.Good + : VisualizationSuggestionScore.OK; + suggestion.options = suggestion.options ?? {}; + suggestion.options.reduceOptions = + suggestion.options.reduceOptions ?? + (shouldUseRawValues + ? { + values: true, + calcs: [], + } + : { + values: false, + calcs: ['lastNotNull'], + }); + return suggestion; +} + /** * @internal * Checks if the panel has data diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index d4284656595..28ba77d5476 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -18,7 +18,7 @@ import { BarChartPanel } from './BarChartPanel'; import { TickSpacingEditor } from './TickSpacingEditor'; import { changeToBarChartPanelMigrationHandler } from './migrations'; import { FieldConfig, Options, defaultFieldConfig, defaultOptions } from './panelcfg.gen'; -import { BarChartSuggestionsSupplier } from './suggestions'; +import { barchartSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(BarChartPanel) .setPanelChangeHandler(changeToBarChartPanelMigrationHandler) @@ -257,7 +257,7 @@ export const plugin = new PanelPlugin(BarChartPanel) commonOptionsBuilder.addLegendOptions(builder); commonOptionsBuilder.addTextSizeOptions(builder, { withValue: true }); }) - .setSuggestionsSupplier(new BarChartSuggestionsSupplier()); + .setSuggestionsSupplier(barchartSuggestionsSupplier); function countNumberFields(data?: DataFrame[]): number { let count = 0; diff --git a/public/app/plugins/panel/barchart/suggestions.ts b/public/app/plugins/panel/barchart/suggestions.ts index f9428d6e709..56918e4b346 100644 --- a/public/app/plugins/panel/barchart/suggestions.ts +++ b/public/app/plugins/panel/barchart/suggestions.ts @@ -1,99 +1,112 @@ -import { VisualizationSuggestionsBuilder, VizOrientation } from '@grafana/data'; +import { defaultsDeep } from 'lodash'; + +import { FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn, VizOrientation } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { LegendDisplayMode, StackingMode, VisibilityMode } from '@grafana/schema'; -import { SuggestionName } from 'app/types/suggestions'; import { FieldConfig, Options } from './panelcfg.gen'; -export class BarChartSuggestionsSupplier { - getListWithDefaults(builder: VisualizationSuggestionsBuilder) { - return builder.getListAppender({ - name: SuggestionName.BarChart, - pluginId: 'barchart', - options: { - showValue: VisibilityMode.Never, - legend: { - calcs: [], - displayMode: LegendDisplayMode.List, - showLegend: true, - placement: 'right', - }, +const withDefaults = (suggestion: VisualizationSuggestion) => + defaultsDeep(suggestion, { + options: { + showValue: VisibilityMode.Never, + legend: { + calcs: [], + displayMode: LegendDisplayMode.List, + showLegend: true, + placement: 'right', }, - fieldConfig: { - defaults: { - unit: 'short', - custom: {}, - }, - overrides: [], + }, + fieldConfig: { + defaults: { + unit: 'short', + custom: {}, }, - cardOptions: { - previewModifier: (s) => { - s.options!.barWidth = 0.8; - }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + s.options!.barWidth = 0.8; + s.fieldConfig!.defaults!.custom!.hideFrom = { tooltip: false, legend: true, viz: false }; // hide legend in preview }, - }); + }, + } satisfies VisualizationSuggestion); + +export const barchartSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (dataSummary.frameCount !== 1) { + return; } - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = this.getListWithDefaults(builder); - const { dataSummary } = builder; + if (!dataSummary.hasFieldType(FieldType.number) || !dataSummary.hasFieldType(FieldType.string)) { + return; + } - if (dataSummary.frameCount !== 1) { - return; - } + // if you have this many rows barchart might not be a good fit + if (dataSummary.rowCountTotal > 50) { + return; + } - if (!dataSummary.hasNumberField || !dataSummary.hasStringField) { - return; - } + const result: Array> = [ + { + name: t('barchart.suggestions.vertical', 'Bar chart'), + }, + ]; - // if you have this many rows barchart might not be a good fit - if (dataSummary.rowCountTotal > 50) { - return; - } - - // Vertical bars - list.append({ - name: SuggestionName.BarChart, - }); - - if (dataSummary.numberFieldCount > 1) { - list.append({ - name: SuggestionName.BarChartStacked, + if (dataSummary.fieldCountByType(FieldType.number) > 1) { + result.push( + { + name: t('barchart.suggestions.vert-stacked', 'Bar chart - stacked'), options: { stacking: StackingMode.Normal, }, - }); - list.append({ - name: SuggestionName.BarChartStackedPercent, + }, + { + name: t('barchart.suggestions.vert-stacked-percent', 'Bar chart - stacked by percentage'), options: { stacking: StackingMode.Percent, }, - }); - } - - // horizontal bars - list.append({ - name: SuggestionName.BarChartHorizontal, - options: { - orientation: VizOrientation.Horizontal, - }, - }); - - if (dataSummary.numberFieldCount > 1) { - list.append({ - name: SuggestionName.BarChartHorizontalStacked, - options: { - stacking: StackingMode.Normal, - orientation: VizOrientation.Horizontal, + fieldConfig: { + overrides: [], + defaults: { + unit: 'percentunit', + }, }, - }); + } + ); + } - list.append({ - name: SuggestionName.BarChartHorizontalStackedPercent, + // horizontal bars + result.push({ + name: t('barchart.suggestions.horizontal', 'Horizontal bar chart'), + options: { + orientation: VizOrientation.Horizontal, + }, + }); + + if (dataSummary.fieldCountByType(FieldType.number) > 1) { + result.push( + { + name: t('barchart.suggestions.hz-stacked', 'Horizontal bar chart - stacked'), + options: { + orientation: VizOrientation.Horizontal, + stacking: StackingMode.Normal, + }, + }, + { + name: t('barchart.suggestions.hz-stacked-percent', 'Horizontal bar chart - stacked by percentage'), options: { orientation: VizOrientation.Horizontal, stacking: StackingMode.Percent, }, - }); - } + fieldConfig: { + overrides: [], + defaults: { + unit: 'percentunit', + }, + }, + } + ); } -} + + return result.map(withDefaults); +}; diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index 3e0262fb237..8e7fd5ccae6 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/public/app/plugins/panel/bargauge/module.tsx @@ -8,7 +8,7 @@ import { addOrientationOption, addStandardDataReduceOptions } from '../stat/comm import { barGaugePanelMigrationHandler } from './BarGaugeMigrations'; import { BarGaugePanel } from './BarGaugePanel'; import { Options, defaultOptions } from './panelcfg.gen'; -import { BarGaugeSuggestionsSupplier } from './suggestions'; +import { barGaugeSugggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(BarGaugePanel) .useFieldConfig() @@ -151,4 +151,4 @@ export const plugin = new PanelPlugin(BarGaugePanel) }) .setPanelChangeHandler(sharedSingleStatPanelChangedHandler) .setMigrationHandler(barGaugePanelMigrationHandler) - .setSuggestionsSupplier(new BarGaugeSuggestionsSupplier()); + .setSuggestionsSupplier(barGaugeSugggestionsSupplier); diff --git a/public/app/plugins/panel/bargauge/suggestions.ts b/public/app/plugins/panel/bargauge/suggestions.ts index 61d48ce104a..dc8a451cac0 100644 --- a/public/app/plugins/panel/bargauge/suggestions.ts +++ b/public/app/plugins/panel/bargauge/suggestions.ts @@ -1,115 +1,60 @@ -import { FieldColorModeId, VisualizationSuggestionsBuilder, VizOrientation } from '@grafana/data'; +import { defaultsDeep } from 'lodash'; + +import { + FieldColorModeId, + FieldType, + VisualizationSuggestion, + VisualizationSuggestionsSupplierFn, + VizOrientation, +} from '@grafana/data'; +import { t } from '@grafana/i18n'; import { BarGaugeDisplayMode } from '@grafana/ui'; -import { SuggestionName } from 'app/types/suggestions'; +import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils'; import { Options } from './panelcfg.gen'; -export class BarGaugeSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if (!dataSummary.hasData || !dataSummary.hasNumberField) { - return; - } - - const list = builder.getListAppender({ - name: '', - pluginId: 'bargauge', - options: {}, - fieldConfig: { - defaults: { - custom: {}, +const withDefaults = (suggestion: VisualizationSuggestion): VisualizationSuggestion => + defaultsDeep(suggestion, { + options: { + displayMode: BarGaugeDisplayMode.Basic, + orientation: VizOrientation.Horizontal, + }, + fieldConfig: { + defaults: { + color: { + mode: FieldColorModeId.ContinuousGrYlRd, }, - overrides: [], }, - }); + overrides: [], + }, + }); - // This is probably not a good option for many numeric fields - if (dataSummary.numberFieldCount > 50) { - return; - } +const BAR_LIMIT = 30; - // To use show individual row values we also need a string field to give each value a name - if (dataSummary.hasStringField && dataSummary.frameCount === 1 && dataSummary.rowCountTotal < 30) { - list.append({ - name: SuggestionName.BarGaugeBasic, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - displayMode: BarGaugeDisplayMode.Basic, - orientation: VizOrientation.Horizontal, - }, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - }, - overrides: [], - }, - }); - - list.append({ - name: SuggestionName.BarGaugeLCD, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - displayMode: BarGaugeDisplayMode.Lcd, - orientation: VizOrientation.Horizontal, - }, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - }, - overrides: [], - }, - }); - } else { - list.append({ - name: SuggestionName.BarGaugeBasic, - options: { - displayMode: BarGaugeDisplayMode.Basic, - orientation: VizOrientation.Horizontal, - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - }, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - }, - overrides: [], - }, - }); - - list.append({ - name: SuggestionName.BarGaugeLCD, - options: { - displayMode: BarGaugeDisplayMode.Lcd, - orientation: VizOrientation.Horizontal, - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - }, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - }, - overrides: [], - }, - }); - } +export const barGaugeSugggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) { + return; } -} + + // This is probably not a good option for many numeric fields + if (dataSummary.fieldCountByType(FieldType.number) > BAR_LIMIT) { + return; + } + + const suggestions: Array> = [ + { name: t('bargauge.suggestions.basic', 'Bar gauge') }, + { + name: t('bargauge.suggestions.lcd', 'Bar gauge - LCD'), + options: { + displayMode: BarGaugeDisplayMode.Lcd, + }, + }, + ]; + + const shouldUseRawValues = + dataSummary.hasFieldType(FieldType.string) && + dataSummary.frameCount === 1 && + dataSummary.rowCountTotal <= BAR_LIMIT; + + return suggestions.map((s) => defaultNumericVizOptions(withDefaults(s), dataSummary, shouldUseRawValues)); +}; diff --git a/public/app/plugins/panel/candlestick/module.tsx b/public/app/plugins/panel/candlestick/module.tsx index 8a57e4801c1..ae43037e1b1 100644 --- a/public/app/plugins/panel/candlestick/module.tsx +++ b/public/app/plugins/panel/candlestick/module.tsx @@ -8,7 +8,7 @@ import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config'; import { CandlestickPanel } from './CandlestickPanel'; import { CandlestickData, getCandlestickFieldsInfo, FieldPickerInfo, prepareCandlestickFields } from './fields'; -import { CandlestickSuggestionsSupplier } from './suggestions'; +import { candlestickSuggestionSupplier } from './suggestions'; import { defaultCandlestickColors, defaultOptions, Options, VizDisplayMode, ColorStrategy, CandleStyle } from './types'; const numericFieldFilter = (f: Field) => f.type === FieldType.number; @@ -147,4 +147,4 @@ export const plugin = new PanelPlugin(CandlestickPane commonOptionsBuilder.addLegendOptions(builder); }) .setDataSupport({ annotations: true, alertStates: true }) - .setSuggestionsSupplier(new CandlestickSuggestionsSupplier()); + .setSuggestionsSupplier(candlestickSuggestionSupplier); diff --git a/public/app/plugins/panel/candlestick/suggestions.ts b/public/app/plugins/panel/candlestick/suggestions.ts index ce22cc276c5..81834af9db5 100644 --- a/public/app/plugins/panel/candlestick/suggestions.ts +++ b/public/app/plugins/panel/candlestick/suggestions.ts @@ -1,54 +1,29 @@ -import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; +import { FieldType, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { SuggestionName } from 'app/types/suggestions'; import { prepareCandlestickFields } from './fields'; import { defaultOptions, Options } from './types'; -export class CandlestickSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if ( - !builder.data?.series || - !dataSummary.hasData || - dataSummary.timeFieldCount < 1 || - dataSummary.numberFieldCount < 2 || - dataSummary.numberFieldCount > 10 - ) { - return; - } - - const info = prepareCandlestickFields(builder.data.series, defaultOptions, config.theme2); - if (!info) { - return; - } - - // Regular timeseries - if (info.open === info.high && info.open === info.low) { - return; - } - - const list = builder.getListAppender({ - name: '', - pluginId: 'candlestick', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - }); - - list.append({ - name: SuggestionName.Candlestick, - options: defaultOptions, - fieldConfig: { - defaults: {}, - overrides: [], - }, - score: info.autoOpenClose ? VisualizationSuggestionScore.Good : VisualizationSuggestionScore.Best, - }); +export const candlestickSuggestionSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if ( + !dataSummary.rawFrames || + !dataSummary.hasData || + dataSummary.fieldCountByType(FieldType.time) < 1 || + dataSummary.fieldCountByType(FieldType.number) < 2 || + dataSummary.fieldCountByType(FieldType.number) > 10 + ) { + return; } -} + + const info = prepareCandlestickFields(dataSummary.rawFrames, defaultOptions, config.theme2); + if (!info) { + return; + } + + // Regular timeseries + if (info.open === info.high && info.open === info.low) { + return; + } + + return [{ score: info.autoOpenClose ? VisualizationSuggestionScore.Good : VisualizationSuggestionScore.Best }]; +}; diff --git a/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx b/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx index e9e8d9c8368..54a68a78af0 100644 --- a/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx +++ b/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx @@ -2,6 +2,8 @@ import { CoreApp, PanelProps } from '@grafana/data'; import { FlameGraph, checkFields, getMessageCheckFieldsResult } from '@grafana/flamegraph'; import { PanelDataErrorView, reportInteraction, config } from '@grafana/runtime'; +import { Options } from './types'; + function interaction(name: string, context: Record = {}) { reportInteraction(`grafana_flamegraph_${name}`, { app: CoreApp.Unknown, @@ -10,7 +12,7 @@ function interaction(name: string, context: Record = {} }); } -export const FlameGraphPanel = (props: PanelProps) => { +export const FlameGraphPanel = (props: PanelProps) => { const wrongFields = checkFields(props.data.series[0]); if (wrongFields) { return ( diff --git a/public/app/plugins/panel/flamegraph/module.tsx b/public/app/plugins/panel/flamegraph/module.tsx index 19fdd0afcfc..a80fcea3eee 100644 --- a/public/app/plugins/panel/flamegraph/module.tsx +++ b/public/app/plugins/panel/flamegraph/module.tsx @@ -1,12 +1,29 @@ import { FieldConfigProperty, PanelPlugin } from '@grafana/data'; +import { checkFields } from '@grafana/flamegraph'; import { FlameGraphPanel } from './FlameGraphPanel'; -import { FlameGraphSuggestionsSupplier } from './suggestions'; +import { Options } from './types'; const flamegraphConfigOptions = [FieldConfigProperty.Unit, FieldConfigProperty.Decimals]; -export const plugin = new PanelPlugin(FlameGraphPanel) - .setSuggestionsSupplier(new FlameGraphSuggestionsSupplier()) +export const plugin = new PanelPlugin(FlameGraphPanel) + // check that the first frame of the data has the required fields for a flamegraph + .setSuggestionsSupplier((ds) => { + if (!ds.rawFrames?.some((frame) => checkFields(frame) === undefined)) { + return; + } + + return [ + { + cardOptions: { + previewModifier: (s) => { + s.options = s.options || {}; + s.options.showFlameGraphOnly = true; + }, + }, + }, + ]; + }) .useFieldConfig({ disableStandardOptions: Object.values(FieldConfigProperty).filter((v) => !flamegraphConfigOptions.includes(v)), }); diff --git a/public/app/plugins/panel/flamegraph/suggestions.ts b/public/app/plugins/panel/flamegraph/suggestions.ts deleted file mode 100644 index eb997ee8078..00000000000 --- a/public/app/plugins/panel/flamegraph/suggestions.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { checkFields } from '@grafana/flamegraph'; -import { SuggestionName } from 'app/types/suggestions'; - -export class FlameGraphSuggestionsSupplier { - getListWithDefaults(builder: VisualizationSuggestionsBuilder) { - return builder.getListAppender<{}, {}>({ - name: SuggestionName.FlameGraph, - pluginId: 'flamegraph', - }); - } - - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - if (!builder.data) { - return; - } - - const dataFrame = builder.data.series[0]; - if (!dataFrame) { - return; - } - const wrongFields = checkFields(dataFrame); - if (wrongFields) { - return; - } - - this.getListWithDefaults(builder).append({ - name: SuggestionName.FlameGraph, - }); - } -} diff --git a/public/app/plugins/panel/flamegraph/types.ts b/public/app/plugins/panel/flamegraph/types.ts new file mode 100644 index 00000000000..8e5544c54a2 --- /dev/null +++ b/public/app/plugins/panel/flamegraph/types.ts @@ -0,0 +1,3 @@ +export interface Options { + showFlameGraphOnly?: boolean; +} diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 690698534a1..61ea853e943 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -8,7 +8,7 @@ import { addOrientationOption, addStandardDataReduceOptions } from '../stat/comm import { gaugePanelMigrationHandler, gaugePanelChangedHandler } from './GaugeMigrations'; import { GaugePanel } from './GaugePanel'; import { Options, defaultOptions } from './panelcfg.gen'; -import { GaugeSuggestionsSupplier } from './suggestions'; +import { gaugeSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(GaugePanel) .useFieldConfig({ @@ -88,5 +88,5 @@ export const plugin = new PanelPlugin(GaugePanel) commonOptionsBuilder.addTextSizeOptions(builder, { withTitle: true, withValue: true }); }) .setPanelChangeHandler(gaugePanelChangedHandler) - .setSuggestionsSupplier(new GaugeSuggestionsSupplier()) + .setSuggestionsSupplier(gaugeSuggestionsSupplier) .setMigrationHandler(gaugePanelMigrationHandler); diff --git a/public/app/plugins/panel/gauge/suggestions.ts b/public/app/plugins/panel/gauge/suggestions.ts index 5f5ebc8f40e..6695741b093 100644 --- a/public/app/plugins/panel/gauge/suggestions.ts +++ b/public/app/plugins/panel/gauge/suggestions.ts @@ -1,89 +1,63 @@ -import { ThresholdsMode, VisualizationSuggestionsBuilder } from '@grafana/data'; -import { GraphFieldConfig } from '@grafana/ui'; -import { SuggestionName } from 'app/types/suggestions'; +import { defaultsDeep } from 'lodash'; + +import { ThresholdsMode, FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils'; import { Options } from './panelcfg.gen'; -export class GaugeSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if (!dataSummary.hasData || !dataSummary.hasNumberField) { - return; - } - - // for many fields / series this is probably not a good fit - if (dataSummary.numberFieldCount >= 50) { - return; - } - - const list = builder.getListAppender({ - name: SuggestionName.Gauge, - pluginId: 'gauge', - options: {}, - fieldConfig: { - defaults: { - thresholds: { - steps: [ - { value: -Infinity, color: 'green' }, - { value: 70, color: 'orange' }, - { value: 85, color: 'red' }, - ], - mode: ThresholdsMode.Percentage, - }, - custom: {}, +const withDefaults = (suggestion: VisualizationSuggestion): VisualizationSuggestion => + defaultsDeep(suggestion, { + fieldConfig: { + defaults: { + thresholds: { + steps: [ + { value: -Infinity, color: 'green' }, + { value: 70, color: 'orange' }, + { value: 85, color: 'red' }, + ], + mode: ThresholdsMode.Percentage, }, - overrides: [], + custom: {}, }, - cardOptions: { - previewModifier: (s) => { - if (s.options?.reduceOptions?.values) { - s.options.reduceOptions.limit = 2; - } - }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + if (s.options?.reduceOptions?.values) { + s.options.reduceOptions.limit = 2; + } }, - }); + }, + } satisfies VisualizationSuggestion); - if (dataSummary.hasStringField && dataSummary.frameCount === 1 && dataSummary.rowCountTotal < 10) { - list.append({ - name: SuggestionName.Gauge, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - }, - }); - list.append({ - name: SuggestionName.GaugeNoThresholds, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - showThresholdMarkers: false, - }, - }); - } else { - list.append({ - name: SuggestionName.Gauge, - options: { - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - }, - }); - list.append({ - name: SuggestionName.GaugeNoThresholds, - options: { - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - showThresholdMarkers: false, - }, - }); - } +const GAUGE_LIMIT = 10; + +export const gaugeSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) { + return; } -} + + // for many fields / series this is probably not a good fit + if (dataSummary.fieldCountByType(FieldType.number) > GAUGE_LIMIT) { + return; + } + + const suggestions: Array> = [ + { name: t('gauge.suggestions.arc', 'Gauge') }, + { + name: t('gauge.suggestions.no-thresholds', 'Gauge - no thresholds'), + options: { + showThresholdMarkers: false, + }, + }, + ]; + + // sometimes, we want to de-aggregate the data for the gauge suggestion + const shouldUseRawValues = + dataSummary.hasFieldType(FieldType.string) && + dataSummary.frameCount === 1 && + dataSummary.rowCountTotal <= GAUGE_LIMIT; + + return suggestions.map((s) => defaultNumericVizOptions(withDefaults(s), dataSummary, shouldUseRawValues)); +}; diff --git a/public/app/plugins/panel/geomap/module.tsx b/public/app/plugins/panel/geomap/module.tsx index 7573f9317aa..a88a3bfbe02 100644 --- a/public/app/plugins/panel/geomap/module.tsx +++ b/public/app/plugins/panel/geomap/module.tsx @@ -8,6 +8,7 @@ import { LayersEditor } from './editor/LayersEditor'; import { MapViewEditor } from './editor/MapViewEditor'; import { getLayerEditor } from './editor/layerEditor'; import { mapPanelChangedHandler, mapMigrationHandler } from './migrations'; +import { geomapSuggestionsSupplier } from './suggestions'; import { defaultMapViewConfig, Options, TooltipMode, GeomapInstanceState } from './types'; export const plugin = new PanelPlugin(GeomapPanel) @@ -171,4 +172,5 @@ export const plugin = new PanelPlugin(GeomapPanel) ], }, }); - }); + }) + .setSuggestionsSupplier(geomapSuggestionsSupplier); diff --git a/public/app/plugins/panel/geomap/suggestions.ts b/public/app/plugins/panel/geomap/suggestions.ts new file mode 100644 index 00000000000..97fe03a7739 --- /dev/null +++ b/public/app/plugins/panel/geomap/suggestions.ts @@ -0,0 +1,46 @@ +import { VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data'; +import { GraphFieldConfig } from '@grafana/ui'; +import { getGeometryField, getDefaultLocationMatchers } from 'app/features/geo/utils/location'; + +import { Options } from './panelcfg.gen'; + +export const geomapSuggestionsSupplier: VisualizationSuggestionsSupplierFn = ( + dataSummary +) => { + if (!dataSummary.hasData || !dataSummary.rawFrames) { + return; + } + + // use getGeometryField to see if any frames have geolocation info + const location = getDefaultLocationMatchers(); + if (!dataSummary.rawFrames.some((frame) => !getGeometryField(frame, location).warning)) { + return; + } + + return [ + { + score: VisualizationSuggestionScore.Best, + fieldConfig: { + defaults: { + custom: {}, + }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + s.options!.controls = { + showZoom: false, + showScale: false, + showAttribution: false, + showMeasure: false, + }; + // FIXME: this doesn't work. I want to disable legends in the preview. + s.options?.layers?.forEach((layer) => { + layer.config = layer.config || {}; + layer.config.showLegend = false; + }); + }, + }, + }, + ]; +}; diff --git a/public/app/plugins/panel/heatmap/module.tsx b/public/app/plugins/panel/heatmap/module.tsx index 897fdbda461..a7d44726886 100644 --- a/public/app/plugins/panel/heatmap/module.tsx +++ b/public/app/plugins/panel/heatmap/module.tsx @@ -18,7 +18,7 @@ import { HeatmapPanel } from './HeatmapPanel'; import { prepareHeatmapData } from './fields'; import { heatmapChangedHandler, heatmapMigrationHandler } from './migrations'; import { colorSchemes, quantizeScheme } from './palettes'; -import { HeatmapSuggestionsSupplier } from './suggestions'; +import { heatmapSuggestionsSupplier } from './suggestions'; import { Options, defaultOptions, HeatmapColorMode, HeatmapColorScale } from './types'; export const plugin = new PanelPlugin(HeatmapPanel) @@ -472,5 +472,5 @@ export const plugin = new PanelPlugin(HeatmapPanel) annotations?.some((df) => df.meta?.custom?.resultType === 'exemplar'), }); }) - .setSuggestionsSupplier(new HeatmapSuggestionsSupplier()) + .setSuggestionsSupplier(heatmapSuggestionsSupplier) .setDataSupport({ annotations: true }); diff --git a/public/app/plugins/panel/heatmap/suggestions.test.ts b/public/app/plugins/panel/heatmap/suggestions.test.ts new file mode 100644 index 00000000000..ef3e73bc0aa --- /dev/null +++ b/public/app/plugins/panel/heatmap/suggestions.test.ts @@ -0,0 +1,226 @@ +import { + createDataFrame, + DataFrameType, + FieldType, + getPanelDataSummary, + VisualizationSuggestionScore, +} from '@grafana/data'; + +import { heatmapSuggestionsSupplier } from './suggestions'; + +describe('heatmap suggestions', () => { + describe('applicability', () => { + it('should not suggest for data without time field', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { name: 'value1', type: FieldType.number, values: [1, 2, 3] }, + { name: 'value2', type: FieldType.number, values: [4, 5, 6] }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toBeUndefined(); + }); + + it('should suggest for data with time and number fields', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [1609459200000, 1609462800000, 1609466400000] }, + { name: 'value1', type: FieldType.number, values: [1, 2, 3] }, + { name: 'value2', type: FieldType.number, values: [4, 5, 6] }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toHaveLength(1); + }); + }); + + describe('scoring', () => { + it('should score this as "OK" if the data is not particularly heatmap-y', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [1609459200000, 1609462800000, 1609466400000] }, + { name: 'value1', type: FieldType.number, values: [1, 2, 3] }, + { name: 'value2', type: FieldType.number, values: [4, 5, 6] }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.OK })]); + }); + + it.each([DataFrameType.HeatmapRows, DataFrameType.HeatmapCells])( + 'should score this as "Best" if the data explicitly has %s frame type', + (frameType: DataFrameType) => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + meta: { type: frameType }, + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609462800000, 1609466400000], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value1', + type: FieldType.number, + values: [1, 2, 3], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value2', + type: FieldType.number, + values: [4, 5, 6], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value3', + type: FieldType.number, + values: [7, 8, 9], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.Best })]); + } + ); + + it('should score this as "Best" if the data has "ge" labels', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609462800000, 1609466400000], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value1', + type: FieldType.number, + values: [1, 2, 3], + labels: { ge: '-Inf' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value2', + type: FieldType.number, + values: [4, 5, 6], + labels: { ge: '0' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value3', + type: FieldType.number, + values: [7, 8, 9], + labels: { ge: '10' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.Best })]); + }); + + it('should score this as "Best" if the data has "le" labels', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609462800000, 1609466400000], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value1', + type: FieldType.number, + values: [1, 2, 3], + labels: { le: '1' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value2', + type: FieldType.number, + values: [4, 5, 6], + labels: { le: '2' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value2', + type: FieldType.number, + values: [6, 2, 6], + labels: { le: '4' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'value3', + type: FieldType.number, + values: [7, 8, 9], + labels: { le: 'Inf' }, + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.Best })]); + }); + + it('should score this as "Best" if the field names are numeric in a way that makes sense', () => { + const dataSummary = getPanelDataSummary([ + createDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1609459200000, 1609462800000, 1609466400000], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: '0', + type: FieldType.number, + values: [1, 2, 3], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: '10', + type: FieldType.number, + values: [4, 5, 6], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: '20', + type: FieldType.number, + values: [7, 8, 9], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + { + name: 'Inf', + type: FieldType.number, + values: [10, 9, 8], + display: jest.fn((v) => ({ text: '' + v, numeric: Number(v) })), + }, + ], + }), + ]); + + const suggestions = heatmapSuggestionsSupplier(dataSummary); + expect(suggestions).toEqual([expect.objectContaining({ score: VisualizationSuggestionScore.Best })]); + }); + }); +}); diff --git a/public/app/plugins/panel/heatmap/suggestions.ts b/public/app/plugins/panel/heatmap/suggestions.ts index f49de761e85..670f76cda63 100644 --- a/public/app/plugins/panel/heatmap/suggestions.ts +++ b/public/app/plugins/panel/heatmap/suggestions.ts @@ -1,45 +1,73 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; +import { + DataFrameType, + FieldType, + PanelDataSummary, + VisualizationSuggestionScore, + VisualizationSuggestionsSupplierFn, +} from '@grafana/data'; import { config } from '@grafana/runtime'; +import { GraphFieldConfig } from '@grafana/schema'; import { prepareHeatmapData } from './fields'; import { quantizeScheme } from './palettes'; import { Options, defaultOptions } from './types'; -export class HeatmapSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if ( - !builder.data?.series || - !dataSummary.hasData || - dataSummary.timeFieldCount < 1 || - dataSummary.numberFieldCount < 2 || - dataSummary.numberFieldCount > 10 - ) { - return; - } - - const palette = quantizeScheme(defaultOptions.color, config.theme2); - const info = prepareHeatmapData({ - frames: builder.data.series, - options: defaultOptions, - palette, - theme: config.theme2, - }); - if (!info || info.warning) { - return; - } - - builder.getListAppender({ - name: '', - pluginId: 'heatmap', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - }); +function determineScore(dataSummary: PanelDataSummary): VisualizationSuggestionScore { + // look to see if the data has an explicity marker for heatmap data on it. + if ([DataFrameType.HeatmapRows, DataFrameType.HeatmapCells].some((t) => dataSummary.hasDataFrameType(t))) { + return VisualizationSuggestionScore.Best; } + + // we'll also look more closely at frames which return between 3 and 10 numeric fields. + if (dataSummary.fieldCountByType(FieldType.number) > 2 || dataSummary.fieldCountByType(FieldType.number) <= 10) { + // look through the names of the panels + const hasPotentialHeatmapSeries = dataSummary.rawFrames!.some((frame) => { + for (const field of frame.fields) { + if (field.type === FieldType.number) { + // if the field name, or "ge" or "le" label on the field, are numeric, then it's very possibly part of a heatmap. + if ([field.name, field.labels?.ge, field.labels?.le].some((v) => !isNaN(Number(v)))) { + return true; + } + } + } + return false; + }); + + // if at least all but 1 of the numeric fields in the frame have numeric names, then this is probably a heatmap. + // (the out-of-place one would be "Inf" or "-Inf") + if (hasPotentialHeatmapSeries) { + return VisualizationSuggestionScore.Best; + } + } + + return VisualizationSuggestionScore.OK; } + +export const heatmapSuggestionsSupplier: VisualizationSuggestionsSupplierFn = ( + dataSummary: PanelDataSummary +) => { + if ( + !dataSummary.rawFrames || + !dataSummary.hasData || + !dataSummary.hasFieldType(FieldType.time) || + !dataSummary.hasFieldType(FieldType.number) + ) { + return; + } + + // parse the frame into a heatmap structure to see if it's possible. + const palette = quantizeScheme(defaultOptions.color, config.theme2); + const info = prepareHeatmapData({ + frames: dataSummary.rawFrames, + options: defaultOptions, + palette, + theme: config.theme2, + }); + + // if we can't parse the data into a heatmap, then bail out and prevent showing suggestions. + if (!info || info.warning) { + return; + } + + return [{ score: determineScore(dataSummary) }]; +}; diff --git a/public/app/plugins/panel/histogram/module.tsx b/public/app/plugins/panel/histogram/module.tsx index 4ee2fa97a09..8855287cc93 100644 --- a/public/app/plugins/panel/histogram/module.tsx +++ b/public/app/plugins/panel/histogram/module.tsx @@ -5,6 +5,9 @@ import { identityOverrideProcessor, PanelPlugin, histogramFieldInfo, + buildHistogram, + VisualizationSuggestionScore, + DataFrameType, } from '@grafana/data'; import { t } from '@grafana/i18n'; import { commonOptionsBuilder, getGraphFieldOptions } from '@grafana/ui'; @@ -149,4 +152,16 @@ export const plugin = new PanelPlugin(HistogramPanel) commonOptionsBuilder.addHideFrom(builder); }, + }) + .setSuggestionsSupplier((ds) => { + if (ds.rawFrames && ds.hasData && buildHistogram(ds.rawFrames)) { + return [ + { + score: ds.hasDataFrameType(DataFrameType.Histogram) + ? VisualizationSuggestionScore.Best + : VisualizationSuggestionScore.OK, + }, + ]; + } + return; }); diff --git a/public/app/plugins/panel/logs/module.tsx b/public/app/plugins/panel/logs/module.tsx index 04ed5e92183..e51ff946902 100644 --- a/public/app/plugins/panel/logs/module.tsx +++ b/public/app/plugins/panel/logs/module.tsx @@ -1,10 +1,10 @@ -import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription } from '@grafana/data'; +import { PanelPlugin, LogsSortOrder, LogsDedupStrategy, LogsDedupDescription, FieldType } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; +import { showDefaultSuggestion } from 'app/features/panel/suggestions/utils'; import { LogsPanel } from './LogsPanel'; import { Options } from './panelcfg.gen'; -import { LogsPanelSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(LogsPanel) .setPanelOptions((builder, context) => { @@ -207,4 +207,6 @@ export const plugin = new PanelPlugin(LogsPanel) defaultValue: LogsSortOrder.Descending, }); }) - .setSuggestionsSupplier(new LogsPanelSuggestionsSupplier()); + .setSuggestionsSupplier( + showDefaultSuggestion((ds) => ds.hasData && ds.hasFieldType(FieldType.time) && ds.hasFieldType(FieldType.string)) + ); diff --git a/public/app/plugins/panel/logs/suggestions.ts b/public/app/plugins/panel/logs/suggestions.ts deleted file mode 100644 index 79b804cb8d7..00000000000 --- a/public/app/plugins/panel/logs/suggestions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; - -import { Options } from './panelcfg.gen'; - -export class LogsPanelSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = builder.getListAppender({ - name: '', - pluginId: 'logs', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - }); - - const { dataSummary: ds } = builder; - - // Require a string & time field - if (!ds.hasData || !ds.hasTimeField || !ds.hasStringField) { - return; - } - - if (ds.preferredVisualisationType === 'logs') { - list.append({ name: SuggestionName.Logs, score: VisualizationSuggestionScore.Best }); - } else { - list.append({ name: SuggestionName.Logs }); - } - } -} diff --git a/public/app/plugins/panel/nodeGraph/module.tsx b/public/app/plugins/panel/nodeGraph/module.tsx index 9bfa3456fc4..4230b30a9a1 100644 --- a/public/app/plugins/panel/nodeGraph/module.tsx +++ b/public/app/plugins/panel/nodeGraph/module.tsx @@ -4,7 +4,7 @@ import { t } from '@grafana/i18n'; import { NodeGraphPanel } from './NodeGraphPanel'; import { ArcOptionsEditor } from './editor/ArcOptionsEditor'; import { LayoutAlgorithm, Options as NodeGraphOptions } from './panelcfg.gen'; -import { NodeGraphSuggestionsSupplier } from './suggestions'; +import { nodeGraphSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(NodeGraphPanel) .useFieldConfig({ @@ -92,4 +92,4 @@ export const plugin = new PanelPlugin(NodeGraphPanel) }, }); }) - .setSuggestionsSupplier(new NodeGraphSuggestionsSupplier()); + .setSuggestionsSupplier(nodeGraphSuggestionsSupplier); diff --git a/public/app/plugins/panel/nodeGraph/suggestions.ts b/public/app/plugins/panel/nodeGraph/suggestions.ts index eb4a4773ef4..d1e5e1e29b4 100644 --- a/public/app/plugins/panel/nodeGraph/suggestions.ts +++ b/public/app/plugins/panel/nodeGraph/suggestions.ts @@ -1,71 +1,60 @@ -import { DataFrame, FieldType, VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; +import { DataFrame, FieldType, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data'; -export class NodeGraphSuggestionsSupplier { - getListWithDefaults(builder: VisualizationSuggestionsBuilder) { - return builder.getListAppender<{}, {}>({ - name: SuggestionName.NodeGraph, - pluginId: 'nodeGraph', - }); - } +import { Options } from './panelcfg.gen'; - hasCorrectFields(frames: DataFrame[]): boolean { - let hasNodesFrame = false; - let hasEdgesFrame = false; +function checkFields(fields: Array<[string, FieldType]>, frame: DataFrame): boolean { + let hasCorrectFields = true; - const nodeFields: Array<[string, FieldType]> = [ - ['id', FieldType.string], - ['title', FieldType.string], - ['mainstat', FieldType.number], - ]; - const edgeFields: Array<[string, FieldType]> = [ - ['id', FieldType.string], - ['source', FieldType.string], - ['target', FieldType.string], - ]; - - for (const frame of frames) { - if (this.checkFields(nodeFields, frame)) { - hasNodesFrame = true; - } - if (this.checkFields(edgeFields, frame)) { - hasEdgesFrame = true; - } - } - - return hasNodesFrame && hasEdgesFrame; - } - - checkFields(fields: Array<[string, FieldType]>, frame: DataFrame): boolean { - let hasCorrectFields = true; - - for (const field of fields) { - const [name, type] = field; - const frameField = frame.fields.find((f) => f.name === name); - if (!frameField || type !== frameField.type) { - hasCorrectFields = false; - break; - } - } - - return hasCorrectFields; - } - - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - if (!builder.data) { - return; - } - - const hasCorrectFields = this.hasCorrectFields(builder.data.series); - const nodeGraphFrames = builder.data.series.filter( - (df) => df.meta && df.meta.preferredVisualisationType === 'nodeGraph' - ); - - if (hasCorrectFields || nodeGraphFrames.length === 2) { - this.getListWithDefaults(builder).append({ - name: SuggestionName.NodeGraph, - score: VisualizationSuggestionScore.Best, - }); + for (const field of fields) { + const [name, type] = field; + const frameField = frame.fields.find((f) => f.name === name); + if (!frameField || type !== frameField.type) { + hasCorrectFields = false; + break; } } + + return hasCorrectFields; } + +function frameHasCorrectFields(frames: DataFrame[]): boolean { + let hasNodesFrame = false; + let hasEdgesFrame = false; + + const nodeFields: Array<[string, FieldType]> = [ + ['id', FieldType.string], + ['title', FieldType.string], + ['mainstat', FieldType.number], + ]; + const edgeFields: Array<[string, FieldType]> = [ + ['id', FieldType.string], + ['source', FieldType.string], + ['target', FieldType.string], + ]; + + for (const frame of frames) { + if (checkFields(nodeFields, frame)) { + hasNodesFrame = true; + } + if (checkFields(edgeFields, frame)) { + hasEdgesFrame = true; + } + } + + return hasNodesFrame && hasEdgesFrame; +} + +export const nodeGraphSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (!dataSummary.rawFrames) { + return; + } + + const hasCorrectFields = frameHasCorrectFields(dataSummary.rawFrames); + const nodeGraphFrames = dataSummary.hasPreferredVisualisationType('nodeGraph'); + + if (!hasCorrectFields && !nodeGraphFrames) { + return; + } + + return [{ score: VisualizationSuggestionScore.Best }]; +}; diff --git a/public/app/plugins/panel/piechart/module.tsx b/public/app/plugins/panel/piechart/module.tsx index 33726bf3a0f..683a0cc0b6a 100644 --- a/public/app/plugins/panel/piechart/module.tsx +++ b/public/app/plugins/panel/piechart/module.tsx @@ -9,7 +9,7 @@ import { addStandardDataReduceOptions } from '../stat/common'; import { PieChartPanel } from './PieChartPanel'; import { PieChartPanelChangedHandler } from './migrations'; import { Options, FieldConfig, PieChartType, PieChartLabels, PieChartLegendValues } from './panelcfg.gen'; -import { PieChartSuggestionsSupplier } from './suggestions'; +import { piechartSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(PieChartPanel) .setPanelChangeHandler(PieChartPanelChangedHandler) @@ -92,4 +92,4 @@ export const plugin = new PanelPlugin(PieChartPanel) showIf: (c) => c.legend.showLegend !== false, }); }) - .setSuggestionsSupplier(new PieChartSuggestionsSupplier()); + .setSuggestionsSupplier(piechartSuggestionsSupplier); diff --git a/public/app/plugins/panel/piechart/suggestions.ts b/public/app/plugins/panel/piechart/suggestions.ts index fd4beed5e6a..405d5722796 100644 --- a/public/app/plugins/panel/piechart/suggestions.ts +++ b/public/app/plugins/panel/piechart/suggestions.ts @@ -1,79 +1,78 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; +import { defaultsDeep } from 'lodash'; + +import { + FieldType, + VisualizationSuggestion, + VisualizationSuggestionScore, + VisualizationSuggestionsSupplierFn, +} from '@grafana/data'; +import { t } from '@grafana/i18n'; import { LegendDisplayMode } from '@grafana/schema'; -import { SuggestionName } from 'app/types/suggestions'; +import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils'; import { PieChartLabels, Options, PieChartType } from './panelcfg.gen'; -export class PieChartSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = builder.getListAppender({ - name: SuggestionName.PieChart, - pluginId: 'piechart', - options: { - reduceOptions: { - values: false, - calcs: ['lastNotNull'], - }, - displayLabels: [PieChartLabels.Percent], - legend: { - calcs: [], - displayMode: LegendDisplayMode.Hidden, - placement: 'right', - values: [], - showLegend: false, - }, +const withDefaults = (suggestion: VisualizationSuggestion): VisualizationSuggestion => + defaultsDeep(suggestion, { + options: { + displayLabels: [PieChartLabels.Percent], + legend: { + calcs: [], + displayMode: LegendDisplayMode.Hidden, + placement: 'right', + values: [], + showLegend: false, }, - }); + }, + } satisfies VisualizationSuggestion); - const { dataSummary } = builder; +const SLICE_MAX = 30; +const SLICE_MIN = 2; - if (!dataSummary.hasNumberField) { - return; - } +export const piechartSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => { + if (!dataSummary.hasFieldType(FieldType.number)) { + return; + } - if (dataSummary.hasStringField && dataSummary.frameCount === 1) { - // if many values this or single value PieChart is not a good option - if (dataSummary.rowCountTotal > 30 || dataSummary.rowCountTotal < 2) { - return; - } - - list.append({ - name: SuggestionName.PieChart, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - }, - }); - - list.append({ - name: SuggestionName.PieChartDonut, - options: { - reduceOptions: { - values: true, - calcs: [], - }, - pieType: PieChartType.Donut, - }, - }); - - return; - } - - if (dataSummary.numberFieldCount > 30 || dataSummary.numberFieldCount < 2) { - return; - } - - list.append({ - name: SuggestionName.PieChart, - }); - - list.append({ - name: SuggestionName.PieChartDonut, + const suggestions: Array> = [ + { + name: t('piechart.suggestions.pie', 'Pie chart'), + }, + { + name: t('piechart.suggestions.donut', 'Donut chart'), options: { pieType: PieChartType.Donut, }, - }); + }, + ]; + + let shouldUseRawValues = false; + + // we're filtering out data which has more than 30 slices or less than 2, and we're also + // determining whether the reduce options should be set based on the data summary. + if (dataSummary.hasFieldType(FieldType.string) && dataSummary.frameCount === 1) { + if (dataSummary.rowCountTotal > SLICE_MAX && dataSummary.rowCountTotal < SLICE_MIN) { + return; + } + + shouldUseRawValues = true; + } else if ( + dataSummary.fieldCountByType(FieldType.number) > SLICE_MAX || + dataSummary.fieldCountByType(FieldType.number) < SLICE_MIN + ) { + return; } -} + + return suggestions.map((s) => { + const result = defaultNumericVizOptions(withDefaults(s), dataSummary, shouldUseRawValues); + // bump the score up to best if we have exactly one numeric and one string field + if ( + dataSummary.fieldCount === 2 && + dataSummary.fieldCountByType(FieldType.string) === 1 && + dataSummary.fieldCountByType(FieldType.number) === 1 + ) { + result.score = VisualizationSuggestionScore.Best; + } + return result; + }); +}; diff --git a/public/app/plugins/panel/radialbar/module.tsx b/public/app/plugins/panel/radialbar/module.tsx index dfbdb875865..33212d3ce1c 100644 --- a/public/app/plugins/panel/radialbar/module.tsx +++ b/public/app/plugins/panel/radialbar/module.tsx @@ -8,7 +8,7 @@ import { EffectsEditor } from './EffectsEditor'; import { gaugePanelChangedHandler, gaugePanelMigrationHandler, shouldMigrateGauge } from './GaugeMigrations'; import { RadialBarPanel } from './RadialBarPanel'; import { defaultGaugePanelEffects, defaultOptions, Options } from './panelcfg.gen'; -import { radialBarSuggestionsHandler } from './suggestions'; +import { radialBarSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(RadialBarPanel) .useFieldConfig({}) @@ -112,6 +112,6 @@ export const plugin = new PanelPlugin(RadialBarPanel) defaultValue: defaultGaugePanelEffects, }); }) - .setSuggestionsSupplier(radialBarSuggestionsHandler) + .setSuggestionsSupplier(radialBarSuggestionsSupplier) .setMigrationHandler(gaugePanelMigrationHandler, shouldMigrateGauge) .setPanelChangeHandler(gaugePanelChangedHandler); diff --git a/public/app/plugins/panel/radialbar/suggestions.test.ts b/public/app/plugins/panel/radialbar/suggestions.test.ts index 21a59267bbb..0d8ead08c87 100644 --- a/public/app/plugins/panel/radialbar/suggestions.test.ts +++ b/public/app/plugins/panel/radialbar/suggestions.test.ts @@ -1,13 +1,13 @@ import { createDataFrame, Field, FieldType, getPanelDataSummary } from '@grafana/data'; -import { radialBarSuggestionsHandler } from './suggestions'; +import { radialBarSuggestionsSupplier } from './suggestions'; describe('RadialBarPanel Suggestions', () => { it('does not suggest gauge if no data is present', () => { - expect(radialBarSuggestionsHandler(getPanelDataSummary([]))).toBeFalsy(); - expect(radialBarSuggestionsHandler(getPanelDataSummary(undefined))).toBeFalsy(); + expect(radialBarSuggestionsSupplier(getPanelDataSummary([]))).toBeFalsy(); + expect(radialBarSuggestionsSupplier(getPanelDataSummary(undefined))).toBeFalsy(); expect( - radialBarSuggestionsHandler( + radialBarSuggestionsSupplier( getPanelDataSummary([ createDataFrame({ fields: [ @@ -27,7 +27,7 @@ describe('RadialBarPanel Suggestions', () => { { name: 'status', type: FieldType.string }, ], }); - expect(radialBarSuggestionsHandler(getPanelDataSummary([df]))).toBeFalsy(); + expect(radialBarSuggestionsSupplier(getPanelDataSummary([df]))).toBeFalsy(); }); it('does not suggest gauge if there are too many numeric fields', () => { @@ -35,12 +35,12 @@ describe('RadialBarPanel Suggestions', () => { for (let i = 0; i < 20; i++) { fields.push({ name: `numeric-${i}`, type: FieldType.number, values: [0, 100, 200, 300, 400, 500], config: {} }); } - expect(radialBarSuggestionsHandler(getPanelDataSummary([createDataFrame({ fields })]))).toBeFalsy(); + expect(radialBarSuggestionsSupplier(getPanelDataSummary([createDataFrame({ fields })]))).toBeFalsy(); }); it('suggests gauge for a single numeric field', () => { expect( - radialBarSuggestionsHandler( + radialBarSuggestionsSupplier( getPanelDataSummary([ createDataFrame({ fields: [ @@ -58,7 +58,7 @@ describe('RadialBarPanel Suggestions', () => { it('suggests gauge for a few numeric fields, with other fields mixed in', () => { expect( - radialBarSuggestionsHandler( + radialBarSuggestionsSupplier( getPanelDataSummary([ createDataFrame({ fields: [ @@ -136,7 +136,7 @@ describe('RadialBarPanel Suggestions', () => { ], }, ])('$description suggests aggregated=$aggregated', ({ dataframes, aggregated }) => { - const suggestions = radialBarSuggestionsHandler(getPanelDataSummary(dataframes)); + const suggestions = radialBarSuggestionsSupplier(getPanelDataSummary(dataframes)); const expected = aggregated ? { values: false, calcs: ['lastNotNull'] } : { values: true, calcs: [] }; if (Array.isArray(suggestions)) { for (const suggestion of suggestions) { diff --git a/public/app/plugins/panel/radialbar/suggestions.ts b/public/app/plugins/panel/radialbar/suggestions.ts index 35cde48ec0f..f8d13a096b3 100644 --- a/public/app/plugins/panel/radialbar/suggestions.ts +++ b/public/app/plugins/panel/radialbar/suggestions.ts @@ -8,10 +8,39 @@ import { } from '@grafana/data'; import { t } from '@grafana/i18n'; import { GraphFieldConfig } from '@grafana/ui'; +import { defaultNumericVizOptions } from 'app/features/panel/suggestions/utils'; import { Options } from './panelcfg.gen'; -export const radialBarSuggestionsHandler: VisualizationSuggestionsSupplierFn = ( +const withDefaults = ( + suggestion: VisualizationSuggestion +): VisualizationSuggestion => + defaultsDeep(suggestion, { + cardOptions: { + previewModifier: (s) => { + if (s.options?.reduceOptions) { + s.options.reduceOptions.limit = 4; + } + }, + }, + // styles: [{ + // name: t('gauge.suggestions.style.circular', 'Glowing'), + // options: { + // effects: { + // rounded: true, + // barGlow: true, + // centerGlow: true, + // spotlight: true, + // }, + // }, + // }, { + // name: t('gauge.suggestions.style.simple', 'Simple'), + // }] + } satisfies VisualizationSuggestion); + +const MAX_GAUGES = 10; + +export const radialBarSuggestionsSupplier: VisualizationSuggestionsSupplierFn = ( dataSummary ) => { if (!dataSummary.hasData || !dataSummary.hasFieldType(FieldType.number)) { @@ -19,69 +48,40 @@ export const radialBarSuggestionsHandler: VisualizationSuggestionsSupplierFn= 10) { + if (dataSummary.fieldCountByType(FieldType.number) > MAX_GAUGES) { return; } - const withDefaults = ( - suggestion: VisualizationSuggestion - ): VisualizationSuggestion => { - // if there is a string field and there are few enough rows, we assume it's tabular data and not numeric series data, - // and the de-aggregated version of the viz probably makes more sense - const isTabularData = - dataSummary.hasFieldType(FieldType.string) && dataSummary.frameCount === 1 && dataSummary.rowCountTotal < 10; - return defaultsDeep(suggestion, { - options: { - reduceOptions: isTabularData - ? { - values: true, - calcs: [], - } - : { - values: false, - calcs: ['lastNotNull'], - }, - }, - fieldConfig: { - defaults: isTabularData - ? { - color: { mode: FieldColorModeId.PaletteClassic }, - } - : {}, - overrides: [], - }, - cardOptions: { - previewModifier: (s) => { - if (s.options?.reduceOptions) { - s.options.reduceOptions.limit = 4; - } - }, - }, - // styles: [{ - // name: t('gauge.suggestions.style.circular', 'Glowing'), - // options: { - // effects: { - // rounded: true, - // barGlow: true, - // centerGlow: true, - // spotlight: true, - // }, - // }, - // }, { - // name: t('gauge.suggestions.style.simple', 'Simple'), - // }] - } satisfies VisualizationSuggestion); - }; - - return [ - withDefaults({ name: t('gauge.suggestions.arc', 'Gauge') }), - withDefaults({ + const suggestions: Array> = [ + { name: t('gauge.suggestions.arc', 'Gauge') }, + { name: t('gauge.suggestions.circular', 'Circular gauge'), options: { shape: 'circle', showThresholdMarkers: false, barWidthFactor: 0.3, }, - }), + }, ]; + + const shouldUseRawValues = + dataSummary.hasFieldType(FieldType.string) && + dataSummary.frameCount === 1 && + dataSummary.rowCountTotal <= MAX_GAUGES; + + return suggestions.map((s) => { + const suggestion = defaultNumericVizOptions(withDefaults(s), dataSummary, shouldUseRawValues); + + if (shouldUseRawValues) { + suggestion.fieldConfig = suggestion.fieldConfig ?? { + defaults: {}, + overrides: [], + }; + suggestion.fieldConfig.defaults.color = suggestion.fieldConfig.defaults.color ?? { + mode: FieldColorModeId.PaletteClassic, + }; + } + + return suggestion; + }); }; diff --git a/public/app/plugins/panel/stat/module.tsx b/public/app/plugins/panel/stat/module.tsx index 969e010352c..ecb146a851b 100644 --- a/public/app/plugins/panel/stat/module.tsx +++ b/public/app/plugins/panel/stat/module.tsx @@ -13,7 +13,7 @@ import { statPanelChangedHandler } from './StatMigrations'; import { StatPanel } from './StatPanel'; import { addStandardDataReduceOptions, addOrientationOption } from './common'; import { defaultOptions, Options } from './panelcfg.gen'; -import { StatSuggestionsSupplier } from './suggestions'; +import { statSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(StatPanel) .useFieldConfig() @@ -137,5 +137,5 @@ export const plugin = new PanelPlugin(StatPanel) }) .setNoPadding() .setPanelChangeHandler(statPanelChangedHandler) - .setSuggestionsSupplier(new StatSuggestionsSupplier()) + .setSuggestionsSupplier(statSuggestionsSupplier) .setMigrationHandler(sharedSingleStatMigrationHandler); diff --git a/public/app/plugins/panel/stat/suggestions.ts b/public/app/plugins/panel/stat/suggestions.ts index 00af3220135..14a45d4b52d 100644 --- a/public/app/plugins/panel/stat/suggestions.ts +++ b/public/app/plugins/panel/stat/suggestions.ts @@ -1,41 +1,46 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { BigValueColorMode, BigValueGraphMode, GraphFieldConfig } from '@grafana/schema'; -import { SuggestionName } from 'app/types/suggestions'; +import { defaultsDeep } from 'lodash'; + +import { FieldType, VisualizationSuggestion, VisualizationSuggestionsSupplierFn } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { BigValueColorMode, BigValueGraphMode } from '@grafana/schema'; import { Options } from './panelcfg.gen'; -export class StatSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary: ds } = builder; - - if (!ds.hasData) { - return; - } - - const list = builder.getListAppender({ - name: SuggestionName.Stat, - pluginId: 'stat', - options: {}, - fieldConfig: { - defaults: { - unit: 'short', - custom: {}, - }, - overrides: [], +const withDefaults = (s: VisualizationSuggestion): VisualizationSuggestion => + defaultsDeep(s, { + fieldConfig: { + defaults: { + unit: 'short', + custom: {}, }, - cardOptions: { - previewModifier: (s) => { - if (s.options?.reduceOptions?.values) { - s.options.reduceOptions.limit = 1; - } - }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + if (s.options?.reduceOptions?.values) { + s.options.reduceOptions.limit = 1; + } }, - }); + }, + } satisfies VisualizationSuggestion); - // String and number field with low row count show individual rows - if (ds.hasStringField && ds.hasNumberField && ds.frameCount === 1 && ds.rowCountTotal < 10) { - list.append({ - name: SuggestionName.Stat, +export const statSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (ds) => { + if (!ds.hasData) { + return; + } + + const suggestions: Array> = []; + + // String and number field with low row count show individual rows + if ( + ds.hasFieldType(FieldType.string) && + ds.hasFieldType(FieldType.number) && + ds.frameCount === 1 && + ds.rowCountTotal < 10 + ) { + suggestions.push( + { + name: t('stat.suggestions.stat-discrete-values', 'Stat - discrete values'), options: { reduceOptions: { values: true, @@ -43,9 +48,9 @@ export class StatSuggestionsSupplier { fields: '/.*/', }, }, - }); - list.append({ - name: SuggestionName.StatColoredBackground, + }, + { + name: t('stat.suggestions.stat-discrete-values-color-background', 'Stat - discrete values - color background'), options: { reduceOptions: { values: true, @@ -54,36 +59,38 @@ export class StatSuggestionsSupplier { }, colorMode: BigValueColorMode.Background, }, - }); - } + } + ); + } - // Just a single string field - if (ds.stringFieldCount === 1 && ds.frameCount === 1 && ds.rowCountTotal < 10 && ds.fieldCount === 1) { - list.append({ - name: SuggestionName.Stat, - options: { - reduceOptions: { - values: true, - calcs: [], - fields: '/.*/', - }, - colorMode: BigValueColorMode.None, + // just a single string field + if (ds.fieldCount === 1 && ds.hasFieldType(FieldType.string)) { + suggestions.push({ + name: t('stat.suggestions.stat-single-string', 'Stat - single string'), + options: { + reduceOptions: { + values: true, + calcs: [], + fields: '/.*/', }, - }); - } + colorMode: BigValueColorMode.None, + }, + }); + } - if (ds.hasNumberField && ds.hasTimeField) { - list.append({ + // aggregated suggestions for number fields + if (ds.hasFieldType(FieldType.number) && ds.hasFieldType(FieldType.time)) { + suggestions.push( + { options: { reduceOptions: { values: false, calcs: ['lastNotNull'], }, }, - }); - - list.append({ - name: SuggestionName.StatColoredBackground, + }, + { + name: t('stat.suggestions.stat-color-background', 'Stat - color background'), options: { reduceOptions: { values: false, @@ -92,7 +99,9 @@ export class StatSuggestionsSupplier { graphMode: BigValueGraphMode.None, colorMode: BigValueColorMode.Background, }, - }); - } + } + ); } -} + + return suggestions.map(withDefaults); +}; diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index 55fd557eea0..89acb338825 100644 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -177,7 +177,7 @@ export const plugin = new PanelPlugin(StateTimelinePanel) } // Probably better ways to filter out this by inspecting the types of string values so view this as temporary - if (ds.preferredVisualisationType === 'logs') { + if (ds.hasPreferredVisualisationType('logs')) { return; } diff --git a/public/app/plugins/panel/status-history/module.tsx b/public/app/plugins/panel/status-history/module.tsx index 1e07588d664..eed5e3546a8 100644 --- a/public/app/plugins/panel/status-history/module.tsx +++ b/public/app/plugins/panel/status-history/module.tsx @@ -1,11 +1,10 @@ -import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; +import { FieldColorModeId, FieldConfigProperty, FieldType, PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; import { AxisPlacement, VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { StatusHistoryPanel } from './StatusHistoryPanel'; import { Options, FieldConfig, defaultFieldConfig } from './panelcfg.gen'; -import { StatusHistorySuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(StatusHistoryPanel) .useFieldConfig({ @@ -113,5 +112,42 @@ export const plugin = new PanelPlugin(StatusHistoryPanel) commonOptionsBuilder.addLegendOptions(builder, false); commonOptionsBuilder.addTooltipOptions(builder); }) - .setSuggestionsSupplier(new StatusHistorySuggestionsSupplier()) + .setSuggestionsSupplier((ds) => { + if (!ds.hasData) { + return; + } + + // This panel needs a time field and a string or number field + if ( + !ds.hasFieldType(FieldType.time) || + (!ds.hasFieldType(FieldType.string) && !ds.hasFieldType(FieldType.number)) + ) { + return; + } + + // If there are many series then they won't fit on y-axis so this panel is not good fit + if (ds.fieldCountByType(FieldType.number) >= 30) { + return; + } + + // if there a lot of data points for each series then this is not a good match + if (ds.rowCountMax > 100) { + return; + } + + // Probably better ways to filter out this by inspecting the types of string values so view this as temporary + if (ds.hasPreferredVisualisationType('logs')) { + return; + } + + return [ + { + cardOptions: { + previewModifier: (s) => { + s.options!.colWidth = 0.7; + }, + }, + }, + ]; + }) .setDataSupport({ annotations: true }); diff --git a/public/app/plugins/panel/status-history/suggestions.ts b/public/app/plugins/panel/status-history/suggestions.ts deleted file mode 100644 index 48add53c83d..00000000000 --- a/public/app/plugins/panel/status-history/suggestions.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { FieldColorModeId, VisualizationSuggestionsBuilder } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; - -import { Options, FieldConfig } from './panelcfg.gen'; - -export class StatusHistorySuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary: ds } = builder; - - if (!ds.hasData) { - return; - } - - // This panel needs a time field and a string or number field - if (!ds.hasTimeField || (!ds.hasStringField && !ds.hasNumberField)) { - return; - } - - // If there are many series then they won't fit on y-axis so this panel is not good fit - if (ds.numberFieldCount >= 30) { - return; - } - - // if there a lot of data points for each series then this is not a good match - if (ds.rowCountMax > 100) { - return; - } - - // Probably better ways to filter out this by inspecting the types of string values so view this as temporary - if (ds.preferredVisualisationType === 'logs') { - return; - } - - const list = builder.getListAppender({ - name: '', - pluginId: 'status-history', - options: {}, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - custom: {}, - }, - overrides: [], - }, - cardOptions: { - previewModifier: (s) => { - s.options!.colWidth = 0.7; - }, - }, - }); - - list.append({ name: SuggestionName.StatusHistory }); - } -} diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index e2ee5b8e97f..5a3a1d21fac 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -13,7 +13,7 @@ import { TableCellOptionEditor } from './TableCellOptionEditor'; import { TablePanel } from './TablePanel'; import { tableMigrationHandler, tablePanelChangedHandler } from './migrations'; import { Options, defaultOptions, FieldConfig } from './panelcfg.gen'; -import { TableSuggestionsSupplier } from './suggestions'; +import { tableSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(TablePanel) .setPanelChangeHandler(tablePanelChangedHandler) @@ -227,4 +227,4 @@ export const plugin = new PanelPlugin(TablePanel) defaultValue: defaultOptions?.enablePagination, }); }) - .setSuggestionsSupplier(new TableSuggestionsSupplier()); + .setSuggestionsSupplier(tableSuggestionsSupplier); diff --git a/public/app/plugins/panel/table/suggestions.ts b/public/app/plugins/panel/table/suggestions.ts index bcfead0fe31..98fc085e1cf 100644 --- a/public/app/plugins/panel/table/suggestions.ts +++ b/public/app/plugins/panel/table/suggestions.ts @@ -1,38 +1,33 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { TableFieldOptions } from '@grafana/schema'; +import { PanelDataSummary, VisualizationSuggestionScore, VisualizationSuggestionsSupplierFn } from '@grafana/data'; import icnTablePanelSvg from 'app/plugins/panel/table/img/icn-table-panel.svg'; -import { SuggestionName } from 'app/types/suggestions'; -import { Options } from './panelcfg.gen'; +import { Options, FieldConfig } from './panelcfg.gen'; -export class TableSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const list = builder.getListAppender({ - name: SuggestionName.Table, - pluginId: 'table', - options: {}, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - cardOptions: { - previewModifier: (s) => { - s.fieldConfig!.defaults.custom!.minWidth = 50; - }, - }, - }); - - // If there are not data suggest table anyway but use icon instead of real preview - if (builder.dataSummary.fieldCount === 0) { - list.append({ - cardOptions: { - imgSrc: icnTablePanelSvg, - }, - }); - } else { - list.append({}); - } +function getTableSuggestionScore(dataSummary: PanelDataSummary): VisualizationSuggestionScore { + if (dataSummary.hasPreferredVisualisationType('table')) { + return VisualizationSuggestionScore.Best; } + + // table is best suited to showing many fields with many rows. + if (dataSummary.fieldCountMax > 5 && dataSummary.rowCountMax > 50) { + return VisualizationSuggestionScore.Good; + } + + return VisualizationSuggestionScore.OK; } + +export const tableSuggestionsSupplier: VisualizationSuggestionsSupplierFn = (dataSummary) => [ + { + score: getTableSuggestionScore(dataSummary), + cardOptions: { + previewModifier: (s) => { + if (s.fieldConfig && s.fieldConfig.defaults.custom) { + s.fieldConfig.defaults.custom.minWidth = 50; + } + }, + // If there is no data, suggest table anyway, but use icon instead of real preview + // TODO: delete this in once "new" suggestions are fully rolled out + imgSrc: dataSummary.fieldCount === 0 ? icnTablePanelSvg : undefined, + }, + }, +]; diff --git a/public/app/plugins/panel/timeseries/module.tsx b/public/app/plugins/panel/timeseries/module.tsx index beccdf480d6..22b0e3a715a 100644 --- a/public/app/plugins/panel/timeseries/module.tsx +++ b/public/app/plugins/panel/timeseries/module.tsx @@ -8,7 +8,7 @@ import { TimezonesEditor } from './TimezonesEditor'; import { defaultGraphConfig, getGraphFieldConfig } from './config'; import { graphPanelChangedHandler } from './migrations'; import { FieldConfig, Options } from './panelcfg.gen'; -import { TimeSeriesSuggestionsSupplier } from './suggestions'; +import { timeseriesSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(TimeSeriesPanel) .setPanelChangeHandler(graphPanelChangedHandler) @@ -26,5 +26,5 @@ export const plugin = new PanelPlugin(TimeSeriesPanel) defaultValue: undefined, }); }) - .setSuggestionsSupplier(new TimeSeriesSuggestionsSupplier()) + .setSuggestionsSupplier(timeseriesSuggestionsSupplier) .setDataSupport({ annotations: true, alertStates: true }); diff --git a/public/app/plugins/panel/timeseries/suggestions.ts b/public/app/plugins/panel/timeseries/suggestions.ts index d41179d3f36..05a73acdcc7 100644 --- a/public/app/plugins/panel/timeseries/suggestions.ts +++ b/public/app/plugins/panel/timeseries/suggestions.ts @@ -1,9 +1,15 @@ +import { defaultsDeep } from 'lodash'; + import { - FieldColorModeId, - VisualizationSuggestionsBuilder, + DataFrameType, DataTransformerID, + FieldType, PanelPluginVisualizationSuggestion, + VisualizationSuggestion, + VisualizationSuggestionScore, + VisualizationSuggestionsSupplierFn, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { GraphDrawStyle, GraphFieldConfig, @@ -13,209 +19,142 @@ import { StackingMode, } from '@grafana/schema'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { SuggestionName } from 'app/types/suggestions'; import { Options } from './panelcfg.gen'; -export class TimeSeriesSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; +const MAX_BARS = 100; +const MAX_ROWS_SMOOTH_CHART = 200; - if (!dataSummary.hasTimeField || !dataSummary.hasNumberField || dataSummary.rowCountTotal < 2) { - return; - } - - const list = builder.getListAppender({ - name: SuggestionName.LineChart, - pluginId: 'timeseries', - options: { - legend: { - calcs: [], - displayMode: LegendDisplayMode.Hidden, - placement: 'right', - showLegend: false, - }, +const withDefaults = ( + suggestion: VisualizationSuggestion +): VisualizationSuggestion => + defaultsDeep(suggestion, { + options: { + legend: { + calcs: [], + displayMode: LegendDisplayMode.Hidden, + placement: 'right', + showLegend: false, }, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], + }, + fieldConfig: { + defaults: { + custom: {}, }, - cardOptions: { - previewModifier: (s) => { - if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { - s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); - } - }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { + s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); + } }, - }); + }, + } satisfies VisualizationSuggestion); - const maxBarsCount = 100; +const areaChart = (name: string, stacking?: StackingMode) => ({ + name, + fieldConfig: { + defaults: { + custom: { + fillOpacity: 25, + ...(stacking ? { stacking: { mode: stacking, group: 'A' } } : {}), + }, + }, + overrides: [], + }, +}); - list.append({ - name: SuggestionName.LineChart, - }); +const barChart = (name: string, stacking?: StackingMode) => ({ + name, + fieldConfig: { + defaults: { + custom: { + drawStyle: GraphDrawStyle.Bars, + fillOpacity: 100, + lineWidth: 1, + gradientMode: GraphGradientMode.Hue, + ...(stacking ? { stacking: { mode: stacking, group: 'A' } } : {}), + }, + }, + overrides: [], + }, +}); - if (dataSummary.rowCountMax < 200) { - list.append({ - name: SuggestionName.LineChartSmooth, - fieldConfig: { - defaults: { - custom: { - lineInterpolation: LineInterpolation.Smooth, - }, - }, - overrides: [], - }, - }); - } +// TODO: all "gradient color scheme" suggestions have been removed. they will be re-added as part of the "styles" feature. - // Single series suggestions - if (dataSummary.numberFieldCount === 1) { - list.append({ - name: SuggestionName.AreaChart, - fieldConfig: { - defaults: { - custom: { - fillOpacity: 25, - }, - }, - overrides: [], - }, - }); +export const timeseriesSuggestionsSupplier: VisualizationSuggestionsSupplierFn = ( + dataSummary +) => { + if ( + !dataSummary.hasFieldType(FieldType.time) || + !dataSummary.hasFieldType(FieldType.number) || + dataSummary.rowCountTotal < 2 + ) { + return; + } - list.append({ - name: SuggestionName.LineChartGradientColorScheme, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - custom: { - gradientMode: GraphGradientMode.Scheme, - lineInterpolation: LineInterpolation.Smooth, - lineWidth: 3, - fillOpacity: 20, - }, - }, - overrides: [], - }, - }); + const score: VisualizationSuggestionScore = + dataSummary.hasDataFrameType(DataFrameType.TimeSeriesLong) || + dataSummary.hasDataFrameType(DataFrameType.TimeSeriesWide) || + dataSummary.hasDataFrameType(DataFrameType.TimeSeriesMulti) + ? VisualizationSuggestionScore.Good + : VisualizationSuggestionScore.OK; - if (dataSummary.rowCountMax < maxBarsCount) { - list.append({ - name: SuggestionName.BarChart, - fieldConfig: { - defaults: { - custom: { - drawStyle: GraphDrawStyle.Bars, - fillOpacity: 100, - lineWidth: 1, - gradientMode: GraphGradientMode.Hue, - }, - }, - overrides: [], - }, - }); + const suggestions: Array> = [ + { + name: t('timeseries.suggestions.line', 'Line chart'), + }, + ]; - list.append({ - name: SuggestionName.BarChartGradientColorScheme, - fieldConfig: { - defaults: { - color: { - mode: FieldColorModeId.ContinuousGrYlRd, - }, - custom: { - drawStyle: GraphDrawStyle.Bars, - fillOpacity: 90, - lineWidth: 1, - gradientMode: GraphGradientMode.Scheme, - }, - }, - overrides: [], - }, - }); - } - - return; - } - - // Multiple series suggestions - - list.append({ - name: SuggestionName.AreaChartStacked, + if (dataSummary.rowCountMax < MAX_ROWS_SMOOTH_CHART) { + suggestions.push({ + name: t('timeseries.suggestions.line-smooth', 'Line chart - smooth'), fieldConfig: { defaults: { custom: { - fillOpacity: 25, - stacking: { - mode: StackingMode.Normal, - group: 'A', - }, + lineInterpolation: LineInterpolation.Smooth, }, }, overrides: [], }, }); + } - list.append({ - name: SuggestionName.AreaChartStackedPercent, - fieldConfig: { - defaults: { - custom: { - fillOpacity: 25, - stacking: { - mode: StackingMode.Percent, - group: 'A', - }, - }, - }, - overrides: [], - }, - }); + // Single-series suggestions + if (dataSummary.fieldCountByType(FieldType.number) === 1) { + suggestions.push(areaChart(t('timeseries.suggestions.area', 'Area chart'))); - if (dataSummary.rowCountTotal / dataSummary.numberFieldCount < maxBarsCount) { - list.append({ - name: SuggestionName.BarChartStacked, - fieldConfig: { - defaults: { - custom: { - drawStyle: GraphDrawStyle.Bars, - fillOpacity: 100, - lineWidth: 1, - gradientMode: GraphGradientMode.Hue, - stacking: { - mode: StackingMode.Normal, - group: 'A', - }, - }, - }, - overrides: [], - }, - }); - - list.append({ - name: SuggestionName.BarChartStackedPercent, - fieldConfig: { - defaults: { - custom: { - drawStyle: GraphDrawStyle.Bars, - fillOpacity: 100, - lineWidth: 1, - gradientMode: GraphGradientMode.Hue, - stacking: { - mode: StackingMode.Percent, - group: 'A', - }, - }, - }, - overrides: [], - }, - }); + if (dataSummary.rowCountMax < MAX_BARS) { + suggestions.push(barChart(t('timeseries.suggestions.bar', 'Bar chart'))); } } -} + // Multiple series suggestions + else { + suggestions.push( + areaChart(t('timeseries.suggestions.area-stacked', 'Area chart - stacked'), StackingMode.Normal), + areaChart( + t('timeseries.suggestions.area-stacked-percentage', 'Area chart - stacked by percentage'), + StackingMode.Percent + ) + ); + + if (dataSummary.rowCountTotal / dataSummary.fieldCountByType(FieldType.number) < MAX_BARS) { + suggestions.push( + barChart(t('timeseries.suggestions.bar-stacked', 'Bar chart - stacked'), StackingMode.Normal), + barChart( + t('timeseries.suggestions.bar-stacked-percent', 'Bar chart - stacked by percentage'), + StackingMode.Percent + ) + ); + } + } + + return suggestions.map((s) => { + s.score = score; + return withDefaults(s); + }); +}; // This will try to get a suggestion that will add a long to wide conversion export function getPrepareTimeseriesSuggestion(panelId: number): PanelPluginVisualizationSuggestion | undefined { diff --git a/public/app/plugins/panel/traces/module.tsx b/public/app/plugins/panel/traces/module.tsx index fdbe35c6c91..07b72f00d53 100644 --- a/public/app/plugins/panel/traces/module.tsx +++ b/public/app/plugins/panel/traces/module.tsx @@ -1,11 +1,11 @@ import { PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { showDefaultSuggestion } from 'app/features/panel/suggestions/utils'; import { migrateToAdhocFilters } from '../../../features/explore/TraceView/useSearch'; import { FiltersEditor } from './FiltersEditor'; import { TracesPanel } from './TracesPanel'; -import { TracesSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(TracesPanel) .setMigrationHandler((panel) => { @@ -44,4 +44,4 @@ export const plugin = new PanelPlugin(TracesPanel) category, }); }) - .setSuggestionsSupplier(new TracesSuggestionsSupplier()); + .setSuggestionsSupplier(showDefaultSuggestion((ds) => ds.hasPreferredVisualisationType('trace'))); diff --git a/public/app/plugins/panel/traces/suggestions.ts b/public/app/plugins/panel/traces/suggestions.ts deleted file mode 100644 index bda2e463927..00000000000 --- a/public/app/plugins/panel/traces/suggestions.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { VisualizationSuggestionsBuilder, VisualizationSuggestionScore } from '@grafana/data'; -import { SuggestionName } from 'app/types/suggestions'; - -export class TracesSuggestionsSupplier { - getListWithDefaults(builder: VisualizationSuggestionsBuilder) { - return builder.getListAppender<{}, {}>({ - name: SuggestionName.Trace, - pluginId: 'traces', - }); - } - - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - if (!builder.data) { - return; - } - - const dataFrame = builder.data.series[0]; - if (!dataFrame) { - return; - } - - if (builder.data.series[0].meta?.preferredVisualisationType === 'trace') { - this.getListWithDefaults(builder).append({ - name: SuggestionName.Trace, - score: VisualizationSuggestionScore.Best, - }); - } - } -} diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 95ce2330283..b446c651733 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -1,7 +1,6 @@ import { useMemo } from 'react'; import { - isLikelyAscendingVector, DataFrame, FieldMatcherID, fieldMatchers, @@ -10,18 +9,17 @@ import { TimeRange, useDataLinksContext, } from '@grafana/data'; -import { config, PanelDataErrorView } from '@grafana/runtime'; +import { PanelDataErrorView } from '@grafana/runtime'; import { KeyboardPlugin, TooltipDisplayMode, TooltipPlugin2, usePanelContext } from '@grafana/ui'; import { TooltipHoverMode } from '@grafana/ui/internal'; import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; import { preparePlotFrame } from 'app/core/components/GraphNG/utils'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; -import { findFieldIndex } from 'app/features/dimensions/utils'; import { TimeSeriesTooltip } from '../timeseries/TimeSeriesTooltip'; -import { prepareGraphableFields } from '../timeseries/utils'; import { Options } from './panelcfg.gen'; +import { prepSeries } from './utils'; export const TrendPanel = ({ data, @@ -51,49 +49,7 @@ export const TrendPanel = ({ return preparePlotFrame(frames, dimFields); }; - const info = useMemo(() => { - if (data.series.length > 1) { - return { - warning: 'Only one frame is supported, consider adding a join transformation', - frames: data.series, - }; - } - - let frames = data.series; - let xFieldIdx: number | undefined; - if (options.xField) { - xFieldIdx = findFieldIndex(options.xField, frames[0]); - if (xFieldIdx == null) { - return { - warning: 'Unable to find field: ' + options.xField, - frames: data.series, - }; - } - } else { - // first number field - // Perhaps we can/should support any ordinal rather than an error here - xFieldIdx = frames[0] ? frames[0].fields.findIndex((f) => f.type === FieldType.number) : -1; - if (xFieldIdx === -1) { - return { - warning: 'No numeric fields found for X axis', - frames, - }; - } - } - - // Make sure values are ascending - if (xFieldIdx != null) { - const field = frames[0].fields[xFieldIdx]; - if (field.type === FieldType.number && !isLikelyAscendingVector(field.values)) { - return { - warning: `Values must be in ascending order`, - frames, - }; - } - } - - return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) }; - }, [data.series, options.xField]); + const info = useMemo(() => prepSeries(data.series, options.xField), [data.series, options.xField]); if (info.warning || !info.frames) { return ( diff --git a/public/app/plugins/panel/trend/module.tsx b/public/app/plugins/panel/trend/module.tsx index ec823e00644..67434a8c542 100644 --- a/public/app/plugins/panel/trend/module.tsx +++ b/public/app/plugins/panel/trend/module.tsx @@ -1,13 +1,14 @@ -import { Field, FieldType, PanelPlugin } from '@grafana/data'; +import { Field, FieldType, PanelPlugin, VisualizationSuggestionScore } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { commonOptionsBuilder } from '@grafana/ui'; +import { GraphDrawStyle } from '@grafana/schema'; +import { commonOptionsBuilder, LegendDisplayMode } from '@grafana/ui'; import { optsWithHideZeros } from '@grafana/ui/internal'; import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config'; import { TrendPanel } from './TrendPanel'; import { FieldConfig, Options } from './panelcfg.gen'; -import { TrendSuggestionsSupplier } from './suggestions'; +import { prepSeries } from './utils'; export const plugin = new PanelPlugin(TrendPanel) .useFieldConfig(getGraphFieldConfig(defaultGraphConfig, false)) @@ -29,5 +30,47 @@ export const plugin = new PanelPlugin(TrendPanel) commonOptionsBuilder.addTooltipOptions(builder, false, true, optsWithHideZeros); commonOptionsBuilder.addLegendOptions(builder); }) - .setSuggestionsSupplier(new TrendSuggestionsSupplier()); + .setSuggestionsSupplier((ds) => { + if ( + !ds.rawFrames || + ds.fieldCountByType(FieldType.number) < 2 || + ds.rowCountTotal < 2 || + ds.rowCountTotal < 2 || + ds.frameCount > 1 + ) { + return; + } + + const info = prepSeries(ds.rawFrames); + if (info.warning || !info.frames) { + return; + } + + return [ + { + score: VisualizationSuggestionScore.Good, + options: { + legend: { + calcs: [], + displayMode: LegendDisplayMode.Hidden, + placement: 'right', + showLegend: false, + }, + }, + fieldConfig: { + defaults: { + custom: {}, + }, + overrides: [], + }, + cardOptions: { + previewModifier: (s) => { + if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { + s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); + } + }, + }, + }, + ]; + }); //.setDataSupport({ annotations: true, alertStates: true }); diff --git a/public/app/plugins/panel/trend/suggestions.ts b/public/app/plugins/panel/trend/suggestions.ts deleted file mode 100644 index c135c5df30e..00000000000 --- a/public/app/plugins/panel/trend/suggestions.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { VisualizationSuggestionsBuilder } from '@grafana/data'; -import { GraphDrawStyle, GraphFieldConfig, LegendDisplayMode } from '@grafana/schema'; -import { SuggestionName } from 'app/types/suggestions'; - -import { Options } from './panelcfg.gen'; - -export class TrendSuggestionsSupplier { - getSuggestionsForData(builder: VisualizationSuggestionsBuilder) { - const { dataSummary } = builder; - - if (dataSummary.numberFieldCount < 2 || dataSummary.rowCountTotal < 2 || dataSummary.rowCountTotal < 2) { - return; - } - - // Super basic - const list = builder.getListAppender({ - name: SuggestionName.LineChart, - pluginId: 'trend', - options: { - legend: { - calcs: [], - displayMode: LegendDisplayMode.Hidden, - placement: 'right', - showLegend: false, - }, - }, - fieldConfig: { - defaults: { - custom: {}, - }, - overrides: [], - }, - cardOptions: { - previewModifier: (s) => { - if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) { - s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2); - } - }, - }, - }); - return list; - } -} diff --git a/public/app/plugins/panel/trend/utils.ts b/public/app/plugins/panel/trend/utils.ts new file mode 100644 index 00000000000..17a02fd5049 --- /dev/null +++ b/public/app/plugins/panel/trend/utils.ts @@ -0,0 +1,48 @@ +import { DataFrame, FieldType, isLikelyAscendingVector } from '@grafana/data'; +import config from 'app/core/config'; +import { findFieldIndex } from 'app/features/dimensions/utils'; + +import { prepareGraphableFields } from '../timeseries/utils'; + +export function prepSeries(frames: DataFrame[], xField?: string): { warning?: string; frames: DataFrame[] | null } { + if (frames.length > 1) { + return { + warning: 'Only one frame is supported, consider adding a join transformation', + frames: frames, + }; + } + + let xFieldIdx: number | undefined; + if (xField) { + xFieldIdx = findFieldIndex(xField, frames[0]); + if (xFieldIdx == null) { + return { + warning: 'Unable to find field: ' + xField, + frames: frames, + }; + } + } else { + // first number field + // Perhaps we can/should support any ordinal rather than an error here + xFieldIdx = frames[0] ? frames[0].fields.findIndex((f) => f.type === FieldType.number) : -1; + if (xFieldIdx === -1) { + return { + warning: 'No numeric fields found for X axis', + frames, + }; + } + } + + // Make sure values are ascending + if (xFieldIdx != null) { + const field = frames[0].fields[xFieldIdx]; + if (field.type === FieldType.number && !isLikelyAscendingVector(field.values)) { + return { + warning: `Values must be in ascending order`, + frames, + }; + } + } + + return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) }; +} diff --git a/public/app/types/suggestions.ts b/public/app/types/suggestions.ts deleted file mode 100644 index aac0dc1bd3c..00000000000 --- a/public/app/types/suggestions.ts +++ /dev/null @@ -1,34 +0,0 @@ -export enum SuggestionName { - LineChart = 'Line chart', - LineChartSmooth = 'Line chart smooth', - LineChartGradientColorScheme = 'Line chart with gradient color scheme', - AreaChart = 'Area chart', - AreaChartStacked = 'Area chart stacked', - AreaChartStackedPercent = 'Area chart 100% stacked', - BarChart = 'Bar chart', - BarChartGradientColorScheme = 'Bar chart with gradient color scheme', - BarChartStacked = 'Bar chart stacked', - BarChartStackedPercent = 'Bar chart 100% stacked', - BarChartHorizontal = 'Bar chart horizontal', - BarChartHorizontalStacked = 'Bar chart horizontal stacked', - BarChartHorizontalStackedPercent = 'Bar chart horizontal 100% stacked', - Candlestick = 'Candlestick', - PieChart = 'Pie chart', - PieChartDonut = 'Pie chart donut', - Stat = 'Stat', - StatColoredBackground = 'Stat colored background', - Gauge = 'Gauge', - GaugeCircular = 'Circular gauge', - GaugeNoThresholds = 'Gauge no thresholds', - BarGaugeBasic = 'Bar gauge basic', - BarGaugeLCD = 'Bar gauge LCD', - Table = 'Table', - StateTimeline = 'State timeline', - StatusHistory = 'Status history', - TextPanel = 'Text', - DashboardList = 'Dashboard list', - Logs = 'Logs', - FlameGraph = 'Flame graph', - Trace = 'Trace', - NodeGraph = 'Node graph', -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9f8b19218de..aa73bf3cd22 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3477,6 +3477,14 @@ "label-negative-y": "Negative Y" } }, + "suggestions": { + "horizontal": "Horizontal bar chart", + "hz-stacked": "Horizontal bar chart - stacked", + "hz-stacked-percent": "Horizontal bar chart - stacked by percentage", + "vert-stacked": "Bar chart - stacked", + "vert-stacked-percent": "Bar chart - stacked by percentage", + "vertical": "Bar chart" + }, "tick-spacing-editor": { "content-require-space-from-the-right-side": "Require space from the right side", "gaps-options": { @@ -3521,6 +3529,10 @@ }, "name-show-unfilled-area": "Show unfilled area", "name-value-display": "Value display", + "suggestions": { + "basic": "Bar gauge", + "lcd": "Bar gauge - LCD" + }, "value-display-options": { "label-hidden": "Hidden", "label-text-color": "Text color", @@ -7849,6 +7861,7 @@ "suggestions": { "arc": "Gauge", "circular": "Circular gauge", + "no-thresholds": "Gauge - no thresholds", "style": { "circular": "Glowing", "simple": "Simple" @@ -11103,6 +11116,10 @@ "pie-chart-type-options": { "label-donut": "Donut", "label-pie": "Pie" + }, + "suggestions": { + "donut": "Donut chart", + "pie": "Pie chart" } }, "playlist": { @@ -13051,6 +13068,12 @@ "label-same-as-value": "Same as Value", "label-standard": "Standard" }, + "suggestions": { + "stat-color-background": "Stat - color background", + "stat-discrete-values": "Stat - discrete values", + "stat-discrete-values-color-background": "Stat - discrete values - color background", + "stat-single-string": "Stat - single string" + }, "text-alignment-options": { "label-auto": "Auto", "label-center": "Center" @@ -13490,6 +13513,16 @@ "label-threshold": "Threshold" } }, + "suggestions": { + "area": "Area chart", + "area-stacked": "Area chart - stacked", + "area-stacked-percentage": "Area chart - stacked by percentage", + "bar": "Bar chart", + "bar-stacked": "Bar chart - stacked", + "bar-stacked-percent": "Bar chart - stacked by percentage", + "line": "Line chart", + "line-smooth": "Line chart - smooth" + }, "timezones-editor": { "tooltip-add-timezone": "Add timezone", "tooltip-remove-timezone": "Remove timezone" From c29ed31c7ab977833642fcfe3aebd987dd09675f Mon Sep 17 00:00:00 2001 From: Tito Lins Date: Wed, 26 Nov 2025 17:59:22 +0100 Subject: [PATCH 34/94] alerting: set model refID if missing/mismatch (#114441) --- pkg/services/ngalert/models/alert_query.go | 20 +++++++++++++++++++ pkg/tests/api/alerting/api_prometheus_test.go | 14 ++++++------- pkg/tests/api/alerting/api_ruler_test.go | 14 ++++++++++++- .../test-data/rulegroup-1-export.json | 2 ++ .../test-data/rulegroup-2-export.json | 1 + .../test-data/rulegroup-3-export.json | 2 ++ 6 files changed, 45 insertions(+), 8 deletions(-) diff --git a/pkg/services/ngalert/models/alert_query.go b/pkg/services/ngalert/models/alert_query.go index 9d25d8cfa71..f86925eaa9b 100644 --- a/pkg/services/ngalert/models/alert_query.go +++ b/pkg/services/ngalert/models/alert_query.go @@ -165,6 +165,21 @@ func (aq *AlertQuery) setMaxDatapoints() error { return nil } +// setRefID sets the model refId if it's missing or invalid +func (aq *AlertQuery) setRefID() error { + if aq.modelProps == nil { + err := aq.setModelProps() + if err != nil { + return err + } + } + + if refID, ok := aq.modelProps["refId"].(string); !ok || refID != aq.RefID { + aq.modelProps["refId"] = aq.RefID + } + return nil +} + func (aq *AlertQuery) GetMaxDatapoints() (int64, error) { err := aq.setMaxDatapoints() if err != nil { @@ -256,6 +271,11 @@ func (aq *AlertQuery) GetModel() ([]byte, error) { return nil, err } + err = aq.setRefID() + if err != nil { + return nil, err + } + err = aq.setIntervalMS() if err != nil { return nil, err diff --git a/pkg/tests/api/alerting/api_prometheus_test.go b/pkg/tests/api/alerting/api_prometheus_test.go index 23681381fc9..61bc3195857 100644 --- a/pkg/tests/api/alerting/api_prometheus_test.go +++ b/pkg/tests/api/alerting/api_prometheus_test.go @@ -252,7 +252,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { "rules": [{ "state": "inactive", "name": "AlwaysFiring", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "duration": 10, "folderUid": "default", "uid": "%s", @@ -270,7 +270,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { }, { "state": "inactive", "name": "AlwaysFiringButSilenced", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "folderUid": "default", "uid": "%s", "health": "ok", @@ -317,7 +317,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { "rules": [{ "state": "inactive", "name": "AlwaysFiring", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "duration": 10, "folderUid": "default", "uid": "%s", @@ -335,7 +335,7 @@ func TestIntegrationPrometheusRules(t *testing.T) { }, { "state": "inactive", "name": "AlwaysFiringButSilenced", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "folderUid": "default", "uid": "%s", "health": "ok", @@ -639,7 +639,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) { "name": "AlwaysFiring", "uid": "%s", "folderUid": "default", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "duration": 10, "keepFiringFor": 15, "annotations": { @@ -656,7 +656,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) { "name": "AlwaysFiringButSilenced", "uid": "%s", "folderUid": "default", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "health": "ok", "isPaused": false, "type": "alerting", @@ -688,7 +688,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) { "name": "AlwaysFiring", "uid": "%s", "folderUid": "default", - "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]", + "query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"refId\":\"A\",\"type\":\"math\"}}]", "duration": 10, "keepFiringFor": 15, "annotations": { diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 18eb018b336..9e8855d8f19 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -1166,6 +1166,7 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "expression": "2 + 3 \u003e 1", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } }], @@ -1209,6 +1210,7 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "expression": "2 + 3 \u003e 1", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } }], @@ -1264,6 +1266,7 @@ func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) { "expression": "2 + 3 \u003e 1", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } }], @@ -1610,7 +1613,7 @@ func TestIntegrationRuleCreate(t *testing.T) { To: apimodels.Duration(15 * time.Minute), }, DatasourceUID: expr.DatasourceUID, - Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"type":"math"}`), + Model: json.RawMessage(`{"expression":"1","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}`), }, }, UpdatedBy: &apimodels.UserInfo{ @@ -2681,6 +2684,7 @@ func TestIntegrationQuota(t *testing.T) { "expression":"2 + 4 \u003E 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -2798,6 +2802,7 @@ func TestIntegrationDeleteFolderWithRules(t *testing.T) { "expression": "2 + 3 > 1", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } @@ -3285,6 +3290,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003e 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3331,6 +3337,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003e 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3683,6 +3690,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003e 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3729,6 +3737,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003e 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3872,6 +3881,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003C 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -3995,6 +4005,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003C 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } @@ -4093,6 +4104,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) { "expression":"2 + 3 \u003C 1", "intervalMs":1000, "maxDataPoints":43200, + "refId":"A", "type":"math" } } diff --git a/pkg/tests/api/alerting/test-data/rulegroup-1-export.json b/pkg/tests/api/alerting/test-data/rulegroup-1-export.json index dbf2ff417b0..18d8b8cea40 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-1-export.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-1-export.json @@ -23,6 +23,7 @@ "expression": "0 \u003e 0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } @@ -55,6 +56,7 @@ "expression": "0 == 0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } diff --git a/pkg/tests/api/alerting/test-data/rulegroup-2-export.json b/pkg/tests/api/alerting/test-data/rulegroup-2-export.json index 5f85d830260..43428689eb1 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-2-export.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-2-export.json @@ -23,6 +23,7 @@ "expression": "0/0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } diff --git a/pkg/tests/api/alerting/test-data/rulegroup-3-export.json b/pkg/tests/api/alerting/test-data/rulegroup-3-export.json index 5a1ba9e0eac..76eda60f08d 100644 --- a/pkg/tests/api/alerting/test-data/rulegroup-3-export.json +++ b/pkg/tests/api/alerting/test-data/rulegroup-3-export.json @@ -23,6 +23,7 @@ "expression": "0 \u003e 0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } @@ -54,6 +55,7 @@ "expression": "0 == 0", "intervalMs": 1000, "maxDataPoints": 43200, + "refId": "A", "type": "math" } } From d732bb2751175b3fd2c1288294dd98d80316dc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 26 Nov 2025 19:26:53 +0100 Subject: [PATCH 35/94] PanelChrome: Enable new panel padding by default (#114492) --- .../configure-grafana/feature-toggles/index.md | 1 + .../grafana-data/src/types/featureToggles.gen.ts | 2 +- pkg/services/featuremgmt/registry.go | 6 +++--- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 14 ++++++++------ 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index c05ecd45bd9..7ca66464680 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -98,6 +98,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | | `newVizSuggestions` | Enable new visualization suggestions | | `preventPanelChromeOverflow` | Restrict PanelChrome contents with overflow: hidden; | +| `newPanelPadding` | Increases panel padding globally | | `transformationsEmptyPlaceholder` | Show transformation quick-start cards in empty transformations state | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 0d20e44b19e..40f2abe4d73 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1150,7 +1150,7 @@ export interface FeatureToggles { pluginStoreServiceLoading?: boolean; /** * Increases panel padding globally - * @default false + * @default true */ newPanelPadding?: boolean; /** diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 65f94c24eac..91b59bf711d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1895,10 +1895,10 @@ var ( { Name: "newPanelPadding", Description: "Increases panel padding globally", - Stage: FeatureStageExperimental, - FrontendOnly: false, + Stage: FeatureStagePublicPreview, + FrontendOnly: true, Owner: grafanaDashboardsSquad, - Expression: "false", + Expression: "true", }, { Name: "onlyStoreActionSets", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 17fdf27f33d..f161087ecb1 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -257,7 +257,7 @@ newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false -newPanelPadding,experimental,@grafana/dashboards-squad,false,false,false +newPanelPadding,preview,@grafana/dashboards-squad,false,false,true onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b88204db708..1217d1f5a28 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -742,10 +742,6 @@ const ( // Load plugins on store service startup instead of wire provider, and call RegisterFixedRoles after all plugins are loaded FlagPluginStoreServiceLoading = "pluginStoreServiceLoading" - // FlagNewPanelPadding - // Increases panel padding globally - FlagNewPanelPadding = "newPanelPadding" - // FlagOnlyStoreActionSets // When storing dashboard and folder resource permissions, only store action sets and not the full list of underlying permission FlagOnlyStoreActionSets = "onlyStoreActionSets" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 1093b643232..8dee7f8206b 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -551,7 +551,6 @@ "description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -565,7 +564,6 @@ "description": "Enables the UI to use rules backend-side filters 100% compatible with the frontend filters", "stage": "experimental", "codeowner": "@grafana/alerting-squad", - "hideFromAdminPage": true, "hideFromDocs": true } }, @@ -2361,14 +2359,18 @@ { "metadata": { "name": "newPanelPadding", - "resourceVersion": "1763734583253", - "creationTimestamp": "2025-11-12T15:40:46Z" + "resourceVersion": "1764168915089", + "creationTimestamp": "2025-11-12T15:40:46Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-26 14:55:15.089551 +0000 UTC" + } }, "spec": { "description": "Increases panel padding globally", - "stage": "experimental", + "stage": "preview", "codeowner": "@grafana/dashboards-squad", - "expression": "false" + "frontend": true, + "expression": "true" } }, { From 7eb467e561ef7b01fb259ba866f5a1e9c956163c Mon Sep 17 00:00:00 2001 From: maicon Date: Wed, 26 Nov 2025 16:24:34 -0300 Subject: [PATCH 36/94] provisioning: acquire server lock before provisioning dashboards+folders (#114488) * provisioning: acquire server lock before provisioning dashboards+folders Signed-off-by: Maicon Costa --------- Signed-off-by: Maicon Costa --- pkg/server/wire_gen.go | 4 +- .../provisioning/dashboards/dashboard.go | 67 ++++++++++++++----- pkg/services/provisioning/provisioning.go | 6 +- .../provisioning/provisioning_test.go | 4 +- pkg/setting/setting.go | 13 ++++ 5 files changed, 75 insertions(+), 19 deletions(-) diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 64ffd48ca9f..75341e77f78 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -668,7 +668,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg) amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg) promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService) - provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl) + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService) if err != nil { return nil, err } @@ -1312,7 +1312,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac azurePromMigrationService := promtypemigration.ProvideAzurePromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg) amazonPromMigrationService := promtypemigration.ProvideAmazonPromMigrationService(service15, inMemory, repoManager, pluginInstaller, cfg) promTypeMigrationProviderImpl := promtypemigration.ProvidePromTypeMigrationProvider(serverLockService, featureToggles, azurePromMigrationService, amazonPromMigrationService) - provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl) + provisioningServiceImpl, err := provisioning.ProvideService(accessControl, cfg, sqlStore, pluginstoreService, dBstore, serviceService, notificationService, dashboardProvisioningService, service15, correlationsService, dashboardService, folderimplService, service13, searchService, quotaService, secretsService, orgService, receiverPermissionsService, tracingService, dualwriteService, promTypeMigrationProviderImpl, serverLockService) if err != nil { return nil, err } diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index e0c24c6ab07..72b980e4198 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -2,6 +2,7 @@ package dashboards import ( "context" + "errors" "fmt" "os" "time" @@ -9,10 +10,12 @@ import ( dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" folderV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/provisioning/utils" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) @@ -28,7 +31,7 @@ type DashboardProvisioner interface { } // DashboardProvisionerFactory creates DashboardProvisioners based on input -type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (DashboardProvisioner, error) +type DashboardProvisionerFactory func(context.Context, string, dashboards.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (DashboardProvisioner, error) // Provisioner is responsible for syncing dashboard from disk to Grafana's database. type Provisioner struct { @@ -38,6 +41,8 @@ type Provisioner struct { duplicateValidator duplicateValidator provisioner dashboards.DashboardProvisioningService dual dualwrite.Service + serverLock *serverlock.ServerLockService + cfg *setting.Cfg } func (provider *Provisioner) HasDashboardSources() bool { @@ -45,7 +50,7 @@ func (provider *Provisioner) HasDashboardSources() bool { } // New returns a new DashboardProvisioner -func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service) (DashboardProvisioner, error) { +func New(ctx context.Context, configDirectory string, provisioner dashboards.DashboardProvisioningService, cfg *setting.Cfg, orgService org.Service, dashboardStore utils.DashboardStore, folderService folder.Service, dual dualwrite.Service, serverLockService *serverlock.ServerLockService) (DashboardProvisioner, error) { logger := log.New("provisioning.dashboard") cfgReader := &configReader{path: configDirectory, log: logger, orgExists: utils.NewOrgExistsChecker(orgService)} configs, err := cfgReader.readConfig(ctx) @@ -78,6 +83,8 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das duplicateValidator: newDuplicateValidator(logger, fileReaders), provisioner: provisioner, dual: dual, + serverLock: serverLockService, + cfg: cfg, } return d, nil @@ -95,23 +102,53 @@ func (provider *Provisioner) Provision(ctx context.Context) error { } } - provider.log.Info("starting to provision dashboards") + var errProvisioning error - for _, reader := range provider.fileReaders { - if err := reader.walkDisk(ctx); err != nil { - if os.IsNotExist(err) { - // don't stop the provisioning service in case the folder is missing. The folder can appear after the startup - provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err) - return nil - } - - return fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err) + // retry obtaining the lock for 20 attempts + retryOpt := func(attempts int) error { + if attempts < 20 { + return nil } + return errors.New("retries exhausted") } - provider.duplicateValidator.validate() - provider.log.Info("finished to provision dashboards") - return nil + lockTimeConfig := serverlock.LockTimeConfig{ + // if a replica crashes while holding the lock, other replicas can obtain the + // lock after this duration (15s default value, might be configured via config file) + MaxInterval: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds) * time.Second, + + // wait beetween 100ms and 1s before retrying to obtain the lock (default values, might be configured via config file) + MinWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMinWaitMs) * time.Millisecond, + MaxWait: time.Duration(provider.cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs) * time.Millisecond, + } + + // this means that if we fail to obtain the lock after ~10 seconds, we return an error + lockErr := provider.serverLock.LockExecuteAndReleaseWithRetries(ctx, "provisioning_dashboards", lockTimeConfig, func(ctx context.Context) { + provider.log.Info("starting to provision dashboards") + + for _, reader := range provider.fileReaders { + if err := reader.walkDisk(ctx); err != nil { + if os.IsNotExist(err) { + // don't stop the provisioning service in case the folder is missing. The folder can appear after the startup + provider.log.Warn("Failed to provision config", "name", reader.Cfg.Name, "error", err) + return + } + + errProvisioning = fmt.Errorf("failed to provision config %v: %w", reader.Cfg.Name, err) + return + } + } + + provider.duplicateValidator.validate() + provider.log.Info("finished to provision dashboards") + }, retryOpt) + + if lockErr != nil { + provider.log.Error("Failed to obtain dashboard provisioning lock", "error", lockErr) + return lockErr + } + + return errProvisioning } // CleanUpOrphanedDashboards deletes provisioned dashboards missing a linked reader. diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index c79a9625d50..bc7c53e0cb2 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/dskit/services" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -64,6 +65,7 @@ func ProvideService( tracer tracing.Tracer, dual dualwrite.Service, promTypeMigrationProvider promtypemigration.PromTypeMigrationProvider, + serverLockService *serverlock.ServerLockService, ) (*ProvisioningServiceImpl, error) { s := &ProvisioningServiceImpl{ Cfg: cfg, @@ -92,6 +94,7 @@ func ProvideService( tracer: tracer, migratePrometheusType: promTypeMigrationProvider.Run, dual: dual, + serverLock: serverLockService, } s.NamedService = services.NewBasicService(s.starting, s.running, nil).WithName(ServiceName) @@ -166,7 +169,7 @@ func (ps *ProvisioningServiceImpl) running(ctx context.Context) error { func (ps *ProvisioningServiceImpl) setDashboardProvisioner() error { dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards") - dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.orgService, ps.dashboardService, ps.folderService, ps.dual) + dashProvisioner, err := ps.newDashboardProvisioner(context.Background(), dashboardPath, ps.dashboardProvisioningService, ps.Cfg, ps.orgService, ps.dashboardService, ps.folderService, ps.dual, ps.serverLock) if err != nil { return fmt.Errorf("%v: %w", "Failed to create provisioner", err) } @@ -242,6 +245,7 @@ type ProvisioningServiceImpl struct { resourcePermissions accesscontrol.ReceiverPermissionsService tracer tracing.Tracer dual dualwrite.Service + serverLock *serverlock.ServerLockService migratePrometheusType func(context.Context) error } diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index 8b513ba321e..b94d7beac37 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/serverlock" dashboardstore "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" @@ -20,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/utils" "github.com/grafana/grafana/pkg/services/searchV2" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" ) @@ -160,7 +162,7 @@ func setup(t *testing.T) *serviceTestStruct { searchStub := searchV2.NewStubSearchService() service, err := newProvisioningServiceImpl( - func(context.Context, string, dashboardstore.DashboardProvisioningService, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service) (dashboards.DashboardProvisioner, error) { + func(context.Context, string, dashboardstore.DashboardProvisioningService, *setting.Cfg, org.Service, utils.DashboardStore, folder.Service, dualwrite.Service, *serverlock.ServerLockService) (dashboards.DashboardProvisioner, error) { serviceTest.dashboardProvisionerInstantiations++ return serviceTest.mock, nil }, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index a8b48c67f29..470b6910751 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -150,6 +150,11 @@ type Cfg struct { PluginsPath string EnterpriseLicensePath string + // Classic Provisioning settings + ClassicProvisioningDashboardsServerLockMaxIntervalSeconds int64 + ClassicProvisioningDashboardsServerLockMinWaitMs int64 + ClassicProvisioningDashboardsServerLockMaxWaitMs int64 + // SMTP email settings Smtp SmtpSettings @@ -1221,6 +1226,8 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { return err } + cfg.readClassicProvisioningSettings(iniFile) + // read dashboard settings dashboards := iniFile.Section("dashboards") cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) @@ -2107,6 +2114,12 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { return nil } +func (cfg *Cfg) readClassicProvisioningSettings(iniFile *ini.File) { + cfg.ClassicProvisioningDashboardsServerLockMinWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_min_wait_ms").MustInt64(100) + cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_wait_ms").MustInt64(1000) + cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_interval_seconds").MustInt64(15) +} + func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error { provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "") cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.HomePath) From b08e7bc3736d9204d1f4d48d49b3315754a69380 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Wed, 26 Nov 2025 16:32:37 -0400 Subject: [PATCH 37/94] TimePicker: Show new shortcut for zoom out when experimental flag toggled on (#114506) * fix(time-picker): show new shortcut for zoom out when flag toggled on * chore(i18n): extract translations --- .../DateTimePickers/TimeRangePicker.test.tsx | 54 ++++++++++++++++++- .../DateTimePickers/TimeRangePicker.tsx | 24 ++++++--- public/locales/en-US/grafana.json | 3 +- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx index c7399e65a4f..51d24c5bc8e 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { dateTime, makeTimeRange, TimeRange } from '@grafana/data'; +import { dateTime, makeTimeRange, TimeRange, BootData } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { TimeRangeProvider } from './TimeRangeContext'; @@ -152,6 +152,58 @@ it('does not submit wrapping forms', async () => { expect(onSubmit).not.toHaveBeenCalled(); }); +it('shows CTRL+Z in zoom out tooltip when feature flag is disabled', async () => { + window.grafanaBootData = { + settings: { + featureToggles: { + newTimeRangeZoomShortcuts: false, + }, + }, + } as BootData; + + render( + {}} + onChange={(value) => {}} + value={value} + onMoveBackward={() => {}} + onMoveForward={() => {}} + onZoom={() => {}} + /> + ); + + const zoomButton = screen.getByLabelText('Zoom out time range'); + await userEvent.hover(zoomButton); + + expect(await screen.findByText(/CTRL\+Z/)).toBeInTheDocument(); +}); + +it('shows t - in zoom out tooltip when feature flag is enabled', async () => { + window.grafanaBootData = { + settings: { + featureToggles: { + newTimeRangeZoomShortcuts: true, + }, + }, + } as BootData; + + render( + {}} + onChange={(value) => {}} + value={value} + onMoveBackward={() => {}} + onMoveForward={() => {}} + onZoom={() => {}} + /> + ); + + const zoomButton = screen.getByLabelText('Zoom out time range'); + await userEvent.hover(zoomButton); + + expect(await screen.findByText(/t -/)).toBeInTheDocument(); +}); + describe('TimePickerTooltip', () => { beforeAll(() => { const mockIntl = { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx index b26a6e4c36d..0719693e625 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.tsx @@ -19,6 +19,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; import { useStyles2 } from '../../themes/ThemeContext'; +import { getFeatureToggle } from '../../utils/featureToggle'; import { ButtonGroup } from '../Button/ButtonGroup'; import { getModalStyles } from '../Modal/getModalStyles'; import { getPortalContainer } from '../Portal/Portal'; @@ -243,13 +244,22 @@ export function TimeRangePicker(props: TimeRangePickerProps) { TimeRangePicker.displayName = 'TimeRangePicker'; -const ZoomOutTooltip = () => ( - <> - - Time range zoom out
CTRL+Z -
- -); +const ZoomOutTooltip = () => { + const newShortcuts = getFeatureToggle('newTimeRangeZoomShortcuts'); + return ( + <> + {newShortcuts ? ( + + Time range zoom out
t - +
+ ) : ( + + Time range zoom out
CTRL+Z +
+ )} + + ); +}; export const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; timeZone?: TimeZone }) => { const styles = useStyles2(getLabelStyles); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index aa73bf3cd22..c525ce2e768 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13411,7 +13411,8 @@ "forwards-time-aria-label": "Move time range forwards", "to": "to", "zoom-out-button": "Zoom out time range", - "zoom-out-tooltip": "Time range zoom out
CTRL+Z" + "zoom-out-tooltip": "Time range zoom out
CTRL+Z", + "zoom-out-tooltip-new": "Time range zoom out
t -" }, "time-range": { "apply": "Apply time range", From 399370bc2c7607a60074b898bd01865f2064f735 Mon Sep 17 00:00:00 2001 From: Jesse David Peterson Date: Wed, 26 Nov 2025 16:41:02 -0400 Subject: [PATCH 38/94] TimeRange: Avoid x-axis pan jump caused by data loading latency (#114496) * fix(time-range): avoid x-axis pan jump caused by data loading latency * refactor(time-range): use a more semantically meaningful names --- .../uPlot/config/UPlotConfigBuilder.ts | 2 +- .../XAxisInteractionAreaPlugin.test.tsx | 16 ++++++++++++++- .../plugins/XAxisInteractionAreaPlugin.tsx | 7 +++++-- .../app/core/components/TimeSeries/utils.ts | 20 +++++++++++++++++-- .../core/components/TimelineChart/utils.ts | 18 +++++++++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 27322714477..6bc91793236 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -29,7 +29,7 @@ const cursorDefaults: Cursor = { type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData; type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData; -type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number }; +type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number; isTimeRangePending?: boolean }; export class UPlotConfigBuilder { readonly uid = Math.random().toString(36).slice(2); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.test.tsx b/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.test.tsx index d9a6e787251..e9e471b82af 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.test.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.test.tsx @@ -137,7 +137,7 @@ describe('XAxisInteractionAreaPlugin', () => { expect(mockQueryZoom).not.toHaveBeenCalled(); }); - it('should set isPanning state during drag and clear on mouseup', () => { + it('should set isPanning state during drag and mark isTimeRangePending on mouseup', () => { setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom); xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true })); @@ -153,6 +153,20 @@ describe('XAxisInteractionAreaPlugin', () => { document.dispatchEvent(new MouseEvent('mouseup', { clientX: 350, bubbles: true })); + expect(mockConfigBuilder.setState).toHaveBeenCalledWith({ + isPanning: true, + min: expectedRange.from, + max: expectedRange.to, + isTimeRangePending: true, + }); + }); + + it('should clear isPanning state immediately for small drags below threshold', () => { + setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom); + + xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true })); + document.dispatchEvent(new MouseEvent('mouseup', { clientX: 402, bubbles: true })); + expect(mockConfigBuilder.setState).toHaveBeenCalledWith({ isPanning: false }); }); }); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.tsx index 694f372307d..5b70546b55a 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/XAxisInteractionAreaPlugin.tsx @@ -96,11 +96,14 @@ export const setupXAxisPan = ( xAxisEl.style.cursor = 'grab'; - config.setState({ isPanning: false }); + const isSignificantDrag = Math.abs(dragPixels) >= MIN_PAN_DIST; - if (Math.abs(dragPixels) >= MIN_PAN_DIST) { + if (isSignificantDrag) { const newRange = calculatePanRange(startMin, startMax, dragPixels, u.bbox.width); + config.setState({ isPanning: true, min: newRange.from, max: newRange.to, isTimeRangePending: true }); queryZoom(newRange); + } else { + config.setState({ isPanning: false }); } document.removeEventListener('mousemove', onMove); diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index f81de163b7f..88a82ec299c 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -138,10 +138,26 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ range: () => { const state = builder.getState(); if (state.isPanning) { + if (state.isTimeRangePending) { + const timeRange = getTimeRange(); + const propsFrom = timeRange.from.valueOf(); + const propsTo = timeRange.to.valueOf(); + + const MIN_TIMESPAN_MS = 1; + const fromMatches = Math.abs(propsFrom - state.min) <= MIN_TIMESPAN_MS; + const toMatches = Math.abs(propsTo - state.max) <= MIN_TIMESPAN_MS; + const timeRangeHasUpdated = fromMatches && toMatches; + + if (timeRangeHasUpdated) { + builder.setState({ isPanning: false }); + return [propsFrom, propsTo]; + } + } + return [state.min, state.max]; } - const r = getTimeRange(); - return [r.from.valueOf(), r.to.valueOf()]; + const timeRange = getTimeRange(); + return [timeRange.from.valueOf(), timeRange.to.valueOf()]; }, }); diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 955f1ea11b2..264c5498be9 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -159,6 +159,24 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ( range: (u) => { const state = builder.getState(); if (state.isPanning) { + if (state.isTimeRangePending) { + const propsRange = coreConfig.xRange(u); + const propsFrom = propsRange[0]; + const propsTo = propsRange[1]; + + if (propsFrom != null && propsTo != null) { + const MIN_TIMESPAN_MS = 1; + const fromMatches = Math.abs(propsFrom - state.min) <= MIN_TIMESPAN_MS; + const toMatches = Math.abs(propsTo - state.max) <= MIN_TIMESPAN_MS; + const timeRangeHasUpdated = fromMatches && toMatches; + + if (timeRangeHasUpdated) { + builder.setState({ isPanning: false }); + return propsRange; + } + } + } + return [state.min, state.max]; } return coreConfig.xRange(u); From a455f9700d6b780133635f7d8af520669b2b0983 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Wed, 26 Nov 2025 22:07:24 +0100 Subject: [PATCH 39/94] Azure: Enable resource picker updates (#114089) * Default resource picker updates to true * Update toggle docs --- .../configure-grafana/feature-toggles/index.md | 2 +- packages/grafana-data/src/types/featureToggles.gen.ts | 2 +- pkg/services/featuremgmt/registry.go | 4 ++-- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.json | 11 +++++++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 7ca66464680..f0f12cce1b6 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -66,6 +66,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `grafanaAssistantInProfilesDrilldown` | Enables integration with Grafana Assistant in Profiles Drilldown | Yes | | `sharingDashboardImage` | Enables image sharing functionality for dashboards | Yes | | `tabularNumbers` | Use fixed-width numbers globally in the UI | | +| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | Yes | | `tempoSearchBackendMigration` | Run search queries through the tempo backend | | ## Public preview feature toggles @@ -95,7 +96,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `localeFormatPreference` | Specifies the locale so the correct format for numbers and dates can be shown | | `logsPanelControls` | Enables a control component for the logs panel in Explore | | `interactiveLearning` | Enables the interactive learning app | -| `azureResourcePickerUpdates` | Enables the updated Azure Monitor resource picker | | `newVizSuggestions` | Enable new visualization suggestions | | `preventPanelChromeOverflow` | Restrict PanelChrome contents with overflow: hidden; | | `newPanelPadding` | Increases panel padding globally | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 40f2abe4d73..33750126afa 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1091,7 +1091,7 @@ export interface FeatureToggles { graphiteBackendMode?: boolean; /** * Enables the updated Azure Monitor resource picker - * @default false + * @default true */ azureResourcePickerUpdates?: boolean; /** diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 91b59bf711d..37ee3001e99 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1801,10 +1801,10 @@ var ( { Name: "azureResourcePickerUpdates", Description: "Enables the updated Azure Monitor resource picker", - Stage: FeatureStagePublicPreview, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaPartnerPluginsSquad, - Expression: "false", + Expression: "true", }, { Name: "prometheusTypeMigration", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index f161087ecb1..f66bc50bbc7 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -245,7 +245,7 @@ teamFolders,experimental,@grafana/grafana-search-navigate-organise,false,false,f interactiveLearning,preview,@grafana/pathfinder,false,false,false alertingTriage,experimental,@grafana/alerting-squad,false,false,false graphiteBackendMode,privatePreview,@grafana/partner-datasources,false,false,false -azureResourcePickerUpdates,preview,@grafana/partner-datasources,false,false,true +azureResourcePickerUpdates,GA,@grafana/partner-datasources,false,false,true prometheusTypeMigration,experimental,@grafana/partner-datasources,false,true,false pluginContainers,privatePreview,@grafana/plugins-platform-backend,false,true,false tempoSearchBackendMigration,GA,@grafana/oss-big-tent,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 8dee7f8206b..46e5eda90a7 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -766,16 +766,19 @@ { "metadata": { "name": "azureResourcePickerUpdates", - "resourceVersion": "1763734583253", + "resourceVersion": "1764153435365", "creationTimestamp": "2025-07-31T22:56:50Z", - "deletionTimestamp": "2025-08-01T11:30:17Z" + "deletionTimestamp": "2025-08-01T11:30:17Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-11-26 10:37:15.365919 +0000 UTC" + } }, "spec": { "description": "Enables the updated Azure Monitor resource picker", - "stage": "preview", + "stage": "GA", "codeowner": "@grafana/partner-datasources", "frontend": true, - "expression": "false" + "expression": "true" } }, { From f116539541d7f9cd56140f546becc5a150ef4bde Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Wed, 26 Nov 2025 15:53:00 -0600 Subject: [PATCH 40/94] Unified Storage: Update readme (#114415) * update readme * Adds message about creating database * update sample storage config --- pkg/storage/unified/README.md | 52 +++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index 034147d84d9..7131178e8a1 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -202,30 +202,58 @@ then run: kubectl --kubeconfig=./grafana.kubeconfig create -f folder-generate.yaml ``` -### Run as a GRPC service +### Run as a separate GRPC service -#### Start GRPC storage-server +It is recommended to use a separate config file for the storage-server. Create a file `conf/storage-server.ini` with the following content: -Make sure you have the gRPC address in the `[grafana-apiserver]` section of your config file: ```ini +app_mode = development + +target = storage-server + +[database] +type = mysql +host = 127.0.0.1:3306 +name = unified-storage +user = root +password = rootpass +skip_migrations = true +ensure_default_org_and_user = false + +[grpc_server] +network = "tcp" +address = "127.0.0.1:10000" + [grafana-apiserver] -; your gRPC server address -address = localhost:10000 -``` +storage_type = unified -You also need the `[grpc_server_authentication]` section to authenticate incoming requests: -```ini [grpc_server_authentication] -; http url to Grafana's signing keys to validate incoming id tokens -signing_keys_url = http://localhost:3000/api/signing-keys/keys +signing_keys_url = http://localhost:3011/api/signing-keys/keys mode = "on-prem" + +[feature_toggles] +kubernetesDashboards = true +kubernetesFolders = true +unifiedStorage = true +unifiedStorageHistoryPruner = true +unifiedStorageSearch = true +unifiedStorageSearchPermissionFiltering = false +unifiedStorageSearchSprinkles = false + +[unified_storage] +enable_search = true +https_skip_verify = true ``` -This currently only works with a separate database configuration (see previous section). +You should also have a MySQL database running. You can create one with our docker blocks by running: +```bash +make devenv sources=mysql +``` +The database credentials in the example above will work with the default mysql docker block. You'll also need to create a database named `unified-storage`. Start the storage-server with: ```sh -GF_DEFAULT_TARGET=storage-server ./bin/grafana server target +./bin/grafana server target --config conf/storage-server.ini ``` The GRPC service will listen on port 10000 From a8aef11926176e442e6fa1cefbdf4b1ada06c828 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:59:21 -0800 Subject: [PATCH 41/94] Geomap: Fix data filter for layers (#114515) * Geomap: Fix data filter for layers * Simplify comments --- .../plugins/panel/geomap/utils/layers.test.ts | 123 ++++++++++++++++++ .../app/plugins/panel/geomap/utils/layers.ts | 8 +- 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/panel/geomap/utils/layers.test.ts diff --git a/public/app/plugins/panel/geomap/utils/layers.test.ts b/public/app/plugins/panel/geomap/utils/layers.test.ts new file mode 100644 index 00000000000..1310d892481 --- /dev/null +++ b/public/app/plugins/panel/geomap/utils/layers.test.ts @@ -0,0 +1,123 @@ +jest.mock('ol-mapbox-style', () => ({})); +jest.mock('geotiff', () => ({})); + +import BaseLayer from 'ol/layer/Base'; + +import { + DataFrame, + DataQueryRequest, + FieldType, + LoadingState, + MapLayerHandler, + MapLayerOptions, + PanelData, + TimeRange, +} from '@grafana/data'; + +import { applyLayerFilter } from './layers'; + +describe('applyLayerFilter', () => { + const createDataFrame = (refId: string): DataFrame => ({ + refId, + fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3], config: {} }], + length: 3, + }); + + it('should apply filter when query exists and is visible', () => { + const update = jest.fn(); + const handler: MapLayerHandler = { + init: () => ({}) as BaseLayer, + update, + }; + const options: MapLayerOptions = { + name: 'Test', + type: 'markers', + filterData: { id: 'byRefId', options: 'A' }, + }; + const panelData: PanelData = { + series: [createDataFrame('A'), createDataFrame('B')], + state: LoadingState.Done, + timeRange: {} as TimeRange, + request: { targets: [{ refId: 'A' }, { refId: 'B' }] } as DataQueryRequest, + }; + + applyLayerFilter(handler, options, panelData); + + expect(update).toHaveBeenCalledWith( + expect.objectContaining({ + series: [createDataFrame('A')], + }) + ); + }); + + it('should return empty series when query exists but is hidden', () => { + const update = jest.fn(); + const handler: MapLayerHandler = { + init: () => ({}) as BaseLayer, + update, + }; + const options: MapLayerOptions = { + name: 'Test', + type: 'markers', + filterData: { id: 'byRefId', options: 'A' }, + }; + const panelData: PanelData = { + series: [createDataFrame('B')], + state: LoadingState.Done, + timeRange: {} as TimeRange, + request: { targets: [{ refId: 'A', hide: true }, { refId: 'B' }] } as DataQueryRequest, + }; + + applyLayerFilter(handler, options, panelData); + + expect(update).toHaveBeenCalledWith( + expect.objectContaining({ + series: [], + }) + ); + }); + + it('should not apply filter when query does not exist', () => { + const update = jest.fn(); + const handler: MapLayerHandler = { + init: () => ({}) as BaseLayer, + update, + }; + const options: MapLayerOptions = { + name: 'Test', + type: 'markers', + filterData: { id: 'byRefId', options: 'C' }, + }; + const panelData: PanelData = { + series: [createDataFrame('A'), createDataFrame('B')], + state: LoadingState.Done, + timeRange: {} as TimeRange, + request: { targets: [{ refId: 'A' }, { refId: 'B' }] } as DataQueryRequest, + }; + + applyLayerFilter(handler, options, panelData); + + expect(update).toHaveBeenCalledWith(panelData); + }); + + it('should pass through all data when no filter is configured', () => { + const update = jest.fn(); + const handler: MapLayerHandler = { + init: () => ({}) as BaseLayer, + update, + }; + const options: MapLayerOptions = { + name: 'Test', + type: 'markers', + }; + const panelData: PanelData = { + series: [createDataFrame('A'), createDataFrame('B')], + state: LoadingState.Done, + timeRange: {} as TimeRange, + }; + + applyLayerFilter(handler, options, panelData); + + expect(update).toHaveBeenCalledWith(panelData); + }); +}); diff --git a/public/app/plugins/panel/geomap/utils/layers.ts b/public/app/plugins/panel/geomap/utils/layers.ts index f20d8891ade..925ec1a82c4 100644 --- a/public/app/plugins/panel/geomap/utils/layers.ts +++ b/public/app/plugins/panel/geomap/utils/layers.ts @@ -26,7 +26,13 @@ export const applyLayerFilter = ( let panelData = panelDataProps; if (options.filterData) { const matcherFunc = getFrameMatchers(options.filterData); - if (panelData.series.some(matcherFunc)) { + + const queryExists = panelData.request?.targets.some((target) => { + return target.refId === options.filterData?.options; + }); + + // Only apply filter if the target query exists + if (queryExists) { panelData = { ...panelData, series: panelData.series.filter(matcherFunc), From 84a07be6e4e1acd8f064c3b390c30188d5703afc Mon Sep 17 00:00:00 2001 From: Eric Shields Date: Wed, 26 Nov 2025 15:47:32 -0800 Subject: [PATCH 42/94] Chore: Finalize removal of updateNode & expandOrFilter (#114202) - Remove references to, and related private functions for, `updateNode` and `expandOrFilter` - Remove obsolete tests - Update all usages of `updateNode` to `filterNode` - Integrate `expandOrFilter` functionality into `filterNode` - Add profiler to `filterNode` - Add `.claude` to `.gitignore` IDE junk section - Unit tests for `toggleExpandedNode` and `filterNode` - Add profiler to `toggleExpandedNode` Fixes: https://github.com/grafana/grafana-operator-experience-squad/issues/1566 --- .gitignore | 1 + .../actions/scopeActions.test.tsx | 28 +- .../commandPalette/actions/scopeActions.tsx | 10 +- .../commandPalette/actions/scopesUtils.ts | 5 +- .../selector/ScopesSelectorService.test.ts | 536 ++++++++++++------ .../scopes/selector/ScopesSelectorService.ts | 134 ++--- 6 files changed, 425 insertions(+), 289 deletions(-) diff --git a/.gitignore b/.gitignore index f1a90d05693..5302a698c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ public/css/*.min.css .vs/ .cursor/ .devcontainer/ +.claude/ .eslintcache .stylelintcache diff --git a/public/app/features/commandPalette/actions/scopeActions.test.tsx b/public/app/features/commandPalette/actions/scopeActions.test.tsx index 0d828d66a81..126877224de 100644 --- a/public/app/features/commandPalette/actions/scopeActions.test.tsx +++ b/public/app/features/commandPalette/actions/scopeActions.test.tsx @@ -22,7 +22,7 @@ jest.mock('./scopesUtils', () => { }); const mockScopeServicesState = { - updateNode: jest.fn(), + filterNode: jest.fn(), selectScope: jest.fn(), resetSelection: jest.fn(), nodes: {}, @@ -99,12 +99,12 @@ describe('useRegisterScopesActions', () => { }); it('should register scope tree actions and return scopesRow when scopes are selected', () => { - const mockUpdateNode = jest.fn(); + const mockFilterNode = jest.fn(); // First run with empty scopes in the scopes service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, selectedScopes: [{ scopeId: 'scope1', name: 'Scope 1' }], }); @@ -112,14 +112,14 @@ describe('useRegisterScopesActions', () => { return useRegisterScopesActions('', jest.fn()); }); - expect(mockUpdateNode).toHaveBeenCalledWith('', true, ''); + expect(mockFilterNode).toHaveBeenCalledWith('', ''); expect(useRegisterActions).toHaveBeenLastCalledWith([rootScopeAction], [[rootScopeAction]]); expect(result.current.scopesRow).toBeDefined(); // Simulate loading of scopes in the service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, selectedScopes: [{ scopeId: 'scope1', name: 'Scope 1' }], nodes, tree, @@ -151,12 +151,12 @@ describe('useRegisterScopesActions', () => { }); it('should load next level of scopes', () => { - const mockUpdateNode = jest.fn(); + const mockFilterNode = jest.fn(); // First run with empty scopes in the scopes service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, nodes, tree, }); @@ -165,7 +165,7 @@ describe('useRegisterScopesActions', () => { return useRegisterScopesActions('', jest.fn(), 'scopes/scope1'); }); - expect(mockUpdateNode).toHaveBeenCalledWith('scope1', true, ''); + expect(mockFilterNode).toHaveBeenCalledWith('scope1', ''); }); it('does not return component if no scopes are selected', () => { @@ -259,12 +259,12 @@ describe('useRegisterScopesActions', () => { }); it('should not use global scope search when searching in some deeper scope category', async () => { - const mockUpdateNode = jest.fn(); + const mockFilterNode = jest.fn(); // First run with empty scopes in the scopes service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, nodes, tree, }); @@ -273,17 +273,17 @@ describe('useRegisterScopesActions', () => { return useRegisterScopesActions('something', jest.fn(), 'scopes/scope1'); }); - expect(mockUpdateNode).toHaveBeenCalledWith('scope1', true, 'something'); + expect(mockFilterNode).toHaveBeenCalledWith('scope1', 'something'); expect(mockScopeServicesState.searchAllNodes).not.toHaveBeenCalled(); }); it('should not use global scope search if feature flag is off', async () => { config.featureToggles.scopeSearchAllLevels = false; - const mockUpdateNode = jest.fn(); + const mockFilterNode = jest.fn(); // First run with empty scopes in the scopes service (useScopeServicesState as jest.Mock).mockReturnValue({ ...mockScopeServicesState, - updateNode: mockUpdateNode, + filterNode: mockFilterNode, nodes, tree, }); @@ -292,7 +292,7 @@ describe('useRegisterScopesActions', () => { return useRegisterScopesActions('something', jest.fn(), ''); }); - expect(mockUpdateNode).toHaveBeenCalledWith('', true, 'something'); + expect(mockFilterNode).toHaveBeenCalledWith('', 'something'); expect(mockScopeServicesState.searchAllNodes).not.toHaveBeenCalled(); }); diff --git a/public/app/features/commandPalette/actions/scopeActions.tsx b/public/app/features/commandPalette/actions/scopeActions.tsx index bde41152237..e85d136d2f6 100644 --- a/public/app/features/commandPalette/actions/scopeActions.tsx +++ b/public/app/features/commandPalette/actions/scopeActions.tsx @@ -58,22 +58,22 @@ export function useRegisterScopesActions( * @param parentId */ function useScopeTreeActions(searchQuery: string, parentId?: string | null) { - const { updateNode, selectScope, resetSelection, nodes, tree, selectedScopes } = useScopeServicesState(); + const { filterNode, selectScope, resetSelection, nodes, tree, selectedScopes } = useScopeServicesState(); // Initialize the scopes the first time this runs and reset the scopes that were selected on unmount. useEffect(() => { - updateNode('', true, ''); + filterNode('', ''); resetSelection(); return () => { resetSelection(); }; - }, [updateNode, resetSelection]); + }, [filterNode, resetSelection]); // Load the next level of scopes when the parentId changes. useEffect(() => { const parentScopeId = !parentId || parentId === 'scopes' ? '' : last(parentId.split('/'))!; - updateNode(parentScopeId, true, searchQuery); - }, [updateNode, searchQuery, parentId]); + filterNode(parentScopeId, searchQuery); + }, [filterNode, searchQuery, parentId]); return useMemo( () => mapScopesNodesTreeToActions(nodes, tree!, selectedScopes, selectScope), diff --git a/public/app/features/commandPalette/actions/scopesUtils.ts b/public/app/features/commandPalette/actions/scopesUtils.ts index 2848569247a..d269a58abb1 100644 --- a/public/app/features/commandPalette/actions/scopesUtils.ts +++ b/public/app/features/commandPalette/actions/scopesUtils.ts @@ -14,7 +14,7 @@ export function useScopeServicesState() { const services = useScopesServices(); if (!services) { return { - updateNode: () => {}, + filterNode: () => Promise.resolve(), selectScope: () => {}, resetSelection: () => {}, searchAllNodes: () => Promise.resolve([]), @@ -32,7 +32,7 @@ export function useScopeServicesState() { }, }; } - const { updateNode, filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply, getScopeNodes } = + const { filterNode, selectScope, resetSelection, searchAllNodes, deselectScope, apply, getScopeNodes } = services.scopesSelectorService; const selectorServiceState: ScopesSelectorServiceState | undefined = useObservable( services.scopesSelectorService.stateObservable ?? new Observable(), @@ -42,7 +42,6 @@ export function useScopeServicesState() { return { getScopeNodes, filterNode, - updateNode, selectScope, resetSelection, searchAllNodes, diff --git a/public/app/features/scopes/selector/ScopesSelectorService.test.ts b/public/app/features/scopes/selector/ScopesSelectorService.test.ts index 57ff735f019..2c89ea58fe5 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.test.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.test.ts @@ -107,162 +107,9 @@ describe('ScopesSelectorService', () => { service = new ScopesSelectorService(apiClient, dashboardsService, store); }); - describe('updateNode', () => { - it('should update node and fetch children when expanded', async () => { - await service.updateNode('', true, ''); - expect(service.state.nodes['test-scope-node']).toEqual(mockNode); - expect(service.state.tree).toMatchObject({ - children: { 'test-scope-node': { expanded: false, scopeNodeId: 'test-scope-node' } }, - expanded: true, - query: '', - scopeNodeId: '', - }); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: '' }); - }); - - it.skip('should update node query and fetch children when query changes', async () => { - await service.updateNode('', true, ''); // Expand first - // Simulate a change in the query - await service.updateNode('', true, 'new-qu'); - await service.updateNode('', true, 'new-query'); - expect(service.state.tree).toMatchObject({ - children: {}, - expanded: true, - query: 'new-query', - scopeNodeId: '', - }); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: 'new-query' }); - }); - - it('should not fetch children when node is collapsed and query is unchanged', async () => { - // First expand the node - await service.updateNode('', true, ''); - // Then collapse it - await service.updateNode('', false, ''); - // Only the first expansion should trigger fetchNodes - expect(apiClient.fetchNodes).toHaveBeenCalledTimes(1); - }); - - it.skip('should clear query on first expansion but keep it when filtering within populated node', async () => { - const mockChildNode: ScopeNode = { - metadata: { name: 'child-node' }, - spec: { linkId: 'child-scope', linkType: 'scope', parentName: '', nodeType: 'leaf', title: 'child-node' }, - }; - - apiClient.fetchNodes.mockResolvedValue([mockChildNode]); - - // Scenario 1: First expansion (no children yet) - clear query for unfiltered view - await service.updateNode('', true, 'search-query'); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: undefined }); - - // Parent query should be cleared and child nodes should have no query (first expansion) - expect(service.state.tree?.query).toBe(''); - let childTreeNode = service.state.tree?.children?.['child-node']; - expect(childTreeNode?.query).toBe(''); - - // Scenario 2: Filtering within node that already has children - await service.updateNode('', true, 'new-search'); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: 'new-search' }); - - // Parent and child nodes should have the filter query (filtering within existing children) - expect(service.state.tree?.query).toBe('new-search'); - childTreeNode = service.state.tree?.children?.['child-node']; - expect(childTreeNode?.query).toBe('new-search'); - - expect(apiClient.fetchNodes).toHaveBeenCalledTimes(2); - }); - - it.skip('should always reset query on any expansion', async () => { - const mockChildNode: ScopeNode = { - metadata: { name: 'child-node' }, - spec: { linkId: 'child-scope', linkType: 'scope', parentName: '', nodeType: 'leaf', title: 'child-node' }, - }; - - apiClient.fetchNodes.mockResolvedValue([mockChildNode]); - - // First expansion with any query should reset parent query and not pass query to API - await service.updateNode('', true, 'some-search-query'); - - // Verify query is reset and API called without query for first expansion - expect(service.state.tree?.query).toBe(''); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: undefined }); - expect(service.state.tree?.children?.['child-node']?.query).toBe(''); - }); - - it.skip('should handle query reset correctly for nested levels beyond root', async () => { - // Set up mock nodes for multi-level hierarchy - const mockParentNode: ScopeNode = { - metadata: { name: 'parent-container' }, - spec: { linkId: '', linkType: 'scope', parentName: '', nodeType: 'container', title: 'Parent Container' }, - }; - - const mockChildNode: ScopeNode = { - metadata: { name: 'child-container' }, - spec: { - linkId: '', - linkType: 'scope', - parentName: 'parent-container', - nodeType: 'container', - title: 'Child Container', - }, - }; - - const mockGrandchildNode: ScopeNode = { - metadata: { name: 'grandchild-leaf' }, - spec: { - linkId: 'leaf-scope', - linkType: 'scope', - parentName: 'child-container', - nodeType: 'leaf', - title: 'Grandchild Leaf', - }, - }; - - // Mock different responses for different parent nodes - apiClient.fetchNodes.mockImplementation((options: { parent?: string; query?: string; limit?: number }) => { - if (options.parent === '') { - return Promise.resolve([mockParentNode]); - } else if (options.parent === 'parent-container') { - return Promise.resolve([mockChildNode]); - } else if (options.parent === 'child-container') { - return Promise.resolve([mockGrandchildNode]); - } - return Promise.resolve([]); - }); - - // Step 1: Expand root node with search query - await service.updateNode('', true, 'search-query'); - - // Root should have query reset, API called without query - expect(service.state.tree?.query).toBe(''); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: '', query: undefined }); - expect(service.state.tree?.children?.['parent-container']?.query).toBe(''); - - // Step 2: Expand first-level child with search query - await service.updateNode('parent-container', true, 'open-search-query'); - - // First-level child should have query reset, API called without query - const parentContainer = service.state.tree?.children?.['parent-container']; - expect(parentContainer?.query).toBe(''); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: 'parent-container', query: undefined }); - expect(parentContainer?.children?.['child-container']?.query).toBe(''); - - // Step 3: Now filter within the first-level child (second call to same node) - await service.updateNode('parent-container', true, 'filter-search'); - - // Now both parent and children should show the filter query since we're filtering within existing children - const newParentContainer = service.state.tree?.children?.['parent-container']; - expect(newParentContainer?.query).toBe('filter-search'); - expect(apiClient.fetchNodes).toHaveBeenCalledWith({ parent: 'parent-container', query: 'filter-search' }); - expect(newParentContainer?.children?.['child-container']?.query).toBe('filter-search'); - - expect(apiClient.fetchNodes).toHaveBeenCalledTimes(3); - }); - }); - describe('selectScope and deselectScope', () => { beforeEach(async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); }); it('should select a scope', async () => { @@ -311,7 +158,7 @@ describe('ScopesSelectorService', () => { it('should set parent node for recent scopes', async () => { // Load mock node - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.changeScopes(['test-scope'], 'test-scope-node'); expect(service.state.appliedScopes).toEqual([{ scopeId: 'test-scope', parentNodeId: 'test-scope-node' }]); @@ -363,7 +210,7 @@ describe('ScopesSelectorService', () => { describe('closeAndApply', () => { it('should close the selector and apply the selected scopes', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.closeAndApply(); expect(service.state.opened).toBe(false); @@ -373,6 +220,7 @@ describe('ScopesSelectorService', () => { describe('apply', () => { it('should apply the selected scopes without closing the selector', async () => { + await service.filterNode('', ''); await service.open(); await service.selectScope('test-scope-node'); await service.apply(); @@ -391,7 +239,7 @@ describe('ScopesSelectorService', () => { describe('removeAllScopes', () => { it('should remove all selected and applied scopes', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.apply(); await service.removeAllScopes(); @@ -399,7 +247,7 @@ describe('ScopesSelectorService', () => { }); it('should clear navigation scope when removing all scopes', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.apply(); await service.removeAllScopes(); @@ -429,7 +277,7 @@ describe('ScopesSelectorService', () => { describe('getRecentScopes', () => { it('should parse and filter scopes', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.apply(); storeValue[RECENT_SCOPES_KEY] = JSON.stringify([[mockScope2], [mockScope]]); @@ -439,7 +287,7 @@ describe('ScopesSelectorService', () => { }); it('should work with old version', async () => { - await service.updateNode('', true, ''); + await service.filterNode('', ''); await service.selectScope('test-scope-node'); await service.apply(); storeValue[RECENT_SCOPES_KEY] = JSON.stringify([ @@ -615,9 +463,347 @@ describe('ScopesSelectorService', () => { }); }); + describe('toggleExpandedNode', () => { + const expandableNode: ScopeNode = { + metadata: { name: 'expandable-node' }, + spec: { + linkId: '', + linkType: undefined, + parentName: '', + nodeType: 'container', + title: 'Expandable Node', + }, + }; + + const childNode: ScopeNode = { + metadata: { name: 'child-node' }, + spec: { + linkId: 'child-scope', + linkType: 'scope', + parentName: 'expandable-node', + nodeType: 'leaf', + title: 'Child Node', + }, + }; + + const leafNode: ScopeNode = { + metadata: { name: 'leaf-node' }, + spec: { + linkId: 'leaf-scope', + linkType: 'scope', + parentName: '', + nodeType: 'leaf', + title: 'Leaf Node', + }, + }; + + beforeEach(async () => { + // Mock fetchNodes to return different nodes based on parent + apiClient.fetchNodes = jest + .fn() + .mockImplementation((options: { parent?: string; query?: string; limit?: number }) => { + if (options.parent === '') { + return [expandableNode, leafNode]; + } else if (options.parent === 'expandable-node') { + return [childNode]; + } + return []; + }); + + // Load root nodes + await service.filterNode('', ''); + }); + + it('should expand a collapsed node and load its children', async () => { + // Node should start collapsed + expect(service.state.tree?.children?.['expandable-node']?.expanded).toBe(false); + + // Expand the node + await service.toggleExpandedNode('expandable-node'); + + // Node should now be expanded + expect(service.state.tree?.children?.['expandable-node']?.expanded).toBe(true); + // Children should be loaded + expect(service.state.tree?.children?.['expandable-node']?.children).toBeDefined(); + expect(service.state.tree?.children?.['expandable-node']?.children?.['child-node']).toBeDefined(); + }); + + it('should collapse an expanded node', async () => { + // First expand the node + await service.toggleExpandedNode('expandable-node'); + expect(service.state.tree?.children?.['expandable-node']?.expanded).toBe(true); + + // Now collapse it + await service.toggleExpandedNode('expandable-node'); + expect(service.state.tree?.children?.['expandable-node']?.expanded).toBe(false); + }); + + it('should reset query to empty string when toggling', async () => { + // First filter with a query + await service.filterNode('expandable-node', 'test-query'); + expect(service.state.tree?.children?.['expandable-node']?.query).toBe('test-query'); + + // Toggle the node + await service.toggleExpandedNode('expandable-node'); + + // Query should be reset + expect(service.state.tree?.children?.['expandable-node']?.query).toBe(''); + }); + + it('should throw error when node not found in tree', async () => { + await expect(service.toggleExpandedNode('non-existent-node')).rejects.toThrow( + 'Node non-existent-node not found in tree' + ); + }); + + it('should throw error when trying to toggle a non-expandable node', async () => { + await expect(service.toggleExpandedNode('leaf-node')).rejects.toThrow( + 'Trying to expand node at id leaf-node that is not expandable' + ); + }); + + it('should reload parent children when collapsing', async () => { + const fetchNodesSpy = jest.spyOn(apiClient, 'fetchNodes'); + + // Expand then collapse + await service.toggleExpandedNode('expandable-node'); + fetchNodesSpy.mockClear(); + + await service.toggleExpandedNode('expandable-node'); + + // Should reload parent's (root) children + expect(fetchNodesSpy).toHaveBeenCalledWith({ parent: '', query: '' }); + }); + + it('should reload parent children with parent query when collapsing', async () => { + // First filter the root with a query + await service.filterNode('', 'parent-query'); + + // Expand a node + await service.toggleExpandedNode('expandable-node'); + + const fetchNodesSpy = jest.spyOn(apiClient, 'fetchNodes'); + + // Collapse the node + await service.toggleExpandedNode('expandable-node'); + + // Should reload parent's children with parent's query + expect(fetchNodesSpy).toHaveBeenCalledWith({ parent: '', query: 'parent-query' }); + }); + }); + + describe('filterNode', () => { + const containerNode: ScopeNode = { + metadata: { name: 'container-node' }, + spec: { + linkId: '', + linkType: undefined, + parentName: '', + nodeType: 'container', + title: 'Container Node', + }, + }; + + const filteredChild: ScopeNode = { + metadata: { name: 'filtered-child' }, + spec: { + linkId: 'filtered-scope', + linkType: 'scope', + parentName: 'container-node', + nodeType: 'leaf', + title: 'Filtered Child', + }, + }; + + const leafNode: ScopeNode = { + metadata: { name: 'leaf-node-2' }, + spec: { + linkId: 'leaf-scope-2', + linkType: 'scope', + parentName: '', + nodeType: 'leaf', + title: 'Leaf Node 2', + }, + }; + + beforeEach(async () => { + // Mock fetchNodes to return different nodes based on query + apiClient.fetchNodes = jest + .fn() + .mockImplementation((options: { parent?: string; query?: string; limit?: number }) => { + if (options.parent === '' && !options.query) { + return [containerNode, leafNode]; + } else if (options.parent === 'container-node' && options.query === 'test-filter') { + return [filteredChild]; + } else if (options.parent === 'container-node' && !options.query) { + return [filteredChild]; + } + return []; + }); + + // Load root nodes + await service.filterNode('', ''); + }); + + it('should filter node with non-empty query', async () => { + await service.filterNode('container-node', 'test-filter'); + + // Node should be expanded + expect(service.state.tree?.children?.['container-node']?.expanded).toBe(true); + // Query should be set + expect(service.state.tree?.children?.['container-node']?.query).toBe('test-filter'); + }); + + it('should load children with the query parameter', async () => { + const fetchNodesSpy = jest.spyOn(apiClient, 'fetchNodes'); + + await service.filterNode('container-node', 'my-query'); + + expect(fetchNodesSpy).toHaveBeenCalledWith({ parent: 'container-node', query: 'my-query' }); + }); + + it('should set expanded to true when filtering', async () => { + // Node starts collapsed + expect(service.state.tree?.children?.['container-node']?.expanded).toBe(false); + + await service.filterNode('container-node', 'test-filter'); + + // Should be expanded after filtering + expect(service.state.tree?.children?.['container-node']?.expanded).toBe(true); + }); + + it('should throw error when node not found', async () => { + await expect(service.filterNode('non-existent-node', 'query')).rejects.toThrow( + 'Trying to filter node at path or id non-existent-node not found' + ); + }); + + it('should throw error when trying to filter a non-expandable node', async () => { + await expect(service.filterNode('leaf-node-2', 'query')).rejects.toThrow( + 'Trying to filter node at id leaf-node-2 that is not expandable' + ); + }); + + it('should handle multiple calls with different queries', async () => { + // First filter + await service.filterNode('container-node', 'first-query'); + expect(service.state.tree?.children?.['container-node']?.query).toBe('first-query'); + + // Second filter with different query + await service.filterNode('container-node', 'second-query'); + expect(service.state.tree?.children?.['container-node']?.query).toBe('second-query'); + + // Third filter with empty query + await service.filterNode('container-node', ''); + expect(service.state.tree?.children?.['container-node']?.query).toBe(''); + }); + + it('should start profiler interaction', async () => { + const profiler = { + startInteraction: jest.fn(), + stopInteraction: jest.fn(), + }; + + // Create new service with profiler + const serviceWithProfiler = new ScopesSelectorService(apiClient, dashboardsService, store, profiler as never); + + await serviceWithProfiler.filterNode('', ''); + + expect(profiler.startInteraction).toHaveBeenCalledWith('scopeNodeFilter'); + expect(profiler.stopInteraction).toHaveBeenCalled(); + }); + + it('should stop profiler even when error is thrown', async () => { + const profiler = { + startInteraction: jest.fn(), + stopInteraction: jest.fn(), + }; + + const serviceWithProfiler = new ScopesSelectorService(apiClient, dashboardsService, store, profiler as never); + + // Load initial nodes + await serviceWithProfiler.filterNode('', ''); + + // Try to filter a non-existent node + await expect(serviceWithProfiler.filterNode('non-existent', 'query')).rejects.toThrow(); + + // Profiler should still be stopped + expect(profiler.stopInteraction).toHaveBeenCalled(); + }); + }); + + describe('interaction between toggleExpandedNode and filterNode', () => { + const expandableNode: ScopeNode = { + metadata: { name: 'interaction-node' }, + spec: { + linkId: '', + linkType: undefined, + parentName: '', + nodeType: 'container', + title: 'Interaction Node', + }, + }; + + const childNode: ScopeNode = { + metadata: { name: 'interaction-child' }, + spec: { + linkId: 'child-scope', + linkType: 'scope', + parentName: 'interaction-node', + nodeType: 'leaf', + title: 'Child Node', + }, + }; + + beforeEach(async () => { + apiClient.fetchNodes = jest + .fn() + .mockImplementation((options: { parent?: string; query?: string; limit?: number }) => { + if (options.parent === '') { + return [expandableNode]; + } else if (options.parent === 'interaction-node') { + return [childNode]; + } + return []; + }); + + await service.filterNode('', ''); + }); + + it('should clear query when toggleExpandedNode is called after filterNode', async () => { + // Filter with a query + await service.filterNode('interaction-node', 'test-query'); + expect(service.state.tree?.children?.['interaction-node']?.query).toBe('test-query'); + + // Toggle should clear the query + await service.toggleExpandedNode('interaction-node'); + expect(service.state.tree?.children?.['interaction-node']?.query).toBe(''); + }); + + it('should set query when filterNode is called after toggleExpandedNode', async () => { + // First toggle (expand) + await service.toggleExpandedNode('interaction-node'); + expect(service.state.tree?.children?.['interaction-node']?.query).toBe(''); + + // Filter should set the query + await service.filterNode('interaction-node', 'new-query'); + expect(service.state.tree?.children?.['interaction-node']?.query).toBe('new-query'); + }); + + it('should maintain expanded state when filtering an already expanded node', async () => { + // Expand the node + await service.toggleExpandedNode('interaction-node'); + expect(service.state.tree?.children?.['interaction-node']?.expanded).toBe(true); + + // Filter should keep it expanded + await service.filterNode('interaction-node', 'query'); + expect(service.state.tree?.children?.['interaction-node']?.expanded).toBe(true); + }); + }); + describe('redirect on scope selection', () => { it('should redirect to the first scopeNavigation with /d/ URL when current URL is not a scopeNavigation', async () => { - const mockNavigations: ScopeNavigation[] = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -632,8 +818,6 @@ describe('ScopesSelectorService', () => { }, }, ]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); @@ -642,7 +826,7 @@ describe('ScopesSelectorService', () => { }); it('should NOT redirect when the first scopeNavigation does not contain /d/ (e.g., logs drilldown)', async () => { - const mockNavigations: ScopeNavigation[] = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -657,8 +841,6 @@ describe('ScopesSelectorService', () => { }, }, ]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); @@ -667,7 +849,7 @@ describe('ScopesSelectorService', () => { }); it('should NOT redirect when current URL matches a scopeNavigation', async () => { - const mockNavigations: ScopeNavigation[] = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -682,8 +864,6 @@ describe('ScopesSelectorService', () => { }, }, ]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/d/dashboard1' }); await service.changeScopes(['test-scope']); @@ -701,7 +881,7 @@ describe('ScopesSelectorService', () => { }); it('should NOT redirect when scopeNavigation does not have a url property', async () => { - const mockNavigations = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -716,8 +896,6 @@ describe('ScopesSelectorService', () => { }, }, ] as unknown as ScopeNavigation[]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); @@ -726,7 +904,7 @@ describe('ScopesSelectorService', () => { }); it('should handle multiple scopeNavigations and redirect to the first dashboard one', async () => { - const mockNavigations: ScopeNavigation[] = [ + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', @@ -754,8 +932,6 @@ describe('ScopesSelectorService', () => { }, }, ]; - - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); @@ -790,7 +966,7 @@ describe('ScopesSelectorService', () => { }); // First update the node to populate the service state - await service.updateNode('', true, ''); + await service.filterNode('', ''); // Then select the scope to set scopeNodeId in selectedScopes await service.selectScope('test-scope-node'); @@ -837,7 +1013,7 @@ describe('ScopesSelectorService', () => { (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); // First update the node to populate the service state - await service.updateNode('', true, ''); + await service.filterNode('', ''); // Then select the scope to set scopeNodeId in selectedScopes await service.selectScope('test-scope-node'); @@ -851,16 +1027,14 @@ describe('ScopesSelectorService', () => { }); it('should fall back to scope navigation when scope node is undefined', async () => { - const mockNavigations: ScopeNavigation[] = [ + // Don't add the node to the service state, so it will be undefined + dashboardsService.state.scopeNavigations = [ { spec: { scope: 'test-scope', url: '/d/dashboard1' }, status: { title: 'Dashboard 1', groups: [] }, metadata: { name: 'dashboard1' }, }, ]; - - // Don't add the node to the service state, so it will be undefined - dashboardsService.state.scopeNavigations = mockNavigations; (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/some-other-page' }); await service.changeScopes(['test-scope']); diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index 14a16be69ac..9dea729ff88 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -129,81 +129,38 @@ export class ScopesSelectorService extends ScopesServiceBase { - const path = getPathOfNode(scopeNodeId, this.state.nodes); - const nodeToToggle = treeNodeAtPath(this.state.tree!, path); - - if (!nodeToToggle) { - throw new Error(`Node ${scopeNodeId} not found in tree`); - } - - if (nodeToToggle.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToToggle.scopeNodeId])) { - throw new Error(`Trying to expand node at id ${scopeNodeId} that is not expandable`); - } - - const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { - treeNode.expanded = !nodeToToggle.expanded; - treeNode.query = ''; - }); - - this.updateState({ tree: newTree }); - // If we are collapsing, we need to make sure that all the parent's children are avilable - if (nodeToToggle.expanded === true) { - const parentPath = path.slice(0, -1); - const parentNode = treeNodeAtPath(this.state.tree!, parentPath); - if (parentNode) { - await this.loadNodeChildren(parentPath, parentNode, parentNode.query); - } - } else { - await this.loadNodeChildren(path, nodeToToggle); - } - }; - - public filterNode = async (scopeNodeId: string, query: string) => { - const path = getPathOfNode(scopeNodeId, this.state.nodes); - const nodeToFilter = treeNodeAtPath(this.state.tree!, path); - - if (!nodeToFilter) { - throw new Error(`Trying to filter node at path or id ${scopeNodeId} not found`); - } - - if (nodeToFilter.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToFilter.scopeNodeId])) { - throw new Error(`Trying to filter node at id ${scopeNodeId} that is not expandable`); - } - - const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { - treeNode.expanded = true; - treeNode.query = query; - }); - this.updateState({ tree: newTree }); - - await this.loadNodeChildren(path, nodeToFilter, query); - }; - - private expandOrFilterNode = async (scopeNodeId: string, query?: string) => { - this.interactionProfiler?.startInteraction('scopeNodeDiscovery'); - - const path = getPathOfNode(scopeNodeId, this.state.nodes); - - const nodeToExpand = treeNodeAtPath(this.state.tree!, path); + this.interactionProfiler?.startInteraction('scopeToggleExpandedNode'); try { - if (!nodeToExpand) { + const path = getPathOfNode(scopeNodeId, this.state.nodes); + const nodeToToggle = treeNodeAtPath(this.state.tree!, path); + + if (!nodeToToggle) { throw new Error(`Node ${scopeNodeId} not found in tree`); } - if (nodeToExpand.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToExpand.scopeNodeId])) { + if (nodeToToggle.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToToggle.scopeNodeId])) { throw new Error(`Trying to expand node at id ${scopeNodeId} that is not expandable`); } - if (!nodeToExpand.expanded || nodeToExpand.query !== query) { - const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { - treeNode.expanded = true; - treeNode.query = query || ''; - }); - this.updateState({ tree: newTree }); + const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { + treeNode.expanded = !nodeToToggle.expanded; + treeNode.query = ''; + }); - await this.loadNodeChildren(path, nodeToExpand, query); + this.updateState({ tree: newTree }); + // If we are collapsing, we need to make sure that all the parent's children are available + if (nodeToToggle.expanded) { + const parentPath = path.slice(0, -1); + const parentNode = treeNodeAtPath(this.state.tree!, parentPath); + if (parentNode) { + await this.loadNodeChildren(parentPath, parentNode, parentNode.query); + } + } else { + await this.loadNodeChildren(path, nodeToToggle); } + // Catch and throw error so we can ensure the profiler is stopped + // todo: leverage component-level } catch (error) { throw error; } finally { @@ -211,20 +168,34 @@ export class ScopesSelectorService extends ScopesServiceBase { - const path = getPathOfNode(scopeNodeId, this.state.nodes); + public filterNode = async (scopeNodeId: string, query: string) => { + this.interactionProfiler?.startInteraction('scopeNodeFilter'); - const nodeToCollapse = treeNodeAtPath(this.state.tree!, path); + try { + const path = getPathOfNode(scopeNodeId, this.state.nodes); + const nodeToFilter = treeNodeAtPath(this.state.tree!, path); - if (!nodeToCollapse) { - throw new Error(`Trying to collapse node at path or id ${scopeNodeId} not found`); + if (!nodeToFilter) { + throw new Error(`Trying to filter node at path or id ${scopeNodeId} not found`); + } + + if (nodeToFilter.scopeNodeId !== '' && !isNodeExpandable(this.state.nodes[nodeToFilter.scopeNodeId])) { + throw new Error(`Trying to filter node at id ${scopeNodeId} that is not expandable`); + } + + const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { + treeNode.expanded = true; + treeNode.query = query; + }); + this.updateState({ tree: newTree }); + + await this.loadNodeChildren(path, nodeToFilter, query); + // Catch and throw error so we can ensure the profiler is stopped + } catch (error) { + throw error; + } finally { + this.interactionProfiler?.stopInteraction(); } - - const newTree = modifyTreeNodeAtPath(this.state.tree!, path, (treeNode) => { - treeNode.expanded = false; - treeNode.query = ''; - }); - this.updateState({ tree: newTree }); }; private loadNodeChildren = async (path: string[], treeNode: TreeNode, query?: string) => { @@ -332,15 +303,6 @@ export class ScopesSelectorService extends ScopesServiceBase { - if (expanded) { - return this.expandOrFilterNode(scopeNodeId, query); - } - return this.collapseNode(scopeNodeId); - }; - changeScopes = (scopeNames: string[], parentNodeId?: string, scopeNodeId?: string, redirectOnApply?: boolean) => { return this.applyScopes( scopeNames.map((id, index) => ({ @@ -494,7 +456,7 @@ export class ScopesSelectorService extends ScopesServiceBase { if (!this.state.tree?.children || Object.keys(this.state.tree?.children).length === 0) { - await this.expandOrFilterNode(''); + await this.filterNode('', ''); } // First close all nodes From cb05a4ae1bb9f0875b75b48c530767db1031ebae Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Thu, 27 Nov 2025 01:03:36 -0500 Subject: [PATCH 43/94] usePullRequestParam: Provisioned dashboard preview banner, decode pull request url before sanitize (#114516) usePullRequestParam: decode url before sanitize --- .../components/Shared/PreviewBannerViewPR.tsx | 11 ++++++++--- .../provisioning/hooks/usePullRequestParam.ts | 8 ++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx b/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx index 8a796ee049c..15b814a14db 100644 --- a/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx +++ b/public/app/features/provisioning/components/Shared/PreviewBannerViewPR.tsx @@ -107,9 +107,14 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl, b {/* when repo type is not local, we show branch information */} {showBranchInfo(repoType, branchInfo) && ( - branch: - {targetBranch} {'\u2192'}{' '} - {configuredBranch} + branch:{' '} + + {targetBranch} + {' '} + {'\u2192'}{' '} + + {configuredBranch} + )} diff --git a/public/app/features/provisioning/hooks/usePullRequestParam.ts b/public/app/features/provisioning/hooks/usePullRequestParam.ts index 81d10210503..8ad124953a4 100644 --- a/public/app/features/provisioning/hooks/usePullRequestParam.ts +++ b/public/app/features/provisioning/hooks/usePullRequestParam.ts @@ -9,9 +9,9 @@ export const usePullRequestParam = () => { const repoType = params.get('repo_type'); return { - prURL: prParam ? textUtil.sanitizeUrl(prParam) : undefined, - newPrURL: newPrParam ? textUtil.sanitizeUrl(newPrParam) : undefined, - repoURL: repoUrl ? textUtil.sanitizeUrl(repoUrl) : undefined, - repoType: repoType ? textUtil.sanitizeUrl(repoType) : undefined, + prURL: prParam ? textUtil.sanitizeUrl(decodeURIComponent(prParam)) : undefined, + newPrURL: newPrParam ? textUtil.sanitizeUrl(decodeURIComponent(newPrParam)) : undefined, + repoURL: repoUrl ? textUtil.sanitizeUrl(decodeURIComponent(repoUrl)) : undefined, + repoType: repoType ? textUtil.sanitizeUrl(decodeURIComponent(repoType)) : undefined, }; }; From b47352478791e6b673d667bd2c0073a4e4b2a781 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Thu, 27 Nov 2025 02:01:40 -0500 Subject: [PATCH 44/94] Provisioning: View in repository open containing folder (#114513) * Provisioning: View in repostiory open containing folder * i18n * comment * tweaks * Simplify for display --------- Co-authored-by: Clarity-89 --- .../Repository/RepositoryLink.tsx | 4 +- ...ositoryCard.tsx => RepositoryListItem.tsx} | 22 ++--- .../provisioning/Shared/RepositoryList.tsx | 4 +- public/app/features/provisioning/utils/git.ts | 98 +++++++++++++------ public/locales/en-US/grafana.json | 3 - 5 files changed, 79 insertions(+), 52 deletions(-) rename public/app/features/provisioning/Repository/{RepositoryCard.tsx => RepositoryListItem.tsx} (84%) diff --git a/public/app/features/provisioning/Repository/RepositoryLink.tsx b/public/app/features/provisioning/Repository/RepositoryLink.tsx index f97a448efa4..fe4dbd4b9c5 100644 --- a/public/app/features/provisioning/Repository/RepositoryLink.tsx +++ b/public/app/features/provisioning/Repository/RepositoryLink.tsx @@ -4,7 +4,7 @@ import { Trans } from '@grafana/i18n'; import { LinkButton, Stack, Text, TextLink } from '@grafana/ui'; import { useGetRepositoryQuery } from 'app/api/clients/provisioning/v0alpha1'; -import { getRepoHref } from '../utils/git'; +import { getRepoHrefForProvider } from '../utils/git'; type RepositoryLinkProps = { name?: string; @@ -19,7 +19,7 @@ export function RepositoryLink({ name, jobType }: RepositoryLinkProps) { return null; } - const repoHref = getRepoHref(repo.spec?.github); + const repoHref = getRepoHrefForProvider(repo.spec); if (jobType === 'sync') { return ( diff --git a/public/app/features/provisioning/Repository/RepositoryCard.tsx b/public/app/features/provisioning/Repository/RepositoryListItem.tsx similarity index 84% rename from public/app/features/provisioning/Repository/RepositoryCard.tsx rename to public/app/features/provisioning/Repository/RepositoryListItem.tsx index 8a8205e9a2f..841ec3bd928 100644 --- a/public/app/features/provisioning/Repository/RepositoryCard.tsx +++ b/public/app/features/provisioning/Repository/RepositoryListItem.tsx @@ -2,12 +2,13 @@ import { ReactNode } from 'react'; import { t, Trans } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; -import { Stack, Text, TextLink, Icon, Card, LinkButton, Badge } from '@grafana/ui'; +import { Badge, Card, LinkButton, Stack, Text, TextLink } from '@grafana/ui'; import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1'; import { RepoIcon } from '../Shared/RepoIcon'; import { StatusBadge } from '../Shared/StatusBadge'; import { PROVISIONING_URL } from '../constants'; +import { getRepoHrefForProvider } from '../utils/git'; import { getIsReadOnlyWorkflows } from '../utils/repository'; import { SyncRepository } from './SyncRepository'; @@ -16,7 +17,7 @@ interface Props { repository: Repository; } -export function RepositoryCard({ repository }: Props) { +export function RepositoryListItem({ repository }: Props) { const isReadOnlyRepo = getIsReadOnlyWorkflows(repository.spec?.workflows); const { metadata, spec, status } = repository; const name = metadata?.name ?? ''; @@ -27,24 +28,13 @@ export function RepositoryCard({ repository }: Props) { if (spec?.type === 'github') { const { url = '', branch } = spec.github ?? {}; const branchUrl = branch ? `${url}/tree/${branch}` : url; + const href = getRepoHrefForProvider(spec) || branchUrl; meta.push( - - {branchUrl} + + {href.split('/').slice(3).join('/')} ); - - if (status?.webhook?.id) { - const webhookUrl = `${url}/settings/hooks/${status.webhook.id}`; - meta.push( - - - Webhook - - - - ); - } } else if (spec?.type === 'local') { meta.push( diff --git a/public/app/features/provisioning/Shared/RepositoryList.tsx b/public/app/features/provisioning/Shared/RepositoryList.tsx index 3bfadfe872a..038359c6e8b 100644 --- a/public/app/features/provisioning/Shared/RepositoryList.tsx +++ b/public/app/features/provisioning/Shared/RepositoryList.tsx @@ -4,7 +4,7 @@ import { t, Trans } from '@grafana/i18n'; import { Alert, Box, EmptyState, FilterInput, Icon, Stack, TextLink } from '@grafana/ui'; import { Repository } from 'app/api/clients/provisioning/v0alpha1'; -import { RepositoryCard } from '../Repository/RepositoryCard'; +import { RepositoryListItem } from '../Repository/RepositoryListItem'; import { useResourceStats } from '../Wizard/hooks/useResourceStats'; import { UPGRADE_URL } from '../constants'; import { useIsProvisionedInstance } from '../hooks/useIsProvisionedInstance'; @@ -89,7 +89,7 @@ export function RepositoryList({ items }: Props) { )} {filteredItems.length ? ( - filteredItems.map((item) => ) + filteredItems.map((item) => ) ) : ( { return `${github.url}/tree/${github.branch}`; }; +// Remove leading and trailing slashes from a string. +const stripSlashes = (s: string) => s.replace(/^\/+|\/+$/g, ''); + +// Split a path into segments and URL-encode each segment. +// Ensures the final URL remains valid for all providers (GitHub, GitLab, etc.). +const splitAndEncode = (s: string) => stripSlashes(s).split('/').map(encodeURIComponent); + +type BuildRepoUrlParams = { + baseUrl?: string; + branch?: string | null; + providerSegments: string[]; + path?: string | null; +}; + +const buildRepoUrl = ({ baseUrl, branch, providerSegments, path }: BuildRepoUrlParams) => { + if (!baseUrl) { + return undefined; + } + + // Normalize base URL: trim whitespace + remove trailing slashes. + const cleanBase = stripSlashes(baseUrl.trim()); + const cleanBranch = branch?.trim() || undefined; + + // Start composing URL parts: + // base URL + provider-specific segments (e.g., "tree", "blob", etc.) + const parts = [cleanBase, ...providerSegments]; + + // Append the branch name if present. + if (cleanBranch) { + parts.push(cleanBranch); + } + + // Append encoded path segments if provided. + // This ensures nested files like "src/utils/index.ts" produce safe URLs. + if (path) { + parts.push(...splitAndEncode(path.trim())); + } + + return parts.join('/'); +}; + export const getRepoHrefForProvider = (spec?: RepositorySpec) => { if (!spec || !spec.type) { return undefined; } switch (spec.type) { - case 'github': { - const url = spec.github?.url; - const branch = spec.github?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/tree/${branch}` : url; - } - - case 'gitlab': { - const url = spec.gitlab?.url; - const branch = spec.gitlab?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/-/tree/${branch}` : url; - } - case 'bitbucket': { - const url = spec.bitbucket?.url; - const branch = spec.bitbucket?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/src/${branch}` : url; - } - case 'git': { - // Return a generic URL for pure git repositories - return spec.git?.url; - } + case 'github': + return buildRepoUrl({ + baseUrl: spec.github?.url, + branch: spec.github?.branch, + providerSegments: ['tree'], + path: spec.github?.path, + }); + case 'gitlab': + return buildRepoUrl({ + baseUrl: spec.gitlab?.url, + branch: spec.gitlab?.branch, + providerSegments: ['-', 'tree'], + path: spec.gitlab?.path, + }); + case 'bitbucket': + return buildRepoUrl({ + baseUrl: spec.bitbucket?.url, + branch: spec.bitbucket?.branch, + providerSegments: ['src'], + path: spec.bitbucket?.path, + }); + case 'git': + return buildRepoUrl({ + baseUrl: spec.git?.url, + branch: spec.git?.branch, + providerSegments: ['tree'], + path: spec.git?.path, + }); default: return undefined; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c525ce2e768..5a55ab0843f 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11920,9 +11920,6 @@ "source-code": "Source code" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Read only", "settings": "Settings", "view": "View" From b90b8f2a3410e8b602a04c265226b9498a289fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 27 Nov 2025 11:00:43 +0100 Subject: [PATCH 45/94] Dashboard: Sidebar / outline style fixes (#114487) * Dashboard: Sidebar / outline style fixes * fixing empty outline node * fixing empty outline node --- eslint-suppressions.json | 5 --- .../edit-pane/DashboardOutline.tsx | 39 ++++++++++++++----- .../scene/DashboardControls.tsx | 2 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index fde5e53587d..3f5773bf84f 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1912,11 +1912,6 @@ "count": 4 } }, - "public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx": { "no-restricted-syntax": { "count": 3 diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index 4ce6536ac5c..97e17c34780 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { SceneObject } from '@grafana/scenes'; -import { Box, Icon, Sidebar, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui'; +import { Box, Icon, Sidebar, Text, useElementSelection, useStyles2 } from '@grafana/ui'; import { isRepeatCloneOrChildOf } from '../utils/clone'; import { DashboardInteractions } from '../utils/interactions'; @@ -85,6 +85,7 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index } aria-selected={isSelected} className={styles.container} onClick={onNodeClicked} + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions style={{ '--depth': depth } as React.CSSProperties} >
@@ -99,7 +100,7 @@ function DashboardOutlineNode({ sceneObject, editPane, isEditing, depth, index } )} -
- {headers.map((header, i) => ( - { - this.onHeaderChange(i, h); - }} - onBlur={this.updateSettings} - onRemove={this.onHeaderRemove} - onReset={this.onHeaderReset} - /> - ))} -
- {!dataSourceConfig.readOnly && ( - - - - - - )} - - ); - } -} + )} + + ); +}); + +CustomHeadersSettings.displayName = 'CustomHeadersSettings'; export default CustomHeadersSettings; diff --git a/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx b/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx index 089fd514418..c63634be362 100644 --- a/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx +++ b/packages/grafana-ui/src/components/JSONFormatter/JSONFormatter.tsx @@ -1,4 +1,4 @@ -import { PureComponent, createRef } from 'react'; +import { memo, useRef, useEffect } from 'react'; import { JsonExplorer, JsonExplorerConfig } from './json_explorer/json_explorer'; // We have made some monkey-patching of json-formatter-js so we can't switch right now @@ -10,45 +10,32 @@ interface Props { onDidRender?: (formattedJson: {}) => void; } -export class JSONFormatter extends PureComponent { - private wrapperRef = createRef(); +export const JSONFormatter = memo( + ({ className, json, config = { animateOpen: true }, open = 3, onDidRender }) => { + const wrapperRef = useRef(null); - static defaultProps = { - open: 3, - config: { - animateOpen: true, - }, - }; + useEffect(() => { + const wrapperEl = wrapperRef.current; + if (!wrapperEl) { + return; + } - componentDidMount() { - this.renderJson(); + const formatter = new JsonExplorer(json, open, config); + const hasChildren = wrapperEl.hasChildNodes(); + + if (hasChildren && wrapperEl.lastChild) { + wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild); + } else { + wrapperEl.appendChild(formatter.render()); + } + + if (onDidRender) { + onDidRender(formatter.json); + } + }, [json, config, open, onDidRender]); + + return
; } +); - componentDidUpdate() { - this.renderJson(); - } - - renderJson = () => { - const { json, config, open, onDidRender } = this.props; - const wrapperEl = this.wrapperRef.current; - const formatter = new JsonExplorer(json, open, config); - // @ts-ignore - const hasChildren: boolean = wrapperEl.hasChildNodes(); - if (hasChildren) { - // @ts-ignore - wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild); - } else { - // @ts-ignore - wrapperEl.appendChild(formatter.render()); - } - - if (onDidRender) { - onDidRender(formatter.json); - } - }; - - render() { - const { className } = this.props; - return
; - } -} +JSONFormatter.displayName = 'JSONFormatter'; diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx index 49c3f4b8966..db0f53084d8 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx @@ -1,45 +1,33 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryFn } from '@storybook/react'; -import { PureComponent } from 'react'; +import { memo, useState } from 'react'; import { Field } from '../Forms/Field'; import { Props, StatsPicker } from './StatsPicker'; -interface State { - stats: string[]; -} +const WrapperWithState = memo(({ placeholder, allowMultiple, menuPlacement, width }) => { + const [stats, setStats] = useState([]); -class WrapperWithState extends PureComponent { - constructor(props: Props) { - super(props); - this.state = { - stats: [], - }; - } + return ( + + { + action('Picked:')(newStats); + setStats(newStats); + }} + menuPlacement={menuPlacement} + width={width} + /> + + ); +}); - render() { - const { placeholder, allowMultiple, menuPlacement, width } = this.props; - const { stats } = this.state; - - return ( - - { - action('Picked:')(stats); - this.setState({ stats }); - }} - menuPlacement={menuPlacement} - width={width} - /> - - ); - } -} +WrapperWithState.displayName = 'WrapperWithState'; const meta: Meta = { title: 'Pickers/StatsPicker', diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx index 009ae565dfe..0bf185e1354 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx @@ -1,5 +1,5 @@ import { difference } from 'lodash'; -import { PureComponent } from 'react'; +import { memo, useEffect } from 'react'; import { fieldReducers, SelectableValue, FieldReducerInfo } from '@grafana/data'; @@ -18,54 +18,47 @@ export interface Props { filterOptions?: (ext: FieldReducerInfo) => boolean; } -export class StatsPicker extends PureComponent { - static defaultProps: Partial = { - allowMultiple: false, - }; +export const StatsPicker = memo( + ({ + placeholder, + onChange, + stats, + allowMultiple = false, + defaultStat, + className, + width, + menuPlacement, + inputId, + filterOptions, + }) => { + useEffect(() => { + const current = fieldReducers.list(stats); + if (current.length !== stats.length) { + const found = current.map((v) => v.id); + const notFound = difference(stats, found); + console.warn('Unknown stats', notFound, stats); + onChange(current.map((stat) => stat.id)); + } - componentDidMount() { - this.checkInput(); - } + // Make sure there is only one + if (!allowMultiple && stats.length > 1) { + console.warn('Removing extra stat', stats); + onChange([stats[0]]); + } - componentDidUpdate(prevProps: Props) { - this.checkInput(); - } + // Set the reducer from callback + if (defaultStat && stats.length < 1) { + onChange([defaultStat]); + } + }, [stats, allowMultiple, defaultStat, onChange]); - checkInput = () => { - const { stats, allowMultiple, defaultStat, onChange } = this.props; - - const current = fieldReducers.list(stats); - if (current.length !== stats.length) { - const found = current.map((v) => v.id); - const notFound = difference(stats, found); - console.warn('Unknown stats', notFound, stats); - onChange(current.map((stat) => stat.id)); - } - - // Make sure there is only one - if (!allowMultiple && stats.length > 1) { - console.warn('Removing extra stat', stats); - onChange([stats[0]]); - } - - // Set the reducer from callback - if (defaultStat && stats.length < 1) { - onChange([defaultStat]); - } - }; - - onSelectionChange = (item: SelectableValue) => { - const { onChange } = this.props; - if (Array.isArray(item)) { - onChange(item.map((v) => v.value)); - } else { - onChange(item && item.value ? [item.value] : []); - } - }; - - render() { - const { stats, allowMultiple, defaultStat, placeholder, className, menuPlacement, width, inputId, filterOptions } = - this.props; + const onSelectionChange = (item: SelectableValue) => { + if (Array.isArray(item)) { + onChange(item.map((v) => v.value)); + } else { + onChange(item && item.value ? [item.value] : []); + } + }; const select = fieldReducers.selectOptions(stats, filterOptions); return ( @@ -78,10 +71,12 @@ export class StatsPicker extends PureComponent { isSearchable={true} options={select.options} placeholder={placeholder} - onChange={this.onSelectionChange} + onChange={onSelectionChange} menuPlacement={menuPlacement} inputId={inputId} /> ); } -} +); + +StatsPicker.displayName = 'StatsPicker'; diff --git a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx index 1e792aa95d8..37e3ae49473 100644 --- a/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx +++ b/packages/grafana-ui/src/components/Tooltip/PopoverController.tsx @@ -1,5 +1,5 @@ import { Placement } from '@popperjs/core'; -import { Component, type JSX } from 'react'; +import { useState, useRef, useCallback, type JSX } from 'react'; import { PopoverContent } from './types'; @@ -21,37 +21,28 @@ interface Props { hideAfter?: number; } -interface State { - show: boolean; -} +const PopoverController = ({ placement = 'auto', content, children, hideAfter }: Props) => { + const [show, setShow] = useState(false); + const hideTimeoutRef = useRef | null>(null); -class PopoverController extends Component { - private hideTimeout: ReturnType | null = null; - state = { show: false }; - - showPopper = () => { - if (this.hideTimeout) { - clearTimeout(this.hideTimeout); + const showPopper = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); } - this.setState({ show: true }); - }; + setShow(true); + }, []); - hidePopper = () => { - this.hideTimeout = setTimeout(() => { - this.setState({ show: false }); - }, this.props.hideAfter); - }; + const hidePopper = useCallback(() => { + hideTimeoutRef.current = setTimeout(() => { + setShow(false); + }, hideAfter); + }, [hideAfter]); - render() { - const { children, content, placement = 'auto' } = this.props; - const { show } = this.state; - - return children(this.showPopper, this.hidePopper, { - show, - placement, - content, - }); - } -} + return children(showPopper, hidePopper, { + show, + placement, + content, + }); +}; export { PopoverController }; diff --git a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx index ac53b424bdb..90b9263f55d 100644 --- a/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx +++ b/packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { memo } from 'react'; import { getValueFormats, SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -19,58 +19,52 @@ function formatCreateLabel(input: string) { /** * https://developers.grafana.com/ui/latest/index.html?path=/docs/pickers-unitpicker--docs */ -export class UnitPicker extends PureComponent { - onChange = (value: SelectableValue) => { - this.props.onChange(value.value); - }; +export const UnitPicker = memo(({ onChange, value, width, id }) => { + // Set the current selection + let current: SelectableValue | undefined = undefined; - render() { - const { value, width, id } = this.props; + // All units + const unitGroups = getValueFormats(); - // Set the current selection - let current: SelectableValue | undefined = undefined; - - // All units - const unitGroups = getValueFormats(); - - // Need to transform the data structure to work well with Select - const groupOptions: CascaderOption[] = unitGroups.map((group) => { - const options = group.submenu.map((unit) => { - const sel = { - label: unit.text, - value: unit.value, - }; - if (unit.value === value) { - current = sel; - } - return sel; - }); - - return { - label: group.text, - value: group.text, - items: options, + // Need to transform the data structure to work well with Select + const groupOptions: CascaderOption[] = unitGroups.map((group) => { + const options = group.submenu.map((unit) => { + const sel = { + label: unit.text, + value: unit.value, }; + if (unit.value === value) { + current = sel; + } + return sel; }); - // Show the custom unit - if (value && !current) { - current = { value, label: value }; - } + return { + label: group.text, + value: group.text, + items: options, + }; + }); - return ( - - ); + // Show the custom unit + if (value && !current) { + current = { value, label: value }; } -} + + return ( + + ); +}); + +UnitPicker.displayName = 'UnitPicker'; diff --git a/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx b/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx index fc6ab425ef9..101bd69ec97 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControlsMenu.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { VariableHide } from '@grafana/data'; @@ -65,15 +65,13 @@ describe('DashboardControlsMenu', () => { }), ]; - act(() => { - render(); - }); + render(); // Should have rendered a dropdown expect(screen.getByRole('button')).toBeInTheDocument(); // Open the dropdown - userEvent.click(screen.getByRole('button')); + await userEvent.click(screen.getByRole('button')); expect(await screen.findByText('textVar1')).toBeInTheDocument(); expect(await screen.findByText('textVar2')).toBeInTheDocument(); expect(await screen.findByText('queryVar')).toBeInTheDocument(); @@ -104,7 +102,7 @@ describe('DashboardControlsMenu', () => { expect(screen.getByRole('button')).toBeInTheDocument(); // Open the dropdown - userEvent.click(screen.getByRole('button')); + await userEvent.click(screen.getByRole('button')); expect(await screen.findByText('textVar1')).toBeInTheDocument(); expect(await screen.findByText('customVar')).toBeInTheDocument(); expect(screen.queryByText('textVar2')).not.toBeInTheDocument(); From 12c6d7e83fb8ba89d74edc1d5e0a5fdd59fdfde4 Mon Sep 17 00:00:00 2001 From: Rafael Bortolon Paulovic Date: Fri, 28 Nov 2025 13:13:35 +0100 Subject: [PATCH 80/94] fix(unified): in-proc SQLite data migration (#114537) * feat: unified storage migrations integration tests * chore: add comment and adjust db path name * chore: refactor test cases into interface * fix: unified SQLite migration with SQLStore migrator * revert changes to newResourceDBProvider --- .../unified/migrations/resource_migration.go | 16 ++ pkg/storage/unified/migrations/service.go | 12 +- pkg/storage/unified/migrations/validator.go | 99 +++++-- pkg/storage/unified/resource/transaction.go | 25 ++ pkg/storage/unified/sql/bulk.go | 250 ++++++++++-------- pkg/storage/unified/sql/db/dbimpl/db.go | 5 + pkg/util/xorm/session.go | 11 +- 7 files changed, 286 insertions(+), 132 deletions(-) create mode 100644 pkg/storage/unified/resource/transaction.go diff --git a/pkg/storage/unified/migrations/resource_migration.go b/pkg/storage/unified/migrations/resource_migration.go index 281ee1c85e3..5bbed9c5722 100644 --- a/pkg/storage/unified/migrations/resource_migration.go +++ b/pkg/storage/unified/migrations/resource_migration.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/xorm" "k8s.io/apimachinery/pkg/runtime/schema" @@ -72,6 +73,17 @@ func (m *ResourceMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) erro m.log.Info("Starting migration for all organizations", "org_count", len(orgs), "resources", m.resources) + if mg.Dialect.DriverName() == migrator.SQLite { + // reuse transaction in SQLite to avoid "database is locked" errors + tx, err := sess.Tx() + if err != nil { + m.log.Error("Failed to get transaction from session", "error", err) + return fmt.Errorf("failed to get transaction: %w", err) + } + ctx = resource.ContextWithTransaction(ctx, tx.Tx) + m.log.Info("Stored migrator transaction in context for bulk operations (SQLite compatibility)") + } + for _, org := range orgs { if err := m.migrateOrg(ctx, sess, org); err != nil { return err @@ -107,6 +119,10 @@ func (m *ResourceMigration) migrateOrg(ctx context.Context, sess *xorm.Session, m.log.Error("Migration failed", "org_id", org.ID, "error", err, "duration", time.Since(startTime)) return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, err) } + if response.Error != nil { + m.log.Error("Migration reported error", "org_id", org.ID, "error", response.Error.String(), "duration", time.Since(startTime)) + return fmt.Errorf("migration failed for org %d (%s): %w", org.ID, org.Name, fmt.Errorf("migration error: %s", response.Error.Message)) + } // Validate the migration results if err := m.validateMigration(migrationCtx, sess, response); err != nil { diff --git a/pkg/storage/unified/migrations/service.go b/pkg/storage/unified/migrations/service.go index 1a78f837066..23b8c2706ae 100644 --- a/pkg/storage/unified/migrations/service.go +++ b/pkg/storage/unified/migrations/service.go @@ -85,8 +85,13 @@ func RegisterMigrations( // Run all registered migrations (blocking) sec := cfg.Raw.Section("database") + migrationLocking := sec.Key("migration_locking").MustBool(true) + if mg.Dialect.DriverName() == sqlstoremigrator.SQLite { + // disable migration locking for SQLite to avoid "database is locked" errors in the bulk operations + migrationLocking = false + } if err := mg.RunMigrations(ctx, - sec.Key("migration_locking").MustBool(true), + migrationLocking, sec.Key("locking_attempt_timeout_sec").MustInt()); err != nil { return fmt.Errorf("unified storage data migration failed: %w", err) } @@ -98,12 +103,14 @@ func RegisterMigrations( func registerDashboardAndFolderMigration(mg *sqlstoremigrator.Migrator, migrator UnifiedMigrator, client resource.ResourceClient) { folders := schema.GroupResource{Group: "folder.grafana.app", Resource: "folders"} dashboards := schema.GroupResource{Group: "dashboard.grafana.app", Resource: "dashboards"} + driverName := mg.Dialect.DriverName() folderCountValidator := NewCountValidator( client, folders, "dashboard", "org_id = ? and is_folder = true", + driverName, ) dashboardCountValidator := NewCountValidator( @@ -111,9 +118,10 @@ func registerDashboardAndFolderMigration(mg *sqlstoremigrator.Migrator, migrator dashboards, "dashboard", "org_id = ? and is_folder = false", + driverName, ) - folderTreeValidator := NewFolderTreeValidator(client, folders) + folderTreeValidator := NewFolderTreeValidator(client, folders, driverName) dashboardsAndFolders := NewResourceMigration( migrator, diff --git a/pkg/storage/unified/migrations/validator.go b/pkg/storage/unified/migrations/validator.go index 7a973976402..0bdc67cc205 100644 --- a/pkg/storage/unified/migrations/validator.go +++ b/pkg/storage/unified/migrations/validator.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/util/xorm" "k8s.io/apimachinery/pkg/runtime/schema" @@ -57,6 +58,7 @@ type CountValidator struct { resource schema.GroupResource table string whereClause string + driverName string } func NewCountValidator( @@ -64,6 +66,7 @@ func NewCountValidator( resource schema.GroupResource, table string, whereClause string, + driverName string, ) Validator { return &CountValidator{ name: "CountValidator", @@ -71,6 +74,7 @@ func NewCountValidator( resource: resource, table: table, whereClause: whereClause, + driverName: driverName, } } @@ -120,22 +124,32 @@ func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, respo return fmt.Errorf("failed to count %s: %w", v.table, err) } - // Get unified storage count using GetStats API - statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ - Namespace: summary.Namespace, - Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, - }) - if err != nil { - return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", - summary.Group, summary.Resource, summary.Namespace, err) - } - - // Find the count for this specific resource type var unifiedCount int64 - for _, stat := range statsResp.Stats { - if stat.Group == summary.Group && stat.Resource == summary.Resource { - unifiedCount = stat.Count - break + if v.driverName == migrator.SQLite { + unifiedCount, err = sess.Table("resource"). + Where("namespace = ? AND `group` = ? AND resource = ?", + summary.Namespace, summary.Group, summary.Resource). + Count() + if err != nil { + return fmt.Errorf("failed to count resource table for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + } else { + // Get unified storage count using GetStats API + statsResp, err := v.client.GetStats(ctx, &resourcepb.ResourceStatsRequest{ + Namespace: summary.Namespace, + Kinds: []string{fmt.Sprintf("%s/%s", summary.Group, summary.Resource)}, + }) + if err != nil { + return fmt.Errorf("failed to get stats for %s/%s in namespace %s: %w", + summary.Group, summary.Resource, summary.Namespace, err) + } + // Find the count for this specific resource type + for _, stat := range statsResp.Stats { + if stat.Group == summary.Group && stat.Resource == summary.Resource { + unifiedCount = stat.Count + break + } } } @@ -162,19 +176,22 @@ func (v *CountValidator) Validate(ctx context.Context, sess *xorm.Session, respo } type FolderTreeValidator struct { - name string - client resourcepb.ResourceIndexClient - resource schema.GroupResource + name string + client resourcepb.ResourceIndexClient + resource schema.GroupResource + driverName string } func NewFolderTreeValidator( client resourcepb.ResourceIndexClient, resource schema.GroupResource, + driverName string, ) Validator { return &FolderTreeValidator{ - name: "FolderTreeValidator", - client: client, - resource: resource, + name: "FolderTreeValidator", + client: client, + resource: resource, + driverName: driverName, } } @@ -185,6 +202,12 @@ type legacyFolder struct { Title string `xorm:"title"` } +type unifiedFolder struct { + GUID string `xorm:"guid"` + Name string `xorm:"name"` + Folder string `xorm:"folder"` +} + func (v *FolderTreeValidator) Name() string { return v.name } @@ -218,7 +241,12 @@ func (v *FolderTreeValidator) Validate(ctx context.Context, sess *xorm.Session, } // Build unified storage folder parent map - unifiedParentMap, err := v.buildUnifiedFolderParentMap(ctx, summary.Namespace, log) + var unifiedParentMap map[string]string + if v.driverName == migrator.SQLite { + unifiedParentMap, err = v.buildUnifiedFolderParentMapSQLite(sess, summary.Namespace, log) + } else { + unifiedParentMap, err = v.buildUnifiedFolderParentMap(ctx, summary.Namespace, log) + } if err != nil { return fmt.Errorf("failed to build unified folder parent map: %w", err) } @@ -348,3 +376,30 @@ func (v *FolderTreeValidator) buildUnifiedFolderParentMap(ctx context.Context, n return parentMap, nil } + +func (v *FolderTreeValidator) buildUnifiedFolderParentMapSQLite(sess *xorm.Session, namespace string, log log.Logger) (map[string]string, error) { + var folders []unifiedFolder + err := sess.Table("resource"). + Cols("guid", "name", "folder"). + Where("namespace = ? AND resource = ?", namespace, "folder"). + Find(&folders) + if err != nil { + return nil, fmt.Errorf("failed to query unified folders: %w", err) + } + + parentMap := make(map[string]string) + for _, folder := range folders { + parentMap[folder.Name] = folder.Folder + } + + if len(parentMap) == 0 { + log.Debug("No unified folders found for namespace", "namespace", namespace) + return make(map[string]string), nil + } + + log.Debug("Built unified folder parent map", + "folder_count", len(parentMap), + "namespace", namespace) + + return parentMap, nil +} diff --git a/pkg/storage/unified/resource/transaction.go b/pkg/storage/unified/resource/transaction.go new file mode 100644 index 00000000000..7d47f02af71 --- /dev/null +++ b/pkg/storage/unified/resource/transaction.go @@ -0,0 +1,25 @@ +package resource + +import ( + "context" + "database/sql" +) + +type transactionContextKey struct{} + +// ContextWithTransaction returns a new context with the transaction stored directly. +// This is used for SQLite migrations where the transaction needs to be shared +// between the migration code and unified storage operations within the same process. +func ContextWithTransaction(ctx context.Context, tx *sql.Tx) context.Context { + return context.WithValue(ctx, transactionContextKey{}, tx) +} + +// TransactionFromContext retrieves the transaction from context +func TransactionFromContext(ctx context.Context) *sql.Tx { + if v := ctx.Value(transactionContextKey{}); v != nil { + if tx, ok := v.(*sql.Tx); ok { + return tx + } + } + return nil +} diff --git a/pkg/storage/unified/sql/bulk.go b/pkg/storage/unified/sql/bulk.go index 7d4d7165d50..6580975a764 100644 --- a/pkg/storage/unified/sql/bulk.go +++ b/pkg/storage/unified/sql/bulk.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/fullstorydev/grpchan/inprocgrpc" "github.com/google/uuid" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" "github.com/grafana/grafana/pkg/storage/unified/sql/db" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" "github.com/grafana/grafana/pkg/storage/unified/sql/dbutil" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) @@ -111,6 +113,19 @@ func (b *backend) ProcessBulk(ctx context.Context, setting resource.BulkSettings } defer b.bulkLock.Finish(setting.Collection) + // If provided, reuse the inproc transaction for SQLite + if clientCtx := inprocgrpc.ClientContext(ctx); clientCtx != nil && b.dialect.DialectName() == "sqlite" { + if externalTx := resource.TransactionFromContext(clientCtx); externalTx != nil { + b.log.Info("Using SQLite transaction from client context") + rsp := &resourcepb.BulkResponse{} + err := b.processBulkWithTx(ctx, dbimpl.NewTx(externalTx), setting, iter, rsp) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + } + return rsp + } + } + // We may want to first write parquet, then read parquet if b.dialect.DialectName() == "sqlite" { file, err := os.CreateTemp("", "grafana-bulk-export-*.parquet") @@ -151,109 +166,134 @@ func (b *backend) ProcessBulk(ctx context.Context, setting resource.BulkSettings func (b *backend) processBulk(ctx context.Context, setting resource.BulkSettings, iter resource.BulkRequestIterator) *resourcepb.BulkResponse { rsp := &resourcepb.BulkResponse{} err := b.db.WithTx(ctx, ReadCommitted, func(ctx context.Context, tx db.Tx) error { - rollbackWithError := func(err error) error { - txerr := tx.Rollback() - if txerr != nil { - b.log.Warn("rollback", "error", txerr) - } else { - b.log.Info("rollback") + return b.processBulkWithTx(ctx, tx, setting, iter, rsp) + }) + if err != nil { + rsp.Error = resource.AsErrorResult(err) + } + return rsp +} + +// processBulkWithTx performs the bulk operation using the provided transaction. +// This is used both when creating our own transaction and when reusing an external one. +func (b *backend) processBulkWithTx(ctx context.Context, tx db.Tx, setting resource.BulkSettings, iter resource.BulkRequestIterator, rsp *resourcepb.BulkResponse) error { + rollbackWithError := func(err error) error { + txerr := tx.Rollback() + if txerr != nil { + b.log.Warn("rollback", "error", txerr) + } else { + b.log.Info("rollback") + } + return err + } + bulk := &bulkWroker{ + ctx: ctx, + tx: tx, + dialect: b.dialect, + logger: logging.FromContext(ctx), + } + + // Calculate the RV based on incoming request timestamps + rv := newBulkRV() + + summaries := make(map[string]*resourcepb.BulkResponse_Summary, len(setting.Collection)) + + // First clear everything in the transaction + if setting.RebuildCollection { + for _, key := range setting.Collection { + summary, err := bulk.deleteCollection(key) + if err != nil { + return rollbackWithError(err) } + summaries[resource.NSGR(key)] = summary + rsp.Summary = append(rsp.Summary, summary) + } + } else { + for _, key := range setting.Collection { + summaries[resource.NSGR(key)] = &resourcepb.BulkResponse_Summary{ + Namespace: key.Namespace, + Group: key.Group, + Resource: key.Resource, + } + } + } + + obj := &unstructured.Unstructured{} + + // Write each event into the history + for iter.Next() { + if iter.RollbackRequested() { + return rollbackWithError(nil) + } + req := iter.Request() + if req == nil { + return rollbackWithError(fmt.Errorf("missing request")) + } + rsp.Processed++ + + if req.Action == resourcepb.BulkRequest_UNKNOWN { + rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ + Key: req.Key, + Action: req.Action, + Error: "unknown action", + }) + continue + } + + err := obj.UnmarshalJSON(req.Value) + if err != nil { + rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ + Key: req.Key, + Action: req.Action, + Error: "unable to unmarshal json", + }) + continue + } + + // Write the event to history + if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ + SQLTemplate: sqltemplate.New(b.dialect), + WriteEvent: resource.WriteEvent{ + Key: req.Key, + Type: resourcepb.WatchEvent_Type(req.Action), + Value: req.Value, + PreviousRV: -1, // Used for WATCH, but we want to skip watch events + }, + Folder: req.Folder, + GUID: uuid.New().String(), + ResourceVersion: rv.next(obj), + }); err != nil { + return rollbackWithError(fmt.Errorf("insert into resource history: %w", err)) + } + } + + // Now update the resource table from history + for _, key := range setting.Collection { + k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource) + summary := summaries[k] + if summary == nil { + return rollbackWithError(fmt.Errorf("missing summary key for: %s", k)) + } + + err := bulk.syncCollection(key, summary) + if err != nil { return err } - bulk := &bulkWroker{ - ctx: ctx, - tx: tx, - dialect: b.dialect, - logger: logging.FromContext(ctx), - } - // Calculate the RV based on incoming request timestamps - rv := newBulkRV() - - summaries := make(map[string]*resourcepb.BulkResponse_Summary, len(setting.Collection)) - - // First clear everything in the transaction - if setting.RebuildCollection { - for _, key := range setting.Collection { - summary, err := bulk.deleteCollection(key) - if err != nil { - return rollbackWithError(err) + if b.dialect.DialectName() == "sqlite" { + nextRV, err := b.rvManager.lock(ctx, tx, key.Group, key.Resource) + if err != nil { + b.log.Error("error locking RV", "error", err, "key", resource.NSGR(key)) + } else { + b.log.Info("successfully locked RV", "nextRV", nextRV, "key", resource.NSGR(key)) + // Save the incremented RV + if err := b.rvManager.saveRV(ctx, tx, key.Group, key.Resource, nextRV); err != nil { + b.log.Error("error saving RV", "error", err, "key", resource.NSGR(key)) + } else { + b.log.Info("successfully saved RV", "rv", nextRV, "key", resource.NSGR(key)) } - summaries[resource.NSGR(key)] = summary - rsp.Summary = append(rsp.Summary, summary) } } else { - for _, key := range setting.Collection { - summaries[resource.NSGR(key)] = &resourcepb.BulkResponse_Summary{ - Namespace: key.Namespace, - Group: key.Group, - Resource: key.Resource, - } - } - } - - obj := &unstructured.Unstructured{} - - // Write each event into the history - for iter.Next() { - if iter.RollbackRequested() { - return rollbackWithError(nil) - } - req := iter.Request() - if req == nil { - return rollbackWithError(fmt.Errorf("missing request")) - } - rsp.Processed++ - - if req.Action == resourcepb.BulkRequest_UNKNOWN { - rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ - Key: req.Key, - Action: req.Action, - Error: "unknown action", - }) - continue - } - - err := obj.UnmarshalJSON(req.Value) - if err != nil { - rsp.Rejected = append(rsp.Rejected, &resourcepb.BulkResponse_Rejected{ - Key: req.Key, - Action: req.Action, - Error: "unable to unmarshal json", - }) - continue - } - - // Write the event to history - if _, err := dbutil.Exec(ctx, tx, sqlResourceHistoryInsert, sqlResourceRequest{ - SQLTemplate: sqltemplate.New(b.dialect), - WriteEvent: resource.WriteEvent{ - Key: req.Key, - Type: resourcepb.WatchEvent_Type(req.Action), - Value: req.Value, - PreviousRV: -1, // Used for WATCH, but we want to skip watch events - }, - Folder: req.Folder, - GUID: uuid.New().String(), - ResourceVersion: rv.next(obj), - }); err != nil { - return rollbackWithError(fmt.Errorf("insert into resource history: %w", err)) - } - } - - // Now update the resource table from history - for _, key := range setting.Collection { - k := fmt.Sprintf("%s/%s/%s", key.Namespace, key.Group, key.Resource) - summary := summaries[k] - if summary == nil { - return rollbackWithError(fmt.Errorf("missing summary key for: %s", k)) - } - - err := bulk.syncCollection(key, summary) - if err != nil { - return err - } - // Make sure the collection RV is above our last written event _, err = b.rvManager.ExecWithRV(ctx, key, func(tx db.Tx) (string, error) { return "", nil @@ -261,19 +301,15 @@ func (b *backend) processBulk(ctx context.Context, setting resource.BulkSettings if err != nil { b.log.Warn("error increasing RV", "error", err) } - - // Update the last import time. This is important to trigger reindexing - // of the resource for a given namespace. - if err := b.updateLastImportTime(ctx, tx, key, time.Now()); err != nil { - return rollbackWithError(err) - } } - return nil - }) - if err != nil { - rsp.Error = resource.AsErrorResult(err) + + // Update the last import time. This is important to trigger reindexing + // of the resource for a given namespace. + if err := b.updateLastImportTime(ctx, tx, key, time.Now()); err != nil { + return rollbackWithError(err) + } } - return rsp + return nil } func (b *backend) updateLastImportTime(ctx context.Context, tx db.Tx, key *resourcepb.ResourceKey, now time.Time) error { diff --git a/pkg/storage/unified/sql/db/dbimpl/db.go b/pkg/storage/unified/sql/db/dbimpl/db.go index 262b03334aa..00099733bcf 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db.go +++ b/pkg/storage/unified/sql/db/dbimpl/db.go @@ -48,6 +48,11 @@ type sqlTx struct { *sql.Tx } +// NewTx wraps an existing *sql.Tx with sqlTx +func NewTx(tx *sql.Tx) db.Tx { + return sqlTx{tx} +} + func (tx sqlTx) QueryContext(ctx context.Context, query string, args ...any) (db.Rows, error) { // // codeql-suppress go/sql-query-built-from-user-controlled-sources "The query comes from a safe template source // and the parameters are passed as arguments." diff --git a/pkg/util/xorm/session.go b/pkg/util/xorm/session.go index b174b8e4ed9..03edf4b2983 100644 --- a/pkg/util/xorm/session.go +++ b/pkg/util/xorm/session.go @@ -7,6 +7,7 @@ package xorm import ( "context" "database/sql" + "errors" "fmt" "hash/crc32" "reflect" @@ -43,7 +44,7 @@ type Session struct { afterProcessors []executedProcessor prepareStmt bool - stmtCache map[uint32]*core.Stmt //key: hash.Hash32 of (queryStr, len(queryStr)) + stmtCache map[uint32]*core.Stmt // key: hash.Hash32 of (queryStr, len(queryStr)) // !evalphobia! stored the last executed query on this session lastSQL string @@ -236,6 +237,14 @@ func (session *Session) DB() *core.DB { return session.db } +// Tx returns the underlying transaction +func (session *Session) Tx() (*core.Tx, error) { + if session.tx == nil { + return nil, errors.New("no open transaction") + } + return session.tx, nil +} + func cleanupProcessorsClosures(slices *[]func(any)) { if len(*slices) > 0 { *slices = make([]func(any), 0) From 43c3322cafe8e1e7ba8600df8394293b374bae52 Mon Sep 17 00:00:00 2001 From: Ana Ivanov <38096095+anaivanov@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:28:19 +0100 Subject: [PATCH 81/94] Increase limitPerPlugin from 40 to 80 (#114573) * Increase limitPerPlugin from 40 to 60 * Increase limitPerPlugin from 60 to 80 --- .../app/features/commandPalette/actions/useExtensionActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/commandPalette/actions/useExtensionActions.ts b/public/app/features/commandPalette/actions/useExtensionActions.ts index 112bdc5ab0d..643a4647e2d 100644 --- a/public/app/features/commandPalette/actions/useExtensionActions.ts +++ b/public/app/features/commandPalette/actions/useExtensionActions.ts @@ -13,7 +13,7 @@ export default function useExtensionActions(): CommandPaletteAction[] { const { links } = usePluginLinks({ extensionPointId: PluginExtensionPoints.CommandPalette, context, - limitPerPlugin: 40, + limitPerPlugin: 80, }); return useMemo(() => { From 3c76e9ee7203f0cddb7ea0a56a8499f86f679824 Mon Sep 17 00:00:00 2001 From: Tom Ratcliffe Date: Fri, 28 Nov 2025 12:42:36 +0000 Subject: [PATCH 82/94] Teams: Refactor most functionality to use hooks (#113713) --- eslint-suppressions.json | 5 - .../src/clients/rtkq/legacy/endpoints.gen.ts | 14 +- .../src/handlers/all-handlers.ts | 6 +- .../handlers/api/access-control/handlers.ts | 11 ++ .../src/handlers/api/teams/handlers.ts | 21 +++ pkg/services/team/model.go | 10 +- pkg/services/team/teamapi/team.go | 4 + public/api-enterprise-spec.json | 11 +- public/api-merged.json | 17 +- public/app/api/clients/iam/v0alpha1/index.ts | 4 +- .../app/core/components/RolePicker/utils.ts | 12 +- public/app/core/reducers/root.test.ts | 64 -------- public/app/features/teams/CreateTeam.test.tsx | 2 +- public/app/features/teams/CreateTeam.tsx | 52 ++++-- .../app/features/teams/TeamGroupSync.test.tsx | 6 +- public/app/features/teams/TeamGroupSync.tsx | 7 +- public/app/features/teams/TeamList.test.tsx | 6 +- public/app/features/teams/TeamList.tsx | 117 ++++++------- public/app/features/teams/TeamPages.tsx | 19 +-- .../app/features/teams/TeamSettings.test.tsx | 28 ++-- public/app/features/teams/TeamSettings.tsx | 25 ++- public/app/features/teams/hooks.ts | 154 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 130 ++------------- .../app/features/teams/state/reducers.test.ts | 74 --------- public/app/features/teams/state/reducers.ts | 60 +------ .../features/teams/state/selectors.test.ts | 22 --- public/app/features/teams/state/selectors.ts | 10 +- public/app/types/accessControl.ts | 15 +- public/app/types/teams.ts | 53 +----- public/locales/en-US/grafana.json | 2 + public/openapi3.json | 19 ++- 31 files changed, 384 insertions(+), 596 deletions(-) create mode 100644 packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts delete mode 100644 public/app/core/reducers/root.test.ts create mode 100644 public/app/features/teams/hooks.ts delete mode 100644 public/app/features/teams/state/reducers.test.ts delete mode 100644 public/app/features/teams/state/selectors.test.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 71b855ae3b9..e3e822abdc4 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3183,11 +3183,6 @@ "count": 1 } }, - "public/app/features/teams/state/reducers.ts": { - "@typescript-eslint/consistent-type-assertions": { - "count": 1 - } - }, "public/app/features/templating/fieldAccessorCache.ts": { "@typescript-eslint/no-explicit-any": { "count": 1 diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts index 45ff0d1e91e..e0f6b885e00 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts @@ -1644,7 +1644,12 @@ const injectedRtkApi = api invalidatesTags: ['teams'], }), getTeamById: build.query({ - query: (queryArg) => ({ url: `/teams/${queryArg.teamId}` }), + query: (queryArg) => ({ + url: `/teams/${queryArg.teamId}`, + params: { + accesscontrol: queryArg.accesscontrol, + }, + }), providesTags: ['teams'], }), updateTeam: build.mutation({ @@ -3474,6 +3479,7 @@ export type DeleteTeamByIdApiArg = { export type GetTeamByIdApiResponse = /** status 200 (empty) */ TeamDto; export type GetTeamByIdApiArg = { teamId: string; + accesscontrol?: boolean; }; export type UpdateTeamApiResponse = /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody; @@ -6051,10 +6057,8 @@ export type SearchTeamGroupsQueryResult = { totalCount?: number; }; export type UpdateTeamCommand = { - Email?: string; - ExternalUID?: string; - ID?: number; - Name?: string; + email?: string; + name?: string; }; export type TeamMemberDto = { auth_module?: string; diff --git a/packages/grafana-test-utils/src/handlers/all-handlers.ts b/packages/grafana-test-utils/src/handlers/all-handlers.ts index 5a0f1cdc140..5fa473b55d5 100644 --- a/packages/grafana-test-utils/src/handlers/all-handlers.ts +++ b/packages/grafana-test-utils/src/handlers/all-handlers.ts @@ -1,5 +1,6 @@ import { HttpHandler } from 'msw'; +import accessControlHandlers from './api/access-control/handlers'; import dashboardsHandlers from './api/dashboards/handlers'; import folderHandlers from './api/folders/handlers'; import pluginsHandlers from './api/plugins/handlers'; @@ -14,11 +15,12 @@ import appPlatformIamv0alpha1Handlers from './apis/iam.grafana.app/v0alpha1/hand const allHandlers: HttpHandler[] = [ // Legacy handlers - ...teamsHandlers, + ...accessControlHandlers, ...dashboardsHandlers, ...folderHandlers, - ...searchHandlers, ...pluginsHandlers, + ...searchHandlers, + ...teamsHandlers, ...userHandlers, // App platform handlers diff --git a/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts b/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts new file mode 100644 index 00000000000..ce5f8f43d5e --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/api/access-control/handlers.ts @@ -0,0 +1,11 @@ +import { HttpResponse, http } from 'msw'; + +const searchTeamRolesHandler = () => + http.post('/api/access-control/teams/roles/search', async () => { + // TODO: Add better mock roles response as needed + return HttpResponse.json([]); + }); + +const handlers = [searchTeamRolesHandler()]; + +export default handlers; diff --git a/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts b/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts index e9fb6c0a112..c8a42bf9bba 100644 --- a/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts +++ b/packages/grafana-test-utils/src/handlers/api/teams/handlers.ts @@ -127,6 +127,26 @@ const createTeamHandler = () => return HttpResponse.json({ message: 'Team created', teamId: 10, uid: 'aethyfifmhwcgd' }, { status: 200 }); }); +const updateTeamHandler = () => + http.put<{ uid: string }, { name: string; email: string }>('/api/teams/:uid', async ({ params, request }) => { + const teamData = mockTeamsMap.get(params.uid); + const body = await request.json(); + if (!teamData) { + return HttpResponse.json({ message: 'Not found' }, { status: 404 }); + } + const updatedTeam = { + ...teamData, + team: { + ...teamData.team, + name: body.name, + email: body.email, + }, + }; + mockTeamsMap.set(params.uid, updatedTeam); + + return HttpResponse.json({ message: 'Team updated' }); + }); + const handlers = [ teamsPreferencesHandler(), teamsGroupsHandler(), @@ -135,6 +155,7 @@ const handlers = [ getTeamHandler(), deleteTeamHandler(), createTeamHandler(), + updateTeamHandler(), ]; export default handlers; diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index 4d01c0a3d0b..3696505139a 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -47,11 +47,11 @@ type CreateTeamCommand struct { } type UpdateTeamCommand struct { - ID int64 - Name string - Email string - ExternalUID string - OrgID int64 `json:"-"` + ID int64 `json:"-"` + Name string `json:"name"` + Email string `json:"email"` + ExternalUID string `json:"-"` + OrgID int64 `json:"-"` } type DeleteTeamCommand struct { diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index 9fca7089d2b..230d4ab0f2d 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -312,6 +312,10 @@ type GetTeamByIDParams struct { // in:path // required:true TeamID string `json:"team_id"` + // in:query + // required:false + // default: false + AccessControl bool `json:"accesscontrol"` } // swagger:parameters deleteTeamByID diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index c976dcb9e7d..054cf609e40 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -8744,17 +8744,10 @@ "UpdateTeamCommand": { "type": "object", "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "type": "integer", - "format": "int64" - }, - "Name": { + "name": { "type": "string" } } diff --git a/public/api-merged.json b/public/api-merged.json index 0d9c4129adc..12dd95170cf 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -10117,6 +10117,12 @@ "name": "team_id", "in": "path", "required": true + }, + { + "type": "boolean", + "default": false, + "name": "accesscontrol", + "in": "query" } ], "responses": { @@ -23164,17 +23170,10 @@ "UpdateTeamCommand": { "type": "object", "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "type": "integer", - "format": "int64" - }, - "Name": { + "name": { "type": "string" } } diff --git a/public/app/api/clients/iam/v0alpha1/index.ts b/public/app/api/clients/iam/v0alpha1/index.ts index 190f7ede868..f54e38f7e15 100644 --- a/public/app/api/clients/iam/v0alpha1/index.ts +++ b/public/app/api/clients/iam/v0alpha1/index.ts @@ -2,7 +2,5 @@ import { generatedAPI } from '@grafana/api-clients/rtkq/iam/v0alpha1'; export const iamAPIv0alpha1 = generatedAPI.enhanceEndpoints({}); -export const { useGetDisplayMappingQuery, useLazyGetDisplayMappingQuery } = iamAPIv0alpha1; - // eslint-disable-next-line no-barrel-files/no-barrel-files -export type { DisplayList } from '@grafana/api-clients/rtkq/iam/v0alpha1'; +export * from '@grafana/api-clients/rtkq/iam/v0alpha1'; diff --git a/public/app/core/components/RolePicker/utils.ts b/public/app/core/components/RolePicker/utils.ts index 413671f0090..f386634e307 100644 --- a/public/app/core/components/RolePicker/utils.ts +++ b/public/app/core/components/RolePicker/utils.ts @@ -1,3 +1,4 @@ +import { RoleDto } from 'app/api/clients/legacy'; import { Role } from 'app/types/accessControl'; export const isNotDelegatable = (role: Role) => { @@ -23,9 +24,10 @@ export const addDisplayNameForFixedRole = (role: Role) => { // Adds a display name for use when the list of roles is filtered // If either group or displayName are undefined, we fall back (see RoleMenuOption.tsx) -export const addFilteredDisplayName = (role: Role) => { - if (role.group && role.displayName) { - role.filteredDisplayName = role.group + ':' + role.displayName; - } - return role; +export const addFilteredDisplayName = (role: RoleDto): Role => { + const filteredDisplayName = role.group && role.displayName ? `${role.group}:${role.displayName}` : ''; + return { + ...role, + filteredDisplayName, + }; }; diff --git a/public/app/core/reducers/root.test.ts b/public/app/core/reducers/root.test.ts deleted file mode 100644 index c9f5d5068bf..00000000000 --- a/public/app/core/reducers/root.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Team } from 'app/types/teams'; - -import { reducerTester } from '../../../test/core/redux/reducerTester'; -import { initialTeamsState, teamsLoaded } from '../../features/teams/state/reducers'; -import { StoreState } from '../../types/store'; -import { cleanUpAction } from '../actions/cleanUp'; - -import { createRootReducer } from './root'; - -describe('rootReducer', () => { - const rootReducer = createRootReducer(); - - describe('when called with any action except cleanUpAction', () => { - it('then it should not clean state', () => { - const teams = [{ id: 1 } as Team]; - const state = { - teams: { ...initialTeamsState }, - } as StoreState; - - reducerTester() - .givenReducer(rootReducer, state) - .whenActionIsDispatched(teamsLoaded({ teams: teams, page: 1, noTeams: false, perPage: 30, totalCount: 1 })) - .thenStatePredicateShouldEqual((resultingState) => { - expect(resultingState.teams).toEqual({ - hasFetched: true, - noTeams: false, - perPage: 30, - totalPages: 1, - query: '', - page: 1, - teams, - }); - return true; - }); - }); - }); - - describe('when called with cleanUpAction', () => { - it('then it should clean state', () => { - const teams = [{ id: 1 }] as Team[]; - const state: StoreState = { - teams: { - hasFetched: true, - query: '', - page: 1, - noTeams: false, - totalPages: 1, - perPage: 30, - teams, - }, - } as StoreState; - - reducerTester() - .givenReducer(rootReducer, state, false, true) - .whenActionIsDispatched( - cleanUpAction({ cleanupAction: (storeState) => (storeState.teams = initialTeamsState) }) - ) - .thenStatePredicateShouldEqual((resultingState) => { - expect(resultingState.teams).toEqual({ ...initialTeamsState }); - return true; - }); - }); - }); -}); diff --git a/public/app/features/teams/CreateTeam.test.tsx b/public/app/features/teams/CreateTeam.test.tsx index 519b835cd52..12db809d40f 100644 --- a/public/app/features/teams/CreateTeam.test.tsx +++ b/public/app/features/teams/CreateTeam.test.tsx @@ -8,7 +8,7 @@ import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; -import { CreateTeam } from './CreateTeam'; +import CreateTeam from './CreateTeam'; setBackendSrv(backendSrv); setupMockServer(); diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index 050f7aefa1e..82ee804f6d3 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -3,16 +3,19 @@ import { useForm } from 'react-hook-form'; import { NavModelItem } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getBackendSrv, locationService } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import { Button, Field, Input, FieldSet, Stack } from '@grafana/ui'; +import { extractErrorMessage } from 'app/api/utils'; import { Page } from 'app/core/components/Page/Page'; import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; -import { updateTeamRoles } from 'app/core/components/RolePicker/api'; import { useRoleOptions } from 'app/core/components/RolePicker/hooks'; +import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/services/context_srv'; -import { Role, AccessControlAction } from 'app/types/accessControl'; +import { Role } from 'app/types/accessControl'; import { TeamDTO } from 'app/types/teams'; +import { useCreateTeam } from './hooks'; + const pageNav: NavModelItem = { icon: 'users-alt', id: 'team-new', @@ -20,8 +23,11 @@ const pageNav: NavModelItem = { subTitle: 'Create a new team. Teams let you grant permissions to a group of users.', }; -export const CreateTeam = (): JSX.Element => { +const CreateTeam = (): JSX.Element => { const currentOrgId = contextSrv.user.orgId; + + const notifyApp = useAppNotification(); + const [createTeamTrigger] = useCreateTeam(); const [pendingRoles, setPendingRoles] = useState([]); const [{ roleOptions }] = useRoleOptions(currentOrgId); const { @@ -30,21 +36,28 @@ export const CreateTeam = (): JSX.Element => { formState: { errors }, } = useForm(); - const canUpdateRoles = - contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && - contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); - const createTeam = async (formModel: TeamDTO) => { try { - const newTeam = await getBackendSrv().post('/api/teams', formModel); - if (newTeam.teamId) { - await contextSrv.fetchUserPermissions(); - if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles) { - await updateTeamRoles(pendingRoles, newTeam.teamId, newTeam.orgId); - } - locationService.push(`/org/teams/edit/${newTeam.uid}`); + const { data, error } = await createTeamTrigger( + { + email: formModel.email || '', + name: formModel.name, + }, + pendingRoles + ); + + const errorMessage = error ? extractErrorMessage(error) : undefined; + + if (errorMessage) { + notifyApp.error(errorMessage); + return; + } + + if (data && data.uid) { + locationService.push(`/org/teams/edit/${data.uid}`); } } catch (e) { + notifyApp.error(t('teams.create-team.failed-to-create', 'Failed to create team')); console.error(e); } }; @@ -85,8 +98,13 @@ export const CreateTeam = (): JSX.Element => { 'This is optional and is primarily used for allowing custom team avatars' )} > - {/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */} - + diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx index e3bd21b7e3a..d3a83d9c5dc 100644 --- a/public/app/features/teams/TeamGroupSync.test.tsx +++ b/public/app/features/teams/TeamGroupSync.test.tsx @@ -4,7 +4,7 @@ import { setBackendSrv } from '@grafana/runtime'; import { setupMockServer } from '@grafana/test-utils/server'; import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; import { backendSrv } from 'app/core/services/backend_srv'; -import { Team, TeamGroup, TeamState } from 'app/types/teams'; +import { TeamGroup, TeamState } from 'app/types/teams'; import TeamGroupSync from './TeamGroupSync'; import { getMockTeamGroups } from './mocks/teamMocks'; @@ -13,12 +13,10 @@ setBackendSrv(backendSrv); setupMockServer(); const setup = (preloadedTeamState?: Partial) => { - return render(, { + return render(, { preloadedState: { team: { - members: [], groups: [], - team: { uid: MOCK_TEAMS[0].metadata.name } as Team, ...preloadedTeamState, }, }, diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index e87a1d4380f..37887ee8d6a 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -29,6 +29,7 @@ const mapDispatchToProps = { interface OwnProps { isReadOnly: boolean; + teamUid: string; } interface State { @@ -52,7 +53,7 @@ export class TeamGroupSync extends PureComponent { } async fetchTeamGroups() { - this.props.loadTeamGroups(); + this.props.loadTeamGroups(this.props.teamUid); } onToggleAdding = () => { @@ -65,12 +66,12 @@ export class TeamGroupSync extends PureComponent { onAddGroup: FormEventHandler = (event) => { event.preventDefault(); - this.props.addTeamGroup(this.state.newGroupId); + this.props.addTeamGroup(this.props.teamUid, this.state.newGroupId); this.setState({ isAdding: false, newGroupId: '' }); }; onRemoveGroup = (group: TeamGroup) => { - this.props.removeTeamGroup(group.groupId); + this.props.removeTeamGroup(this.props.teamUid, group.groupId); }; isNewGroupValid() { diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index a78cbe2eceb..017b6ad2d25 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -40,16 +40,16 @@ describe('TeamList', () => { it('should enable the new team button', async () => { render(); - expect(screen.getByRole('link', { name: /new team/i })).not.toHaveStyle('pointer-events: none'); + expect(await screen.findByRole('link', { name: /new team/i })).not.toHaveStyle('pointer-events: none'); }); }); describe('when user does not have access to create a team', () => { - it('should disable the new team button', () => { + it('should disable the new team button', async () => { jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); render(); - expect(screen.getByRole('link', { name: /new team/i })).toHaveStyle('pointer-events: none'); + expect(await screen.findByRole('link', { name: /new team/i })).toHaveStyle('pointer-events: none'); }); }); }); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index e2ecbf07c4a..3ffd09b94fe 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import Skeleton from 'react-loading-skeleton'; -import { connect, ConnectedProps } from 'react-redux'; +import { SortingRule } from 'react-table'; import { Trans, t } from '@grafana/i18n'; import { @@ -14,6 +14,7 @@ import { InlineField, InteractiveTable, LinkButton, + LoadingPlaceholder, Pagination, Stack, Tag, @@ -24,16 +25,14 @@ import { Page } from 'app/core/components/Page/Page'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/services/context_srv'; import { Role, AccessControlAction } from 'app/types/accessControl'; -import { StoreState } from 'app/types/store'; import { TeamWithRoles } from 'app/types/teams'; import { TeamRolePicker } from '../../core/components/RolePicker/TeamRolePicker'; import { EnterpriseAuthFeaturesCard } from '../admin/EnterpriseAuthFeaturesCard'; -import { deleteTeam, loadTeams, changePage, changeQuery, changeSort } from './state/actions'; +import { useDeleteTeam, useGetTeams } from './hooks'; type Cell = CellProps; -export interface OwnProps {} export interface State { roleOptions: Role[]; @@ -49,26 +48,31 @@ const skeletonData: TeamWithRoles[] = new Array(3).fill(null).map((_, index) => isProvisioned: false, })); -const TeamList = ({ - teams, - query, - noTeams, - hasFetched, - loadTeams, - deleteTeam, - changeQuery, - totalPages, - page, - rolesLoading, - changePage, - changeSort, -}: Props) => { +const TeamList = () => { + const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate); + const displayRolePicker = shouldDisplayRolePicker(); + const pageSize = 20; + const [roleOptions, setRoleOptions] = useState([]); const styles = useStyles2(getStyles); + const [query, setQuery] = useState(''); + const [page, setPage] = useState(1); + const [sort, setSort] = useState(); + const { data, isLoading } = useGetTeams({ query, pageSize, page, sort }); + const [deleteTeam] = useDeleteTeam(); - useEffect(() => { - loadTeams(true); - }, [loadTeams]); + const teams = data?.teams || []; + const totalPages = Math.ceil((data?.totalCount || 0) / pageSize) || 0; + const noTeams = teams?.length === 0; + const changeSort = useCallback( + (sort: SortingRule) => { + setSort(`${sort.id}-${sort.desc ? 'desc' : 'asc'}`); + }, + [setSort] + ); + const changePage = (page: number) => { + setPage(page); + }; useEffect(() => { if (contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionRolesList)) { @@ -76,9 +80,6 @@ const TeamList = ({ } }, []); - const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate); - const displayRolePicker = shouldDisplayRolePicker(); - const columns: Array> = useMemo( () => [ { @@ -86,7 +87,7 @@ const TeamList = ({ header: '', disableGrow: true, cell: ({ cell: { value } }: Cell<'avatarUrl'>) => { - if (!hasFetched) { + if (isLoading) { return ; } @@ -97,7 +98,7 @@ const TeamList = ({ id: 'name', header: 'Name', cell: ({ cell: { value }, row: { original } }: Cell<'name'>) => { - if (!hasFetched) { + if (isLoading) { return ; } @@ -123,7 +124,7 @@ const TeamList = ({ id: 'email', header: 'Email', cell: ({ cell: { value } }: Cell<'email'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return value; @@ -135,7 +136,7 @@ const TeamList = ({ header: 'Members', disableGrow: true, cell: ({ cell: { value } }: Cell<'memberCount'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return value; @@ -147,8 +148,8 @@ const TeamList = ({ { id: 'role', header: 'Role', - cell: ({ cell: { value }, row: { original } }: Cell<'memberCount'>) => { - if (!hasFetched) { + cell: ({ row: { original } }: Cell<'memberCount'>) => { + if (isLoading) { return ; } const canSeeTeamRoles = contextSrv.hasPermissionInMetadata( @@ -160,7 +161,7 @@ const TeamList = ({ @@ -174,7 +175,7 @@ const TeamList = ({ id: 'isProvisioned', header: '', cell: ({ cell: { value } }: Cell<'isProvisioned'>) => { - if (!hasFetched) { + if (isLoading) { return ; } return !!value && ; @@ -185,7 +186,7 @@ const TeamList = ({ header: '', disableGrow: true, cell: ({ row: { original } }: Cell) => { - if (!hasFetched) { + if (isLoading) { return ( @@ -216,14 +217,14 @@ const TeamList = ({ })} size="sm" disabled={!canDelete} - onConfirm={() => deleteTeam(original.uid)} + onConfirm={() => deleteTeam({ uid: original.uid })} /> ); }, }, ], - [displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles] + [displayRolePicker, isLoading, styles.blockSkeleton, roleOptions, deleteTeam] ); return ( @@ -238,7 +239,7 @@ const TeamList = ({ } > - {noTeams ? ( + {!isLoading && !query && teams.length === 0 ? (
- {hasFetched && teams.length === 0 ? ( + {!isLoading && teams.length === 0 && ( - ) : ( + )} + {isLoading && } + {!isLoading && teams.length > 0 && ( String(team.id)} - fetchData={changeSort} + fetchData={({ sortBy }) => { + const sortingRule = sortBy.at(0); + if (sortingRule) { + return changeSort(sortingRule); + } + }} /> ; -export default connector(TeamList); +export default TeamList; const getStyles = () => ({ blockSkeleton: css({ diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 89a5cb5e032..3391a08c17d 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -1,7 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { memo, useRef } from 'react'; import { useParams } from 'react-router-dom-v5-compat'; -import { useAsync } from 'react-use'; import { featureEnabled } from '@grafana/runtime'; import { Page } from 'app/core/components/Page/Page'; @@ -10,14 +9,13 @@ import config from 'app/core/config'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types/accessControl'; -import { StoreState, useDispatch, useSelector } from 'app/types/store'; +import { StoreState, useSelector } from 'app/types/store'; import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync'; import TeamPermissions from './TeamPermissions'; import TeamSettings from './TeamSettings'; -import { loadTeam } from './state/actions'; +import { useGetTeam } from './hooks'; import { getTeamLoadingNav } from './state/navModel'; -import { getTeam } from './state/selectors'; type TeamPageRouteParams = { uid: string; @@ -32,11 +30,6 @@ enum PageTypes { const PAGES = ['members', 'settings', 'groupsync']; -const teamSelector = createSelector( - [(state: StoreState) => state.team, (_: StoreState, teamUid: string) => teamUid], - (team, teamUid) => getTeam(team, teamUid) -); - const pageNavSelector = createSelector( [ (state: StoreState) => state.navIndex, @@ -52,7 +45,8 @@ const pageNavSelector = createSelector( const TeamPages = memo(() => { const isSyncEnabled = useRef(featureEnabled('teamsync')); const { uid: teamUid = '', page } = useParams(); - const team = useSelector((state) => teamSelector(state, teamUid)); + + const { data: team, isLoading } = useGetTeam({ uid: teamUid }); let defaultPage = 'members'; // With RBAC the settings page will always be available @@ -62,9 +56,6 @@ const TeamPages = memo(() => { const pageName = page ?? defaultPage; const pageNav = useSelector((state) => pageNavSelector(state, pageName, teamUid)); - const dispatch = useDispatch(); - const { loading: isLoading } = useAsync(async () => dispatch(loadTeam(teamUid)), [teamUid]); - const renderPage = () => { const currentPage = PAGES.includes(pageName) ? pageName : PAGES[0]; @@ -89,7 +80,7 @@ const TeamPages = memo(() => { case PageTypes.GroupSync: if (isSyncEnabled.current) { if (canReadTeamPermissions) { - return ; + return ; } } else if (config.featureToggles.featureHighlights) { return ( diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx index 9d31644657a..f5cd0a6fcf3 100644 --- a/public/app/features/teams/TeamSettings.test.tsx +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -1,12 +1,13 @@ -import { render, screen, waitFor } from 'test/test-utils'; +import { render, screen } from 'test/test-utils'; import { setBackendSrv } from '@grafana/runtime'; import { setupMockServer } from '@grafana/test-utils/server'; import { MOCK_TEAMS } from '@grafana/test-utils/unstable'; +import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; -import { Props, TeamSettings } from './TeamSettings'; +import TeamSettings from './TeamSettings'; jest.spyOn(contextSrv, 'hasPermission').mockImplementation(() => true); jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockImplementation(() => true); @@ -14,9 +15,9 @@ jest.spyOn(contextSrv, 'hasPermissionInMetadata').mockImplementation(() => true) setBackendSrv(backendSrv); setupMockServer(); -const setup = (propOverrides?: object) => { +const setup = () => { const team = MOCK_TEAMS[0]; - const props: Props = { + const props = { team: { id: Number(team.metadata.labels['grafana.app/deprecatedInternalID']), uid: team.metadata.name, @@ -25,12 +26,14 @@ const setup = (propOverrides?: object) => { orgId: 1, isProvisioned: false, }, - updateTeam: jest.fn(), }; - Object.assign(props, propOverrides); - - return render(); + return render( + <> + + + + ); }; describe('Team settings', () => { @@ -41,8 +44,7 @@ describe('Team settings', () => { }); it('should validate required fields', async () => { - const mockUpdate = jest.fn(); - const { user } = setup({ updateTeam: mockUpdate }); + const { user } = setup(); await screen.findByText('Team details'); await user.clear(screen.getByRole('textbox', { name: /Name/ })); @@ -50,12 +52,10 @@ describe('Team settings', () => { await user.click(screen.getByRole('button', { name: 'Save team details' })); expect(await screen.findByText('Name is required')).toBeInTheDocument(); - await waitFor(() => expect(mockUpdate).not.toHaveBeenCalled()); }); it('should submit form with correct values', async () => { - const mockUpdate = jest.fn(); - const { user } = setup({ updateTeam: mockUpdate }); + const { user } = setup(); await user.clear(screen.getByRole('textbox', { name: /Name/ })); await user.clear(screen.getByLabelText(/Email/i)); @@ -63,6 +63,6 @@ describe('Team settings', () => { await user.type(screen.getByLabelText(/Email/i), 'team@test.com'); await user.click(screen.getByRole('button', { name: 'Save team details' })); - await waitFor(() => expect(mockUpdate).toHaveBeenCalledWith('New team', 'team@test.com')); + expect(await screen.findByText('Team updated')).toBeInTheDocument(); }); }); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index a4779df63cc..7a847d9ebf3 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,5 +1,4 @@ import { useForm } from 'react-hook-form'; -import { ConnectedProps, connect } from 'react-redux'; import { Trans, t } from '@grafana/i18n'; import { Button, Field, FieldSet, Input, Stack } from '@grafana/ui'; @@ -10,22 +9,16 @@ import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types/accessControl'; import { Team } from 'app/types/teams'; -import { updateTeam } from './state/actions'; +import { useUpdateTeam } from './hooks'; -const mapDispatchToProps = { - updateTeam, -}; - -const connector = connect(null, mapDispatchToProps); - -interface OwnProps { +interface Props { team: Team; } -export type Props = ConnectedProps & OwnProps; -export const TeamSettings = ({ team, updateTeam }: Props) => { +const TeamSettings = ({ team }: Props) => { const canWriteTeamSettings = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsWrite, team); const currentOrgId = contextSrv.user.orgId; + const [updateTeam] = useUpdateTeam(); const [{ roleOptions }] = useRoleOptions(currentOrgId); const { @@ -43,7 +36,13 @@ export const TeamSettings = ({ team, updateTeam }: Props) => { contextSrv.hasPermission(AccessControlAction.ActionRolesList); const onSubmit = async (formTeam: Team) => { - updateTeam(formTeam.name, formTeam.email || ''); + return updateTeam({ + uid: team.uid, + team: { + name: formTeam.name, + email: formTeam.email || '', + }, + }); }; return ( @@ -103,4 +102,4 @@ export const TeamSettings = ({ team, updateTeam }: Props) => { ); }; -export default connector(TeamSettings); +export default TeamSettings; diff --git a/public/app/features/teams/hooks.ts b/public/app/features/teams/hooks.ts new file mode 100644 index 00000000000..2c563ef3d2f --- /dev/null +++ b/public/app/features/teams/hooks.ts @@ -0,0 +1,154 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useEffect, useMemo } from 'react'; + +import { + useSearchTeamsQuery as useLegacySearchTeamsQuery, + useCreateTeamMutation, + useDeleteTeamByIdMutation, + useListTeamsRolesQuery, + CreateTeamCommand, + useSetTeamRolesMutation, + useGetTeamByIdQuery, + useUpdateTeamMutation, + UpdateTeamCommand, +} from 'app/api/clients/legacy'; +import { updateNavIndex } from 'app/core/actions'; +import { addFilteredDisplayName } from 'app/core/components/RolePicker/utils'; +import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction, Role } from 'app/types/accessControl'; +import { useDispatch } from 'app/types/store'; + +import { buildNavModel } from './state/navModel'; + +const rolesEnabled = + contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList); + +const canUpdateRoles = () => + contextSrv.hasPermission(AccessControlAction.ActionUserRolesAdd) && + contextSrv.hasPermission(AccessControlAction.ActionUserRolesRemove); + +/** + * Get list of teams and their associated roles (if roles are enabled) + */ +export const useGetTeams = ({ + query, + pageSize, + page, + sort, +}: { + query?: string; + pageSize?: number; + page?: number; + sort?: string; +}) => { + const legacyResponse = useLegacySearchTeamsQuery({ perpage: pageSize, accesscontrol: true, page, sort, query }); + + const teamIds = useMemo(() => { + const teams = legacyResponse.data?.teams || []; + const ids = teams.map((team) => team.id); + return ids.filter((id): id is number => id !== undefined); + }, [legacyResponse.data?.teams]); + + const teamsRolesResponse = useListTeamsRolesQuery( + rolesEnabled && teamIds.length ? { rolesSearchQuery: { teamIds } } : skipToken + ); + + const teamsWithRoles = useMemo(() => { + if (!rolesEnabled || (rolesEnabled && teamsRolesResponse.isLoading)) { + return legacyResponse.data?.teams || []; + } + return (legacyResponse.data?.teams || []).map((team) => { + const roles = team.id ? teamsRolesResponse.data?.[team.id] || [] : []; + const mappedRoles = roles.map((role) => addFilteredDisplayName(role)); + return { + ...team, + roles: mappedRoles, + }; + }); + }, [legacyResponse, teamsRolesResponse]); + + return { + ...legacyResponse, + isLoading: legacyResponse.isLoading || (rolesEnabled ? teamsRolesResponse.isLoading : false), + data: { + teams: teamsWithRoles, + totalCount: legacyResponse.data?.totalCount, + }, + }; +}; + +/** + * Get a single team by UID + */ +export const useGetTeam = ({ uid }: { uid: string }) => { + const response = useGetTeamByIdQuery({ teamId: uid, accesscontrol: true }); + const dispatch = useDispatch(); + + // TODO: Eventually remove and handle nav index logic elsewhere + useEffect(() => { + if (response.data) { + dispatch(updateNavIndex(buildNavModel(response.data))); + } + }, [response.data, dispatch]); + + return response; +}; + +/** + * Update a team by UID + */ +export const useUpdateTeam = () => { + const [updateTeam, response] = useUpdateTeamMutation(); + + const trigger = async ({ uid, team }: { uid: string; team: UpdateTeamCommand }) => { + const mutationResult = await updateTeam({ + teamId: uid, + updateTeamCommand: team, + }); + + return mutationResult; + }; + + return [trigger, response] as const; +}; + +/** + * Delete a team by UID + */ +export const useDeleteTeam = () => { + const [deleteTeam, response] = useDeleteTeamByIdMutation(); + + return [({ uid }: { uid: string }) => deleteTeam({ teamId: uid }), response] as const; +}; + +/** + * Create a new team, and link any pending roles + */ +export const useCreateTeam = () => { + const [createTeam, response] = useCreateTeamMutation(); + const [setTeamRoles] = useSetTeamRolesMutation(); + + const trigger = async (team: CreateTeamCommand, pendingRoles?: Role[]) => { + const mutationResult = await createTeam({ + createTeamCommand: team, + }); + + const { data } = mutationResult; + + if (data && data.teamId && pendingRoles && pendingRoles.length) { + await contextSrv.fetchUserPermissions(); + if (contextSrv.licensedAccessControlEnabled() && canUpdateRoles()) { + await setTeamRoles({ + teamId: data.teamId, + setTeamRolesCommand: { + roleUids: pendingRoles.map((role) => role.uid), + }, + }); + } + } + + return mutationResult; + }; + + return [trigger, response] as const; +}; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 49c22c2c449..d0d05e0946c 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,134 +1,26 @@ -import { debounce } from 'lodash'; - import { getBackendSrv } from '@grafana/runtime'; -import { FetchDataArgs } from '@grafana/ui'; -import { updateNavIndex } from 'app/core/actions'; -import { contextSrv } from 'app/core/services/context_srv'; -import { accessControlQueryParam } from 'app/core/utils/accessControl'; -import { AccessControlAction } from 'app/types/accessControl'; import { ThunkResult } from 'app/types/store'; -import { Team, TeamWithRoles } from 'app/types/teams'; -import { buildNavModel } from './navModel'; -import { - teamGroupsLoaded, - queryChanged, - pageChanged, - teamLoaded, - teamsLoaded, - sortChanged, - rolesFetchBegin, - rolesFetchEnd, -} from './reducers'; +import { teamGroupsLoaded } from './reducers'; -export function loadTeams(initial = false): ThunkResult { - return async (dispatch, getState) => { - const { query, page, perPage, sort } = getState().teams; - // Early return if the user cannot list teams - if (!contextSrv.hasPermission(AccessControlAction.ActionTeamsRead)) { - dispatch(teamsLoaded({ teams: [], totalCount: 0, page: 1, perPage, noTeams: true })); - return; - } - - const response = await getBackendSrv().get( - '/api/teams/search', - accessControlQueryParam({ query, page, perpage: perPage, sort }) - ); - - // We only want to check if there is no teams on the initial request. - // A query that returns no teams should not render the empty list banner. - let noTeams = false; - if (initial) { - noTeams = response.teams.length === 0; - } - - if ( - contextSrv.licensedAccessControlEnabled() && - contextSrv.hasPermission(AccessControlAction.ActionTeamsRolesList) - ) { - dispatch(rolesFetchBegin()); - const teamIds = response?.teams.map((t: TeamWithRoles) => t.id); - const roles = await getBackendSrv().post(`/api/access-control/teams/roles/search`, { teamIds }); - response.teams.forEach((t: TeamWithRoles) => { - t.roles = roles ? roles[t.id] || [] : []; - }); - dispatch(rolesFetchEnd()); - } - - dispatch(teamsLoaded({ noTeams, ...response })); - }; -} - -const loadTeamsWithDebounce = debounce((dispatch) => dispatch(loadTeams()), 500); - -export function loadTeam(uid: string): ThunkResult> { +export function loadTeamGroups(teamUid: string): ThunkResult { return async (dispatch) => { - const response = await getBackendSrv().get(`/api/teams/${uid}`, accessControlQueryParam()); - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }; -} - -export function deleteTeam(uid: string): ThunkResult { - return async (dispatch) => { - await getBackendSrv().delete(`/api/teams/${uid}`); - // Update users permissions in case they lost teams.read with the deletion - await contextSrv.fetchUserPermissions(); - dispatch(loadTeams()); - }; -} - -export function changeQuery(query: string): ThunkResult { - return async (dispatch) => { - dispatch(queryChanged(query)); - loadTeamsWithDebounce(dispatch); - }; -} - -export function changePage(page: number): ThunkResult { - return async (dispatch) => { - dispatch(pageChanged(page)); - dispatch(loadTeams()); - }; -} - -export function changeSort({ sortBy }: FetchDataArgs): ThunkResult { - const sort = sortBy.length ? `${sortBy[0].id}-${sortBy[0].desc ? 'desc' : 'asc'}` : undefined; - return async (dispatch) => { - dispatch(sortChanged(sort)); - dispatch(loadTeams()); - }; -} - -export function updateTeam(name: string, email: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - await getBackendSrv().put(`/api/teams/${team.uid}`, { name, email }); - dispatch(loadTeam(team.uid)); - }; -} - -export function loadTeamGroups(): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - const response = await getBackendSrv().get(`/api/teams/${team.uid}/groups`); + const response = await getBackendSrv().get(`/api/teams/${teamUid}/groups`); dispatch(teamGroupsLoaded(response)); }; } -export function addTeamGroup(groupId: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; - await getBackendSrv().post(`/api/teams/${team.uid}/groups`, { groupId: groupId }); - dispatch(loadTeamGroups()); +export function addTeamGroup(teamUid: string, groupId: string): ThunkResult { + return async (dispatch) => { + await getBackendSrv().post(`/api/teams/${teamUid}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups(teamUid)); }; } -export function removeTeamGroup(groupId: string): ThunkResult { - return async (dispatch, getStore) => { - const team = getStore().team.team; +export function removeTeamGroup(teamUid: string, groupId: string): ThunkResult { + return async (dispatch) => { // need to use query parameter due to escaped characters in the request - await getBackendSrv().delete(`/api/teams/${team.uid}/groups?groupId=${encodeURIComponent(groupId)}`); - dispatch(loadTeamGroups()); + await getBackendSrv().delete(`/api/teams/${teamUid}/groups?groupId=${encodeURIComponent(groupId)}`); + dispatch(loadTeamGroups(teamUid)); }; } diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts deleted file mode 100644 index 4ea5c963739..00000000000 --- a/public/app/features/teams/state/reducers.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { TeamsState, TeamState } from 'app/types/teams'; - -import { reducerTester } from '../../../../test/core/redux/reducerTester'; -import { getMockTeam, getMockTeamGroups } from '../mocks/teamMocks'; - -import { - initialTeamsState, - initialTeamState, - teamGroupsLoaded, - teamLoaded, - queryChanged, - teamReducer, - teamsLoaded, - teamsReducer, -} from './reducers'; - -describe('teams reducer', () => { - describe('when teamsLoaded is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamsReducer, { ...initialTeamsState }) - .whenActionIsDispatched( - teamsLoaded({ teams: [getMockTeam()], page: 1, perPage: 30, noTeams: false, totalCount: 100 }) - ) - .thenStateShouldEqual({ - ...initialTeamsState, - hasFetched: true, - teams: [getMockTeam()], - noTeams: false, - totalPages: 4, - perPage: 30, - page: 1, - }); - }); - }); - - describe('when setSearchQueryAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamsReducer, { ...initialTeamsState }) - .whenActionIsDispatched(queryChanged('test')) - .thenStateShouldEqual({ - ...initialTeamsState, - query: 'test', - }); - }); - }); -}); - -describe('team reducer', () => { - describe('when loadTeamsAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamReducer, { ...initialTeamState }) - .whenActionIsDispatched(teamLoaded(getMockTeam())) - .thenStateShouldEqual({ - ...initialTeamState, - team: getMockTeam(), - }); - }); - }); - - describe('when loadTeamGroupsAction is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(teamReducer, { ...initialTeamState }) - .whenActionIsDispatched(teamGroupsLoaded(getMockTeamGroups(1))) - .thenStateShouldEqual({ - ...initialTeamState, - groups: getMockTeamGroups(1), - }); - }); - }); -}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 2ef9dde3838..50011316a72 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,60 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { TeamsState, Team, TeamState, TeamGroup } from 'app/types/teams'; - -export const initialTeamsState: TeamsState = { - teams: [], - page: 1, - query: '', - perPage: 30, - totalPages: 0, - noTeams: false, - hasFetched: false, -}; - -type TeamsFetched = { - teams: Team[]; - page: number; - perPage: number; - noTeams: boolean; - totalCount: number; -}; - -const teamsSlice = createSlice({ - name: 'teams', - initialState: initialTeamsState, - reducers: { - teamsLoaded: (state, action: PayloadAction): TeamsState => { - const { totalCount, perPage, ...rest } = action.payload; - const totalPages = Math.ceil(totalCount / perPage); - return { ...state, ...rest, totalPages, perPage, hasFetched: true }; - }, - queryChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, page: 1, query: action.payload }; - }, - pageChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, page: action.payload }; - }, - sortChanged: (state, action: PayloadAction): TeamsState => { - return { ...state, sort: action.payload, page: 1 }; - }, - rolesFetchBegin: (state) => { - return { ...state, rolesLoading: true }; - }, - rolesFetchEnd: (state) => { - return { ...state, rolesLoading: false }; - }, - }, -}); - -export const { teamsLoaded, queryChanged, pageChanged, sortChanged, rolesFetchBegin, rolesFetchEnd } = - teamsSlice.actions; - -export const teamsReducer = teamsSlice.reducer; +import { TeamState, TeamGroup } from 'app/types/teams'; export const initialTeamState: TeamState = { - team: {} as Team, - members: [], groups: [], }; @@ -62,20 +10,16 @@ const teamSlice = createSlice({ name: 'team', initialState: initialTeamState, reducers: { - teamLoaded: (state, action: PayloadAction): TeamState => { - return { ...state, team: action.payload }; - }, teamGroupsLoaded: (state, action: PayloadAction): TeamState => { return { ...state, groups: action.payload }; }, }, }); -export const { teamLoaded, teamGroupsLoaded } = teamSlice.actions; +export const { teamGroupsLoaded } = teamSlice.actions; export const teamReducer = teamSlice.reducer; export default { - teams: teamsReducer, team: teamReducer, }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts deleted file mode 100644 index 983c5d35a71..00000000000 --- a/public/app/features/teams/state/selectors.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { TeamState } from 'app/types/teams'; - -import { getMockTeam } from '../mocks/teamMocks'; - -import { getTeam } from './selectors'; - -describe('Team selectors', () => { - describe('Get team', () => { - const mockTeam = getMockTeam(); - - it('should return team if matching with location team', () => { - const mockState: TeamState = { - team: mockTeam, - members: [], - groups: [], - }; - - const team = getTeam(mockState, 'aaaaaa'); - expect(team).toEqual(mockTeam); - }); - }); -}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index ec99297158c..4ada9ee8d46 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,11 +1,3 @@ -import { Team, TeamState } from 'app/types/teams'; +import { TeamState } from 'app/types/teams'; export const getTeamGroups = (state: TeamState) => state.groups; - -export const getTeam = (state: TeamState, currentTeamUid: string): Team | null => { - if (state.team.uid === currentTeamUid) { - return state.team; - } - - return null; -}; diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index d09459b9f9b..b2e2fc31a9b 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -1,3 +1,5 @@ +import { RoleDto } from 'app/api/clients/legacy'; + /** * UserPermission is a map storing permissions in a form of * { @@ -174,17 +176,6 @@ export enum AccessControlAction { MigrationAssistantMigrate = 'migrationassistant:migrate', } -export interface Role { - uid: string; - name: string; - displayName: string; +export interface Role extends RoleDto { filteredDisplayName: string; // name to be shown in filtered role list - description: string; - group: string; - global: boolean; - delegatable?: boolean; - mapped?: boolean; - version: number; - created: string; - updated: string; } diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index f592b362894..72202a21295 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -1,4 +1,4 @@ -import { WithAccessControlMetadata } from '@grafana/data'; +import { TeamDto as TeamDtoLegacy } from 'app/api/clients/legacy'; import { Role } from './accessControl'; @@ -13,42 +13,7 @@ export interface TeamDTO { name: string; } -// This is the team resource with permissions and metadata expanded -export interface Team extends WithAccessControlMetadata { - /** - * Internal id of team - * @deprecated use uid instead - */ - id: number; - /** - * A unique identifier for the team. - */ - uid: string; // Prefer UUID - /** - * AvatarUrl is the team's avatar URL. - */ - avatarUrl?: string; - /** - * Email of the team. - */ - email?: string; - /** - * MemberCount is the number of the team members. - */ - memberCount: number; - /** - * Name of the team. - */ - name: string; - /** - * OrgId is the ID of an organisation the team belongs to. - */ - orgId: number; - /** - * isProvisioned is set if the team has been provisioned from IdP. - */ - isProvisioned: boolean; -} +export type Team = TeamDtoLegacy; export interface TeamWithRoles extends Team { /** @@ -73,20 +38,6 @@ export interface TeamGroup { teamId: number; } -export interface TeamsState { - teams: Team[]; - page: number; - query: string; - perPage: number; - noTeams: boolean; - totalPages: number; - hasFetched: boolean; - sort?: string; - rolesLoading?: boolean; -} - export interface TeamState { - team: Team; - members: TeamMember[]; groups: TeamGroup[]; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f2d2366b9cd..6230117bcf2 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13277,6 +13277,7 @@ "create-team": { "create": "Create", "description-email": "This is optional and is primarily used for allowing custom team avatars", + "failed-to-create": "Failed to create team", "label-email": "Email", "label-name": "Name", "label-role": "Role" @@ -13308,6 +13309,7 @@ "title-edit-team": "Edit team", "tooltip-edit-team": "Edit team" }, + "loading-teams": "Loading teams...", "new-team": "New Team", "placeholder-search-teams": "Search teams" }, diff --git a/public/openapi3.json b/public/openapi3.json index 20b97b3f14d..52322514490 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -12703,17 +12703,10 @@ }, "UpdateTeamCommand": { "properties": { - "Email": { + "email": { "type": "string" }, - "ExternalUID": { - "type": "string" - }, - "ID": { - "format": "int64", - "type": "integer" - }, - "Name": { + "name": { "type": "string" } }, @@ -24935,6 +24928,14 @@ "schema": { "type": "string" } + }, + { + "in": "query", + "name": "accesscontrol", + "schema": { + "default": false, + "type": "boolean" + } } ], "responses": { From f0e9c2e8a386b476c16f84f4686507d0389601d8 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 28 Nov 2025 12:53:38 +0000 Subject: [PATCH 83/94] FS: Fix HTML loader jumping a few pixels (#114581) --- pkg/services/frontend/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 0a589f81a49..b0364fcbac9 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -79,6 +79,7 @@ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + line-height: 1; /* prevent shift when css loads in that changes the body line-height */ } .fs-variant-loader, .fs-variant-error, .fs-custom-domain-error { From d3d294c99eee0755e6f24e3dd057834dbf6050c5 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Fri, 28 Nov 2025 15:03:11 +0200 Subject: [PATCH 84/94] Dashboards: Per panel GroupBy action (#113445) * wip per panel group by * wip groupBy per panel * wip groupBy per panel * groupBy per panel action tests * fix * fix * fix * fix * CR mods * switch to dropdown * adjust apply * optimise action logic to avoid unnecessary triggers * canary scenes * wip (cherry picked from commit 51a00db93d0805f481a9e48213382468f1eb2986) * optimise action logic to avoid unnecessary triggers (cherry picked from commit c4de2dfff88c02c5aef61d1cbee070b5f6e5ccbc) * refactor * refactor * memoize values/ refactor * refactor * refactor components - do not make async call unless queries/groupByOptions change * canary scenes * fix test * Optimise handlers * Reset options if they are not applied * refactor subscriptions * refactor * scenes bump * fixes * properly deactivate header actions on panel edit * list * refactor showing menu using css, remove header deactivation code from panel-edit * cleanup * cleanup * cleanup + action redesign * i18n * pr mods * translations * fix * fix * fix design --------- Co-authored-by: Sergej-Vlasov Co-authored-by: Dominik Prokop --- .../src/types/featureToggles.gen.ts | 4 + .../src/selectors/components.ts | 3 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.json | 13 ++ .../scene/VizPanelHeaderActions.test.tsx | 154 +++++++++++++++++ .../scene/VizPanelHeaderActions.tsx | 136 +++++++++++++++ .../scene/VizPanelSubHeader.tsx | 5 +- .../PanelGroupByAction/PanelGroupByAction.tsx | 150 +++++++++++++++++ .../PanelGroupByActionPopover.tsx | 156 ++++++++++++++++++ .../serialization/layoutSerializers/utils.ts | 7 + .../transformSaveModelToScene.ts | 4 + .../features/dashboard-scene/utils/utils.ts | 4 + public/locales/en-US/grafana.json | 6 + 14 files changed, 648 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx create mode 100644 public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx create mode 100644 public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 35cccd78375..d853a64395a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -377,6 +377,10 @@ export interface FeatureToggles { */ perPanelNonApplicableDrilldowns?: boolean; /** + * Enabled a group by action per panel + */ + panelGroupBy?: boolean; + /** * Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard */ panelFilterVariable?: boolean; diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 98fa6053d11..36d0088b258 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -443,6 +443,9 @@ export const versionedComponents = { PanelDataErrorMessage: { '10.4.0': 'data-testid Panel data error message', }, + PanelGroupByHeaderAction: { + '12.4.0': 'data-testid Panel group by header action', + }, }, Visualization: { Graph: { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 950476d1a8f..c112e0b77a0 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -607,6 +607,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "panelGroupBy", + Description: "Enabled a group by action per panel", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, { Name: "panelFilterVariable", Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index a7423d583ab..1382204ce0f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -84,6 +84,7 @@ kubernetesDashboardsV2,experimental,@grafana/dashboards-squad,false,false,false dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true perPanelNonApplicableDrilldowns,experimental,@grafana/dashboards-squad,false,false,true +panelGroupBy,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ea80af0831e..8451faca5d0 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2512,6 +2512,19 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "panelGroupBy", + "resourceVersion": "1764257043719", + "creationTimestamp": "2025-11-27T15:24:03Z" + }, + "spec": { + "description": "Enabled a group by action per panel", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, { "metadata": { "name": "panelTimeSettings", diff --git a/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx new file mode 100644 index 00000000000..24b83e853fe --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.test.tsx @@ -0,0 +1,154 @@ +import { of } from 'rxjs'; + +import { DataQueryRequest, DataSourceApi, LoadingState } from '@grafana/data'; +import { getPanelPlugin } from '@grafana/data/test'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { + GroupByVariable, + SceneDataTransformer, + SceneQueryRunner, + SceneVariableSet, + VizPanel, + VizPanelState, +} from '@grafana/scenes'; + +import { activateFullSceneTree } from '../utils/test-utils'; + +import { DashboardScene } from './DashboardScene'; +import { VizPanelHeaderActions } from './VizPanelHeaderActions'; +import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; + +const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => { + return of({ + state: LoadingState.Loading, + series: [], + timeRange: request.range, + }); +}); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => { + return runRequestMock(ds, request); + }, + getDataSourceSrv: () => ({ + get: jest.fn().mockResolvedValue({ + getRef: () => ({ uid: 'ds-1', type: 'test' }), + }), + getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds-1', type: 'test' }), + }), + getPluginImportUtils: () => ({ + getPanelPluginFromCache: jest.fn(() => undefined), + }), +})); + +setPluginImportUtils({ + importPanelPlugin: () => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: () => undefined, +}); + +describe('VizPanelHeaderActions', () => { + it('renders PanelGroupByAction when the group by variable applies to the panel', async () => { + const { headerActions } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + }); + + it('does not set when applicability is disabled', async () => { + const { headerActions } = await buildScene({ applicabilityEnabled: false }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set when the datasource uid does not match', async () => { + const { headerActions } = await buildScene({ + variableDatasourceUid: 'other-ds', + }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set if variable ds changes to a different type', async () => { + const { headerActions, groupByVariable } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + groupByVariable.setState({ datasource: { uid: 'ds-2' } }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('does not set if variable applicability becomes disabled', async () => { + const { headerActions, groupByVariable } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + groupByVariable.setState({ applicabilityEnabled: false }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); + + it('sdoes not set if queryRunner changes datasource to different one than vars', async () => { + const { headerActions, queryRunner } = await buildScene(); + + expect(headerActions.state.supportsApplicability).toBe(true); + + queryRunner.setState({ datasource: { uid: 'ds-2' } }); + + expect(headerActions.state.supportsApplicability).toBe(false); + }); +}); + +interface BuildSceneOptions { + applicabilityEnabled?: boolean; + variableDatasourceUid?: string; +} + +async function buildScene(options?: BuildSceneOptions) { + const headerActions = new VizPanelHeaderActions({}); + + const queryRunner = new SceneQueryRunner({ + datasource: { uid: 'ds-1' }, + queries: [{ refId: 'A', datasource: { uid: 'ds-1' } }], + }); + + const groupByVariable = new GroupByVariable({ + name: 'group', + label: 'group', + value: [], + text: [], + options: [], + applicabilityEnabled: options?.applicabilityEnabled ?? true, + datasource: { uid: options?.variableDatasourceUid ?? 'ds-1' }, + }); + + const dataProvider = new SceneDataTransformer({ + $data: queryRunner, + transformations: [], + }); + + const panelState: VizPanelState = { + key: 'panel-1', + title: 'Panel A', + pluginId: 'timeseries', + headerActions, + $data: dataProvider, + options: {}, + fieldConfig: { defaults: {}, overrides: [] }, + }; + + const panel = new VizPanel(panelState); + + const scene = new DashboardScene({ + $variables: new SceneVariableSet({ + variables: [groupByVariable], + }), + body: DefaultGridLayoutManager.fromVizPanels([panel]), + }); + + activateFullSceneTree(scene); + + await new Promise((r) => setTimeout(r, 1)); + + return { headerActions, panel, groupByVariable, queryRunner }; +} diff --git a/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx new file mode 100644 index 00000000000..3130d746c94 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VizPanelHeaderActions.tsx @@ -0,0 +1,136 @@ +import { Unsubscribable } from 'rxjs'; + +import { + GroupByVariable, + SceneComponentProps, + sceneGraph, + SceneObjectBase, + SceneObjectState, + SceneQueryRunner, + VizPanel, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +import { verifyDrilldownApplicability } from '../utils/drilldownUtils'; + +import { PanelGroupByAction } from './panel-actions/PanelGroupByAction/PanelGroupByAction'; + +export interface VizPanelHeaderActionsState extends SceneObjectState { + hideGroupByAction?: boolean; + supportsApplicability?: boolean; +} + +export class VizPanelHeaderActions extends SceneObjectBase { + static Component = VizPanelHeaderActionsRenderer; + + private _groupByVar?: GroupByVariable; + private _groupBySub?: Unsubscribable; + private _queryRunnerDatasource?: DataSourceRef; + + constructor(state: Partial) { + super({ + hideGroupByAction: state.hideGroupByAction ?? false, + ...state, + }); + + this.addActivationHandler(this._onActivate); + } + + private _onActivate = () => { + if (!this.parent || !(this.parent instanceof VizPanel)) { + throw new Error('VizPanelHeaderActions must be a child of a VizPanel'); + } + + if (!this.state.hideGroupByAction) { + this.subscribeToGroupByChanges(); + } + + return () => { + this._groupBySub?.unsubscribe(); + }; + }; + + private setAplicabilitySupport(groupByDs?: DataSourceRef | null, groupByApplicability?: boolean) { + this.setState({ + supportsApplicability: verifyDrilldownApplicability( + this, + this._queryRunnerDatasource, + groupByDs ?? this._groupByVar?.state.datasource ?? null, + groupByApplicability ?? this._groupByVar?.state.applicabilityEnabled ?? false + ), + }); + } + + private subscribeToGroupByChanges() { + const vars = sceneGraph.getVariables(this); + const queryRunner = this.getQueryRunner(); + + this._groupByVar = vars.state.variables.find((variable) => variable instanceof GroupByVariable); + this._queryRunnerDatasource = queryRunner?.state.datasource; + + this.setAplicabilitySupport(); + + // check when var set updates and search for groupBy var + this._subs.add( + vars.subscribeToState((n) => { + this._groupByVar = n.variables.find((variable) => variable instanceof GroupByVariable); + + if (this._groupByVar) { + this._groupBySub?.unsubscribe(); + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setAplicabilitySupport(n.datasource, n.applicabilityEnabled); + } + }); + } + }) + ); + + // update query runner datasource changes + this._subs.add( + queryRunner?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource) { + this._queryRunnerDatasource = n.datasource; + + this.setAplicabilitySupport(); + } + }) + ); + + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { + if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { + this.setAplicabilitySupport(n.datasource, n.applicabilityEnabled); + } + }); + } + + public getQueryRunner() { + const panel = this.parent; + const dataObject = panel ? sceneGraph.getData(panel) : undefined; + const queryRunner = dataObject?.state.$data; + + if (!queryRunner || !(queryRunner instanceof SceneQueryRunner)) { + return null; + } + + return queryRunner; + } +} + +export function VizPanelHeaderActionsRenderer({ model }: SceneComponentProps) { + const { hideGroupByAction, supportsApplicability } = model.useState(); + const variables = sceneGraph.getVariables(model); + const groupByVariable = variables.state.variables.find((variable) => variable instanceof GroupByVariable); + const queryRunner = model.getQueryRunner(); + const queries = queryRunner?.state.data?.request?.targets ?? []; + + return ( + <> + {!hideGroupByAction && supportsApplicability && ( +
+ +
+ )} + + ); +} diff --git a/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx index f2247661e04..ff6f6c0712e 100644 --- a/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx +++ b/public/app/features/dashboard-scene/scene/VizPanelSubHeader.tsx @@ -116,7 +116,7 @@ export class VizPanelSubHeader extends SceneObjectBase { private refreshDrilldownVarsSubscriptions() { if (this._groupByVar) { this._groupBySub?.unsubscribe(); - this._groupByVar?.subscribeToState((n, p) => { + this._groupBySub = this._groupByVar?.subscribeToState((n, p) => { if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { this.setDrilldownApplicabilitySupportHelper(undefined, { datasource: n.datasource, @@ -127,7 +127,8 @@ export class VizPanelSubHeader extends SceneObjectBase { } if (this._adHocVar) { - this._adHocVar?.subscribeToState((n, p) => { + this._adHocSub?.unsubscribe(); + this._adHocSub = this._adHocVar?.subscribeToState((n, p) => { if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) { this.setDrilldownApplicabilitySupportHelper({ datasource: n.datasource, diff --git a/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx new file mode 100644 index 00000000000..b12cc52cbe3 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByAction.tsx @@ -0,0 +1,150 @@ +import { useState, useCallback, useEffect, useMemo, useRef } from 'react'; +import { lastValueFrom } from 'rxjs'; + +import { fuzzySearch } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { Trans } from '@grafana/i18n'; +import { GroupByVariable, SceneDataQuery, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; +import { Button, Icon, Popover } from '@grafana/ui'; + +import { PanelGroupByActionPopover } from './PanelGroupByActionPopover'; + +interface Props { + groupByVariable: GroupByVariable; + queries: SceneDataQuery[]; +} + +export function PanelGroupByAction({ groupByVariable, queries }: Props) { + const { options: groupByOptions } = groupByVariable.useState(); + + const [options, setOptions] = useState([]); + const [selectedValues, setSelectedValues] = useState([]); + const [searchValue, setSearchValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [reloadOptions, setReloadOptions] = useState(false); + const [isPopoverVisible, setPopoverVisible] = useState(false); + + const ref = useRef(null); + + const fetchOptions = useCallback(async () => { + if (!groupByVariable || !reloadOptions) { + return; + } + + setIsLoading(true); + try { + if (groupByOptions.length === 0) { + await lastValueFrom(groupByVariable.validateAndUpdate()); + const options = await getApplicableGroupByOptions(groupByVariable, groupByVariable.state.options, queries); + + setSelectedValues(getGroupByValue(groupByVariable)); + setOptions(options); + return; + } + + const options = await getApplicableGroupByOptions(groupByVariable, groupByOptions, queries); + + setSelectedValues(getGroupByValue(groupByVariable)); + setOptions(options); + } catch (error) { + setSelectedValues([]); + setOptions([]); + } finally { + setIsLoading(false); + setReloadOptions(false); + } + }, [groupByOptions, groupByVariable, queries, reloadOptions]); + + useEffect(() => { + setReloadOptions(true); + }, [groupByOptions, queries]); + + useEffect(() => { + if (isPopoverVisible) { + fetchOptions(); + } + }, [fetchOptions, isPopoverVisible]); + + const filteredOptions = useMemo(() => { + if (!searchValue) { + return options; + } + + const haystack = options.map((option) => option.label); + const indices = fuzzySearch(haystack, searchValue); + return indices.map((idx) => options[idx]); + }, [options, searchValue]); + + const onCancel = () => { + setSearchValue(''); + setPopoverVisible(false); + }; + + const openPopover = () => { + // Reset checked state to match current variable value when opening + setSelectedValues(getGroupByValue(groupByVariable)); + setPopoverVisible(true); + }; + + return ( + + ); +} + +function getGroupByValue(groupByVariable: GroupByVariable) { + return Array.isArray(groupByVariable.state.value) + ? groupByVariable.state.value + : groupByVariable.state.value + ? [groupByVariable.state.value] + : []; +} + +async function getApplicableGroupByOptions( + groupByVariable: GroupByVariable, + options: VariableValueOption[], + queries: SceneDataQuery[] +) { + const values = options.map((option) => option.value); + const applicability = await groupByVariable.getGroupByApplicabilityForQueries(values, queries); + + return applicability + ? applicability.filter((item) => item.applicable).map((item) => ({ label: item.key, value: item.key })) + : options; +} diff --git a/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx new file mode 100644 index 00000000000..06191e1f7bf --- /dev/null +++ b/public/app/features/dashboard-scene/scene/panel-actions/PanelGroupByAction/PanelGroupByActionPopover.tsx @@ -0,0 +1,156 @@ +import { css, cx } from '@emotion/css'; +import { useCallback } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t, Trans } from '@grafana/i18n'; +import { GroupByVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes'; +import { Button, Checkbox, ClickOutsideWrapper, Icon, Input, Stack, useStyles2 } from '@grafana/ui'; + +interface Props { + groupByVariable: GroupByVariable; + onCancel: () => void; + isLoading: boolean; + searchValue: string; + setSearchValue: (value: string) => void; + options: VariableValueOption[]; + values: VariableValueSingle[]; + onValuesChange: (value: VariableValueSingle[]) => void; +} + +export function PanelGroupByActionPopover({ + groupByVariable, + onCancel, + isLoading, + searchValue, + setSearchValue, + options, + values, + onValuesChange, +}: Props) { + const styles = useStyles2(getStyles); + + const onCheckedChanged = useCallback( + (option: VariableValueOption) => (event: React.FormEvent) => { + const newValues = event.currentTarget.checked + ? values.concat(option.value) + : values.filter((c) => c !== option.value); + + onValuesChange(newValues); + }, + [onValuesChange, values] + ); + + const isChecked = (option: VariableValueOption) => { + return values.includes(option.value); + }; + + const handleApply = useCallback(() => { + if (!values.length) { + return; + } + + groupByVariable.changeValueTo(values, values.map(String), true); + onCancel(); + }, [groupByVariable, onCancel, values]); + + const isAnyOptionChecked = () => { + if (!values.length) { + return false; + } + + return values.some((value) => options.find((option) => option.value === value)); + }; + + return ( + + {/* This is just blocking click events from bubbeling and should not have a keyboard interaction. */} + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */} +
ev.stopPropagation()}> + +
+ } + placeholder={t('panel-group-by.search-placeholder', 'Search')} + value={searchValue} + onChange={(e) => setSearchValue(e.currentTarget.value)} + /> +
+ +
+ {isLoading ? ( +
+ Loading options +
+ ) : options.length === 0 ? ( +
+ No options found +
+ ) : ( + options.map((option) => { + return ( +
+ +
+ ); + }) + )} +
+ + + + + +
+
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + menuContainer: css({ + display: 'flex', + flexDirection: 'column', + background: theme.colors.background.elevated, + border: `1px solid ${theme.colors.border.weak}`, + borderRadius: theme.shape.radius.default, + boxShadow: theme.shadows.z3, + padding: theme.spacing(2), + }), + searchContainer: css({ + width: '100%', + paddingBottom: theme.spacing(1), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + listContainer: css({ + flex: 1, + overflow: 'auto', + minHeight: '100px', + maxHeight: '300px', + padding: theme.spacing(0.5), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + option: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: theme.spacing(1), + cursor: 'pointer', + borderRadius: theme.shape.radius.default, + '&:hover': { + background: theme.colors.background.secondary, + }, + '&:focus-visible': { + outline: `2px solid ${theme.colors.primary.border}`, + outlineOffset: '-2px', + }, + }), + emptyMessage: css({ + padding: theme.spacing(2), + textAlign: 'center', + color: theme.colors.text.secondary, + }), +}); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index 512631961d3..788dd53d11a 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -31,6 +31,7 @@ import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior'; import { PanelNotices } from '../../scene/PanelNotices'; +import { VizPanelHeaderActions } from '../../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../../scene/VizPanelSubHeader'; import { AutoGridItem } from '../../scene/layout-auto-grid/AutoGridItem'; import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem'; @@ -71,6 +72,9 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel { seriesLimit: config.panelSeriesLimit, $data: createPanelDataProvider(panel), titleItems, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), subHeader: new VizPanelSubHeader({ hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, }), @@ -121,6 +125,9 @@ export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPane }), ], extendPanelContext: setDashboardPanelContext, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID, title: panel.spec.title, options: {}, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 9194adf6a7b..3c334b022c1 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -46,6 +46,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { PanelNotices } from '../scene/PanelNotices'; +import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager'; @@ -442,6 +443,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem { hoverHeaderOffset: 0, $data: createPanelDataProvider(panel), titleItems, + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), subHeader: new VizPanelSubHeader({ hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns, }), diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 7188756b055..6c486021e62 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -24,6 +24,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel'; +import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions'; import { VizPanelSubHeader } from '../scene/VizPanelSubHeader'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { setDashboardPanelContext } from '../scene/setDashboardPanelContext'; @@ -283,6 +284,9 @@ export function getDefaultVizPanel(): VizPanel { menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior], }), + headerActions: new VizPanelHeaderActions({ + hideGroupByAction: !config.featureToggles.panelGroupBy, + }), $data: new SceneDataTransformer({ $data: new SceneQueryRunner({ queries: [{ refId: 'A' }], diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6230117bcf2..d9efaadc4d6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11094,6 +11094,12 @@ "could-anything-matching-query": "Could not find anything matching your query" } }, + "panel-group-by": { + "button": "Group by", + "loading": "Loading options", + "no-options": "No options found", + "search-placeholder": "Search" + }, "panel-type-filter": { "clear-button": "Clear types", "select-aria-label": "Panel type filter", From 8227ecb499784dd466e168be28fe87b8179719d3 Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Fri, 28 Nov 2025 06:17:59 -0700 Subject: [PATCH 85/94] Dashboard Controls: Display dashboard links on the right side of the toolbar (#114378) * have dashboard links on the right side * Lets try a compromise (#114389) Try to compromise on white space. Having a lot of links will create unessecary empty space under the time controls, but it is necessary if we want to be able to have the links on the right --------- Co-authored-by: Oscar Kilhed Co-authored-by: Sergej-Vlasov --- .../scene/DashboardControls.tsx | 23 +++++++++++++++---- .../scene/DashboardLinksControls.tsx | 23 +++++++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 66b87804f76..0679ab32f88 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -176,8 +176,13 @@ function DashboardControlsRenderer({ model }: SceneComponentProps
)} - {!hideDashboardControls && model.hasDashboardControls() && } + {!hideDashboardControls && model.hasDashboardControls() && ( +
+ +
+ )} {config.featureToggles.dashboardNewLayouts && } + {!hideLinksControls && !editPanel && }
{!hideVariableControls && ( <> @@ -185,7 +190,6 @@ function DashboardControlsRenderer({ model }: SceneComponentProps )} - {!hideLinksControls && !editPanel && } {editPanel && } {showDebugger && }
@@ -268,17 +272,26 @@ function getStyles(theme: GrafanaTheme2) { }), rightControls: css({ display: 'flex', - justifyContent: 'flex-end', gap: theme.spacing(1), - marginBottom: theme.spacing(1), float: 'right', - alignItems: 'flex-start', + alignItems: 'center', + flexWrap: 'wrap', + maxWidth: '100%', + minWidth: 0, }), timeControls: css({ display: 'flex', justifyContent: 'flex-end', gap: theme.spacing(1), marginBottom: theme.spacing(1), + order: 2, + marginLeft: 'auto', + flexShrink: 0, + alignSelf: 'flex-start', + }), + dashboardControlsButton: css({ + order: 2, + marginLeft: 'auto', }), rightControlsWrap: css({ flexWrap: 'wrap', diff --git a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx index 8053055fe81..11639554e5d 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLinksControls.tsx @@ -1,5 +1,9 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; import { sceneGraph } from '@grafana/scenes'; import { DashboardLink } from '@grafana/schema'; +import { useStyles2 } from '@grafana/ui'; import { DashboardLinkRenderer } from './DashboardLinkRenderer'; import { DashboardScene } from './DashboardScene'; @@ -12,18 +16,33 @@ export interface Props { export function DashboardLinksControls({ links, dashboard }: Props) { sceneGraph.getTimeRange(dashboard).useState(); const uid = dashboard.state.uid; + const styles = useStyles2(getStyles); if (!links || !uid) { return null; } return ( - <> +
{links .filter((link) => link.placement === undefined) .map((link: DashboardLink, index: number) => ( ))} - +
); } + +function getStyles(theme: GrafanaTheme2) { + return { + linksContainer: css({ + display: 'flex', + flexWrap: 'wrap', + gap: theme.spacing(1), + maxWidth: '100%', + minWidth: 0, + order: 1, + flex: '1 1 0%', + }), + }; +} From c75137b2d3706adadb382cd5e4c58891601bf2e3 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:18:14 +0000 Subject: [PATCH 86/94] QueryVariableForm: Refil query variable query on default data source update (#114491) * refil qury variable query on default ds update * refactor datasource update logic * adjust test --- .../components/QueryVariableForm.test.tsx | 2 +- .../variables/components/QueryVariableForm.tsx | 16 +++++++++++----- .../editors/QueryVariableEditor.test.tsx | 10 ++++++---- .../variables/editors/QueryVariableEditor.tsx | 4 ++-- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx index 63a1912fde7..d3d23396974 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.test.tsx @@ -178,7 +178,7 @@ describe('QueryVariableEditorForm', () => { await userEvent.click(screen.getByText(/prometheus/i)); expect(mockOnDataSourceChange).toHaveBeenCalledTimes(1); - expect(mockOnDataSourceChange).toHaveBeenCalledWith(promDatasource, undefined); + expect(mockOnDataSourceChange).toHaveBeenCalledWith(promDatasource); }); it('should call onQueryChange when changing the query', async () => { diff --git a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx index 3849fb0a2c5..030de016a5f 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/QueryVariableForm.tsx @@ -1,4 +1,4 @@ -import { FormEvent } from 'react'; +import { FormEvent, useCallback } from 'react'; import { useAsync } from 'react-use'; import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data'; @@ -27,7 +27,7 @@ type VariableQueryType = QueryVariable['state']['query']; interface QueryVariableEditorFormProps { datasource?: DataSourceRef; - onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void; + onDataSourceChange: (dsSettings: DataSourceInstanceSettings, preserveQuery?: boolean) => void; query: VariableQueryType; onQueryChange: (query: VariableQueryType) => void; onLegacyQueryChange: (query: VariableQueryType, definition: string) => void; @@ -89,17 +89,23 @@ export function QueryVariableEditorForm({ onQueryChange(query); } + // update data source if it is not defined in variable model if (!datasourceRef) { const instanceSettings = getDataSourceSrv().getInstanceSettings({ type: datasource.type, uid: datasource.uid }); - if (instanceSettings) { - onDataSourceChange(instanceSettings); + onDataSourceChange(instanceSettings, true); } } return { datasource, VariableQueryEditor }; }, [datasourceRef]); + // adjusting type miss match between DataSourcePicker onChange and onDataSourceChange + const datasourceChangeHandler = useCallback( + (dsSettings: DataSourceInstanceSettings) => onDataSourceChange(dsSettings), + [onDataSourceChange] + ); + const { datasource, VariableQueryEditor } = dsConfig ?? {}; return ( @@ -111,7 +117,7 @@ export function QueryVariableEditorForm({ label={t('dashboard-scene.query-variable-editor-form.label-data-source', 'Data source')} htmlFor="data-source-picker" > - + {datasource && VariableQueryEditor && ( diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx index 26f9516e40a..99385438e87 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.test.tsx @@ -152,10 +152,12 @@ describe('QueryVariableEditor', () => { const onRunQueryMock = jest.fn(); const variable = new QueryVariable({ datasource: undefined, query: '' }); - await setup({ - variable, - onRunQuery: onRunQueryMock, - }); + await act(() => + setup({ + variable, + onRunQuery: onRunQueryMock, + }) + ); await waitFor(async () => { expect(variable.state.datasource).not.toBe(undefined); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx index da1649776b2..106468b2b31 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx @@ -68,10 +68,10 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito const onAllowCustomValueChange = (event: FormEvent) => { variable.setState({ allowCustomValue: event.currentTarget.checked }); }; - const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings) => { + const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings, preserveQuery = false) => { const datasource = getDataSourceRef(dsInstanceSettings); - if ((variable.state.datasource?.type || '') !== datasource.type) { + if (!preserveQuery && (variable.state.datasource?.type || '') !== datasource.type) { variable.setState({ datasource, query: '', definition: '' }); return; } From 930f7ce48991c2e5eb4b2f2e10d2146f446bfd2c Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 28 Nov 2025 14:21:33 +0100 Subject: [PATCH 87/94] Dashboards: Cover the Switch variable in schema transformations - part 2. (#114549) * feat: add v2alpha_1 conversion for the switch variable * chore: gofmt fixes * chore: update comments in tests * chore: fix gofmt * Update specs * tests: update the v2alpha1 openapi snapshot --------- Co-authored-by: Ivan Ortega --- .../kinds/v2alpha1/dashboard_spec.cue | 21 +- .../dashboard/v2alpha1/dashboard_spec.cue | 21 +- .../dashboard/v2alpha1/dashboard_spec_gen.go | 71 +++- .../v2alpha1/zz_generated.openapi.go | 349 ++++++++++++------ ...enerated.openapi_violation_exceptions.list | 17 +- .../input/v1beta1.variable-conversions.json | 26 ++ ...v1beta1.variable-conversions.v0alpha1.json | 26 ++ ...v1beta1.variable-conversions.v2alpha1.json | 13 + .../v1beta1.variable-conversions.v2beta1.json | 13 + .../conversion/v1beta1_to_v2alpha1.go | 73 ++++ .../conversion/v2alpha1_to_v2beta1.go | 16 + .../conversion/v2alpha1_to_v2beta1_test.go | 144 ++++++++ .../conversion/v2beta1_to_v2alpha1.go | 17 + .../conversion/v2beta1_to_v2alpha1_test.go | 45 +++ .../dashboard/v2alpha1/types.spec.gen.ts | 33 +- .../dashboard.grafana.app-v2alpha1.json | 72 +++- 16 files changed, 809 insertions(+), 148 deletions(-) create mode 100644 apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 5bca05229db..5768c358c24 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -710,9 +710,9 @@ VariableCustomFormatterFn: { // `custom`: Define the variable options manually using a comma-separated list. // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | - "system" | "snapshot" + "system" | "snapshot" | "switch" -VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind // Sort variable options // Accepted values are: @@ -970,6 +970,23 @@ AdhocVariableKind: { spec: AdhocVariableSpec } +// Switch variable specification +SwitchVariableSpec: { + name: string | *"" + current: string | *"false" + enabledValue: string | *"true" + disabledValue: string | *"false" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +SwitchVariableKind: { + kind: "SwitchVariable" + spec: SwitchVariableSpec +} + ConditionalRenderingGroupKind: { kind: "ConditionalRenderingGroup" spec: ConditionalRenderingGroupSpec diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue index 76e0921eb88..9a8a621345f 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec.cue @@ -714,9 +714,9 @@ VariableCustomFormatterFn: { // `custom`: Define the variable options manually using a comma-separated list. // `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" | - "system" | "snapshot" + "system" | "snapshot" | "switch" -VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind +VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind // Sort variable options // Accepted values are: @@ -974,6 +974,23 @@ AdhocVariableKind: { spec: AdhocVariableSpec } +// Switch variable specification +SwitchVariableSpec: { + name: string | *"" + current: string | *"false" + enabledValue: string | *"true" + disabledValue: string | *"false" + label?: string + hide: VariableHide + skipUrlSync: bool | *false + description?: string +} + +SwitchVariableKind: { + kind: "SwitchVariable" + spec: SwitchVariableSpec +} + ConditionalRenderingGroupKind: { kind: "ConditionalRenderingGroup" spec: ConditionalRenderingGroupSpec diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 37d9ae8897f..b83cbbd6fb3 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -1291,11 +1291,11 @@ func NewDashboardTimeRangeOption() *DashboardTimeRangeOption { } // +k8s:openapi-gen=true -type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind +type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind // NewDashboardVariableKind creates a new DashboardVariableKind object. func NewDashboardVariableKind() *DashboardVariableKind { - return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind() + return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind() } // Query variable kind @@ -1814,6 +1814,45 @@ func NewDashboardMetricFindValue() *DashboardMetricFindValue { return &DashboardMetricFindValue{} } +// +k8s:openapi-gen=true +type DashboardSwitchVariableKind struct { + Kind string `json:"kind"` + Spec DashboardSwitchVariableSpec `json:"spec"` +} + +// NewDashboardSwitchVariableKind creates a new DashboardSwitchVariableKind object. +func NewDashboardSwitchVariableKind() *DashboardSwitchVariableKind { + return &DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: *NewDashboardSwitchVariableSpec(), + } +} + +// Switch variable specification +// +k8s:openapi-gen=true +type DashboardSwitchVariableSpec struct { + Name string `json:"name"` + Current string `json:"current"` + EnabledValue string `json:"enabledValue"` + DisabledValue string `json:"disabledValue"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` +} + +// NewDashboardSwitchVariableSpec creates a new DashboardSwitchVariableSpec object. +func NewDashboardSwitchVariableSpec() *DashboardSwitchVariableSpec { + return &DashboardSwitchVariableSpec{ + Name: "", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Hide: DashboardVariableHideDontHide, + SkipUrlSync: false, + } +} + // +k8s:openapi-gen=true type DashboardSpec struct { Annotations []DashboardAnnotationQueryKind `json:"annotations"` @@ -2404,7 +2443,7 @@ func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTab } // +k8s:openapi-gen=true -type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind struct { +type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind struct { QueryVariableKind *DashboardQueryVariableKind `json:"QueryVariableKind,omitempty"` TextVariableKind *DashboardTextVariableKind `json:"TextVariableKind,omitempty"` ConstantVariableKind *DashboardConstantVariableKind `json:"ConstantVariableKind,omitempty"` @@ -2413,15 +2452,16 @@ type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasou CustomVariableKind *DashboardCustomVariableKind `json:"CustomVariableKind,omitempty"` GroupByVariableKind *DashboardGroupByVariableKind `json:"GroupByVariableKind,omitempty"` AdhocVariableKind *DashboardAdhocVariableKind `json:"AdhocVariableKind,omitempty"` + SwitchVariableKind *DashboardSwitchVariableKind `json:"SwitchVariableKind,omitempty"` } -// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind object. -func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind { - return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind{} +// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind object. +func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind { + return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind{} } -// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind` as JSON. -func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind) MarshalJSON() ([]byte, error) { +// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind` as JSON. +func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind) MarshalJSON() ([]byte, error) { if resource.QueryVariableKind != nil { return json.Marshal(resource.QueryVariableKind) } @@ -2446,12 +2486,15 @@ func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKin if resource.AdhocVariableKind != nil { return json.Marshal(resource.AdhocVariableKind) } + if resource.SwitchVariableKind != nil { + return json.Marshal(resource.SwitchVariableKind) + } return []byte("null"), nil } -// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind` from JSON. -func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind) UnmarshalJSON(raw []byte) error { +// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind` from JSON. +func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind) UnmarshalJSON(raw []byte) error { if raw == nil { return nil } @@ -2524,6 +2567,14 @@ func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKi resource.QueryVariableKind = &dashboardQueryVariableKind return nil + case "SwitchVariable": + var dashboardSwitchVariableKind DashboardSwitchVariableKind + if err := json.Unmarshal(raw, &dashboardSwitchVariableKind); err != nil { + return err + } + + resource.SwitchVariableKind = &dashboardSwitchVariableKind + return nil case "TextVariable": var dashboardTextVariableKind DashboardTextVariableKind if err := json.Unmarshal(raw, &dashboardTextVariableKind); err != nil { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index a167aa6d36e..f595b8a3040 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -14,125 +14,127 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationActions": schema_pkg_apis_dashboard_v2alpha1_AnnotationActions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v2alpha1_AnnotationPermission(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.Dashboard": schema_pkg_apis_dashboard_v2alpha1_Dashboard(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAccess": schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAction": schema_pkg_apis_dashboard_v2alpha1_DashboardAction(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardActionVariable": schema_pkg_apis_dashboard_v2alpha1_DashboardActionVariable(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha1_DashboardAdHocFilterWithLabels(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardClient": schema_pkg_apis_dashboard_v2alpha1_DashboardClient(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDashboardLink(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDataLink(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDataQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef": schema_pkg_apis_dashboard_v2alpha1_DashboardDataSourceRef(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardDataTransformerConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2alpha1_DashboardDynamicConfigValue(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardElementReference": schema_pkg_apis_dashboard_v2alpha1_DashboardElementReference(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFetchOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardFetchOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldColor": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldColor(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfigSource(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardInfinityOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardInfinityOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardJSONCodec": schema_pkg_apis_dashboard_v2alpha1_DashboardJSONCodec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKindSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelRef(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardList": schema_pkg_apis_dashboard_v2alpha1_DashboardList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardMatcherConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v2alpha1_DashboardMetadata(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2alpha1_DashboardMetricFindValue(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKindOrLibraryPanelKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQueryKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQuerySpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryOptionsSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRangeMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRangeMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRegexMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRegexMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRowRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardSpecialValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrArrayOfString(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThreshold": schema_pkg_apis_dashboard_v2alpha1_DashboardThreshold(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardThresholdsConfig(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeRangeOption(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeSettingsSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTransformationKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTransformationKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1ActionStyle": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1ActionStyle(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1FieldConfigSourceOverrides(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RangeMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RangeMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RegexMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RegexMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1SpecialValueMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1SpecialValueMapOptions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMappingResult(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption": schema_pkg_apis_dashboard_v2alpha1_DashboardVariableOption(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigKind(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardWithAccessInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanel": schema_pkg_apis_dashboard_v2alpha1_LibraryPanel(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v2alpha1_VersionsQueryOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationActions": schema_pkg_apis_dashboard_v2alpha1_AnnotationActions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v2alpha1_AnnotationPermission(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.Dashboard": schema_pkg_apis_dashboard_v2alpha1_Dashboard(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAccess": schema_pkg_apis_dashboard_v2alpha1_DashboardAccess(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAction": schema_pkg_apis_dashboard_v2alpha1_DashboardAction(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardActionVariable": schema_pkg_apis_dashboard_v2alpha1_DashboardActionVariable(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdHocFilterWithLabels": schema_pkg_apis_dashboard_v2alpha1_DashboardAdHocFilterWithLabels(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAdhocVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationPanelFilter": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationPanelFilter(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAnnotationQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAutoGridRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardAutoGridRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardClient": schema_pkg_apis_dashboard_v2alpha1_DashboardClient(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingDataSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingDataSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingTimeRangeSizeSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingTimeRangeSizeSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableKindOrConditionalRenderingDataKindOrConditionalRenderingTimeRangeSizeKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConditionalRenderingVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardConstantVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardConversionStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardCustomVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDashboardLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataLink": schema_pkg_apis_dashboard_v2alpha1_DashboardDataLink(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDataQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataSourceRef": schema_pkg_apis_dashboard_v2alpha1_DashboardDataSourceRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDataTransformerConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardDataTransformerConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardDatasourceVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDynamicConfigValue": schema_pkg_apis_dashboard_v2alpha1_DashboardDynamicConfigValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardElementReference": schema_pkg_apis_dashboard_v2alpha1_DashboardElementReference(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFetchOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardFetchOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldColor": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldColor(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardFieldConfigSource": schema_pkg_apis_dashboard_v2alpha1_DashboardFieldConfigSource(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutItemSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutItemSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGridLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardGroupByVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardInfinityOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardInfinityOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardIntervalVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardJSONCodec": schema_pkg_apis_dashboard_v2alpha1_DashboardJSONCodec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelKindSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelKindSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardLibraryPanelRef": schema_pkg_apis_dashboard_v2alpha1_DashboardLibraryPanelRef(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardList": schema_pkg_apis_dashboard_v2alpha1_DashboardList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMatcherConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardMatcherConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v2alpha1_DashboardMetadata(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardMetricFindValue": schema_pkg_apis_dashboard_v2alpha1_DashboardMetricFindValue(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelKindOrLibraryPanelKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQueryKind": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQueryKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelQuerySpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelQuerySpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryGroupSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryGroupSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryOptionsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryOptionsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRangeMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRangeMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRegexMap": schema_pkg_apis_dashboard_v2alpha1_DashboardRegexMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardRowRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowKind": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutRowSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutRowSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardRowsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardRowsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrArrayOfString(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTextVariableSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThreshold": schema_pkg_apis_dashboard_v2alpha1_DashboardThreshold(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardThresholdsConfig": schema_pkg_apis_dashboard_v2alpha1_DashboardThresholdsConfig(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeRangeOption": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeRangeOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTimeSettingsSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTransformationKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTransformationKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1ActionStyle": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1ActionStyle(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1FieldConfigSourceOverrides": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1FieldConfigSourceOverrides(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RangeMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RangeMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1RegexMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1RegexMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardV2alpha1SpecialValueMapOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardV2alpha1SpecialValueMapOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMapOrRangeMapOrRegexMapOrSpecialValueMap(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardValueMappingResult": schema_pkg_apis_dashboard_v2alpha1_DashboardValueMappingResult(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption": schema_pkg_apis_dashboard_v2alpha1_DashboardVariableOption(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVersionList": schema_pkg_apis_dashboard_v2alpha1_DashboardVersionList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigKind": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigKind(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVizConfigSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardVizConfigSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v2alpha1_DashboardWithAccessInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanel": schema_pkg_apis_dashboard_v2alpha1_LibraryPanel(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v2alpha1_LibraryPanelStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.VersionsQueryOptions": schema_pkg_apis_dashboard_v2alpha1_VersionsQueryOptions(ref), } } @@ -3457,7 +3459,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKind(ref common.Re } } -func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -3503,11 +3505,16 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableKindOrTextVariable Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind"), }, }, + "SwitchVariableKind": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind"), + }, + }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConstantVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardCustomVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDatasourceVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGroupByVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardIntervalVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTextVariableKind"}, } } @@ -4064,7 +4071,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref common.ReferenceCallba Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind"), + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind"), }, }, }, @@ -4075,7 +4082,7 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardSpec(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardAnnotationQueryKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardDashboardLink", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardPanelKindOrLibraryPanelKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTimeSettingsSpec"}, } } @@ -4184,6 +4191,102 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref common.Refe } } +func schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec"), + }, + }, + }, + Required: []string{"kind", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardSwitchVariableSpec"}, + } +} + +func schema_pkg_apis_dashboard_v2alpha1_DashboardSwitchVariableSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Switch variable specification", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "current": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "enabledValue": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "disabledValue": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "hide": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "skipUrlSync": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name", "current", "enabledValue", "disabledValue", "hide", "skipUrlSync"}, + }, + }, + } +} + func schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list index 25c9bf58f1c..31ac411ad28 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi_violation_exceptions.list @@ -55,14 +55,15 @@ API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/ap API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardIntervalVariableSpec,AutoMin API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardPanelKindOrLibraryPanelKind,LibraryPanelKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardPanelKindOrLibraryPanelKind,PanelKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,AdhocVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,ConstantVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,CustomVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,DatasourceVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,GroupByVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,IntervalVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,QueryVariableKind -API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind,TextVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,AdhocVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,ConstantVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,CustomVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,DatasourceVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,GroupByVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,IntervalVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,QueryVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,SwitchVariableKind +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind,TextVariableKind API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrArrayOfString,ArrayOfString API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrArrayOfString,String API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardStringOrFloat64,Float64 diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json index a8d22b09eee..ae1ef7cd04e 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.variable-conversions.json @@ -258,6 +258,32 @@ "multi": true, "skipUrlSync": false }, + { + "name": "switch_var", + "type": "switch", + "label": "Enable Feature", + "description": "Toggle feature on/off", + "query": "", + "current": { + "selected": false, + "text": "false", + "value": "false" + }, + "options": [ + { + "selected": false, + "text": "true", + "value": "true" + }, + { + "selected": true, + "text": "false", + "value": "false" + } + ], + "hide": 0, + "skipUrlSync": false + }, { "name": "legacy_string_var", "type": "query", diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json index e4a5b7b74eb..4e12e6982ef 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v0alpha1.json @@ -277,6 +277,32 @@ "skipUrlSync": false, "type": "groupby" }, + { + "current": { + "selected": false, + "text": "false", + "value": "false" + }, + "description": "Toggle feature on/off", + "hide": 0, + "label": "Enable Feature", + "name": "switch_var", + "options": [ + { + "selected": false, + "text": "true", + "value": "true" + }, + { + "selected": true, + "text": "false", + "value": "false" + } + ], + "query": "", + "skipUrlSync": false, + "type": "switch" + }, { "current": { "selected": false, diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json index a4740089365..c7c6c646a94 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2alpha1.json @@ -317,6 +317,19 @@ "description": "Group results by field" } }, + { + "kind": "SwitchVariable", + "spec": { + "name": "switch_var", + "current": "false", + "enabledValue": "true", + "disabledValue": "false", + "label": "Enable Feature", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Toggle feature on/off" + } + }, { "kind": "QueryVariable", "spec": { diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json index d35e57fa601..7b1a899d7fa 100644 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json +++ b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.variable-conversions.v2beta1.json @@ -318,6 +318,19 @@ "description": "Group results by field" } }, + { + "kind": "SwitchVariable", + "spec": { + "name": "switch_var", + "current": "false", + "enabledValue": "true", + "disabledValue": "false", + "label": "Enable Feature", + "hide": "dontHide", + "skipUrlSync": false, + "description": "Toggle feature on/off" + } + }, { "kind": "QueryVariable", "spec": { diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index d20855a9509..7ef4d6ed475 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -956,6 +956,10 @@ func transformVariables(ctx context.Context, dashboard map[string]interface{}, d if textVar, err := buildTextVariable(varMap, commonProps); err == nil { variables = append(variables, textVar) } + case "switch": + if switchVar, err := buildSwitchVariable(varMap, commonProps); err == nil { + variables = append(variables, switchVar) + } case "groupby": if groupByVar, err := buildGroupByVariable(ctx, varMap, commonProps, dsIndexProvider); err == nil { variables = append(variables, groupByVar) @@ -1415,6 +1419,75 @@ func buildTextVariable(varMap map[string]interface{}, commonProps CommonVariable }, nil } +// Helper function to extract string value from an option map (value or text field) +func getOptionValue(optMap map[string]interface{}) string { + if val, ok := optMap["value"].(string); ok && val != "" { + return val + } + if val, ok := optMap["text"].(string); ok && val != "" { + return val + } + return "" +} + +// Switch Variable +func buildSwitchVariable(varMap map[string]interface{}, commonProps CommonVariableProperties) (dashv2alpha1.DashboardVariableKind, error) { + current := "" + if currentVal, exists := varMap["current"]; exists { + if currentMap, ok := currentVal.(map[string]interface{}); ok { + current = getOptionValue(currentMap) + } + } + + // In V1 the enabled value is the first value of the options array, + // while the disabled value is second one. + // (Falling back to "true" and "false" if options are not available) + enabledValue := "true" + disabledValue := "false" + + if options, ok := varMap["options"].([]interface{}); ok { + // Get enabledValue from first option + if len(options) > 0 { + if opt1, ok := options[0].(map[string]interface{}); ok { + if val := getOptionValue(opt1); val != "" { + enabledValue = val + } + } + } + // Get disabledValue from second option + if len(options) > 1 { + if opt2, ok := options[1].(map[string]interface{}); ok { + if val := getOptionValue(opt2); val != "" { + disabledValue = val + } + } + } + } + + // Set current to disabledValue if not set + if current == "" { + current = disabledValue + } + + switchVar := &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: commonProps.Name, + Current: current, + EnabledValue: enabledValue, + DisabledValue: disabledValue, + Label: commonProps.Label, + Description: commonProps.Description, + Hide: commonProps.Hide, + SkipUrlSync: commonProps.SkipUrlSync, + }, + } + + return dashv2alpha1.DashboardVariableKind{ + SwitchVariableKind: switchVar, + }, nil +} + // Adhoc Variable func buildAdhocVariable(ctx context.Context, varMap map[string]interface{}, commonProps CommonVariableProperties, dsIndexProvider schemaversion.DataSourceIndexProvider) (dashv2alpha1.DashboardVariableKind, error) { datasource := varMap["datasource"] diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go index e5bd1f834d3..b09198c72e2 100644 --- a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1.go @@ -733,6 +733,22 @@ func convertVariable_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardVariableKind, } } + if in.SwitchVariableKind != nil { + out.SwitchVariableKind = &dashv2beta1.DashboardSwitchVariableKind{ + Kind: in.SwitchVariableKind.Kind, + Spec: dashv2beta1.DashboardSwitchVariableSpec{ + Name: in.SwitchVariableKind.Spec.Name, + Current: in.SwitchVariableKind.Spec.Current, + EnabledValue: in.SwitchVariableKind.Spec.EnabledValue, + DisabledValue: in.SwitchVariableKind.Spec.DisabledValue, + Label: in.SwitchVariableKind.Spec.Label, + Hide: dashv2beta1.DashboardVariableHide(in.SwitchVariableKind.Spec.Hide), + SkipUrlSync: in.SwitchVariableKind.Spec.SkipUrlSync, + Description: in.SwitchVariableKind.Spec.Description, + }, + } + } + return nil } diff --git a/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go new file mode 100644 index 00000000000..3451234cdec --- /dev/null +++ b/apps/dashboard/pkg/migration/conversion/v2alpha1_to_v2beta1_test.go @@ -0,0 +1,144 @@ +package conversion + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1" + "github.com/grafana/grafana/apps/dashboard/pkg/migration" + migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" +) + +// TestV2alpha1ToV2beta1 tests the conversion logic for v2alpha1 to v2beta1. +func TestV2alpha1ToV2beta1(t *testing.T) { + // Initialize the migrator with test providers + dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) + leProvider := migrationtestutil.NewLibraryElementProvider() + migration.Initialize(dsProvider, leProvider) + + // Set up conversion scheme + scheme := runtime.NewScheme() + err := RegisterConversions(scheme, dsProvider, leProvider) + require.NoError(t, err) + + testCases := []struct { + name string + createV2alpha1 func() *dashv2alpha1.Dashboard + validateV2beta1 func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) + }{ + { + name: "dashboard with switch variable", + createV2alpha1: func() *dashv2alpha1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2alpha1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Label: &label, + Description: &description, + Hide: dashv2alpha1.DashboardVariableHideDontHide, + SkipUrlSync: false, + }, + }, + }, + }, + }, + } + }, + validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) { + require.Len(t, v2beta1.Spec.Variables, 1) + variable := v2beta1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind, "SwitchVariableKind should not be nil") + assert.Equal(t, "SwitchVariable", variable.SwitchVariableKind.Kind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.DisabledValue) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Label) + assert.Equal(t, "Enable Feature", *variable.SwitchVariableKind.Spec.Label) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, "Toggle feature", *variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, dashv2beta1.DashboardVariableHideDontHide, variable.SwitchVariableKind.Spec.Hide) + assert.False(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, + { + name: "dashboard with switch variable - custom values for enabled and disable states", + createV2alpha1: func() *dashv2alpha1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2alpha1.Dashboard{ + Spec: dashv2alpha1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2alpha1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "true", + EnabledValue: "enabled", + DisabledValue: "disabled", + Label: &label, + Description: &description, + Hide: dashv2alpha1.DashboardVariableHideHideLabel, + SkipUrlSync: true, + }, + }, + }, + }, + }, + } + }, + validateV2beta1: func(t *testing.T, v2beta1 *dashv2beta1.Dashboard) { + require.Len(t, v2beta1.Spec.Variables, 1) + variable := v2beta1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "enabled", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "disabled", variable.SwitchVariableKind.Spec.DisabledValue) + assert.Equal(t, dashv2beta1.DashboardVariableHideHideLabel, variable.SwitchVariableKind.Spec.Hide) + assert.True(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create v2alpha1 dashboard + v2alpha1 := tc.createV2alpha1() + + // Collect original statistics + originalStats := collectStatsV2alpha1(v2alpha1.Spec) + + // Convert to v2beta1 + var v2beta1 dashv2beta1.Dashboard + err := scheme.Convert(v2alpha1, &v2beta1, nil) + require.NoError(t, err, "Failed to convert v2alpha1 to v2beta1") + + // Collect v2beta1 statistics + v2beta1Stats := collectStatsV2beta1(v2beta1.Spec) + + // Verify no data loss + err = detectConversionDataLoss(originalStats, v2beta1Stats, "V2alpha1", "V2beta1") + assert.NoError(t, err, "Data loss detected in conversion") + + // Run custom validation + tc.validateV2beta1(t, &v2beta1) + }) + } +} diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go index 580fef1b3ea..f0f06957b63 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1.go @@ -771,6 +771,23 @@ func convertVariable_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardVariableKind, } } + if in.SwitchVariableKind != nil { + out.SwitchVariableKind = &dashv2alpha1.DashboardSwitchVariableKind{ + Kind: in.SwitchVariableKind.Kind, + Spec: dashv2alpha1.DashboardSwitchVariableSpec{ + Name: in.SwitchVariableKind.Spec.Name, + Current: in.SwitchVariableKind.Spec.Current, + EnabledValue: in.SwitchVariableKind.Spec.EnabledValue, + DisabledValue: in.SwitchVariableKind.Spec.DisabledValue, + Label: in.SwitchVariableKind.Spec.Label, + Hide: dashv2alpha1.DashboardVariableHide(in.SwitchVariableKind.Spec.Hide), + SkipUrlSync: in.SwitchVariableKind.Spec.SkipUrlSync, + Description: in.SwitchVariableKind.Spec.Description, + }, + } + return nil + } + return nil } diff --git a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go index 773961a31fb..25703b56aa7 100644 --- a/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go +++ b/apps/dashboard/pkg/migration/conversion/v2beta1_to_v2alpha1_test.go @@ -479,6 +479,51 @@ func TestV2beta1ToV2alpha1(t *testing.T) { assert.Equal(t, "", variable.QueryVariableKind.Spec.Query.Kind, "Empty group should result in empty kind") }, }, + { + name: "dashboard with switch variable", + createV2beta1: func() *dashv2beta1.Dashboard { + label := "Enable Feature" + description := "Toggle feature" + return &dashv2beta1.Dashboard{ + Spec: dashv2beta1.DashboardSpec{ + Title: "Test Dashboard", + Variables: []dashv2beta1.DashboardVariableKind{ + { + SwitchVariableKind: &dashv2beta1.DashboardSwitchVariableKind{ + Kind: "SwitchVariable", + Spec: dashv2beta1.DashboardSwitchVariableSpec{ + Name: "switch_var", + Current: "false", + EnabledValue: "true", + DisabledValue: "false", + Label: &label, + Description: &description, + Hide: dashv2beta1.DashboardVariableHideDontHide, + SkipUrlSync: false, + }, + }, + }, + }, + }, + } + }, + validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { + require.Len(t, v2alpha1.Spec.Variables, 1) + variable := v2alpha1.Spec.Variables[0] + require.NotNil(t, variable.SwitchVariableKind, "SwitchVariableKind should not be nil") + assert.Equal(t, "SwitchVariable", variable.SwitchVariableKind.Kind) + assert.Equal(t, "switch_var", variable.SwitchVariableKind.Spec.Name) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.Current) + assert.Equal(t, "true", variable.SwitchVariableKind.Spec.EnabledValue) + assert.Equal(t, "false", variable.SwitchVariableKind.Spec.DisabledValue) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Label) + assert.Equal(t, "Enable Feature", *variable.SwitchVariableKind.Spec.Label) + assert.NotNil(t, variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, "Toggle feature", *variable.SwitchVariableKind.Spec.Description) + assert.Equal(t, dashv2alpha1.DashboardVariableHideDontHide, variable.SwitchVariableKind.Spec.Hide) + assert.False(t, variable.SwitchVariableKind.Spec.SkipUrlSync) + }, + }, { name: "dashboard with rows layout", createV2beta1: func() *dashv2beta1.Dashboard { diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index 3cb2c8aab5b..060a0e3945f 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -1050,7 +1050,7 @@ export const defaultTimeRangeOption = (): TimeRangeOption => ({ to: "now", }); -export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind; +export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind; export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind()); @@ -1436,6 +1436,37 @@ export const defaultMetricFindValue = (): MetricFindValue => ({ text: "", }); +export interface SwitchVariableKind { + kind: "SwitchVariable"; + spec: SwitchVariableSpec; +} + +export const defaultSwitchVariableKind = (): SwitchVariableKind => ({ + kind: "SwitchVariable", + spec: defaultSwitchVariableSpec(), +}); + +// Switch variable specification +export interface SwitchVariableSpec { + name: string; + current: string; + enabledValue: string; + disabledValue: string; + label?: string; + hide: VariableHide; + skipUrlSync: boolean; + description?: string; +} + +export const defaultSwitchVariableSpec = (): SwitchVariableSpec => ({ + name: "", + current: "false", + enabledValue: "true", + disabledValue: "false", + hide: "dontHide", + skipUrlSync: false, +}); + export interface Spec { annotations: AnnotationQueryKind[]; // Configuration of dashboard cursor sync behavior. diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index 90e70f63baf..1d5fff1367b 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -3100,7 +3100,7 @@ } } }, - "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind": { + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind": { "type": "object", "properties": { "AdhocVariableKind": { @@ -3124,6 +3124,9 @@ "QueryVariableKind": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKind" }, + "SwitchVariableKind": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind" + }, "TextVariableKind": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTextVariableKind" } @@ -3520,7 +3523,7 @@ "description": "Configured template variables.", "type": "array", "items": { - "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKind" + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind" } } } @@ -3587,6 +3590,71 @@ } } }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableKind": { + "type": "object", + "required": [ + "kind", + "spec" + ], + "properties": { + "kind": { + "type": "string", + "default": "" + }, + "spec": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec" + } + ] + } + } + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardSwitchVariableSpec": { + "description": "Switch variable specification", + "type": "object", + "required": [ + "name", + "current", + "enabledValue", + "disabledValue", + "hide", + "skipUrlSync" + ], + "properties": { + "current": { + "type": "string", + "default": "" + }, + "description": { + "type": "string" + }, + "disabledValue": { + "type": "string", + "default": "" + }, + "enabledValue": { + "type": "string", + "default": "" + }, + "hide": { + "type": "string", + "default": "" + }, + "label": { + "type": "string" + }, + "name": { + "type": "string", + "default": "" + }, + "skipUrlSync": { + "type": "boolean", + "default": false + } + } + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions": { "type": "object", "required": [ From ce72dc6224329ca2b95730ac71abc859d609f89d Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 28 Nov 2025 15:41:19 +0200 Subject: [PATCH 88/94] API clients: Remove duplicate config entries (#114569) --- packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts index 776ad4131ae..06e6f5739b8 100644 --- a/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts +++ b/packages/grafana-api-clients/src/scripts/generate-rtk-apis.ts @@ -108,8 +108,6 @@ const config: ConfigFile = { ...createAPIConfig('preferences', 'v1alpha1'), ...createAPIConfig('provisioning', 'v0alpha1'), ...createAPIConfig('shorturl', 'v1beta1'), - ...createAPIConfig('shorturl', 'v1beta1'), - ...createAPIConfig('shorturl', 'v1beta1'), // PLOP_INJECT_API_CLIENT - Used by the API client generator }, }; From b5eac7baadbbb5b9a17574d0d69805d9a22dfe00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 28 Nov 2025 15:07:15 +0100 Subject: [PATCH 89/94] Sidebar: Clickout side to close pane (#114523) * Sidebar: Clickout side to close pane * fixes * fixing tests * fix * ignore clicks in portals * fix e2e * fix test * Fix click away issues * add missing file --- e2e-playwright/dashboard-new-layouts/utils.ts | 1 + .../src/selectors/components.ts | 3 ++ .../src/components/Sidebar/Sidebar.story.tsx | 7 ++-- .../src/components/Sidebar/Sidebar.test.tsx | 3 +- .../src/components/Sidebar/Sidebar.tsx | 18 +++++++++-- .../components/Sidebar/SidebarPaneHeader.tsx | 16 +++++++--- .../src/components/Sidebar/useSidebar.tsx | 6 ++++ .../components/Sidebar/useSidebarClickAway.ts | 32 +++++++++++++++++++ .../DashboardEditPaneRenderer.test.tsx | 4 +-- .../edit-pane/DashboardEditPaneSplitter.tsx | 1 + .../edit-pane/DashboardOutline.test.tsx | 12 +++++-- .../edit-pane/DashboardOutline.tsx | 5 +-- .../edit-pane/EditPaneHeader.test.tsx | 19 +++++++++-- .../edit-pane/EditPaneHeader.tsx | 2 +- 14 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index 89f4de0eec3..83508063ef5 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -50,6 +50,7 @@ export const flows = { await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.dockToggle).click(); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) .click(); diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 36d0088b258..c643a0d7f2c 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -61,6 +61,9 @@ export const versionedComponents = { closePane: { '12.4.0': 'data-testid Sidebar close pane', }, + dockToggle: { + '12.4.0': 'data-testid sidebar-dock-toggle', + }, }, EditPaneHeader: { deleteButton: { diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx index 8590f91a8b4..175e792d451 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.story.tsx @@ -62,6 +62,7 @@ export const Example: StoryFn = (args) => { position: args.position, bottomMargin: 0, edgeMargin: 0, + onClosePane: () => setOpenPane(''), }); return ( @@ -79,7 +80,7 @@ export const Example: StoryFn = (args) => { {openPane === 'settings' && ( - togglePane('')}> + @@ -88,12 +89,12 @@ export const Example: StoryFn = (args) => { )} {openPane === 'outline' && ( - togglePane('')} /> + )} {openPane === 'add' && ( - togglePane('')} /> + )} diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx index 187fbc90c4b..5e5116b3ada 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx @@ -30,6 +30,7 @@ function TestSetup() { const contextValue = useSidebar({ position: 'right', hasOpenPane: openPane !== '', + onClosePane: () => setOpenPane(''), }); return ( @@ -37,7 +38,7 @@ function TestSetup() { {openPane === 'settings' && ( - setOpenPane('')} /> + )} diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx index 8c1bf784e53..311e11f390c 100644 --- a/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx +++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.tsx @@ -2,14 +2,17 @@ import { css, cx } from '@emotion/css'; import { ReactNode, useContext } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; +import { getPortalContainer } from '../Portal/Portal'; import { SidebarButton } from './SidebarButton'; import { SidebarPaneHeader } from './SidebarPaneHeader'; import { SidebarResizer } from './SidebarResizer'; import { SIDE_BAR_WIDTH_ICON_ONLY, SIDE_BAR_WIDTH_WITH_TEXT, SidebarContext, SidebarContextValue } from './useSidebar'; +import { useCustomClickAway } from './useSidebarClickAway'; export interface Props { children?: ReactNode; @@ -30,9 +33,20 @@ export function SidebarComp({ children, contextValue }: Props) { const style = { [position]: theme.spacing(edgeMargin), bottom: theme.spacing(bottomMargin) }; + const ref = useCustomClickAway((evt) => { + const portalContainer = getPortalContainer(); + // ignore clicks inside portal container + if (evt.target instanceof Node && portalContainer && portalContainer.contains(evt.target)) { + return; + } + if (!isDocked && hasOpenPane) { + contextValue.onClosePane?.(); + } + }); + return ( -
+
{!tabsMode && } {children}
@@ -61,7 +75,7 @@ export function SiderbarToolbar({ children }: SiderbarToolbarProps) { icon={'web-section-alt'} onClick={context.onToggleDock} title={context.isDocked ? t('grafana-ui.sidebar.undock', 'Undock') : t('grafana-ui.sidebar.dock', 'Dock')} - data-testid="sidebar-dock-toggle" + data-testid={selectors.components.Sidebar.dockToggle} /> )}
diff --git a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx index 42fe30b083c..e1e5e0bca59 100644 --- a/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx +++ b/packages/grafana-ui/src/components/Sidebar/SidebarPaneHeader.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { ReactNode } from 'react'; +import { ReactNode, useContext } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -9,23 +9,29 @@ import { useStyles2 } from '../../themes/ThemeContext'; import { IconButton } from '../IconButton/IconButton'; import { Text } from '../Text/Text'; +import { SidebarContext } from './useSidebar'; + export interface Props { children?: ReactNode; title: string; - onClose?: () => void; } -export function SidebarPaneHeader({ children, onClose, title }: Props) { +export function SidebarPaneHeader({ children, title }: Props) { const styles = useStyles2(getStyles); + const context = useContext(SidebarContext); + + if (!context) { + throw new Error('SidebarPaneHeader must be used within a Sidebar'); + } return (
- {onClose && ( + {context.onClosePane && ( void; onResize: (diff: number) => void; + /** Called when pane is closed or clicked outside of (in undocked mode) */ + onClosePane?: () => void; } export const SidebarContext: React.Context = React.createContext< @@ -35,6 +37,8 @@ export interface UseSideBarOptions { edgeMargin?: number; /** defaults to 2 grid units (16px) */ contentMargin?: number; + /** Called when pane is closed or clicked outside of (in undocked mode) */ + onClosePane?: () => void; } export const SIDE_BAR_WIDTH_ICON_ONLY = 5; @@ -48,6 +52,7 @@ export function useSidebar({ bottomMargin = 2, edgeMargin = 2, contentMargin = 2, + onClosePane, }: UseSideBarOptions): SidebarContextValue { const theme = useTheme2(); const [isDocked, setIsDocked] = React.useState(false); @@ -109,5 +114,6 @@ export function useSidebar({ edgeMargin, bottomMargin, contentMargin, + onClosePane, }; } diff --git a/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts b/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts new file mode 100644 index 00000000000..5f1f2ccfb89 --- /dev/null +++ b/packages/grafana-ui/src/components/Sidebar/useSidebarClickAway.ts @@ -0,0 +1,32 @@ +import React from 'react'; + +/** + * Cannot use the react-use useClickAway directly as it relies on mousedown event which is not ideal as the element selection uses pointerdown + * @param ref + * @param onClickAway + */ +export function useCustomClickAway(onClickAway: (evt: MouseEvent | TouchEvent) => void) { + const ref = React.useRef(null); + const refCb = React.useRef(onClickAway); + + React.useLayoutEffect(() => { + refCb.current = onClickAway; + }); + + React.useEffect(() => { + const handler = (e: MouseEvent | TouchEvent) => { + const element = ref.current; + if (element && e.target instanceof Node && !element.contains(e.target)) { + refCb.current(e); + } + }; + + document.addEventListener('pointerdown', handler); + + return () => { + document.removeEventListener('pointerdown', handler); + }; + }, []); + + return ref; +} diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx index 41c81222661..d7f5c73280d 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneRenderer.test.tsx @@ -63,9 +63,9 @@ describe('DashboardEditPaneRenderer', () => { act(() => screen.getByLabelText('Outline').click()); - expect(await screen.findByTestId('sidebar-dock-toggle')).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.components.Sidebar.dockToggle)).toBeInTheDocument(); - act(() => screen.getByTestId('sidebar-dock-toggle').click()); + act(() => screen.getByTestId(selectors.components.Sidebar.dockToggle).click()); expect(scene.state.editPane.state.isDocked).toBe(true); }); diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx index ce7df7a460b..59af972e3b0 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx @@ -69,6 +69,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls hasOpenPane: Boolean(openPane), contentMargin: 1, position: 'right', + onClosePane: () => editPane.closePane(), }); /** diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx index 7b461551773..f743f0a3ae7 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.test.tsx @@ -5,7 +5,7 @@ import { getPanelPlugin } from '@grafana/data/test'; import { selectors } from '@grafana/e2e-selectors'; import { setPluginImportUtils } from '@grafana/runtime'; import { SceneVariableSet, VizPanel } from '@grafana/scenes'; -import { ElementSelectionContext } from '@grafana/ui'; +import { ElementSelectionContext, Sidebar, useSidebar } from '@grafana/ui'; import { DashboardScene } from '../scene/DashboardScene'; import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem'; @@ -86,6 +86,12 @@ function buildTestScene() { return testScene; } +function WrapSidebar({ children }: { children: React.ReactElement }) { + const sidebarContext = useSidebar({}); + + return {children}; +} + describe('DashboardOutline', () => { afterEach(() => { jest.clearAllMocks(); @@ -101,7 +107,9 @@ describe('DashboardOutline', () => { render( - + + + ); // select Row lvl 1 diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx index 97e17c34780..de80db73254 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx @@ -25,10 +25,7 @@ export function DashboardOutline({ editPane, isEditing }: Props) { return ( <> - editPane.closePane()} - /> + diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx index 627ceb96d3b..e3dc190a073 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import { selectors } from '@grafana/e2e-selectors'; import { SceneTimeRange } from '@grafana/scenes'; +import { Sidebar, useSidebar } from '@grafana/ui'; import { DashboardScene } from '../scene/DashboardScene'; import { RowItem } from '../scene/layout-rows/RowItem'; @@ -53,6 +54,12 @@ const buildTestScene = (scene: DashboardScene) => { return scene; }; +function WrapSidebar({ children }: { children: React.ReactElement }) { + const sidebarContext = useSidebar({}); + + return {children}; +} + describe('EditPaneHeader', () => { const mockEditPane = { state: { selection: null }, @@ -71,7 +78,11 @@ describe('EditPaneHeader', () => { const elementSelection = new ElementSelection([['row-test', row.getRef()]]); const editableElement = elementSelection.createSelectionElement()!; - render(); + render( + + + + ); await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton)); expect(DashboardInteractions.trackRemoveRowClick).toHaveBeenCalled(); @@ -84,7 +95,11 @@ describe('EditPaneHeader', () => { const elementSelection = new ElementSelection([['tab-test', tab.getRef()]]); const editableElement = elementSelection.createSelectionElement()!; - render(); + render( + + + + ); await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton)); expect(DashboardInteractions.trackRemoveTabClick).toHaveBeenCalled(); diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx index ea14464fc64..2f096293171 100644 --- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx +++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx @@ -30,7 +30,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) { }; return ( - editPane.closePane()}> + {element.renderActions && element.renderActions()} {(onCopy || onDuplicate) && ( From 0856cac9ad164b34f3418d0d8018fb876ec20fa4 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:25:28 +0000 Subject: [PATCH 90/94] QueryEditorRows: Clear hideSeriesFrom override on query edit (#114315) clear hideSeriesFrom override on query edit --- .../PanelDataPane/PanelDataQueriesTab.tsx | 1 + .../query/components/QueryEditorRows.tsx | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index cea1dfc6cb0..d70fbf9c9c4 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -403,6 +403,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 2cbdc1aba87..e0dacee2669 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -9,8 +9,10 @@ import { HistoryItem, PanelData, getDataSourceRef, + isSystemOverrideWithRef, } from '@grafana/data'; import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { SceneObjectRef, VizPanel } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; @@ -44,6 +46,7 @@ export interface Props { queryLibraryRef?: string; onCancelQueryLibraryEdit?: () => void; isOpen?: boolean; + panelRef?: SceneObjectRef; } export class QueryEditorRows extends PureComponent { @@ -63,6 +66,20 @@ export class QueryEditorRows extends PureComponent { return item; }) ); + + if (this.props.panelRef) { + const panel = this.props.panelRef.resolve(); + const hideSeriesOverrideIndex = panel.state.fieldConfig.overrides.findIndex( + isSystemOverrideWithRef('hideSeriesFrom') + ); + + if (hideSeriesOverrideIndex !== -1) { + const newOverrides = [...panel.state.fieldConfig.overrides]; + newOverrides.splice(hideSeriesOverrideIndex, 1); + + panel.setState({ fieldConfig: { ...panel.state.fieldConfig, overrides: newOverrides } }); + } + } } onReplaceQuery(query: DataQuery, index: number) { From a2e34d229667b0a857b9a0c192cc7edcef0e06b8 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 16:13:02 +0100 Subject: [PATCH 91/94] Alerting: Fetch only silenced alerts on the Silences page (#114564) --- .../alerting/unified/Silences.test.tsx | 23 +++++++++++++++++++ .../components/silences/SilencesTable.tsx | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 4ff786b2bc4..e5bce1433f2 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -1,3 +1,4 @@ +import { HttpResponse, http } from 'msw'; import { Route, Routes } from 'react-router-dom-v5-compat'; import { render, screen, userEvent, waitFor, within } from 'test/test-utils'; import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector'; @@ -154,6 +155,28 @@ describe('Silences', () => { TEST_TIMEOUT ); + it( + 'fetches silenced alerts with correct filter parameters', + async () => { + let capturedParams: URLSearchParams | undefined; + server.use( + http.get('/api/alertmanager/:datasourceUid/api/v2/alerts', ({ request }) => { + capturedParams = new URL(request.url).searchParams; + return HttpResponse.json([]); + }) + ); + + renderSilences(); + + await waitFor(() => expect(capturedParams).toBeDefined()); + + expect(capturedParams?.get('silenced')).toBe('true'); + expect(capturedParams?.get('active')).toBe('false'); + expect(capturedParams?.get('inhibited')).toBe('false'); + }, + TEST_TIMEOUT + ); + it( 'shows the correct number of silenced alerts', async () => { diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 8d3c80acad1..a06acda7c35 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -56,7 +56,7 @@ const SilencesTable = () => { const { data: alertManagerAlerts = [], isLoading: amAlertsIsLoading } = alertmanagerApi.endpoints.getAlertmanagerAlerts.useQuery( - { amSourceName: alertManagerSourceName, filter: { silenced: true, active: true, inhibited: true } }, + { amSourceName: alertManagerSourceName, filter: { silenced: true, active: false, inhibited: false } }, { ...API_QUERY_OPTIONS, skip: !canPreview } ); From a5d00d6264b8c83a56b898258957a13c62063bde Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Fri, 28 Nov 2025 16:17:38 +0100 Subject: [PATCH 92/94] Alerting: Fix make update-workspace for historian app (#114578) --- apps/iam/go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/iam/go.mod b/apps/iam/go.mod index ad6c6039cc5..894e28c7a61 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -20,6 +20,8 @@ replace github.com/grafana/grafana/apps/alerting/notifications => ../alerting/no replace github.com/grafana/grafana/apps/alerting/rules => ../alerting/rules +replace github.com/grafana/grafana/apps/alerting/historian => ../alerting/historian + replace github.com/grafana/grafana/apps/correlations => ../correlations replace github.com/grafana/grafana/apps/investigations => ../investigations From e53236e9cdbe315beee75146a5df0fb16ab1325e Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 16:53:00 +0100 Subject: [PATCH 93/94] Alerting: Add historian app to go.work (#114591) --- Dockerfile | 1 + apps/alerting/historian/go.mod | 29 ++++++++------ apps/alerting/historian/go.sum | 70 +++++++++++++++++----------------- go.work | 1 + 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/Dockerfile b/Dockerfile index c07eb0360e6..5bcca3643b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,6 +111,7 @@ COPY apps/iam apps/iam COPY apps apps COPY kindsv2 kindsv2 COPY apps/alerting/alertenrichment apps/alerting/alertenrichment +COPY apps/alerting/historian apps/alerting/historian COPY apps/alerting/notifications apps/alerting/notifications COPY apps/alerting/rules apps/alerting/rules COPY pkg/codegen pkg/codegen diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index eec687eb79f..234cca1eabe 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/grafana/grafana-app-sdk v0.48.2 k8s.io/apimachinery v0.34.2 - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) require ( @@ -15,16 +15,20 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect @@ -32,20 +36,23 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect + github.com/onsi/gomega v1.36.2 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -58,20 +65,20 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.9.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.2 // indirect diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index a0f2396b250..7eadc30e628 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= @@ -23,16 +23,18 @@ 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/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +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/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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-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.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +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= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -42,8 +44,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= @@ -71,8 +73,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -87,10 +89,10 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -102,14 +104,14 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q= +github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU= github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -148,8 +150,8 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -163,8 +165,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -181,8 +183,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -197,14 +199,14 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -224,8 +226,8 @@ k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= diff --git a/go.work b/go.work index f0f5d9acc19..a33973ac23f 100644 --- a/go.work +++ b/go.work @@ -7,6 +7,7 @@ use ( . // skip:golangci-lint ./apps/advisor ./apps/alerting/alertenrichment + ./apps/alerting/historian ./apps/alerting/notifications ./apps/alerting/rules ./apps/annotation From 32c1ad1b53cce639732f30d257de3db62b4165f7 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Fri, 28 Nov 2025 18:20:57 +0100 Subject: [PATCH 94/94] chore: update grafana-app-sdk to v0.48.4 (#114563) (chore): update grafana-app-sdk to v0.48.4 --- apps/advisor/go.mod | 18 ++++---- apps/advisor/go.sum | 60 +++++++++++++-------------- apps/alerting/alertenrichment/go.mod | 4 +- apps/alerting/alertenrichment/go.sum | 8 ++-- apps/alerting/historian/go.mod | 15 ++++--- apps/alerting/historian/go.sum | 28 ++++++------- apps/alerting/notifications/go.mod | 17 ++++---- apps/alerting/notifications/go.sum | 28 ++++++------- apps/alerting/rules/go.mod | 15 ++++--- apps/alerting/rules/go.sum | 28 ++++++------- apps/annotation/go.mod | 15 ++++--- apps/annotation/go.sum | 28 ++++++------- apps/collections/go.mod | 6 +-- apps/collections/go.sum | 12 +++--- apps/correlations/go.mod | 15 ++++--- apps/correlations/go.sum | 28 ++++++------- apps/dashboard/go.mod | 17 ++++---- apps/dashboard/go.sum | 34 +++++++-------- apps/example/go.mod | 17 ++++---- apps/example/go.sum | 34 +++++++-------- apps/folder/go.mod | 6 +-- apps/folder/go.sum | 12 +++--- apps/iam/go.mod | 29 +++++++------ apps/iam/go.sum | 62 ++++++++++++++-------------- apps/investigations/go.mod | 15 ++++--- apps/investigations/go.sum | 28 ++++++------- apps/logsdrilldown/go.mod | 15 ++++--- apps/logsdrilldown/go.sum | 28 ++++++------- apps/playlist/go.mod | 15 ++++--- apps/playlist/go.sum | 28 ++++++------- apps/plugins/go.mod | 16 +++---- apps/plugins/go.sum | 32 +++++++------- apps/preferences/go.mod | 6 +-- apps/preferences/go.sum | 12 +++--- apps/provisioning/go.mod | 14 +++---- apps/provisioning/go.sum | 28 ++++++------- apps/scope/go.mod | 2 +- apps/scope/go.sum | 4 +- apps/sdk.mk | 2 +- apps/secret/go.mod | 11 +++-- apps/secret/go.sum | 24 +++++------ apps/shorturl/go.mod | 17 ++++---- apps/shorturl/go.sum | 34 +++++++-------- go.mod | 29 +++++++------ go.sum | 62 ++++++++++++++-------------- go.work.sum | 9 ++++ pkg/aggregator/go.mod | 10 ++--- pkg/aggregator/go.sum | 20 ++++----- pkg/apimachinery/go.mod | 12 +++--- pkg/apimachinery/go.sum | 22 +++++----- pkg/apiserver/go.mod | 14 +++---- pkg/apiserver/go.sum | 28 ++++++------- pkg/build/go.mod | 8 ++-- pkg/build/go.sum | 16 +++---- pkg/promlib/go.mod | 10 ++--- pkg/promlib/go.sum | 24 +++++------ 56 files changed, 554 insertions(+), 577 deletions(-) diff --git a/apps/advisor/go.mod b/apps/advisor/go.mod index 6e944d56c3b..2db5b66ac8c 100644 --- a/apps/advisor/go.mod +++ b/apps/advisor/go.mod @@ -8,8 +8,8 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + 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/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 @@ -43,7 +43,7 @@ replace github.com/grafana/grafana/apps/plugins => ../plugins replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 require ( - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect @@ -104,7 +104,7 @@ require ( github.com/gchaincl/sqlhooks v1.3.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect @@ -256,7 +256,7 @@ require ( 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.opentelemetry.io/auto/sdk v1.1.0 // 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 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect @@ -290,9 +290,9 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -314,7 +314,7 @@ require ( modernc.org/sqlite v1.39.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect xorm.io/builder v0.3.13 // indirect ) diff --git a/apps/advisor/go.sum b/apps/advisor/go.sum index 61d3d00b8c2..eb864bd4834 100644 --- a/apps/advisor/go.sum +++ b/apps/advisor/go.sum @@ -49,8 +49,8 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= @@ -107,8 +107,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= @@ -266,8 +266,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= @@ -346,9 +346,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= @@ -384,8 +384,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -616,10 +616,10 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= 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-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= 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= @@ -1074,8 +1074,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1134,8 +1134,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -1160,12 +1158,12 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -1707,10 +1705,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1742,8 +1740,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1863,8 +1861,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/alertenrichment/go.mod b/apps/alerting/alertenrichment/go.mod index c717b9bf364..7a015ce4580 100644 --- a/apps/alerting/alertenrichment/go.mod +++ b/apps/alerting/alertenrichment/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/alertenrichment go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -36,5 +36,5 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/apps/alerting/alertenrichment/go.sum b/apps/alerting/alertenrichment/go.sum index fc4692a07a9..ba98672f90c 100644 --- a/apps/alerting/alertenrichment/go.sum +++ b/apps/alerting/alertenrichment/go.sum @@ -23,8 +23,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28 h1:PgMfX4OPENz/iXmtDDIW9+poZY4UD0hhmXm7flVclDo= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250901080157-a0280d701b28/go.mod h1:av5N0Naq+8VV9MLF7zAkihy/mVq5UbS2EvRSJukDHlY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -114,7 +114,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/historian/go.mod b/apps/alerting/historian/go.mod index 234cca1eabe..934f9ac5772 100644 --- a/apps/alerting/historian/go.mod +++ b/apps/alerting/historian/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/alerting/historian go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -30,7 +30,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -52,11 +52,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -75,9 +74,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/historian/go.sum b/apps/alerting/historian/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/alerting/historian/go.sum +++ b/apps/alerting/historian/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 24c17cc900a..c0ea062e70b 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/alerting/notifications go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -37,7 +37,6 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -62,14 +61,13 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect 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.opentelemetry.io/auto/sdk v1.1.0 // 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/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect @@ -84,7 +82,6 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect @@ -93,9 +90,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -109,6 +106,6 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index 9b84e11f3d6..7a8ccd1d4e8 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -71,10 +71,10 @@ 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-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= @@ -187,8 +187,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= 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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -279,13 +279,13 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -324,7 +324,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/alerting/rules/go.mod b/apps/alerting/rules/go.mod index 17aa567f077..dceeb576100 100644 --- a/apps/alerting/rules/go.mod +++ b/apps/alerting/rules/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/alerting/rules go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/prometheus/common v0.67.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -52,11 +52,10 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -75,9 +74,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/alerting/rules/go.sum b/apps/alerting/rules/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/alerting/rules/go.sum +++ b/apps/alerting/rules/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/annotation/go.mod b/apps/annotation/go.mod index 988575c7ce1..5173ad8c627 100644 --- a/apps/annotation/go.mod +++ b/apps/annotation/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/annotation go 1.24.0 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -52,11 +52,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -75,9 +74,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/annotation/go.sum b/apps/annotation/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/annotation/go.sum +++ b/apps/annotation/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/collections/go.mod b/apps/collections/go.mod index 281a6579c67..621440d3bfe 100644 --- a/apps/collections/go.mod +++ b/apps/collections/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/collections go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 @@ -25,7 +25,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -64,6 +64,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/collections/go.sum b/apps/collections/go.sum index 8d576bfaa8b..eb2916cb29a 100644 --- a/apps/collections/go.sum +++ b/apps/collections/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/correlations/go.mod b/apps/correlations/go.mod index 988f18b89d6..42072c1cb2b 100644 --- a/apps/correlations/go.mod +++ b/apps/correlations/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/correlations go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -52,11 +52,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -75,9 +74,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/correlations/go.sum b/apps/correlations/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/correlations/go.sum +++ b/apps/correlations/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/dashboard/go.mod b/apps/dashboard/go.mod index e5a1352e7d8..56d8b7715a1 100644 --- a/apps/dashboard/go.mod +++ b/apps/dashboard/go.mod @@ -5,8 +5,8 @@ go 1.25.3 require ( cuelang.org/go v0.11.1 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + 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/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e github.com/prometheus/client_golang v1.23.2 @@ -30,7 +30,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect 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 @@ -92,7 +92,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.opentelemetry.io/auto/sdk v1.1.0 // 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 go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -106,7 +106,6 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect @@ -119,9 +118,9 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -130,6 +129,6 @@ require ( k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/dashboard/go.sum b/apps/dashboard/go.sum index 135eec59afc..17fbe2222d2 100644 --- a/apps/dashboard/go.sum +++ b/apps/dashboard/go.sum @@ -39,8 +39,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -85,10 +85,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= @@ -239,8 +239,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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -280,8 +280,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -337,12 +335,12 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -375,7 +373,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/example/go.mod b/apps/example/go.mod index 66b3b93f6df..61574462d8a 100644 --- a/apps/example/go.mod +++ b/apps/example/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/example go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 @@ -21,7 +21,7 @@ require ( github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect 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 @@ -62,7 +62,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -73,7 +73,6 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect @@ -82,9 +81,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -95,6 +94,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/example/go.sum b/apps/example/go.sum index aaf74c34d44..3211e083601 100644 --- a/apps/example/go.sum +++ b/apps/example/go.sum @@ -18,8 +18,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -56,10 +56,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe h1:pPoFj2bQKDBg5EyEdOU+Jn+0hQN+M775Qihk73RbdSs= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20251017153501-8512b219c5fe/go.mod h1:zn/yoxKpWA2KUsxOhQbSbL8OCkF2JNLpSEHs+hQYvdM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -140,8 +140,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -169,8 +169,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -213,12 +211,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -250,7 +248,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/folder/go.mod b/apps/folder/go.mod index 1c9249745a9..4e336935537 100644 --- a/apps/folder/go.mod +++ b/apps/folder/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/folder go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -24,7 +24,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -64,6 +64,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/folder/go.sum b/apps/folder/go.sum index 944e0d69721..ab70319f432 100644 --- a/apps/folder/go.sum +++ b/apps/folder/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/iam/go.mod b/apps/iam/go.mod index 894e28c7a61..0821652b3d8 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -52,8 +52,8 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler require ( github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/prometheus/client_golang v1.23.2 @@ -68,7 +68,7 @@ require ( cloud.google.com/go v0.121.4 // indirect cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/storage v1.55.0 // indirect @@ -84,7 +84,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect @@ -139,7 +139,7 @@ require ( 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-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -159,7 +159,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -169,7 +169,7 @@ require ( github.com/gchaincl/sqlhooks v1.3.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect @@ -362,7 +362,7 @@ require ( github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect @@ -375,15 +375,14 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/zeebo/errs v1.4.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect 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.opentelemetry.io/auto/sdk v1.1.0 // 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.37.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // 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 @@ -432,9 +431,9 @@ require ( gonum.org/v1/gonum v0.16.0 // indirect google.golang.org/api v0.242.0 // indirect google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -461,7 +460,7 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect xorm.io/builder v0.3.13 // indirect ) diff --git a/apps/iam/go.sum b/apps/iam/go.sum index c91c571fa7d..5bab810e8d0 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -53,8 +53,8 @@ cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJW cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= @@ -153,8 +153,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= @@ -405,8 +405,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -519,10 +519,10 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -565,8 +565,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -833,10 +833,10 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe h1:q+QaVANzNZxvTovycpQvDTfsNZ2rHh4XIIaccMnrIR4= github.com/grafana/gomemcache v0.0.0-20250828162811-a96f6acee2fe/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= 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= @@ -1452,8 +1452,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= @@ -1543,8 +1543,6 @@ github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 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/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -1577,8 +1575,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= go.opentelemetry.io/collector/pdata v1.44.0 h1:q/EfWDDKrSaf4hjTIzyPeg1ZcCRg1Uj7VTFnGfNVdk8= @@ -1587,8 +1585,8 @@ go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/ go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -2153,10 +2151,10 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2190,8 +2188,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -2315,8 +2313,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index d4f3685a2aa..6741017390f 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/investigations go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -31,7 +31,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -53,11 +53,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -76,9 +75,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/logsdrilldown/go.mod b/apps/logsdrilldown/go.mod index 712d99405d0..f7d81af4b9d 100644 --- a/apps/logsdrilldown/go.mod +++ b/apps/logsdrilldown/go.mod @@ -5,8 +5,8 @@ go 1.24.0 toolchain go1.24.6 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 ) @@ -54,11 +54,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -77,9 +76,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -90,6 +89,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/logsdrilldown/go.sum b/apps/logsdrilldown/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/logsdrilldown/go.sum +++ b/apps/logsdrilldown/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index 8a254e980d1..bdc831e87ba 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/playlist go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 k8s.io/klog/v2 v2.130.1 @@ -32,7 +32,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -54,11 +54,10 @@ require ( github.com/prometheus/common v0.67.3 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -77,9 +76,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,6 +87,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index 7eadc30e628..d8f706b0944 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -48,10 +48,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -128,8 +128,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -199,12 +199,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -234,7 +234,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/plugins/go.mod b/apps/plugins/go.mod index ef1b58517a3..cebf6594566 100644 --- a/apps/plugins/go.mod +++ b/apps/plugins/go.mod @@ -11,8 +11,8 @@ replace github.com/grafana/grafana/pkg/apiserver => ../../pkg/apiserver require ( github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 github.com/grafana/grafana v0.0.0-00010101000000-000000000000 - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/stretchr/testify v1.11.1 k8s.io/apimachinery v0.34.2 @@ -46,7 +46,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -146,7 +146,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.opentelemetry.io/auto/sdk v1.1.0 // 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 go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -175,9 +175,9 @@ require ( golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect @@ -190,6 +190,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/plugins/go.sum b/apps/plugins/go.sum index 06683624e65..de829f9be57 100644 --- a/apps/plugins/go.sum +++ b/apps/plugins/go.sum @@ -71,8 +71,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -161,10 +161,10 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= 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-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= 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= @@ -412,8 +412,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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -536,12 +536,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -594,7 +594,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/preferences/go.mod b/apps/preferences/go.mod index cd702672de6..18bc0d81e19 100644 --- a/apps/preferences/go.mod +++ b/apps/preferences/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/apps/preferences go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -24,7 +24,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -64,6 +64,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/preferences/go.sum b/apps/preferences/go.sum index 8d576bfaa8b..eb2916cb29a 100644 --- a/apps/preferences/go.sum +++ b/apps/preferences/go.sum @@ -33,10 +33,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2/go.mod h1:RRvSjHH12/PnQaXraMO65jUhVu8n59mzvhfIMBETnV4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -181,7 +181,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/provisioning/go.mod b/apps/provisioning/go.mod index 0fb19b230c8..65293ff0f33 100644 --- a/apps/provisioning/go.mod +++ b/apps/provisioning/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/go-github/v70 v70.0.0 github.com/google/uuid v1.6.0 github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 github.com/grafana/nanogit v0.3.0 @@ -18,7 +18,7 @@ require ( k8s.io/apiserver v0.34.2 k8s.io/client-go v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -29,7 +29,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect 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 @@ -44,7 +44,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect - github.com/grafana/grafana-app-sdk v0.48.2 // indirect + github.com/grafana/grafana-app-sdk v0.48.4 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -62,7 +62,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect @@ -75,8 +75,8 @@ require ( golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/apps/provisioning/go.sum b/apps/provisioning/go.sum index db6bd3cf414..76238334303 100644 --- a/apps/provisioning/go.sum +++ b/apps/provisioning/go.sum @@ -16,8 +16,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -62,10 +62,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/apps/secret v0.0.0-20250902093454-b56b7add012f h1:f+Z5Xpfp1WNYjUe23ginerWsHWUsRgOWrr3WGu3SlWs= github.com/grafana/grafana/apps/secret v0.0.0-20250902093454-b56b7add012f/go.mod h1:RA8mP8KVIwKXBx3Ssqa/uEBABib5LvUWYPVMxrNvnP0= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250804150913-990f1c69ecc2 h1:X0cnaFdR+iz+sDSuoZmkryFSjOirchHe2MdKSRwBWgM= @@ -133,8 +133,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -224,10 +224,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -260,7 +260,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/scope/go.mod b/apps/scope/go.mod index 9b739eb3d0b..c7d21d96f4a 100644 --- a/apps/scope/go.mod +++ b/apps/scope/go.mod @@ -39,5 +39,5 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/apps/scope/go.sum b/apps/scope/go.sum index 1796d287d60..3e1f8bc7d54 100644 --- a/apps/scope/go.sum +++ b/apps/scope/go.sum @@ -114,7 +114,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/sdk.mk b/apps/sdk.mk index 6956168a7df..62d89ed8ed3 100644 --- a/apps/sdk.mk +++ b/apps/sdk.mk @@ -1,4 +1,4 @@ -APP_SDK_VERSION = v0.48.2 +APP_SDK_VERSION = v0.48.4 APP_SDK_DIR = $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) APP_SDK_BIN = $(APP_SDK_DIR)/grafana-app-sdk diff --git a/apps/secret/go.mod b/apps/secret/go.mod index 2ac03ea3f8b..d8de93c5cff 100644 --- a/apps/secret/go.mod +++ b/apps/secret/go.mod @@ -3,11 +3,11 @@ module github.com/grafana/grafana/apps/secret go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 + github.com/grafana/grafana-app-sdk v0.48.4 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v3 v3.0.4 - google.golang.org/grpc v1.76.0 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 k8s.io/apimachinery v0.34.2 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 @@ -29,7 +29,7 @@ require ( github.com/go-test/deep v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grafana/grafana-app-sdk/logging v0.48.1 // indirect + github.com/grafana/grafana-app-sdk/logging v0.48.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -51,7 +51,6 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.47.0 // indirect @@ -60,13 +59,13 @@ require ( golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/client-go v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/secret/go.sum b/apps/secret/go.sum index 74024f8df34..a18c6f39a89 100644 --- a/apps/secret/go.sum +++ b/apps/secret/go.sum @@ -37,10 +37,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -111,8 +111,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -168,10 +168,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -199,7 +199,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/apps/shorturl/go.mod b/apps/shorturl/go.mod index 740aea32a72..eb9936ab643 100644 --- a/apps/shorturl/go.mod +++ b/apps/shorturl/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/apps/shorturl go 1.25.3 require ( - github.com/grafana/grafana-app-sdk v0.48.2 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk v0.48.4 + github.com/grafana/grafana-app-sdk/logging v0.48.3 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba k8s.io/apimachinery v0.34.2 k8s.io/apiserver v0.34.2 @@ -22,7 +22,7 @@ require ( github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect 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 @@ -63,7 +63,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect @@ -74,7 +74,6 @@ require ( go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect @@ -83,9 +82,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -95,6 +94,6 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/apps/shorturl/go.sum b/apps/shorturl/go.sum index f8b394c0f37..4508fbd82a6 100644 --- a/apps/shorturl/go.sum +++ b/apps/shorturl/go.sum @@ -18,8 +18,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -56,10 +56,10 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba h1:Qam8QzVRsyZN39zgZ9Vj6e8PEfswvv2McnqCZ/v5NcI= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250915132226-585b53bc7dba/go.mod h1:rlJ/mmE0RQolOB2+HV3+bw+ZifHyPDQurBwZEus+Wm0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -140,8 +140,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -169,8 +169,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -213,12 +211,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -250,7 +248,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/go.mod b/go.mod index 3d277125a1f..371cd742c72 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/fullstorydev/grpchan v1.1.1 // @grafana/grafana-backend-group github.com/gchaincl/sqlhooks v1.3.0 // @grafana/grafana-search-and-storage github.com/getkin/kin-openapi v0.133.0 // @grafana/grafana-app-platform-squad - github.com/go-jose/go-jose/v4 v4.1.2 // @grafana/identity-access-team + github.com/go-jose/go-jose/v4 v4.1.3 // @grafana/identity-access-team 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 @@ -97,8 +97,8 @@ require ( github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend - github.com/grafana/grafana-app-sdk v0.48.2 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/logging v0.48.1 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk v0.48.4 // @grafana/grafana-app-platform-squad + github.com/grafana/grafana-app-sdk/logging v0.48.3 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.3.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad @@ -211,7 +211,7 @@ require ( golang.org/x/tools v0.39.0 // indirect; @grafana/grafana-as-code gonum.org/v1/gonum v0.16.0 // @grafana/oss-big-tent google.golang.org/api v0.242.0 // @grafana/grafana-backend-group - google.golang.org/grpc v1.76.0 // @grafana/plugins-platform-backend + google.golang.org/grpc v1.77.0 // @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.10 // @grafana/plugins-platform-backend gopkg.in/ini.v1 v1.67.0 // @grafana/alerting-backend gopkg.in/mail.v2 v2.3.1 // @grafana/grafana-backend-group @@ -227,7 +227,7 @@ require ( modernc.org/sqlite v1.39.1 // @grafana/grafana-backend-group pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // @grafana/grafana-app-platform-squad + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // @grafana/grafana-app-platform-squad xorm.io/builder v0.3.13 // @grafana/grafana-backend-group ) @@ -298,7 +298,7 @@ require ( cloud.google.com/go v0.121.4 // indirect cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect @@ -316,7 +316,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect github.com/FZambia/eagle v0.2.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -391,7 +391,7 @@ require ( 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-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -414,7 +414,7 @@ require ( github.com/elazarl/goproxy v1.7.2 // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -583,7 +583,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/viper v1.21.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/stoewer/go-strcase v1.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect @@ -601,18 +601,17 @@ require ( github.com/yudai/pp v2.0.1+incompatible // indirect github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zclconf/go-cty v1.16.3 // indirect - github.com/zeebo/errs v1.4.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.etcd.io/bbolt v1.4.2 // indirect 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.opentelemetry.io/auto/sdk v1.1.0 // 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 go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect @@ -638,8 +637,8 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect diff --git a/go.sum b/go.sum index 971b7b06f74..ff568126e42 100644 --- a/go.sum +++ b/go.sum @@ -194,8 +194,8 @@ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -725,8 +725,8 @@ github.com/FZambia/eagle v0.2.0 h1:1kQaZpJvbkvAXFRE/9K2ucBMuVqo+E29EMLYB74hIis= github.com/FZambia/eagle v0.2.0/go.mod h1:LKMYBwGYhao5sJI0TppvQ4SvvldFj9gITxrl8NvGwG0= github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc= github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= @@ -1040,8 +1040,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -1169,10 +1169,10 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -1232,8 +1232,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -1633,10 +1633,10 @@ github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw= github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8= github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y= -github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= -github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0= +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-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM= 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= @@ -2454,8 +2454,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ= github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= @@ -2589,8 +2589,6 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= 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/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= @@ -2627,8 +2625,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/collector v0.124.0 h1:g/dfdGFhBcQI0ggGxTmGlJnJ6Yl6T2gVxQoIj4UfXCc= go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= @@ -2640,8 +2638,8 @@ go.opentelemetry.io/collector/semconv v0.124.0 h1:YTdo3UFwNyDQCh9DiSm2rbzAgBuwn/ go.opentelemetry.io/collector/semconv v0.124.0/go.mod h1:te6VQ4zZJO5Lp8dM2XIhDxDiL45mwX0YAQQWRQ0Qr9U= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0 h1:RyrtJzu5MAmIcbRrwg75b+w3RlZCP0vJByDVzcpAe3M= go.opentelemetry.io/contrib/bridges/prometheus v0.61.0/go.mod h1:tirr4p9NXbzjlbruiRGp53IzlYrDk5CO2fdHj0sSSaY= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0 h1:XfzKtKSrbtYk9TNCF8dkO0Y9M7IOfb4idCwBOTwGBiI= go.opentelemetry.io/contrib/exporters/autoexport v0.61.0/go.mod h1:N6otC+qXTD5bAnbK2O1f/1SXq3cX+3KYSWrkBUqG0cw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= @@ -3517,15 +3515,15 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go. google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -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-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -3572,8 +3570,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -3763,8 +3761,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= diff --git a/go.work.sum b/go.work.sum index c4b732eaadb..3bc86d10781 100644 --- a/go.work.sum +++ b/go.work.sum @@ -72,6 +72,7 @@ cloud.google.com/go/cloudtasks v1.13.6/go.mod h1:/IDaQqGKMixD+ayM43CfsvWF2k36Geo cloud.google.com/go/compute v1.40.0 h1:dlEzKo/BtyEGNc+SflXwwoBh52dNl/A5BaSYurT0k0k= cloud.google.com/go/compute v1.40.0/go.mod h1:P1doTJnlwurJDzIQFMp4mgU+vyCe9HU2NWTlqTfq3MY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= cloud.google.com/go/contactcenterinsights v1.17.3 h1:lenyU3uzHwKDveCwmpfNxHYvLS3uEBWdn+O7+rSxy+Q= cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8= cloud.google.com/go/container v1.43.0 h1:A6J92FJPfxTvyX7MHF+w4t2W9WCqvHOi9UB5SAeSy3w= @@ -1535,6 +1536,8 @@ github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+ github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= @@ -1917,6 +1920,7 @@ golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -1932,6 +1936,7 @@ golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2059,6 +2064,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= 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-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= @@ -2066,6 +2072,7 @@ google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFL google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= @@ -2079,6 +2086,8 @@ google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr 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= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds= +google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 24f0d25efae..264fb259202 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -17,7 +17,7 @@ require ( k8s.io/component-base v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -101,7 +101,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.opentelemetry.io/auto/sdk v1.1.0 // 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 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect @@ -130,9 +130,9 @@ require ( golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 313c0b7c200..1c52723a0ac 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -269,8 +269,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= 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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -382,13 +382,13 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -426,7 +426,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index fdcf1562f1f..3a111a9028b 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.25.3 require ( - github.com/go-jose/go-jose/v4 v4.1.2 + github.com/go-jose/go-jose/v4 v4.1.3 github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team github.com/stretchr/testify v1.11.1 @@ -34,27 +34,25 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 3b59ab0b8b9..42e2598d7f5 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -6,8 +6,8 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -77,8 +77,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -96,8 +96,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -130,10 +128,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -157,7 +155,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 4a3a9d7a84b..08a647a145b 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,7 +5,7 @@ go 1.25.3 require ( github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 - github.com/grafana/grafana-app-sdk/logging v0.48.1 + github.com/grafana/grafana-app-sdk/logging v0.48.3 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 @@ -17,7 +17,7 @@ require ( k8s.io/component-base v0.34.2 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 ) require ( @@ -31,7 +31,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect 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 @@ -71,7 +71,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.opentelemetry.io/auto/sdk v1.1.0 // 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/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect @@ -92,9 +92,9 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 52c046f42a0..ff63cdc69ca 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -25,8 +25,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -71,8 +71,8 @@ github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGr github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw= 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-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= +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/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/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= @@ -170,8 +170,8 @@ go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= 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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -260,13 +260,13 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -303,7 +303,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 80952661b2b..e08dfce7d74 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -16,7 +16,7 @@ require ( golang.org/x/net v0.47.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources golang.org/x/sync v0.18.0 // @grafana/alerting-backend golang.org/x/text v0.31.0 // indirect; @grafana/grafana-backend-group - google.golang.org/grpc v1.76.0 // indirect; @grafana/plugins-platform-backend + google.golang.org/grpc v1.77.0 // indirect; @grafana/plugins-platform-backend google.golang.org/protobuf v1.36.10 // indirect; @grafana/plugins-platform-backend ) @@ -30,8 +30,8 @@ require ( github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect golang.org/x/sys v0.38.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect ) require ( @@ -51,7 +51,7 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index b5a55f1d488..3d794bfccf8 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -59,8 +59,8 @@ github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTd github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= @@ -105,12 +105,12 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 314d19580a4..6aea33c1df8 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -86,7 +86,7 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // 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 go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect @@ -111,9 +111,9 @@ require ( golang.org/x/tools v0.39.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.242.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect - google.golang.org/grpc v1.76.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/grpc v1.77.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/client-go v0.34.2 // indirect @@ -122,5 +122,5 @@ require ( k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect ) diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 85d212bc772..eaa9d903015 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -3,8 +3,8 @@ cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= @@ -278,8 +278,8 @@ 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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 h1:2pn7OzMewmYRiNtv1doZnLo3gONcnMHlFnmOR8Vgt+8= @@ -378,12 +378,12 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -410,7 +410,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=