From ce8a874bf0c6af272c7084b6a0838683ffcbc019 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Tue, 25 Feb 2025 13:37:41 +0100 Subject: [PATCH 01/51] Advisor: Preinstall app plugin if enabled (#101289) --- pkg/setting/setting_plugins.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/setting/setting_plugins.go b/pkg/setting/setting_plugins.go index d798c1206c9..8a459cc7687 100644 --- a/pkg/setting/setting_plugins.go +++ b/pkg/setting/setting_plugins.go @@ -55,6 +55,9 @@ func (cfg *Cfg) readPluginSettings(iniFile *ini.File) error { for _, plugin := range defaultPreinstallPlugins { preinstallPlugins[plugin.ID] = plugin } + if cfg.IsFeatureToggleEnabled("grafanaAdvisor") { // Use literal string to avoid circular dependency + preinstallPlugins["grafana-advisor-app"] = InstallPlugin{"grafana-advisor-app", "", ""} + } // Add the plugins defined in the configuration for _, plugin := range rawInstallPlugins { parts := strings.Split(plugin, "@") From 1a65154e746d01b69787a8c56a0d34cfe54bfa53 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:38:32 -0300 Subject: [PATCH 02/51] fix (unified-storage): Fix error when trying to get parents of folder as a viewer (#101245) * Fix error when trying to get parents of folder as a viewer with unified-storage enabled --- .../folder/folderimpl/unifiedstore.go | 8 ++ .../folder/folderimpl/unifiedstore_test.go | 87 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index 12452a7f2de..b1615eb4b1e 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -2,7 +2,9 @@ package folderimpl import ( "context" + "errors" "fmt" + "net/http" "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -144,6 +146,12 @@ func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetPa for parentUid != "" { out, err := ss.k8sclient.Get(ctx, parentUid, q.OrgID, v1.GetOptions{}) if err != nil { + var statusError *apierrors.StatusError + if errors.As(err, &statusError) && statusError.ErrStatus.Code == http.StatusForbidden { + // If we get a Forbidden error when requesting the parent folder, it means the user does not have access + // to it, nor its parents. So we can stop looping + break + } return nil, err } diff --git a/pkg/services/folder/folderimpl/unifiedstore_test.go b/pkg/services/folder/folderimpl/unifiedstore_test.go index 89c68f6d0df..f056ae96ba5 100644 --- a/pkg/services/folder/folderimpl/unifiedstore_test.go +++ b/pkg/services/folder/folderimpl/unifiedstore_test.go @@ -2,6 +2,7 @@ package folderimpl import ( "context" + "net/http" "testing" claims "github.com/grafana/authlib/types" @@ -12,6 +13,8 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/selection" ) @@ -85,6 +88,90 @@ func TestComputeFullPath(t *testing.T) { } } +func TestGetParents(t *testing.T) { + mockCli := new(client.MockK8sHandler) + store := FolderUnifiedStoreImpl{ + k8sclient: mockCli, + } + + ctx := context.Background() + orgID := int64(1) + + t.Run("should return list of parent folders of a given folder uid", func(t *testing.T) { + mockCli.On("Get", mock.Anything, "parentone", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "parentone", + "annotations": map[string]interface{}{"grafana.app/folder": "parenttwo"}, + }, + }, + }, nil).Once() + mockCli.On("Get", mock.Anything, "parenttwo", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "parenttwo", + "annotations": map[string]interface{}{"grafana.app/folder": "parentthree"}, + }, + }, + }, nil).Once() + mockCli.On("Get", mock.Anything, "parentthree", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "parentthree", + "annotations": map[string]interface{}{"grafana.app/folder": "parentfour"}, + }, + }, + }, nil).Once() + mockCli.On("Get", mock.Anything, "parentfour", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "parentfour", + }, + }, + }, nil).Once() + result, err := store.GetParents(ctx, folder.GetParentsQuery{ + UID: "parentone", + OrgID: orgID, + }) + + require.NoError(t, err) + require.Len(t, result, 3) + require.Equal(t, "parentfour", result[0].UID) + require.Equal(t, "parentthree", result[1].UID) + require.Equal(t, "parenttwo", result[2].UID) + }) + + t.Run("should stop if user doesnt have access to the parent folder", func(t *testing.T) { + mockCli.On("Get", mock.Anything, "parentone", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "parentone", + "annotations": map[string]interface{}{"grafana.app/folder": "parenttwo"}, + }, + }, + }, nil).Once() + mockCli.On("Get", mock.Anything, "parenttwo", orgID, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "parenttwo", + "annotations": map[string]interface{}{"grafana.app/folder": "parentthree"}, + }, + }, + }, nil).Once() + mockCli.On("Get", mock.Anything, "parentthree", orgID, mock.Anything, mock.Anything).Return(nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{Code: http.StatusForbidden}, + }).Once() + result, err := store.GetParents(ctx, folder.GetParentsQuery{ + UID: "parentone", + OrgID: orgID, + }) + + require.NoError(t, err) + require.Len(t, result, 1) + require.Equal(t, "parenttwo", result[0].UID) + }) +} + func TestGetChildren(t *testing.T) { mockCli := new(client.MockK8sHandler) store := FolderUnifiedStoreImpl{ From f3433fd47235de1301457d76ad50ebd8e8b4e70d Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Tue, 25 Feb 2025 13:44:40 +0100 Subject: [PATCH 03/51] RBAC: Remove accessControlOnCall feature toggle (#101222) * RBAC: Remove accessControlOnCall feature toggle * Leave the other one in place * Tests * frontend * Readd empty ft to frontend test * Remove legacy RBAC check * Fix test * no need for context * Remove unused variable * Remove unecessary param * remove unecessary param from tests * More tests :D --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 1 - pkg/api/api.go | 2 +- pkg/api/pluginproxy/ds_proxy.go | 3 +- pkg/api/pluginproxy/ds_proxy_test.go | 2 +- pkg/api/pluginproxy/pluginproxy.go | 3 +- pkg/api/pluginproxy/pluginproxy_test.go | 2 +- pkg/middleware/auth.go | 10 +- pkg/middleware/auth_test.go | 12 +- pkg/services/accesscontrol/acimpl/service.go | 9 +- .../accesscontrol/acimpl/service_test.go | 1 - pkg/services/accesscontrol/api/api.go | 10 +- pkg/services/accesscontrol/api/api_test.go | 7 +- .../resourcepermissions/service.go | 2 +- .../resourcepermissions/service_test.go | 12 +- pkg/services/featuremgmt/registry.go | 8 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 1 + pkg/services/navtree/navtreeimpl/applinks.go | 5 +- .../navtree/navtreeimpl/applinks_test.go | 161 +++++++----------- .../plugins/components/AppRootPage.test.tsx | 4 +- .../plugins/components/AppRootPage.tsx | 2 +- 23 files changed, 88 insertions(+), 175 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 2b446b11047..699ecbe13c5 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -28,7 +28,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `featureHighlights` | Highlight Grafana Enterprise features | | | `correlations` | Correlations page | Yes | | `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | -| `accessControlOnCall` | Access control primitives for OnCall | Yes | | `nestedFolders` | Enable folder nesting | Yes | | `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes | | `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index cda45576882..b64ba1ed238 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -43,7 +43,6 @@ export interface FeatureToggles { cloudWatchCrossAccountQuerying?: boolean; showDashboardValidationWarnings?: boolean; mysqlAnsiQuotes?: boolean; - accessControlOnCall?: boolean; nestedFolders?: boolean; alertingBacktesting?: boolean; editPanelCSVDragAndDrop?: boolean; diff --git a/pkg/api/api.go b/pkg/api/api.go index 5b085c89d00..18e52419ece 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -66,7 +66,7 @@ func (hs *HTTPServer) registerRoutes() { reqSignedInNoAnonymous := middleware.ReqSignedInNoAnonymous reqGrafanaAdmin := middleware.ReqGrafanaAdmin reqOrgAdmin := middleware.ReqOrgAdmin - reqRoleForAppRoute := middleware.RoleAppPluginAuth(hs.AccessControl, hs.pluginStore, hs.Features, hs.log) + reqRoleForAppRoute := middleware.RoleAppPluginAuth(hs.AccessControl, hs.pluginStore, hs.log) reqSnapshotPublicModeOrCreate := middleware.SnapshotPublicModeOrCreate(hs.Cfg, hs.AccessControl) reqSnapshotPublicModeOrDelete := middleware.SnapshotPublicModeOrDelete(hs.Cfg, hs.AccessControl) redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 1fac3d64fb6..c8f158138bf 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -340,8 +340,7 @@ func (proxy *DataSourceProxy) validateRequest() error { func (proxy *DataSourceProxy) hasAccessToRoute(route *plugins.Route) bool { ctxLogger := logger.FromContext(proxy.ctx.Req.Context()) - useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != "" - if useRBAC { + if route.ReqAction != "" { routeEval := pluginac.GetDataSourceRouteEvaluator(proxy.ds.UID, route.ReqAction) hasAccess := routeEval.Evaluate(proxy.ctx.GetPermissions()) if !hasAccess { diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index e6b56e7c882..a07e36f61da 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -1094,7 +1094,7 @@ func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasource cfg := setting.NewCfg() secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger()) - features := featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall) + features := featuremgmt.WithFeatures() dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features), &actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index 8288c15276f..c804cdc0136 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -129,8 +129,7 @@ func (proxy *PluginProxy) HandleRequest() { } func (proxy *PluginProxy) hasAccessToRoute(route *plugins.Route) bool { - useRBAC := proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagAccessControlOnCall) && route.ReqAction != "" - if useRBAC { + if route.ReqAction != "" { routeEval := pluginac.GetPluginRouteEvaluator(proxy.ps.PluginID, route.ReqAction) hasAccess := ac.HasAccess(proxy.accessControl, proxy.ctx)(routeEval) if !hasAccess { diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go index ae122d558a4..14034835c49 100644 --- a/pkg/api/pluginproxy/pluginproxy_test.go +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -557,7 +557,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) { SecureJSONData: map[string][]byte{}, } cfg := &setting.Cfg{} - proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)) + proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featuremgmt.WithFeatures()) require.NoError(t, err) proxy.HandleRequest() diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 815e3af6cc7..f9a3a6a842d 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -16,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -138,9 +137,7 @@ func CanAdminPlugins(cfg *setting.Cfg, accessControl ac.AccessControl) func(c *c } } -func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, features featuremgmt.FeatureToggles, - logger log.Logger, -) func(c *contextmodel.ReqContext) { +func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, logger log.Logger) func(c *contextmodel.ReqContext) { return func(c *contextmodel.ReqContext) { pluginID := web.Params(c.Req)[":id"] p, exists := ps.Plugin(c.Req.Context(), pluginID) @@ -164,12 +161,11 @@ func RoleAppPluginAuth(accessControl ac.AccessControl, ps pluginstore.Store, fea } if normalizeIncludePath(u.Path) == path { - useRBAC := features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && i.RequiresRBACAction() - if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, i.Action)) { + if i.RequiresRBACAction() && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, i.Action)) { logger.Debug("Plugin include is covered by RBAC, user doesn't have access", "plugin", pluginID, "include", i.Name) permitted = false break - } else if !useRBAC && !c.HasUserRole(i.Role) { + } else if !i.RequiresRBACAction() && !c.HasUserRole(i.Role) { permitted = false break } diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index b9b83ae9bae..fdca1d04ee3 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -204,11 +204,10 @@ func TestRoleAppPluginAuth(t *testing.T) { 0: tc.role, }, }) - features := featuremgmt.WithFeatures() logger := &logtest.Fake{} ac := &actest.FakeAccessControl{} - sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, ps, features, logger), func(c *contextmodel.ReqContext) { + sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, ps, logger), func(c *contextmodel.ReqContext) { c.JSON(http.StatusOK, map[string]interface{}{}) }) sc.fakeReq("GET", path).exec() @@ -227,10 +226,9 @@ func TestRoleAppPluginAuth(t *testing.T) { 0: org.RoleViewer, }, }) - features := featuremgmt.WithFeatures() logger := &logtest.Fake{} ac := &actest.FakeAccessControl{} - sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, &pluginstore.FakePluginStore{}, features, logger), func(c *contextmodel.ReqContext) { + sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, &pluginstore.FakePluginStore{}, logger), func(c *contextmodel.ReqContext) { c.JSON(http.StatusOK, map[string]interface{}{}) }) sc.fakeReq("GET", "/a/test-app/test").exec() @@ -245,7 +243,6 @@ func TestRoleAppPluginAuth(t *testing.T) { 0: org.RoleViewer, }, }) - features := featuremgmt.WithFeatures() logger := &logtest.Fake{} ac := &actest.FakeAccessControl{} sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, pluginstore.NewFakePluginStore(pluginstore.Plugin{ @@ -259,7 +256,7 @@ func TestRoleAppPluginAuth(t *testing.T) { }, }, }, - }), features, logger), func(c *contextmodel.ReqContext) { + }), logger), func(c *contextmodel.ReqContext) { c.JSON(http.StatusOK, map[string]interface{}{}) }) sc.fakeReq("GET", "/a/test-app/notExistingPath").exec() @@ -307,7 +304,6 @@ func TestRoleAppPluginAuth(t *testing.T) { }, }) logger := &logtest.Fake{} - features := featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall) ac := &actest.FakeAccessControl{ ExpectedEvaluate: tc.evalResult, ExpectedErr: tc.evalErr, @@ -327,7 +323,7 @@ func TestRoleAppPluginAuth(t *testing.T) { }, }) - sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, ps, features, logger), func(c *contextmodel.ReqContext) { + sc.m.Get("/a/:id/*", RoleAppPluginAuth(ac, ps, logger), func(c *contextmodel.ReqContext) { c.JSON(http.StatusOK, map[string]interface{}{}) }) sc.fakeReq("GET", path).exec() diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index d98c5dc8c2a..1a2751978cd 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -68,7 +68,7 @@ func ProvideService( lock, ) - api.NewAccessControlAPI(routeRegister, accessControl, service, userService, features).RegisterAPIEndpoints() + api.NewAccessControlAPI(routeRegister, accessControl, service, userService).RegisterAPIEndpoints() if err := accesscontrol.DeclareFixedRoles(service, cfg); err != nil { return nil, err } @@ -472,14 +472,9 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error { // DeclarePluginRoles allow the caller to declare, to the service, plugin roles and their assignments // to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs []plugins.RoleRegistration) error { - ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeclarePluginRoles") + _, span := tracer.Start(ctx, "accesscontrol.acimpl.DeclarePluginRoles") defer span.End() - // Protect behind feature toggle - if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) { - return nil - } - acRegs := pluginutils.ToRegistrations(ID, name, regs) for _, r := range acRegs { if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil { diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index 74acae53652..2d991d98ab6 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -253,7 +253,6 @@ func TestService_DeclarePluginRoles(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ac := setupTestEnv(t) - ac.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall) // Reset the registations ac.registrations = accesscontrol.RegistrationList{} diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go index 1620ce261a7..be79a93eaf5 100644 --- a/pkg/services/accesscontrol/api/api.go +++ b/pkg/services/accesscontrol/api/api.go @@ -17,20 +17,17 @@ import ( "github.com/grafana/grafana/pkg/middleware/requestmeta" ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/user" ) var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/accesscontrol/api") -func NewAccessControlAPI(router routing.RouteRegister, accesscontrol ac.AccessControl, service ac.Service, - userSvc user.Service, features featuremgmt.FeatureToggles) *AccessControlAPI { +func NewAccessControlAPI(router routing.RouteRegister, accesscontrol ac.AccessControl, service ac.Service, userSvc user.Service) *AccessControlAPI { return &AccessControlAPI{ RouteRegister: router, Service: service, userSvc: userSvc, AccessControl: accesscontrol, - features: features, } } @@ -39,7 +36,6 @@ type AccessControlAPI struct { AccessControl ac.AccessControl RouteRegister routing.RouteRegister userSvc user.Service - features featuremgmt.FeatureToggles } func (api *AccessControlAPI) RegisterAPIEndpoints() { @@ -48,9 +44,7 @@ func (api *AccessControlAPI) RegisterAPIEndpoints() { api.RouteRegister.Group("/api/access-control", func(rr routing.RouteRegister) { rr.Get("/user/actions", middleware.ReqSignedIn, routing.Wrap(api.getUserActions)) rr.Get("/user/permissions", middleware.ReqSignedIn, routing.Wrap(api.getUserPermissions)) - if api.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) { - rr.Get("/users/permissions/search", authorize(ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.searchUsersPermissions)) - } + rr.Get("/users/permissions/search", authorize(ac.EvalPermission(ac.ActionUsersPermissionsRead)), routing.Wrap(api.searchUsersPermissions)) }, requestmeta.SetOwner(requestmeta.TeamAuth)) } diff --git a/pkg/services/accesscontrol/api/api_test.go b/pkg/services/accesscontrol/api/api_test.go index 4068b3204d7..1826ac65100 100644 --- a/pkg/services/accesscontrol/api/api_test.go +++ b/pkg/services/accesscontrol/api/api_test.go @@ -12,7 +12,6 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/util" @@ -42,7 +41,7 @@ func TestAPI_getUserActions(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { acSvc := actest.FakeService{ExpectedPermissions: tt.permissions} - api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, &usertest.FakeUserService{}, featuremgmt.WithFeatures()) + api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, &usertest.FakeUserService{}) api.RegisterAPIEndpoints() server := webtest.NewServer(t, api.RouteRegister) @@ -95,7 +94,7 @@ func TestAPI_getUserPermissions(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { acSvc := actest.FakeService{ExpectedPermissions: tt.permissions} - api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, &usertest.FakeUserService{}, featuremgmt.WithFeatures()) + api := NewAccessControlAPI(routing.NewRouteRegister(), actest.FakeAccessControl{}, acSvc, &usertest.FakeUserService{}) api.RegisterAPIEndpoints() server := webtest.NewServer(t, api.RouteRegister) @@ -192,7 +191,7 @@ func TestAccessControlAPI_searchUsersPermissions(t *testing.T) { mockUserSvc := usertest.NewMockService(t) mockUserSvc.On("GetByUID", mock.Anything, &user.GetUserByUIDQuery{UID: "user_2_uid"}).Return(&user.User{ID: 2}, nil).Maybe() mockUserSvc.On("GetByUID", mock.Anything, &user.GetUserByUIDQuery{UID: "non_existent_uid"}).Return(nil, user.ErrUserNotFound).Maybe() - api := NewAccessControlAPI(routing.NewRouteRegister(), accessControl, acSvc, mockUserSvc, featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)) + api := NewAccessControlAPI(routing.NewRouteRegister(), accessControl, acSvc, mockUserSvc) api.RegisterAPIEndpoints() server := webtest.NewServer(t, api.RouteRegister) diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index 2a6f89a2106..37334ce35eb 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -583,7 +583,7 @@ func (a *ActionSetSvc) RegisterActionSets(ctx context.Context, pluginID string, ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.RegisterActionSets") defer span.End() - if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) || !a.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) { + if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) { return nil } for _, reg := range registrations { diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index 29d534cf218..3022dd6fb46 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -328,7 +328,7 @@ func TestStore_RegisterActionSet(t *testing.T) { tests := []actionSetTest{ { desc: "should be able to register a plugin action set if the right feature toggles are enabled", - features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall), + features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets), pluginID: "test-app", pluginActions: []plugins.ActionSet{ { @@ -345,7 +345,7 @@ func TestStore_RegisterActionSet(t *testing.T) { }, { desc: "should not register plugin action set if feature toggles are missing", - features: featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall), + features: featuremgmt.WithFeatures(), pluginID: "test-app", pluginActions: []plugins.ActionSet{ { @@ -357,7 +357,7 @@ func TestStore_RegisterActionSet(t *testing.T) { }, { desc: "should be able to register multiple plugin action sets", - features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall), + features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets), pluginID: "test-app", pluginActions: []plugins.ActionSet{ { @@ -382,7 +382,7 @@ func TestStore_RegisterActionSet(t *testing.T) { }, { desc: "action set actions should be added not replaced", - features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall), + features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets), pluginID: "test-app", pluginActions: []plugins.ActionSet{ { @@ -425,7 +425,7 @@ func TestStore_RegisterActionSet(t *testing.T) { }, { desc: "should not be able to register an action that doesn't have a plugin prefix", - features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall), + features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets), pluginID: "test-app", pluginActions: []plugins.ActionSet{ { @@ -441,7 +441,7 @@ func TestStore_RegisterActionSet(t *testing.T) { }, { desc: "should not be able to register action set that is not in the allow list", - features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets, featuremgmt.FlagAccessControlOnCall), + features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets), pluginID: "test-app", pluginActions: []plugins.ActionSet{ { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6ea80687ab7..937cd429810 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -198,14 +198,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaSearchAndStorageSquad, }, - { - Name: "accessControlOnCall", - Description: "Access control primitives for OnCall", - Stage: FeatureStageGeneralAvailability, - Owner: identityAccessTeam, - HideFromAdminPage: true, - Expression: "true", // enabled by default - }, { Name: "nestedFolders", Description: "Enable folder nesting", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 1a74645d2a3..f6c75931392 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -24,7 +24,6 @@ grpcServer,preview,@grafana/search-and-storage,false,false,false cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false mysqlAnsiQuotes,experimental,@grafana/search-and-storage,false,false,false -accessControlOnCall,GA,@grafana/identity-access-team,false,false,false nestedFolders,GA,@grafana/search-and-storage,false,false,false alertingBacktesting,experimental,@grafana/alerting-squad,false,false,false editPanelCSVDragAndDrop,experimental,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 043d9446a3a..eb23fb7815b 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -107,10 +107,6 @@ const ( // Use double quotes to escape keyword in a MySQL query FlagMysqlAnsiQuotes = "mysqlAnsiQuotes" - // FlagAccessControlOnCall - // Access control primitives for OnCall - FlagAccessControlOnCall = "accessControlOnCall" - // FlagNestedFolders // Enable folder nesting FlagNestedFolders = "nestedFolders" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 67a16387684..fcf2d517304 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -52,6 +52,7 @@ "name": "accessControlOnCall", "resourceVersion": "1726562036211", "creationTimestamp": "2022-10-19T16:10:09Z", + "deletionTimestamp": "2025-02-24T14:40:54Z", "annotations": { "grafana.app/updatedTimestamp": "2024-09-17 08:33:56.211355566 +0000 UTC" } diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 5c6b88756d3..4c71eb150ed 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -268,13 +268,12 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID string) func(include *plugins.Includes) bool { hasAccess := ac.HasAccess(s.accessControl, c) return func(include *plugins.Includes) bool { - useRBAC := s.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && include.RequiresRBACAction() - if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, include.Action)) { + if include.RequiresRBACAction() && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, include.Action)) { s.log.Debug("plugin include is covered by RBAC, user doesn't have access", "plugin", pluginID, "include", include.Name) return false - } else if !useRBAC && !c.HasUserRole(include.Role) { + } else if !include.RequiresRBACAction() && !c.HasUserRole(include.Role) { return false } return true diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index b064430e7fc..a5a7fc15e99 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -450,114 +450,67 @@ func TestAddAppLinksAccessControl(t *testing.T) { }, } - t.Run("Without plugin RBAC - Enforce role", func(t *testing.T) { - t.Run("Should not add app links when the user cannot access app plugins", func(t *testing.T) { - treeRoot := navtree.NavTreeRoot{} - user.Permissions = map[int64]map[string][]string{} - user.OrgRole = identity.RoleAdmin + t.Run("Should not see any includes with no app access", func(t *testing.T) { + treeRoot := navtree.NavTreeRoot{} + user.Permissions = map[int64]map[string][]string{ + 1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:not-the-test-app1"}}, + } + user.OrgRole = identity.RoleNone + service.features = featuremgmt.WithFeatures() - err := service.addAppLinks(&treeRoot, reqCtx) - require.NoError(t, err) - require.Len(t, treeRoot.Children, 0) - }) - t.Run(" Should add all includes when the user is an editor", func(t *testing.T) { - treeRoot := navtree.NavTreeRoot{} - user.Permissions = map[int64]map[string][]string{ - 1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}}, - } - user.OrgRole = identity.RoleEditor - - err := service.addAppLinks(&treeRoot, reqCtx) - require.NoError(t, err) - appsNode := treeRoot.FindById(navtree.NavIDApps) - require.Len(t, appsNode.Children, 1) - require.Equal(t, "Test app1 name", appsNode.Children[0].Text) - require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url) - require.Len(t, appsNode.Children[0].Children, 2) - require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Children[0].Url) - require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[1].Url) - }) - t.Run("Should add two includes when the user is a viewer", func(t *testing.T) { - treeRoot := navtree.NavTreeRoot{} - user.Permissions = map[int64]map[string][]string{ - 1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}}, - } - user.OrgRole = identity.RoleViewer - - err := service.addAppLinks(&treeRoot, reqCtx) - require.NoError(t, err) - appsNode := treeRoot.FindById(navtree.NavIDApps) - require.Len(t, appsNode.Children, 1) - require.Equal(t, "Test app1 name", appsNode.Children[0].Text) - require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url) - require.Len(t, appsNode.Children[0].Children, 1) - require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url) - }) + err := service.addAppLinks(&treeRoot, reqCtx) + require.NoError(t, err) + require.Len(t, treeRoot.Children, 0) }) + t.Run("Should only see the announcements as a none role user with app access", func(t *testing.T) { + treeRoot := navtree.NavTreeRoot{} + user.Permissions = map[int64]map[string][]string{ + 1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:test-app1"}}, + } + user.OrgRole = identity.RoleNone + service.features = featuremgmt.WithFeatures() - t.Run("With plugin RBAC - Enforce action first", func(t *testing.T) { - t.Run("Should not see any includes with no app access", func(t *testing.T) { - treeRoot := navtree.NavTreeRoot{} - user.Permissions = map[int64]map[string][]string{ - 1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:not-the-test-app1"}}, - } - user.OrgRole = identity.RoleNone - service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall) + err := service.addAppLinks(&treeRoot, reqCtx) + require.NoError(t, err) + appsNode := treeRoot.FindById(navtree.NavIDApps) + require.Len(t, appsNode.Children, 1) + require.Equal(t, "Test app1 name", appsNode.Children[0].Text) + require.Len(t, appsNode.Children[0].Children, 1) + require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url) + }) + t.Run("Should now see the catalog as a viewer with catalog read", func(t *testing.T) { + treeRoot := navtree.NavTreeRoot{} + user.Permissions = map[int64]map[string][]string{ + 1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:test-app1"}, catalogReadAction: []string{}}, + } + user.OrgRole = identity.RoleViewer + service.features = featuremgmt.WithFeatures() - err := service.addAppLinks(&treeRoot, reqCtx) - require.NoError(t, err) - require.Len(t, treeRoot.Children, 0) - }) - t.Run("Should only see the announcements as a none role user with app access", func(t *testing.T) { - treeRoot := navtree.NavTreeRoot{} - user.Permissions = map[int64]map[string][]string{ - 1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:test-app1"}}, - } - user.OrgRole = identity.RoleNone - service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall) + err := service.addAppLinks(&treeRoot, reqCtx) + require.NoError(t, err) + appsNode := treeRoot.FindById(navtree.NavIDApps) + require.Len(t, appsNode.Children, 1) + require.Equal(t, "Test app1 name", appsNode.Children[0].Text) + require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url) + require.Len(t, appsNode.Children[0].Children, 2) + require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Children[0].Url) + require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[1].Url) + }) + t.Run("Should not see the catalog include as an editor without catalog read", func(t *testing.T) { + treeRoot := navtree.NavTreeRoot{} + user.Permissions = map[int64]map[string][]string{ + 1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}}, + } + user.OrgRole = identity.RoleEditor + service.features = featuremgmt.WithFeatures() - err := service.addAppLinks(&treeRoot, reqCtx) - require.NoError(t, err) - appsNode := treeRoot.FindById(navtree.NavIDApps) - require.Len(t, appsNode.Children, 1) - require.Equal(t, "Test app1 name", appsNode.Children[0].Text) - require.Len(t, appsNode.Children[0].Children, 1) - require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url) - }) - t.Run("Should now see the catalog as a viewer with catalog read", func(t *testing.T) { - treeRoot := navtree.NavTreeRoot{} - user.Permissions = map[int64]map[string][]string{ - 1: {pluginaccesscontrol.ActionAppAccess: []string{"plugins:id:test-app1"}, catalogReadAction: []string{}}, - } - user.OrgRole = identity.RoleViewer - service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall) - - err := service.addAppLinks(&treeRoot, reqCtx) - require.NoError(t, err) - appsNode := treeRoot.FindById(navtree.NavIDApps) - require.Len(t, appsNode.Children, 1) - require.Equal(t, "Test app1 name", appsNode.Children[0].Text) - require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url) - require.Len(t, appsNode.Children[0].Children, 2) - require.Equal(t, "/a/test-app1/catalog", appsNode.Children[0].Children[0].Url) - require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[1].Url) - }) - t.Run("Should not see the catalog include as an editor without catalog read", func(t *testing.T) { - treeRoot := navtree.NavTreeRoot{} - user.Permissions = map[int64]map[string][]string{ - 1: {pluginaccesscontrol.ActionAppAccess: []string{"*"}}, - } - user.OrgRole = identity.RoleEditor - service.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall) - - err := service.addAppLinks(&treeRoot, reqCtx) - require.NoError(t, err) - appsNode := treeRoot.FindById(navtree.NavIDApps) - require.Len(t, appsNode.Children, 1) - require.Equal(t, "Test app1 name", appsNode.Children[0].Text) - require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url) - require.Len(t, appsNode.Children[0].Children, 1) - require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url) - }) + err := service.addAppLinks(&treeRoot, reqCtx) + require.NoError(t, err) + appsNode := treeRoot.FindById(navtree.NavIDApps) + require.Len(t, appsNode.Children, 1) + require.Equal(t, "Test app1 name", appsNode.Children[0].Text) + require.Equal(t, "/a/test-app1/home", appsNode.Children[0].Url) + require.Len(t, appsNode.Children[0].Children, 1) + require.Equal(t, "/a/test-app1/announcements", appsNode.Children[0].Children[0].Url) }) } diff --git a/public/app/features/plugins/components/AppRootPage.test.tsx b/public/app/features/plugins/components/AppRootPage.test.tsx index c9e5ec616be..594028bcebf 100644 --- a/public/app/features/plugins/components/AppRootPage.test.tsx +++ b/public/app/features/plugins/components/AppRootPage.test.tsx @@ -30,9 +30,7 @@ jest.mock('../plugin_loader', () => ({ jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), config: { - featureToggles: { - accessControlOnCall: true, - }, + featureToggles: {}, apps: {}, theme2: { breakpoints: { diff --git a/public/app/features/plugins/components/AppRootPage.tsx b/public/app/features/plugins/components/AppRootPage.tsx index 95a280027df..22e324e7478 100644 --- a/public/app/features/plugins/components/AppRootPage.tsx +++ b/public/app/features/plugins/components/AppRootPage.tsx @@ -135,7 +135,7 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) { } // Check if action exists and give access if user has the required permission. - if (pluginInclude?.action && config.featureToggles.accessControlOnCall) { + if (pluginInclude?.action) { return contextSrv.hasPermission(pluginInclude.action); } From 2e78bcfb412ca2578808420feecfdbe1683fd00f Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 25 Feb 2025 07:50:32 -0600 Subject: [PATCH 04/51] Transformations: Add round() to Unary mode of `Add field from calc` (#101295) --- packages/grafana-data/src/utils/unaryOperators.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/grafana-data/src/utils/unaryOperators.ts b/packages/grafana-data/src/utils/unaryOperators.ts index a7e9a85b782..e440382f31e 100644 --- a/packages/grafana-data/src/utils/unaryOperators.ts +++ b/packages/grafana-data/src/utils/unaryOperators.ts @@ -4,6 +4,7 @@ export enum UnaryOperationID { Abs = 'abs', Exp = 'exp', Ln = 'ln', + Round = 'round', Floor = 'floor', Ceil = 'ceil', } @@ -35,6 +36,12 @@ export const unaryOperators = new Registry(() => { operation: (value: number) => Math.log(value), unaryOperationID: UnaryOperationID.Ln, }, + { + id: UnaryOperationID.Round, + name: 'Round', + operation: (value: number) => Math.round(value), + unaryOperationID: UnaryOperationID.Round, + }, { id: UnaryOperationID.Floor, name: 'Floor', From 8d7108d7747eb450d36a1113671ad5bc30a8e870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 25 Feb 2025 15:03:29 +0100 Subject: [PATCH 05/51] Dashboard: Add new elements logic (#101162) --- .../dashboard-scene/scene/DashboardScene.tsx | 12 +++-- .../scene/layout-rows/RowsLayoutManager.tsx | 13 ------ .../scene/layout-tabs/TabsLayoutManager.tsx | 4 -- .../scene/layouts-shared/addNew.ts | 45 +++++++++++++++++++ .../scene/types/DashboardLayoutManager.ts | 10 ----- 5 files changed, 54 insertions(+), 30 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 3048549bcb9..9978a002fe7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -78,7 +78,9 @@ import { isUsingAngularDatasourcePlugin, isUsingAngularPanelPlugin } from './ang import { setupKeyboardShortcuts } from './keyboardShortcuts'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; +import { addNewRowTo, addNewTabTo } from './layouts-shared/addNew'; import { DashboardLayoutManager } from './types/DashboardLayoutManager'; +import { LayoutParent } from './types/LayoutParent'; export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta', 'preload']; export const PANEL_SEARCH_VAR = 'systemPanelFilterVar'; @@ -141,7 +143,7 @@ export interface DashboardSceneState extends SceneObjectState { editPane: DashboardEditPane; } -export class DashboardScene extends SceneObjectBase { +export class DashboardScene extends SceneObjectBase implements LayoutParent { static Component = DashboardSceneRenderer; /** @@ -593,11 +595,11 @@ export class DashboardScene extends SceneObjectBase { } public onCreateNewRow() { - this.state.body.addNewRow(); + addNewRowTo(this.state.body); } public onCreateNewTab() { - this.state.body.addNewTab(); + addNewTabTo(this.state.body); } public onCreateNewPanel(): VizPanel { @@ -613,6 +615,10 @@ export class DashboardScene extends SceneObjectBase { layout.activateRepeaters?.(); } + public getLayout(): DashboardLayoutManager { + return this.state.body; + } + /** * Called by the SceneQueryRunner to provide contextual parameters (tracking) props for the request */ diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 54fb186937d..89a75f29769 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -3,11 +3,9 @@ import { t } from 'app/core/internationalization'; import { isClonedKey } from '../../utils/clone'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; -import { getDashboardSceneFor } from '../../utils/utils'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior'; -import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -85,17 +83,6 @@ export class RowsLayoutManager extends SceneObjectBase i this.setState({ rows: [...this.state.rows, new RowItem()] }); } - public addNewTab() { - const shouldAddTab = this.hasVizPanels(); - const tabsLayout = TabsLayoutManager.createFromLayout(this); - - if (shouldAddTab) { - tabsLayout.addNewTab(); - } - - getDashboardSceneFor(this).switchLayout(tabsLayout); - } - public editModeChanged(isEditing: boolean) { this.state.rows.forEach((row) => row.getLayout().editModeChanged?.(isEditing)); } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 3e2c338b26f..cc4bb9c5a0b 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -95,10 +95,6 @@ export class TabsLayoutManager extends SceneObjectBase i return false; } - public addNewRow() { - this.getCurrentTab().getLayout().addNewRow(); - } - public addNewTab() { const currentTab = new TabItem(); this.setState({ tabs: [...this.state.tabs, currentTab], currentTabIndex: this.state.tabs.length }); diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts b/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts new file mode 100644 index 00000000000..11385b322a2 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts @@ -0,0 +1,45 @@ +import { SceneObject } from '@grafana/scenes'; + +import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; +import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; +import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; +import { isLayoutParent } from '../types/LayoutParent'; + +export function addNewTabTo(sceneObject: SceneObject) { + if (sceneObject instanceof TabsLayoutManager) { + sceneObject.addNewTab(); + return; + } + + const layoutParent = sceneObject.parent!; + if (!isLayoutParent(layoutParent)) { + throw new Error('Parent layout is not a LayoutParent'); + } + + layoutParent.switchLayout(TabsLayoutManager.createFromLayout(layoutParent.getLayout())); +} + +export function addNewRowTo(sceneObject: SceneObject) { + if (sceneObject instanceof RowsLayoutManager) { + sceneObject.addNewRow(); + return; + } + + if (sceneObject instanceof DefaultGridLayoutManager) { + sceneObject.addNewRow(); + return; + } + + if (sceneObject instanceof TabsLayoutManager) { + const currentTab = sceneObject.getCurrentTab(); + addNewRowTo(currentTab.state.layout); + return; + } + + const layoutParent = sceneObject.parent!; + if (!isLayoutParent(layoutParent)) { + throw new Error('Parent layout is not a LayoutParent'); + } + + layoutParent.switchLayout(RowsLayoutManager.createFromLayout(layoutParent.getLayout())); +} diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts index 6041799d8db..53862b2578d 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutManager.ts @@ -44,16 +44,6 @@ export interface DashboardLayoutManager extends SceneObject { */ hasVizPanels(): boolean; - /** - * Add row - */ - addNewRow(): void; - - /** - * Add tab - */ - addNewTab(): void; - /** * Notify the layout manager that the edit mode has changed * @param isEditing From c1d9d4d15a90c330d1846441dacf61126ea7c7a5 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:06:25 +0100 Subject: [PATCH 06/51] User: Handle unique constraints errors (#101274) * Handle unique constraints errors --- pkg/services/user/userimpl/store.go | 33 +++++++++++++++++++++++- pkg/services/user/userimpl/store_test.go | 13 ++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index f26cf95158f..6ada365b1b0 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -2,11 +2,15 @@ package userimpl import ( "context" + "errors" "fmt" "strconv" "strings" "time" + "github.com/go-sql-driver/mysql" + "github.com/mattn/go-sqlite3" + "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -72,7 +76,7 @@ func (ss *sqlStore) Insert(ctx context.Context, cmd *user.User) (int64, error) { return nil }) if err != nil { - return 0, err + return 0, handleSQLError(err) } return cmd.ID, nil @@ -580,3 +584,30 @@ func setOptional[T any](v *T, add func(v T)) { add(*v) } } + +func handleSQLError(err error) error { + if isUniqueConstraintError(err) { + return user.ErrUserAlreadyExists + } + return err +} + +func isUniqueConstraintError(err error) bool { + // check mysql error code + var me *mysql.MySQLError + if errors.As(err, &me) && me.Number == 1062 { + return true + } + + // for postgres we check the error message + if strings.Contains(err.Error(), "duplicate key value") { + return true + } + + var se sqlite3.Error + if errors.As(err, &se) && se.ExtendedCode == sqlite3.ErrConstraintUnique { + return true + } + + return false +} diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index 9d84d64cf17..bca7ac13a49 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -68,6 +68,19 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.NoError(t, err) }) + t.Run("error on duplicated user", func(t *testing.T) { + _, err := userStore.Insert(context.Background(), + &user.User{ + Email: "test@email.com", + Name: "test1", + Login: "test1", + Created: time.Now(), + Updated: time.Now(), + }, + ) + require.ErrorIs(t, err, user.ErrUserAlreadyExists) + }) + t.Run("get user", func(t *testing.T) { _, err := userStore.GetByEmail(context.Background(), &user.GetUserByEmailQuery{Email: "test@email.com"}, From d83db31a230b18ef57842ba3a9228a7b7a344cb8 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Tue, 25 Feb 2025 09:16:55 -0500 Subject: [PATCH 07/51] Elasticsearch: Replace level in adhoc filters with level field name (#100315) Elasticsearch: replace level in adhoc filters with level field name --- .../datasource/elasticsearch/datasource.test.ts | 7 +++++++ .../plugins/datasource/elasticsearch/datasource.ts | 2 +- .../plugins/datasource/elasticsearch/modifyQuery.ts | 11 ++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index 9cab4431395..f80d969bfb0 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -812,6 +812,13 @@ describe('ElasticDatasource', () => { const query = ds.addAdHocFilters('', filters); expect(query).toBe('field\\:name:/field value\\//'); }); + + it('should replace level with the log level field', () => { + const ds = createElasticDatasource({ jsonData: { logLevelField: 'level_field' } }); + const filters = [{ key: 'level', operator: '=', value: 'foo', condition: '' }]; + const query = ds.addAdHocFilters('', filters); + expect(query).toBe('level_field:"foo"'); + }); }); }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 34d22b1e33a..bccb678babf 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -1108,7 +1108,7 @@ export class ElasticDatasource } let finalQuery = query; adhocFilters.forEach((filter) => { - finalQuery = addAddHocFilter(finalQuery, filter); + finalQuery = addAddHocFilter(finalQuery, filter, this.logLevelField); }); return finalQuery; diff --git a/public/app/plugins/datasource/elasticsearch/modifyQuery.ts b/public/app/plugins/datasource/elasticsearch/modifyQuery.ts index 3971e3b1396..467d5bea494 100644 --- a/public/app/plugins/datasource/elasticsearch/modifyQuery.ts +++ b/public/app/plugins/datasource/elasticsearch/modifyQuery.ts @@ -83,7 +83,7 @@ function concatenate(query: string, filter: string, condition = 'AND'): string { /** * Adds a label:"value" expression to the query. */ -export function addAddHocFilter(query: string, filter: AdHocVariableFilter): string { +export function addAddHocFilter(query: string, filter: AdHocVariableFilter, logLevelField?: string): string { if (!filter.key || !filter.value) { return query; } @@ -94,15 +94,20 @@ export function addAddHocFilter(query: string, filter: AdHocVariableFilter): str value: filter.value.toString(), }; + let key = filter.key; + if (logLevelField && key === 'level') { + key = logLevelField; + } + const equalityFilters = ['=', '!=']; if (equalityFilters.includes(filter.operator)) { - return addFilterToQuery(query, filter.key, filter.value, filter.operator === '=' ? '' : '-'); + return addFilterToQuery(query, key, filter.value, filter.operator === '=' ? '' : '-'); } /** * Keys and values in ad hoc filters may contain characters such as * colons, which needs to be escaped. */ - const key = escapeFilter(filter.key); + key = escapeFilter(key); const value = escapeFilterValue(filter.value); const regexValue = escapeFilterValue(filter.value, false); let addHocFilter = ''; From 6eb335a8ceeedaa25d5af882c7634e81c7a05629 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 25 Feb 2025 15:49:08 +0100 Subject: [PATCH 08/51] Alerting: API to read rule groups using mimirtool (#100674) --- .../ngalert/api/api_convert_prometheus.go | 169 ++++++++++- .../api/api_convert_prometheus_test.go | 280 +++++++++++++++++- pkg/services/ngalert/api/api_provisioning.go | 4 +- pkg/services/ngalert/api/tooling/api.json | 1 + .../definitions/convert_prometheus_api.go | 20 +- pkg/services/ngalert/api/tooling/post.json | 20 +- pkg/services/ngalert/api/tooling/spec.json | 20 +- pkg/services/ngalert/models/alert_rule.go | 18 ++ pkg/services/ngalert/models/testing.go | 8 + .../ngalert/provisioning/alert_rules.go | 43 ++- .../ngalert/provisioning/alert_rules_test.go | 4 +- pkg/services/ngalert/store/alert_rule.go | 32 ++ pkg/services/ngalert/store/alert_rule_test.go | 58 ++++ pkg/services/ngalert/tests/fakes/rules.go | 3 +- .../alerting/api_convert_prometheus_test.go | 166 ++++++++--- pkg/tests/api/alerting/testing.go | 113 +++++-- public/api-merged.json | 1 + public/openapi3.json | 1 + 18 files changed, 836 insertions(+), 125 deletions(-) diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index c27bf9b118a..e9dec2d4481 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -1,15 +1,21 @@ package api import ( + "errors" "fmt" "net/http" "strconv" "strings" + "time" + + prommodel "github.com/prometheus/common/model" + "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/folder" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -17,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/prom" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) const ( @@ -39,6 +46,18 @@ func errInvalidHeaderValue(header string) error { return errInvalidHeaderValueBase.Build(errutil.TemplateData{Public: map[string]any{"Header": header}}) } +// ConvertPrometheusSrv converts Prometheus rules to Grafana rules +// and retrieves them in a Prometheus-compatible format. +// +// It is designed to support mimirtool integration, so that rules that work with Mimir +// can be imported into Grafana. It works similarly to the provisioning API, +// where once a rule group is created, it is marked as "provisioned" (via provenance mechanism) +// and is not editable in the UI. +// +// This service returns only rule groups that were initially imported from Prometheus-compatible sources. +// Rule groups not imported from Prometheus are excluded because their original rule definitions are unavailable. +// When a rule group is converted from Prometheus to Grafana, the original definition is preserved alongside +// the Grafana rule and used for reading requests here. type ConvertPrometheusSrv struct { cfg *setting.UnifiedAlertingSettings logger log.Logger @@ -57,27 +76,117 @@ func NewConvertPrometheusSrv(cfg *setting.UnifiedAlertingSettings, logger log.Lo } } +// RouteConvertPrometheusGetRules returns all Grafana-managed alert rules in all namespaces (folders) +// that were imported from a Prometheus-compatible source. +// It responds with a YAML containing a mapping of folders to arrays of Prometheus rule groups. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response { - return response.Error(501, "Not implemented", nil) + logger := srv.logger.FromContext(c.Req.Context()) + + filterOpts := &provisioning.FilterOptions{ + ImportedPrometheusRule: util.Pointer(true), + } + groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts) + if err != nil { + logger.Error("Failed to get alert groups", "error", err) + return errorToResponse(err) + } + + namespaces, err := grafanaNamespacesToPrometheus(groups) + if err != nil { + logger.Error("Failed to convert Grafana rules to Prometheus format", "error", err) + return errorToResponse(err) + } + + return response.YAML(http.StatusOK, namespaces) } +// RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source +// within a specified namespace. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response { return response.Error(501, "Not implemented", nil) } +// RouteConvertPrometheusDeleteRuleGroup deletes a specific rule group if it was imported from a Prometheus-compatible source. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { return response.Error(501, "Not implemented", nil) } +// RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder). +// It responds with a YAML containing a mapping of a single folder to an array of Prometheus rule groups. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response { - return response.Error(501, "Not implemented", nil) + logger := srv.logger.FromContext(c.Req.Context()) + + logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle) + namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser) + if err != nil { + logger.Error("Failed to get folder", "error", err) + return namespaceErrorResponse(err) + } + + filterOpts := &provisioning.FilterOptions{ + ImportedPrometheusRule: util.Pointer(true), + NamespaceUIDs: []string{namespace.UID}, + } + groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts) + if err != nil { + logger.Error("Failed to get alert groups", "error", err) + return errorToResponse(err) + } + + ns, err := grafanaNamespacesToPrometheus(groups) + if err != nil { + logger.Error("Failed to convert Grafana rules to Prometheus format", "error", err) + return errorToResponse(err) + } + + return response.YAML(http.StatusOK, ns) } +// RouteConvertPrometheusGetRuleGroup retrieves a single rule group for a given namespace (folder) +// in Prometheus-compatible YAML format if it was imported from a Prometheus-compatible source. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response { - // Just to make the mimirtool rules load work. It first checks if the group exists, and if the endpoint returns 501 it fails. - return response.YAML(http.StatusOK, apimodels.PrometheusRuleGroup{}) + logger := srv.logger.FromContext(c.Req.Context()) + + logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle) + namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser) + if err != nil { + logger.Error("Failed to get folder", "error", err) + return namespaceErrorResponse(err) + } + + filterOpts := &provisioning.FilterOptions{ + ImportedPrometheusRule: util.Pointer(true), + NamespaceUIDs: []string{namespace.UID}, + RuleGroups: []string{group}, + } + groupsWithFolders, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts) + if err != nil { + logger.Error("Failed to get alert group", "error", err) + return errorToResponse(err) + } + if len(groupsWithFolders) == 0 { + return response.Error(http.StatusNotFound, "Rule group not found", nil) + } + if len(groupsWithFolders) > 1 { + logger.Error("Multiple rule groups found when only one was expected", "folder_title", namespaceTitle, "group", group) + // It shouldn't happen, but if we get more than 1 group, we return an error. + return response.Error(http.StatusInternalServerError, "Multiple rule groups found", nil) + } + + promGroup, err := grafanaRuleGroupToPrometheus(groupsWithFolders[0].Title, groupsWithFolders[0].Rules) + if err != nil { + logger.Error("Failed to convert Grafana rule to Prometheus format", "error", err) + return errorToResponse(err) + } + + return response.YAML(http.StatusOK, promGroup) } +// RouteConvertPrometheusPostRuleGroup converts a Prometheus rule group into a Grafana rule group +// and creates or updates it within the specified namespace (folder). +// +// If the group already exists and was not imported from a Prometheus-compatible source initially, +// it will not be replaced and an error will be returned. func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, promGroup apimodels.PrometheusRuleGroup) response.Response { logger := srv.logger.FromContext(c.Req.Context()) logger = logger.New("folder_title", namespaceTitle, "group", promGroup.Name) @@ -101,6 +210,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm group, err := srv.convertToGrafanaRuleGroup(c, ds, ns.UID, promGroup, logger) if err != nil { + logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err) return errorToResponse(err) } @@ -202,3 +312,54 @@ func parseBooleanHeader(header string, headerName string) (bool, error) { } return val, nil } + +func grafanaNamespacesToPrometheus(groups []models.AlertRuleGroupWithFolderFullpath) (map[string][]apimodels.PrometheusRuleGroup, error) { + result := map[string][]apimodels.PrometheusRuleGroup{} + + for _, group := range groups { + promGroup, err := grafanaRuleGroupToPrometheus(group.Title, group.Rules) + if err != nil { + return nil, err + } + result[group.FolderFullpath] = append(result[group.FolderFullpath], promGroup) + } + + return result, nil +} + +func grafanaRuleGroupToPrometheus(group string, rules []models.AlertRule) (apimodels.PrometheusRuleGroup, error) { + if len(rules) == 0 { + return apimodels.PrometheusRuleGroup{}, nil + } + + interval := time.Duration(rules[0].IntervalSeconds) * time.Second + promGroup := apimodels.PrometheusRuleGroup{ + Name: group, + Interval: prommodel.Duration(interval), + Rules: make([]apimodels.PrometheusRule, len(rules)), + } + + for i, rule := range rules { + promDefinition := rule.PrometheusRuleDefinition() + if promDefinition == "" { + return apimodels.PrometheusRuleGroup{}, fmt.Errorf("failed to get the Prometheus definition of the rule with UID %s", rule.UID) + } + var r apimodels.PrometheusRule + if err := yaml.Unmarshal([]byte(promDefinition), &r); err != nil { + return apimodels.PrometheusRuleGroup{}, fmt.Errorf("failed to unmarshal Prometheus rule definition of the rule with UID %s: %w", rule.UID, err) + } + promGroup.Rules[i] = r + } + + return promGroup, nil +} + +func namespaceErrorResponse(err error) response.Response { + if errors.Is(err, dashboards.ErrFolderAccessDenied) { + // If there is no such folder, the error is ErrFolderAccessDenied. + // We should return 404 in this case, otherwise mimirtool does not work correctly. + return response.Empty(http.StatusNotFound) + } + + return toNamespaceErrorResponse(err) +} diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index 8dea6f3f178..05f84347334 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "net/http" "net/http/httptest" "testing" @@ -8,14 +9,17 @@ import ( prommodel "github.com/prometheus/common/model" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" dsfakes "github.com/grafana/grafana/pkg/services/datasources/fakes" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/user" @@ -45,7 +49,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { } t.Run("without datasource UID header should return 400", func(t *testing.T) { - srv, _ := createConvertPrometheusSrv(t) + srv, _, _, _ := createConvertPrometheusSrv(t) rc := createRequestCtx() rc.Req.Header.Set(datasourceUIDHeader, "") @@ -56,7 +60,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { }) t.Run("with invalid datasource should return error", func(t *testing.T) { - srv, _ := createConvertPrometheusSrv(t) + srv, _, _, _ := createConvertPrometheusSrv(t) rc := createRequestCtx() rc.Req.Header.Set(datasourceUIDHeader, "non-existing-ds") @@ -66,7 +70,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { }) t.Run("with rule group without evaluation interval should return 202", func(t *testing.T) { - srv, _ := createConvertPrometheusSrv(t) + srv, _, _, _ := createConvertPrometheusSrv(t) rc := createRequestCtx() response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup) @@ -103,7 +107,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - srv, _ := createConvertPrometheusSrv(t) + srv, _, _, _ := createConvertPrometheusSrv(t) rc := createRequestCtx() rc.Req.Header.Set(tc.headerName, tc.headerValue) @@ -136,7 +140,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - srv, _ := createConvertPrometheusSrv(t) + srv, _, _, _ := createConvertPrometheusSrv(t) rc := createRequestCtx() rc.Req.Header.Set(tc.headerName, tc.headerValue) @@ -148,7 +152,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { }) t.Run("with valid request should return 202", func(t *testing.T) { - srv, _ := createConvertPrometheusSrv(t) + srv, _, _, _ := createConvertPrometheusSrv(t) rc := createRequestCtx() response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup) @@ -156,7 +160,267 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { }) } -func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService) { +func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) { + promRule := apimodels.PrometheusRule{ + Alert: "test alert", + Expr: "vector(1) > 0", + For: util.Pointer(prommodel.Duration(5 * time.Minute)), + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{ + "summary": "test alert", + }, + } + promRuleYAML, err := yaml.Marshal(promRule) + require.NoError(t, err) + + t.Run("with non-existent folder should return 404", func(t *testing.T) { + srv, _, _, _ := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusGetRuleGroup(rc, "non-existent", "test") + require.Equal(t, http.StatusNotFound, response.Status(), string(response.Body())) + }) + + t.Run("with non-existent group should return 404", func(t *testing.T) { + srv, _, _, _ := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusGetRuleGroup(rc, "test", "non-existent") + require.Equal(t, http.StatusNotFound, response.Status(), string(response.Body())) + }) + + t.Run("with valid request should return 200", func(t *testing.T) { + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + // Create two folders in the root folder + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + // Create rules in both folders + groupKey := models.GenerateGroupKey(rc.SignedInUser.OrgID) + groupKey.NamespaceUID = fldr.UID + groupKey.RuleGroup = "test-group" + rule := models.RuleGen. + With(models.RuleGen.WithGroupKey(groupKey)). + With(models.RuleGen.WithTitle("TestAlert")). + With(models.RuleGen.WithIntervalSeconds(60)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + + // Create a rule in another group + groupKeyNotFromProm := models.GenerateGroupKey(rc.SignedInUser.OrgID) + groupKeyNotFromProm.NamespaceUID = fldr.UID + groupKeyNotFromProm.RuleGroup = "test-group-2" + ruleInOtherFolder := models.RuleGen. + With(models.RuleGen.WithGroupKey(groupKeyNotFromProm)). + With(models.RuleGen.WithTitle("in another group")). + With(models.RuleGen.WithIntervalSeconds(60)). + GenerateRef() + ruleStore.PutRule(context.Background(), ruleInOtherFolder) + + getResp := srv.RouteConvertPrometheusGetRuleGroup(rc, fldr.Title, groupKey.RuleGroup) + require.Equal(t, http.StatusOK, getResp.Status()) + + var respGroup apimodels.PrometheusRuleGroup + err := yaml.Unmarshal(getResp.Body(), &respGroup) + require.NoError(t, err) + + require.Equal(t, groupKey.RuleGroup, respGroup.Name) + require.Equal(t, prommodel.Duration(time.Duration(rule.IntervalSeconds)*time.Second), respGroup.Interval) + require.Len(t, respGroup.Rules, 1) + require.Equal(t, promRule.Alert, respGroup.Rules[0].Alert) + }) +} + +func TestRouteConvertPrometheusGetNamespace(t *testing.T) { + promRule1 := apimodels.PrometheusRule{ + Alert: "test alert", + Expr: "vector(1) > 0", + For: util.Pointer(prommodel.Duration(5 * time.Minute)), + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{ + "summary": "test alert", + }, + } + + promRule2 := apimodels.PrometheusRule{ + Alert: "test alert 2", + Expr: "vector(1) > 0", + For: util.Pointer(prommodel.Duration(5 * time.Minute)), + Labels: map[string]string{ + "severity": "also critical", + }, + Annotations: map[string]string{ + "summary": "test alert 2", + }, + } + + promGroup1 := apimodels.PrometheusRuleGroup{ + Name: "Test Group", + Interval: prommodel.Duration(1 * time.Minute), + Rules: []apimodels.PrometheusRule{ + promRule1, + }, + } + promGroup2 := apimodels.PrometheusRuleGroup{ + Name: "Test Group 2", + Interval: prommodel.Duration(1 * time.Minute), + Rules: []apimodels.PrometheusRule{ + promRule2, + }, + } + + t.Run("with non-existent folder should return 404", func(t *testing.T) { + srv, _, _, _ := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusGetNamespace(rc, "non-existent") + require.Equal(t, http.StatusNotFound, response.Status()) + }) + + t.Run("with valid request should return 200", func(t *testing.T) { + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + // Create two folders in the root folder + fldr := randFolder() + fldr.ParentUID = "" + fldr2 := randFolder() + fldr2.ParentUID = "" + folderService.ExpectedFolders = []*folder.Folder{fldr, fldr2} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr, fldr2) + + // Create a Grafana rule for each Prometheus rule + for _, promGroup := range []apimodels.PrometheusRuleGroup{promGroup1, promGroup2} { + groupKey := models.GenerateGroupKey(rc.SignedInUser.OrgID) + groupKey.NamespaceUID = fldr.UID + groupKey.RuleGroup = promGroup.Name + promRuleYAML, err := yaml.Marshal(promGroup.Rules[0]) + require.NoError(t, err) + rule := models.RuleGen. + With(models.RuleGen.WithGroupKey(groupKey)). + With(models.RuleGen.WithTitle(promGroup.Rules[0].Alert)). + With(models.RuleGen.WithIntervalSeconds(60)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + } + + response := srv.RouteConvertPrometheusGetNamespace(rc, fldr.Title) + require.Equal(t, http.StatusOK, response.Status()) + + var respNamespaces map[string][]apimodels.PrometheusRuleGroup + err := yaml.Unmarshal(response.Body(), &respNamespaces) + require.NoError(t, err) + + require.Len(t, respNamespaces, 1) + require.Contains(t, respNamespaces, fldr.Fullpath) + require.ElementsMatch(t, respNamespaces[fldr.Fullpath], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2}) + }) +} + +func TestRouteConvertPrometheusGetRules(t *testing.T) { + promRule1 := apimodels.PrometheusRule{ + Alert: "test alert", + Expr: "vector(1) > 0", + For: util.Pointer(prommodel.Duration(5 * time.Minute)), + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{ + "summary": "test alert", + }, + } + + promRule2 := apimodels.PrometheusRule{ + Alert: "test alert 2", + Expr: "vector(1) > 0", + For: util.Pointer(prommodel.Duration(5 * time.Minute)), + Labels: map[string]string{ + "severity": "also critical", + }, + Annotations: map[string]string{ + "summary": "test alert 2", + }, + } + + promGroup1 := apimodels.PrometheusRuleGroup{ + Name: "Test Group", + Interval: prommodel.Duration(1 * time.Minute), + Rules: []apimodels.PrometheusRule{ + promRule1, + }, + } + promGroup2 := apimodels.PrometheusRuleGroup{ + Name: "Test Group 2", + Interval: prommodel.Duration(1 * time.Minute), + Rules: []apimodels.PrometheusRule{ + promRule2, + }, + } + + t.Run("with no rules should return empty response", func(t *testing.T) { + srv, _, _, _ := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + response := srv.RouteConvertPrometheusGetRules(rc) + require.Equal(t, http.StatusOK, response.Status()) + + var respNamespaces map[string][]apimodels.PrometheusRuleGroup + err := yaml.Unmarshal(response.Body(), &respNamespaces) + require.NoError(t, err) + require.Empty(t, respNamespaces) + }) + + t.Run("with rules should return 200 with rules", func(t *testing.T) { + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t) + rc := createRequestCtx() + + // Create a folder in the root + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + // Create a Grafana rule for each Prometheus rule + for _, promGroup := range []apimodels.PrometheusRuleGroup{promGroup1, promGroup2} { + groupKey := models.GenerateGroupKey(rc.SignedInUser.OrgID) + groupKey.NamespaceUID = fldr.UID + groupKey.RuleGroup = promGroup.Name + promRuleYAML, err := yaml.Marshal(promGroup.Rules[0]) + require.NoError(t, err) + rule := models.RuleGen. + With(models.RuleGen.WithGroupKey(groupKey)). + With(models.RuleGen.WithTitle(promGroup.Rules[0].Alert)). + With(models.RuleGen.WithIntervalSeconds(60)). + With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))). + GenerateRef() + ruleStore.PutRule(context.Background(), rule) + } + + response := srv.RouteConvertPrometheusGetRules(rc) + require.Equal(t, http.StatusOK, response.Status()) + + var respNamespaces map[string][]apimodels.PrometheusRuleGroup + err := yaml.Unmarshal(response.Body(), &respNamespaces) + require.NoError(t, err) + + require.Len(t, respNamespaces, 1) + require.Contains(t, respNamespaces, fldr.Fullpath) + require.ElementsMatch(t, respNamespaces[fldr.Fullpath], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2}) + }) +} + +func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) { t.Helper() ruleStore := fakes.NewRuleStore(t) @@ -195,7 +459,7 @@ func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasource srv := NewConvertPrometheusSrv(cfg, log.NewNopLogger(), ruleStore, dsCache, alertRuleService) - return srv, dsCache + return srv, dsCache, ruleStore, folderService } func createRequestCtx() *contextmodel.ReqContext { diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 06805da5b19..6eb8f38a3c4 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -77,7 +77,7 @@ type AlertRuleService interface { DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error) GetAlertRuleGroupWithFolderFullpath(ctx context.Context, u identity.Requester, folder, group string) (alerting_models.AlertRuleGroupWithFolderFullpath, error) - GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, folderUIDs []string) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error) + GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, opts *provisioning.FilterOptions) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error) } func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response { @@ -452,7 +452,7 @@ func (srv *ProvisioningSrv) RouteGetAlertRulesExport(c *contextmodel.ReqContext) return srv.RouteGetAlertRuleGroupExport(c, folderUIDs[0], group) } - groupsWithFullpath, err := srv.alertRules.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, folderUIDs) + groupsWithFullpath, err := srv.alertRules.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, &provisioning.FilterOptions{NamespaceUIDs: folderUIDs}) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "failed to get alert rules", err) } diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 0a2fb0af0d4..7fc64a8291f 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -4932,6 +4932,7 @@ "type": "object" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert", "type": "object" diff --git a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go index 5b6454d1d84..9442ba9fc0f 100644 --- a/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go +++ b/pkg/services/ngalert/api/tooling/definitions/convert_prometheus_api.go @@ -6,10 +6,10 @@ import ( // swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusGetRules // -// Gets all namespaces with their rule groups in Prometheus format. +// Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace. // // Produces: -// - application/json +// - application/yaml // // Responses: // 200: PrometheusNamespace @@ -18,10 +18,10 @@ import ( // swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusGetNamespace // -// Gets rules in prometheus format for a given namespace. +// Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder). // // Produces: -// - application/json +// - application/yaml // // Responses: // 200: PrometheusNamespace @@ -30,10 +30,10 @@ import ( // swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusGetRuleGroup // -// Gets a rule group in Prometheus format. +// Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source. // // Produces: -// - application/json +// - application/yaml // // Responses: // 200: PrometheusRuleGroup @@ -42,7 +42,9 @@ import ( // swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup // -// Creates or updates a rule group in Prometheus format. +// Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace. +// If the group already exists and was not imported from a Prometheus-compatible source initially, +// it will not be replaced and an error will be returned. // // Consumes: // - application/yaml @@ -59,7 +61,7 @@ import ( // swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusDeleteNamespace // -// Deletes all rule groups in the given namespace. +// Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace. // // Produces: // - application/json @@ -70,7 +72,7 @@ import ( // swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusDeleteRuleGroup // -// Deletes a rule group in Prometheus format. +// Deletes a specific rule group if it was imported from a Prometheus-compatible source. // // Produces: // - application/json diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 825af6c52af..b48c13e2d7e 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -4770,7 +4770,6 @@ "type": "object" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup", "type": "object" @@ -6403,7 +6402,7 @@ "get": { "operationId": "RouteConvertPrometheusGetRules", "produces": [ - "application/json" + "application/yaml" ], "responses": { "200": { @@ -6425,7 +6424,7 @@ } } }, - "summary": "Gets all namespaces with their rule groups in Prometheus format.", + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", "tags": [ "convert_prometheus" ] @@ -6459,7 +6458,7 @@ } } }, - "summary": "Deletes all rule groups in the given namespace.", + "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", "tags": [ "convert_prometheus" ] @@ -6475,7 +6474,7 @@ } ], "produces": [ - "application/json" + "application/yaml" ], "responses": { "200": { @@ -6497,7 +6496,7 @@ } } }, - "summary": "Gets rules in prometheus format for a given namespace.", + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", "tags": [ "convert_prometheus" ] @@ -6506,6 +6505,7 @@ "consumes": [ "application/yaml" ], + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", "operationId": "RouteConvertPrometheusPostRuleGroup", "parameters": [ { @@ -6554,7 +6554,7 @@ } } }, - "summary": "Creates or updates a rule group in Prometheus format.", + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", "tags": [ "convert_prometheus" ], @@ -6595,7 +6595,7 @@ } } }, - "summary": "Deletes a rule group in Prometheus format.", + "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", "tags": [ "convert_prometheus" ] @@ -6617,7 +6617,7 @@ } ], "produces": [ - "application/json" + "application/yaml" ], "responses": { "200": { @@ -6639,7 +6639,7 @@ } } }, - "summary": "Gets a rule group in Prometheus format.", + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", "tags": [ "convert_prometheus" ] diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index e8f3f798fda..571a497cc6b 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1105,12 +1105,12 @@ "/convert/prometheus/config/v1/rules": { "get": { "produces": [ - "application/json" + "application/yaml" ], "tags": [ "convert_prometheus" ], - "summary": "Gets all namespaces with their rule groups in Prometheus format.", + "summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.", "operationId": "RouteConvertPrometheusGetRules", "responses": { "200": { @@ -1137,12 +1137,12 @@ "/convert/prometheus/config/v1/rules/{NamespaceTitle}": { "get": { "produces": [ - "application/json" + "application/yaml" ], "tags": [ "convert_prometheus" ], - "summary": "Gets rules in prometheus format for a given namespace.", + "summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).", "operationId": "RouteConvertPrometheusGetNamespace", "parameters": [ { @@ -1174,6 +1174,7 @@ } }, "post": { + "description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.", "consumes": [ "application/yaml" ], @@ -1183,7 +1184,7 @@ "tags": [ "convert_prometheus" ], - "summary": "Creates or updates a rule group in Prometheus format.", + "summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.", "operationId": "RouteConvertPrometheusPostRuleGroup", "parameters": [ { @@ -1238,7 +1239,7 @@ "tags": [ "convert_prometheus" ], - "summary": "Deletes all rule groups in the given namespace.", + "summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.", "operationId": "RouteConvertPrometheusDeleteNamespace", "parameters": [ { @@ -1267,12 +1268,12 @@ "/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}": { "get": { "produces": [ - "application/json" + "application/yaml" ], "tags": [ "convert_prometheus" ], - "summary": "Gets a rule group in Prometheus format.", + "summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.", "operationId": "RouteConvertPrometheusGetRuleGroup", "parameters": [ { @@ -1316,7 +1317,7 @@ "tags": [ "convert_prometheus" ], - "summary": "Deletes a rule group in Prometheus format.", + "summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.", "operationId": "RouteConvertPrometheusDeleteRuleGroup", "parameters": [ { @@ -8710,7 +8711,6 @@ } }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "type": "object", diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 66be69d0079..bef50b1b4e7 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -394,6 +394,22 @@ func WithoutInternalLabels() LabelOption { } } +func (alertRule *AlertRule) ImportedFromPrometheus() bool { + if alertRule.Metadata.PrometheusStyleRule == nil { + return false + } + + return alertRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition != "" +} + +func (alertRule *AlertRule) PrometheusRuleDefinition() string { + if !alertRule.ImportedFromPrometheus() { + return "" + } + + return alertRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition +} + // GetLabels returns the labels specified as part of the alert rule. func (alertRule *AlertRule) GetLabels(opts ...LabelOption) map[string]string { labels := alertRule.Labels @@ -806,6 +822,8 @@ type ListAlertRulesQuery struct { ReceiverName string TimeIntervalName string + + ImportedPrometheusRule *bool } // CountAlertRulesQuery is the query for counting alert rules diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 04869e53750..6d553057000 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -216,6 +216,14 @@ func (a *AlertRuleMutators) WithEditorSettingsSimplifiedNotificationsSection(ena } } +func (a *AlertRuleMutators) WithPrometheusOriginalRuleDefinition(definition string) AlertRuleMutator { + return func(rule *AlertRule) { + rule.Metadata.PrometheusStyleRule = &PrometheusStyleRule{ + OriginalRuleDefinition: definition, + } + } +} + func (a *AlertRuleMutators) WithGroupIndex(groupIndex int) AlertRuleMutator { return func(rule *AlertRule) { rule.RuleGroupIndex = groupIndex diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index e58e674eff2..7e6f8387273 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -262,12 +262,41 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, user ident return rule, nil } +// FilterOptions provides filtering for alert rule queries. +// All fields are optional and will be applied as filters if provided. +type FilterOptions struct { + ImportedPrometheusRule *bool + RuleGroups []string + NamespaceUIDs []string +} + +func (opts *FilterOptions) apply(q models.ListAlertRulesQuery) models.ListAlertRulesQuery { + if opts == nil { + return q + } + + if opts.ImportedPrometheusRule != nil { + q.ImportedPrometheusRule = opts.ImportedPrometheusRule + } + + if len(opts.NamespaceUIDs) > 0 { + q.NamespaceUIDs = opts.NamespaceUIDs + } + + if len(opts.RuleGroups) > 0 { + q.RuleGroups = opts.RuleGroups + } + + return q +} + func (service *AlertRuleService) GetRuleGroup(ctx context.Context, user identity.Requester, namespaceUID, group string) (models.AlertRuleGroup, error) { q := models.ListAlertRulesQuery{ OrgID: user.GetOrgID(), NamespaceUIDs: []string{namespaceUID}, RuleGroups: []string{group}, } + ruleList, err := service.ruleStore.ListAlertRules(ctx, &q) if err != nil { return models.AlertRuleGroup{}, err @@ -748,15 +777,17 @@ func (service *AlertRuleService) GetAlertRuleGroupWithFolderFullpath(ctx context return res, nil } -// GetAlertGroupsWithFolderFullpath returns all groups with folder's full path in the folders identified by folderUID that have at least one alert. If argument folderUIDs is nil or empty - returns groups in all folders. -func (service *AlertRuleService) GetAlertGroupsWithFolderFullpath(ctx context.Context, user identity.Requester, folderUIDs []string) ([]models.AlertRuleGroupWithFolderFullpath, error) { +// GetAlertGroupsWithFolderFullpath returns all groups that have at least one alert with the full folder path for each group. + +// It queries all alert rules for the user's organization, applies optional filtering specified in filterOpts, +// and groups the rules by groups. The function then fetches folder details (including the full path) +// for each namespace (folder UID) associated with the rule groups. If the user lacks blanket read permissions, +// only the groups that the user is authorized to view are returned. +func (service *AlertRuleService) GetAlertGroupsWithFolderFullpath(ctx context.Context, user identity.Requester, filterOpts *FilterOptions) ([]models.AlertRuleGroupWithFolderFullpath, error) { q := models.ListAlertRulesQuery{ OrgID: user.GetOrgID(), } - - if len(folderUIDs) > 0 { - q.NamespaceUIDs = folderUIDs - } + q = filterOpts.apply(q) ruleList, err := service.ruleStore.ListAlertRules(ctx, &q) if err != nil { diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index 88ff3dcd8fd..3a10b26fd39 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -1644,7 +1644,7 @@ func TestProvisiongWithFullpath(t *testing.T) { require.NoError(t, err) assert.Equal(t, namespaceTitle, res2.FolderFullpath) - res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, []string{namespaceUID}) + res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, &FilterOptions{NamespaceUIDs: []string{namespaceUID}}) require.NoError(t, err) assert.Equal(t, namespaceTitle, res3[0].FolderFullpath) }) @@ -1675,7 +1675,7 @@ func TestProvisiongWithFullpath(t *testing.T) { require.NoError(t, err) assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res2.FolderFullpath) - res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, []string{otherNamespaceUID}) + res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, &FilterOptions{NamespaceUIDs: []string{otherNamespaceUID}}) require.NoError(t, err) assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res3[0].FolderFullpath) }) diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 0d11d9eaaaa..c95c8de835f 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -554,6 +554,13 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR } } + if query.ImportedPrometheusRule != nil { + q, err = st.filterImportedPrometheusRules(*query.ImportedPrometheusRule, q) + if err != nil { + return err + } + } + q = q.Asc("namespace_uid", "rule_group", "rule_group_idx", "id") alertRules := make([]*ngmodels.AlertRule, 0) @@ -593,6 +600,14 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR continue } } + if query.ImportedPrometheusRule != nil { // remove false-positive hits from the result + hasOriginalRuleDefinition := converted.Metadata.PrometheusStyleRule != nil && len(converted.Metadata.PrometheusStyleRule.OriginalRuleDefinition) > 0 + if *query.ImportedPrometheusRule && !hasOriginalRuleDefinition { + continue + } else if !*query.ImportedPrometheusRule && hasOriginalRuleDefinition { + continue + } + } // MySQL (and potentially other databases) can use case-insensitive comparison. // This code makes sure we return groups that only exactly match the filter. if groupsMap != nil { @@ -928,6 +943,23 @@ func (st DBstore) filterByContentInNotificationSettings(value string, sess *xorm return sess.And(fmt.Sprintf("notification_settings %s ?", st.SQLStore.GetDialect().LikeStr()), "%"+search+"%"), nil } +func (st DBstore) filterImportedPrometheusRules(value bool, sess *xorm.Session) (*xorm.Session, error) { + if value { + // Filter for rules that have both prometheus_style_rule and original_rule_definition in metadata + return sess.And( + "metadata LIKE ? AND metadata LIKE ?", + "%prometheus_style_rule%", + "%original_rule_definition%", + ), nil + } + // Filter for rules that don't have prometheus_style_rule and original_rule_definition in metadata + return sess.And( + "metadata NOT LIKE ? AND metadata NOT LIKE ?", + "%prometheus_style_rule%", + "%original_rule_definition%", + ), nil +} + func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(ngmodels.Provenance) bool, dryRun bool) ([]ngmodels.AlertRuleKey, []ngmodels.AlertRuleKey, error) { // fetch entire rules because Update method requires it because it copies rules to version table rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{ diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 6f6f9453e18..1f833a3e10f 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -1828,6 +1828,64 @@ func TestIntegration_AlertRuleVersionsCleanup(t *testing.T) { }) } +func TestIntegration_ListAlertRules(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + sqlStore := db.InitTestDB(t) + cfg := setting.NewCfg() + cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{ + BaseInterval: time.Duration(rand.Int63n(100)) * time.Second, + } + folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()) + b := &fakeBus{} + orgID := int64(1) + ruleGen := models.RuleGen + ruleGen = ruleGen.With( + ruleGen.WithIntervalMatching(cfg.UnifiedAlerting.BaseInterval), + ruleGen.WithOrgID(orgID), + ) + t.Run("filter by ImportedPrometheusRule", func(t *testing.T) { + store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b) + regularRule := createRule(t, store, ruleGen) + importedRule := createRule(t, store, ruleGen.With( + models.RuleMuts.WithPrometheusOriginalRuleDefinition("data"), + )) + tc := []struct { + name string + importedPrometheusRule *bool + expectedRules []*models.AlertRule + }{ + { + name: "should return only imported prometheus rules when filter is true", + importedPrometheusRule: util.Pointer(true), + expectedRules: []*models.AlertRule{importedRule}, + }, + { + name: "should return only non-imported rules when filter is false", + importedPrometheusRule: util.Pointer(false), + expectedRules: []*models.AlertRule{regularRule}, + }, + { + name: "should return all rules when filter is not set", + importedPrometheusRule: nil, + expectedRules: []*models.AlertRule{regularRule, importedRule}, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + query := &models.ListAlertRulesQuery{ + OrgID: orgID, + ImportedPrometheusRule: tt.importedPrometheusRule, + } + result, err := store.ListAlertRules(context.Background(), query) + require.NoError(t, err) + require.ElementsMatch(t, tt.expectedRules, result) + }) + } + }) +} + func createTestStore( sqlStore db.DB, folderService folder.Service, diff --git a/pkg/services/ngalert/tests/fakes/rules.go b/pkg/services/ngalert/tests/fakes/rules.go index 15ebf90f40c..2fa4714b19d 100644 --- a/pkg/services/ngalert/tests/fakes/rules.go +++ b/pkg/services/ngalert/tests/fakes/rules.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/util" @@ -289,7 +290,7 @@ func (f *RuleStore) GetNamespaceInRootByTitle(ctx context.Context, title string, } } - return nil, fmt.Errorf("namespace with title '%s' not found", title) + return nil, dashboards.ErrFolderNotFound } func (f *RuleStore) UpdateAlertRules(_ context.Context, _ *models.UserUID, q []models.UpdateRule) error { diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index 326063a15ce..674d708250f 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -1,6 +1,7 @@ package alerting import ( + "net/http" "testing" "time" @@ -15,31 +16,8 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { - testinfra.SQLiteIntegrationTest(t) - - // Setup Grafana and its Database - dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ - DisableLegacyAlerting: true, - EnableUnifiedAlerting: true, - DisableAnonymous: true, - AppModeProduction: true, - EnableFeatureToggles: []string{"alertingConversionAPI"}, - }) - - grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) - - // Create a user to make authenticated requests - createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ - DefaultOrgRole: string(org.RoleAdmin), - Password: "password", - Login: "admin", - }) - - apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") - namespace := "test-namespace" - - promGroup1 := apimodels.PrometheusRuleGroup{ +var ( + promGroup1 = apimodels.PrometheusRuleGroup{ Name: "test-group-1", Interval: prommodel.Duration(60 * time.Second), Rules: []apimodels.PrometheusRule{ @@ -80,7 +58,7 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { }, } - promGroup2 := apimodels.PrometheusRuleGroup{ + promGroup2 = apimodels.PrometheusRuleGroup{ Name: "test-group-2", Interval: prommodel.Duration(60 * time.Second), Rules: []apimodels.PrometheusRule{ @@ -99,25 +77,129 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { }, } + promGroup3 = apimodels.PrometheusRuleGroup{ + Name: "test-group-3", + Interval: prommodel.Duration(60 * time.Second), + Rules: []apimodels.PrometheusRule{ + { + Alert: "ServiceDown", + Expr: "up == 0", + For: util.Pointer(prommodel.Duration(2 * time.Minute)), + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{ + "annotation-1": "value-1", + }, + }, + }, + } +) + +func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + + // Setup Grafana and its Database + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "password", + Login: "viewer", + }) + viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password") + + namespace1 := "test-namespace-1" + namespace2 := "test-namespace-2" + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) - t.Run("create two rule groups and get them back", func(t *testing.T) { - apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil) - apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup2, nil) + t.Run("create rule groups and get them back", func(t *testing.T) { + _, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusAccepted, status, body) + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil) + requireStatusCode(t, http.StatusAccepted, status, body) - ns, _, _ := apiClient.GetAllRulesWithStatus(t) + // create a third group in a different namespace + _, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil) + requireStatusCode(t, http.StatusAccepted, status, body) - require.Len(t, ns[namespace], 2) + // And a non-provisioned rule in another namespace + namespace3UID := util.GenerateShortUID() + apiClient.CreateFolder(t, namespace3UID, "folder") + createRule(t, apiClient, namespace3UID) - rulesByGroupName := map[string][]apimodels.GettableExtendedRuleNode{} - for _, group := range ns[namespace] { - rulesByGroupName[group.Name] = append(rulesByGroupName[group.Name], group.Rules...) + // Now get the first group + group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name) + require.Equal(t, promGroup1, group1) + + // Get namespace1 + ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1) + expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup1, promGroup2}, } + require.Equal(t, expectedNs1, ns1) - require.Len(t, rulesByGroupName[promGroup1.Name], 3) - require.Len(t, rulesByGroupName[promGroup2.Name], 1) + // Get all namespaces + namespaces := apiClient.ConvertPrometheusGetAllRules(t) + expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{ + namespace1: {promGroup1, promGroup2}, + namespace2: {promGroup3}, + } + require.Equal(t, expectedNamespaces, namespaces) }) + t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) { + _, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil) + requireStatusCode(t, http.StatusForbidden, status, raw) + }) +} + +func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) { + testinfra.SQLiteIntegrationTest(t) + + // Setup Grafana and its Database + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI"}, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + + // Create users to make authenticated requests + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + + ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + + namespace1 := "test-namespace-1" + + namespace1UID := util.GenerateShortUID() + apiClient.CreateFolder(t, namespace1UID, namespace1) + t.Run("when pausing header is set, rules should be paused", func(t *testing.T) { tests := []struct { name string @@ -155,21 +237,17 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) { if tc.alertPaused { headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true" } - apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, headers) - ns, _, _ := apiClient.GetAllRulesWithStatus(t) + apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers) - rulesByGroupName := map[string][]apimodels.GettableExtendedRuleNode{} - for _, group := range ns[namespace] { - rulesByGroupName[group.Name] = append(rulesByGroupName[group.Name], group.Rules...) - } + gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name) - require.Len(t, rulesByGroupName[promGroup1.Name], 3) + require.Len(t, gr.Rules, 3) pausedRecordingRules := 0 pausedAlertRules := 0 - for _, rule := range rulesByGroupName[promGroup1.Name] { + for _, rule := range gr.Rules { if rule.GrafanaManagedAlert.IsPaused { if rule.GrafanaManagedAlert.Record != nil { pausedRecordingRules++ diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index de927dc890c..6594eaa4bb6 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -546,14 +546,14 @@ func (a apiClient) PostSilence(t *testing.T, s apimodels.PostableSilence) (apimo req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/alertmanager/grafana/api/v2/silences", a.url), bytes.NewReader(b)) require.NoError(t, err) req.Header.Set("Content-Type", "application/json") - return sendRequest[apimodels.PostSilencesOKBody](t, req, http.StatusAccepted) + return sendRequestJSON[apimodels.PostSilencesOKBody](t, req, http.StatusAccepted) } func (a apiClient) GetSilence(t *testing.T, id string) (apimodels.GettableSilence, int, string) { t.Helper() req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/grafana/api/v2/silence/%s", a.url, id), nil) require.NoError(t, err) - return sendRequest[apimodels.GettableSilence](t, req, http.StatusOK) + return sendRequestJSON[apimodels.GettableSilence](t, req, http.StatusOK) } func (a apiClient) GetSilences(t *testing.T, filters ...string) (apimodels.GettableSilences, int, string) { @@ -568,7 +568,7 @@ func (a apiClient) GetSilences(t *testing.T, filters ...string) (apimodels.Getta req, err := http.NewRequest(http.MethodGet, u.String(), nil) require.NoError(t, err) - return sendRequest[apimodels.GettableSilences](t, req, http.StatusOK) + return sendRequestJSON[apimodels.GettableSilences](t, req, http.StatusOK) } func (a apiClient) DeleteSilence(t *testing.T, id string) (any, int, string) { @@ -580,7 +580,7 @@ func (a apiClient) DeleteSilence(t *testing.T, id string) (any, int, string) { Message string `json:"message"` } - return sendRequest[dynamic](t, req, http.StatusOK) + return sendRequestJSON[dynamic](t, req, http.StatusOK) } func (a apiClient) GetRulesGroup(t *testing.T, folder string, group string) (apimodels.RuleGroupConfigResponse, int) { @@ -694,7 +694,7 @@ func (a apiClient) GetRuleGroupProvisioning(t *testing.T, folderUID string, grou req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/folder/%s/rule-groups/%s", a.url, folderUID, groupName), nil) require.NoError(t, err) - return sendRequest[apimodels.AlertRuleGroup](t, req, http.StatusOK) + return sendRequestJSON[apimodels.AlertRuleGroup](t, req, http.StatusOK) } func (a apiClient) CreateOrUpdateRuleGroupProvisioning(t *testing.T, group apimodels.AlertRuleGroup) (apimodels.AlertRuleGroup, int, string) { @@ -709,7 +709,7 @@ func (a apiClient) CreateOrUpdateRuleGroupProvisioning(t *testing.T, group apimo require.NoError(t, err) req.Header.Add("Content-Type", "application/json") - return sendRequest[apimodels.AlertRuleGroup](t, req, http.StatusOK) + return sendRequestJSON[apimodels.AlertRuleGroup](t, req, http.StatusOK) } func (a apiClient) SubmitRuleForBacktesting(t *testing.T, config apimodels.BacktestConfig) (int, string) { @@ -808,7 +808,7 @@ func (a apiClient) GetAllMuteTimingsWithStatus(t *testing.T) (apimodels.MuteTimi req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/mute-timings", a.url), nil) require.NoError(t, err) - return sendRequest[apimodels.MuteTimings](t, req, http.StatusOK) + return sendRequestJSON[apimodels.MuteTimings](t, req, http.StatusOK) } func (a apiClient) GetMuteTimingByNameWithStatus(t *testing.T, name string) (apimodels.MuteTimeInterval, int, string) { @@ -817,7 +817,7 @@ func (a apiClient) GetMuteTimingByNameWithStatus(t *testing.T, name string) (api req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/mute-timings/%s", a.url, name), nil) require.NoError(t, err) - return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusOK) + return sendRequestJSON[apimodels.MuteTimeInterval](t, req, http.StatusOK) } func (a apiClient) CreateMuteTimingWithStatus(t *testing.T, interval apimodels.MuteTimeInterval) (apimodels.MuteTimeInterval, int, string) { @@ -832,7 +832,7 @@ func (a apiClient) CreateMuteTimingWithStatus(t *testing.T, interval apimodels.M req.Header.Add("Content-Type", "application/json") require.NoError(t, err) - return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusCreated) + return sendRequestJSON[apimodels.MuteTimeInterval](t, req, http.StatusCreated) } func (a apiClient) EnsureMuteTiming(t *testing.T, interval apimodels.MuteTimeInterval) { @@ -854,7 +854,7 @@ func (a apiClient) UpdateMuteTimingWithStatus(t *testing.T, interval apimodels.M req.Header.Add("Content-Type", "application/json") require.NoError(t, err) - return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusAccepted) + return sendRequestJSON[apimodels.MuteTimeInterval](t, req, http.StatusAccepted) } func (a apiClient) DeleteMuteTimingWithStatus(t *testing.T, name string) (int, string) { @@ -908,7 +908,7 @@ func (a apiClient) GetRouteWithStatus(t *testing.T) (apimodels.Route, int, strin req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/policies", a.url), nil) require.NoError(t, err) - return sendRequest[apimodels.Route](t, req, http.StatusOK) + return sendRequestJSON[apimodels.Route](t, req, http.StatusOK) } func (a apiClient) GetRoute(t *testing.T) apimodels.Route { @@ -989,7 +989,7 @@ func (a apiClient) GetRuleHistoryWithStatus(t *testing.T, ruleUID string) (data. req, err := http.NewRequest(http.MethodGet, u.String(), nil) require.NoError(t, err) - return sendRequest[data.Frame](t, req, http.StatusOK) + return sendRequestJSON[data.Frame](t, req, http.StatusOK) } func (a apiClient) GetAllTimeIntervalsWithStatus(t *testing.T) ([]apimodels.GettableTimeIntervals, int, string) { @@ -998,7 +998,7 @@ func (a apiClient) GetAllTimeIntervalsWithStatus(t *testing.T) ([]apimodels.Gett req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/notifications/time-intervals", a.url), nil) require.NoError(t, err) - return sendRequest[[]apimodels.GettableTimeIntervals](t, req, http.StatusOK) + return sendRequestJSON[[]apimodels.GettableTimeIntervals](t, req, http.StatusOK) } func (a apiClient) GetTimeIntervalByNameWithStatus(t *testing.T, name string) (apimodels.GettableTimeIntervals, int, string) { @@ -1007,7 +1007,7 @@ func (a apiClient) GetTimeIntervalByNameWithStatus(t *testing.T, name string) (a req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/notifications/time-intervals/%s", a.url, name), nil) require.NoError(t, err) - return sendRequest[apimodels.GettableTimeIntervals](t, req, http.StatusOK) + return sendRequestJSON[apimodels.GettableTimeIntervals](t, req, http.StatusOK) } func (a apiClient) CreateReceiverWithStatus(t *testing.T, receiver apimodels.EmbeddedContactPoint) (apimodels.EmbeddedContactPoint, int, string) { @@ -1022,7 +1022,7 @@ func (a apiClient) CreateReceiverWithStatus(t *testing.T, receiver apimodels.Emb req.Header.Add("Content-Type", "application/json") require.NoError(t, err) - return sendRequest[apimodels.EmbeddedContactPoint](t, req, http.StatusAccepted) + return sendRequestJSON[apimodels.EmbeddedContactPoint](t, req, http.StatusAccepted) } func (a apiClient) EnsureReceiver(t *testing.T, receiver apimodels.EmbeddedContactPoint) { @@ -1075,33 +1075,33 @@ func (a apiClient) GetAlertmanagerConfigWithStatus(t *testing.T) (apimodels.Gett req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/grafana/config/api/v1/alerts", a.url), nil) require.NoError(t, err) - return sendRequest[apimodels.GettableUserConfig](t, req, http.StatusOK) + return sendRequestJSON[apimodels.GettableUserConfig](t, req, http.StatusOK) } func (a apiClient) GetActiveAlertsWithStatus(t *testing.T) (apimodels.AlertGroups, int, string) { t.Helper() req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/grafana/api/v2/alerts/groups", a.url), nil) require.NoError(t, err) - return sendRequest[apimodels.AlertGroups](t, req, http.StatusOK) + return sendRequestJSON[apimodels.AlertGroups](t, req, http.StatusOK) } func (a apiClient) GetRuleVersionsWithStatus(t *testing.T, ruleUID string) (apimodels.GettableRuleVersions, int, string) { t.Helper() req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/ruler/grafana/api/v1/rule/%s/versions", a.url, ruleUID), nil) require.NoError(t, err) - return sendRequest[apimodels.GettableRuleVersions](t, req, http.StatusOK) + return sendRequestJSON[apimodels.GettableRuleVersions](t, req, http.StatusOK) } func (a apiClient) GetRuleByUID(t *testing.T, ruleUID string) apimodels.GettableExtendedRuleNode { t.Helper() req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/ruler/grafana/api/v1/rule/%s", a.url, ruleUID), nil) require.NoError(t, err) - rule, status, raw := sendRequest[apimodels.GettableExtendedRuleNode](t, req, http.StatusOK) + rule, status, raw := sendRequestJSON[apimodels.GettableExtendedRuleNode](t, req, http.StatusOK) requireStatusCode(t, http.StatusOK, status, raw) return rule } -func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, datasourceUID string, promGroup apimodels.PrometheusRuleGroup, headers map[string]string) { +func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, datasourceUID string, promGroup apimodels.PrometheusRuleGroup, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) { t.Helper() data, err := yaml.Marshal(promGroup) @@ -1116,30 +1116,85 @@ func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, req.Header.Add(key, value) } - _, status, raw := sendRequest[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) - requireStatusCode(t, http.StatusAccepted, status, raw) + return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted) } -func sendRequest[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) { +func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string) apimodels.PrometheusRuleGroup { + t.Helper() + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil) + require.NoError(t, err) + rule, status, raw := sendRequestYAML[apimodels.PrometheusRuleGroup](t, req, http.StatusOK) + requireStatusCode(t, http.StatusOK, status, raw) + return rule +} + +func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTitle string) map[string][]apimodels.PrometheusRuleGroup { + t.Helper() + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil) + require.NoError(t, err) + ns, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK) + requireStatusCode(t, http.StatusOK, status, raw) + return ns +} + +func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimodels.PrometheusRuleGroup { + t.Helper() + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules", a.url), nil) + require.NoError(t, err) + result, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK) + requireStatusCode(t, http.StatusOK, status, raw) + return result +} + +func sendRequestRaw(t *testing.T, req *http.Request) ([]byte, int, error) { t.Helper() client := &http.Client{} resp, err := client.Do(req) - require.NoError(t, err) + if err != nil { + return nil, 0, err + } defer func() { _ = resp.Body.Close() }() - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, 0, err + } + + return body, resp.StatusCode, nil +} + +func sendRequestJSON[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) { + t.Helper() var result T - if resp.StatusCode != successStatusCode { - return result, resp.StatusCode, string(body) + body, statusCode, err := sendRequestRaw(t, req) + require.NoError(t, err) + + if statusCode != successStatusCode { + return result, statusCode, string(body) } err = json.Unmarshal(body, &result) require.NoError(t, err) - return result, resp.StatusCode, string(body) + return result, statusCode, string(body) +} + +func sendRequestYAML[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) { + t.Helper() + var result T + + body, statusCode, err := sendRequestRaw(t, req) + require.NoError(t, err) + + if statusCode != successStatusCode { + return result, statusCode, string(body) + } + + err = yaml.Unmarshal(body, &result) + require.NoError(t, err) + return result, statusCode, string(body) } func requireStatusCode(t *testing.T, expected, actual int, response string) { diff --git a/public/api-merged.json b/public/api-merged.json index dc455b6a36f..9ddc287f6f6 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -22771,6 +22771,7 @@ } }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "type": "object", diff --git a/public/openapi3.json b/public/openapi3.json index fabc90f92cf..8f339a39b59 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -12838,6 +12838,7 @@ "type": "object" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/components/schemas/gettableAlert" }, From 1302ee48b994cfb2243fbc595459d5ef293e712a Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Tue, 25 Feb 2025 14:59:58 +0000 Subject: [PATCH 09/51] OpenTSDB: Support v2.4 (#100673) * Add version 2.4 to frontend * Update settings and types - Set all properties on backend for consistency * Update query logic to parse new and old format - Minor naming updates - Extract logic for initial frame creation - When parsing old api responses, ensure data is in ascending order - Update tests * Update docs and provisioning file * Fix lint * Update docs/sources/datasources/opentsdb/_index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Update docs/sources/datasources/opentsdb/_index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Review nit --------- Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> --- devenv/datasources.yaml | 8 + docs/sources/datasources/opentsdb/_index.md | 8 +- pkg/tsdb/opentsdb/opentsdb.go | 148 +++++++++++++----- pkg/tsdb/opentsdb/opentsdb_test.go | 129 ++++++++++++++- pkg/tsdb/opentsdb/types.go | 15 +- .../opentsdb/components/OpenTsdbDetails.tsx | 1 + 6 files changed, 263 insertions(+), 46 deletions(-) diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml index 452d2d3d1bc..00efa243c1c 100644 --- a/devenv/datasources.yaml +++ b/devenv/datasources.yaml @@ -136,6 +136,14 @@ datasources: tsdbResolution: 1 tsdbVersion: 3 + - name: gdev-opentsdb-v2.4 + type: opentsdb + access: proxy + url: http://localhost:4242 + jsonData: + tsdbResolution: 1 + tsdbVersion: 4 + - name: gdev-elasticsearch type: elasticsearch uid: gdev-elasticsearch diff --git a/docs/sources/datasources/opentsdb/_index.md b/docs/sources/datasources/opentsdb/_index.md index 560ba731bb5..07a5a66add0 100644 --- a/docs/sources/datasources/opentsdb/_index.md +++ b/docs/sources/datasources/opentsdb/_index.md @@ -62,7 +62,7 @@ To configure basic settings for the data source, complete the following steps: | **Default** | Default data source that will be be pre-selected for new panels. | | **URL** | The HTTP protocol, IP, and port of your OpenTSDB server (default port is usually 4242). | | **Allowed cookies** | Listing of cookies to forward to the data source. | -| **Version** | The OpenTSDB version. | +| **Version** | The OpenTSDB version (supported versions are: 2.4, 2.3, 2.2 and versions less than 2.1). | | **Resolution** | Metrics from OpenTSDB may have data points with either second or millisecond resolution. | | **Lookup limit** | Default is 1000. | @@ -98,9 +98,13 @@ can be used to query OpenTSDB. Fill Policy is also introduced in OpenTSDB 2.2. While using OpenTSDB 2.2 data source, make sure you use either Filters or Tags as they are mutually exclusive. If used together, might give you weird results. {{% /admonition %}} +{{% admonition type="note" %}} +When using OpenTSDB 2.4 with alerting, queries are executed with the parameter `arrays=true`. This causes OpenTSDB to return data points as an array of arrays instead of a map of key-value pairs. Grafana then converts this data into the appropriate data frame format. +{{% /admonition %}} + ### Auto complete suggestions -As soon as you start typing metric names, tag names and tag values , you should see highlighted auto complete suggestions for them. +As you begin typing metric names, tag names, or tag values, highlighted autocomplete suggestions will appear. The autocomplete only works if the OpenTSDB suggest API is enabled. ## Templating queries diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 15a1fe27046..798b27e57ab 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -8,6 +8,8 @@ import ( "net/http" "net/url" "path" + "sort" + "strconv" "strings" "time" @@ -35,12 +37,21 @@ func ProvideService(httpClientProvider httpclient.Provider) *Service { } type datasourceInfo struct { - HTTPClient *http.Client - URL string + HTTPClient *http.Client + URL string + TSDBVersion float32 + TSDBResolution int32 + LookupLimit int32 } type DsAccess string +type JSONData struct { + TSDBVersion float32 `json:"tsdbVersion"` + TSDBResolution int32 `json:"tsdbResolution"` + LookupLimit int32 `json:"lookupLimit"` +} + func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { opts, err := settings.HTTPClientOptions(ctx) @@ -53,9 +64,18 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst return nil, err } + jsonData := JSONData{} + err = json.Unmarshal(settings.JSONData, &jsonData) + if err != nil { + return nil, fmt.Errorf("error reading settings: %w", err) + } + model := &datasourceInfo{ - HTTPClient: client, - URL: settings.URL, + HTTPClient: client, + URL: settings.URL, + TSDBVersion: jsonData.TSDBVersion, + TSDBResolution: jsonData.TSDBResolution, + LookupLimit: jsonData.LookupLimit, } return model, nil @@ -69,7 +89,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) q := req.Queries[0] - myRefID := q.RefID + refID := q.RefID tsdbQuery.Start = q.TimeRange.From.UnixNano() / int64(time.Millisecond) tsdbQuery.End = q.TimeRange.To.UnixNano() / int64(time.Millisecond) @@ -106,7 +126,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) } }() - result, err := s.parseResponse(logger, res, myRefID) + result, err := s.parseResponse(logger, res, refID, dsInfo.TSDBVersion) if err != nil { return &backend.QueryDataResponse{}, err } @@ -120,9 +140,11 @@ func (s *Service) createRequest(ctx context.Context, logger log.Logger, dsInfo * return nil, err } u.Path = path.Join(u.Path, "api/query") - queryParams := u.Query() - queryParams.Set("arrays", "true") - u.RawQuery = queryParams.Encode() + if dsInfo.TSDBVersion == 4 { + queryParams := u.Query() + queryParams.Set("arrays", "true") + u.RawQuery = queryParams.Encode() + } postData, err := json.Marshal(data) if err != nil { @@ -140,7 +162,67 @@ func (s *Service) createRequest(ctx context.Context, logger log.Logger, dsInfo * return req, nil } -func (s *Service) parseResponse(logger log.Logger, res *http.Response, myRefID string) (*backend.QueryDataResponse, error) { +func createInitialFrame(val OpenTsdbCommon, length int, refID string) *data.Frame { + labels := data.Labels{} + for label, value := range val.Tags { + labels[label] = value + } + + frame := data.NewFrameOfFieldTypes(val.Metric, length, data.FieldTypeTime, data.FieldTypeFloat64) + frame.Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}} + frame.RefID = refID + timeField := frame.Fields[0] + timeField.Name = data.TimeSeriesTimeFieldName + dataField := frame.Fields[1] + dataField.Name = val.Metric + dataField.Labels = labels + + return frame +} + +// Parse response function for OpenTSDB version 2.4 +func parseResponse24(responseData []OpenTsdbResponse24, refID string, frames data.Frames) data.Frames { + for _, val := range responseData { + frame := createInitialFrame(val.OpenTsdbCommon, len(val.DataPoints), refID) + + for i, point := range val.DataPoints { + frame.SetRow(i, time.Unix(int64(point[0]), 0).UTC(), point[1]) + } + + frames = append(frames, frame) + } + + return frames +} + +// Parse response function for OpenTSDB versions < 2.4 +func parseResponseLT24(responseData []OpenTsdbResponse, refID string, frames data.Frames) (data.Frames, error) { + for _, val := range responseData { + frame := createInitialFrame(val.OpenTsdbCommon, len(val.DataPoints), refID) + + // Order the timestamps in ascending order to avoid issues like https://github.com/grafana/grafana/issues/38729 + timestamps := make([]string, 0, len(val.DataPoints)) + for timestamp := range val.DataPoints { + timestamps = append(timestamps, timestamp) + } + sort.Strings(timestamps) + + for i, timeString := range timestamps { + timestamp, err := strconv.ParseInt(timeString, 10, 64) + if err != nil { + logger.Info("Failed to unmarshal opentsdb timestamp", "timestamp", timeString) + return frames, err + } + frame.SetRow(i, time.Unix(timestamp, 0).UTC(), val.DataPoints[timeString]) + } + + frames = append(frames, frame) + } + + return frames, nil +} + +func (s *Service) parseResponse(logger log.Logger, res *http.Response, refID string, tsdbVersion float32) (*backend.QueryDataResponse, error) { resp := backend.NewQueryDataResponse() body, err := io.ReadAll(res.Body) @@ -158,38 +240,34 @@ func (s *Service) parseResponse(logger log.Logger, res *http.Response, myRefID s return nil, fmt.Errorf("request failed, status: %s", res.Status) } - var responseData []OpenTsdbResponse - err = json.Unmarshal(body, &responseData) - if err != nil { - logger.Info("Failed to unmarshal opentsdb response", "error", err, "status", res.Status, "body", string(body)) - return nil, err - } - frames := data.Frames{} - for _, val := range responseData { - labels := data.Labels{} - for label, value := range val.Tags { - labels[label] = value + + var responseData []OpenTsdbResponse + var responseData24 []OpenTsdbResponse24 + if tsdbVersion == 4 { + err = json.Unmarshal(body, &responseData24) + if err != nil { + logger.Info("Failed to unmarshal opentsdb response", "error", err, "status", res.Status, "body", string(body)) + return nil, err } - frame := data.NewFrameOfFieldTypes(val.Metric, len(val.DataPoints), data.FieldTypeTime, data.FieldTypeFloat64) - frame.Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}} - frame.RefID = myRefID - timeField := frame.Fields[0] - timeField.Name = data.TimeSeriesTimeFieldName - dataField := frame.Fields[1] - dataField.Name = "value" - dataField.Labels = labels + frames = parseResponse24(responseData24, refID, frames) + } else { + err = json.Unmarshal(body, &responseData) + if err != nil { + logger.Info("Failed to unmarshal opentsdb response", "error", err, "status", res.Status, "body", string(body)) + return nil, err + } - points := val.DataPoints - for i, point := range points { - frame.SetRow(i, time.Unix(int64(point[0]), 0).UTC(), point[1]) + frames, err = parseResponseLT24(responseData, refID, frames) + if err != nil { + return nil, err } - frames = append(frames, frame) } - result := resp.Responses[myRefID] + + result := resp.Responses[refID] result.Frames = frames - resp.Responses[myRefID] = result + resp.Responses[refID] = result return resp, nil } diff --git a/pkg/tsdb/opentsdb/opentsdb_test.go b/pkg/tsdb/opentsdb/opentsdb_test.go index 6c5e142c250..44c21f6df15 100644 --- a/pkg/tsdb/opentsdb/opentsdb_test.go +++ b/pkg/tsdb/opentsdb/opentsdb_test.go @@ -33,12 +33,13 @@ func TestOpenTsdbExecutor(t *testing.T) { t.Run("Parse response should handle invalid JSON", func(t *testing.T) { response := `{ invalid }` - result, err := service.parseResponse(logger, &http.Response{Body: io.NopCloser(strings.NewReader(response))}, "A") + tsdbVersion := float32(4) + result, err := service.parseResponse(logger, &http.Response{Body: io.NopCloser(strings.NewReader(response))}, "A", tsdbVersion) require.Nil(t, result) require.Error(t, err) }) - t.Run("Parse response should handle JSON", func(t *testing.T) { + t.Run("Parse response should handle JSON (v2.4 and above)", func(t *testing.T) { response := ` [ { @@ -57,7 +58,7 @@ func TestOpenTsdbExecutor(t *testing.T) { data.NewField("Time", nil, []time.Time{ time.Date(2014, 7, 16, 20, 55, 46, 0, time.UTC), }), - data.NewField("value", map[string]string{"env": "prod", "app": "grafana"}, []float64{ + data.NewField("test", map[string]string{"env": "prod", "app": "grafana"}, []float64{ 50}), ) testFrame.Meta = &data.FrameMeta{ @@ -65,10 +66,124 @@ func TestOpenTsdbExecutor(t *testing.T) { TypeVersion: data.FrameTypeVersion{0, 1}, } testFrame.RefID = "A" + tsdbVersion := float32(4) resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} resp.StatusCode = 200 - result, err := service.parseResponse(logger, &resp, "A") + result, err := service.parseResponse(logger, &resp, "A", tsdbVersion) + require.NoError(t, err) + + frame := result.Responses["A"] + + if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("Parse response should handle JSON (v2.3 and below)", func(t *testing.T) { + response := ` + [ + { + "metric": "test", + "dps": { + "1405544146": 50.0 + }, + "tags" : { + "env": "prod", + "app": "grafana" + } + } + ]` + + testFrame := data.NewFrame("test", + data.NewField("Time", nil, []time.Time{ + time.Date(2014, 7, 16, 20, 55, 46, 0, time.UTC), + }), + data.NewField("test", map[string]string{"env": "prod", "app": "grafana"}, []float64{ + 50}), + ) + testFrame.Meta = &data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMulti, + TypeVersion: data.FrameTypeVersion{0, 1}, + } + testFrame.RefID = "A" + tsdbVersion := float32(3) + + resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} + resp.StatusCode = 200 + result, err := service.parseResponse(logger, &resp, "A", tsdbVersion) + require.NoError(t, err) + + frame := result.Responses["A"] + + if diff := cmp.Diff(testFrame, frame.Frames[0], data.FrameTestCompareOptions()...); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("Parse response should handle unordered JSON (v2.3 and below)", func(t *testing.T) { + response := ` + [ + { + "metric": "test", + "dps": { + "1405094109": 55.0, + "1405124146": 124.0, + "1405124212": 1284.0, + "1405019246": 50.0, + "1408352146": 812.0, + "1405534153": 153.0, + "1405124397": 9035.0, + "1401234774": 215.0, + "1409712532": 356.0, + "1491523811": 8953.0, + "1405239823": 258.0 + }, + "tags" : { + "env": "prod", + "app": "grafana" + } + } + ]` + + testFrame := data.NewFrame("test", + data.NewField("Time", nil, []time.Time{ + time.Date(2014, 5, 27, 23, 52, 54, 0, time.UTC), + time.Date(2014, 7, 10, 19, 7, 26, 0, time.UTC), + time.Date(2014, 7, 11, 15, 55, 9, 0, time.UTC), + time.Date(2014, 7, 12, 0, 15, 46, 0, time.UTC), + time.Date(2014, 7, 12, 0, 16, 52, 0, time.UTC), + time.Date(2014, 7, 12, 0, 19, 57, 0, time.UTC), + time.Date(2014, 7, 13, 8, 23, 43, 0, time.UTC), + time.Date(2014, 7, 16, 18, 9, 13, 0, time.UTC), + time.Date(2014, 8, 18, 8, 55, 46, 0, time.UTC), + time.Date(2014, 9, 3, 2, 48, 52, 0, time.UTC), + time.Date(2017, 4, 7, 0, 10, 11, 0, time.UTC), + }), + data.NewField("test", map[string]string{"env": "prod", "app": "grafana"}, []float64{ + 215, + 50, + 55, + 124, + 1284, + 9035, + 258, + 153, + 812, + 356, + 8953, + }), + ) + testFrame.Meta = &data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMulti, + TypeVersion: data.FrameTypeVersion{0, 1}, + } + testFrame.RefID = "A" + tsdbVersion := float32(3) + + resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} + resp.StatusCode = 200 + result, err := service.parseResponse(logger, &resp, "A", tsdbVersion) require.NoError(t, err) frame := result.Responses["A"] @@ -99,7 +214,7 @@ func TestOpenTsdbExecutor(t *testing.T) { data.NewField("Time", nil, []time.Time{ time.Date(2014, 7, 16, 20, 55, 46, 0, time.UTC), }), - data.NewField("value", map[string]string{"env": "prod", "app": "grafana"}, []float64{ + data.NewField("test", map[string]string{"env": "prod", "app": "grafana"}, []float64{ 50}), ) testFrame.Meta = &data.FrameMeta{ @@ -108,9 +223,11 @@ func TestOpenTsdbExecutor(t *testing.T) { } testFrame.RefID = myRefid + tsdbVersion := float32(4) + resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} resp.StatusCode = 200 - result, err := service.parseResponse(logger, &resp, myRefid) + result, err := service.parseResponse(logger, &resp, myRefid, tsdbVersion) require.NoError(t, err) if diff := cmp.Diff(testFrame, result.Responses[myRefid].Frames[0], data.FrameTestCompareOptions()...); diff != "" { diff --git a/pkg/tsdb/opentsdb/types.go b/pkg/tsdb/opentsdb/types.go index 171a24e2067..19d2ba75197 100644 --- a/pkg/tsdb/opentsdb/types.go +++ b/pkg/tsdb/opentsdb/types.go @@ -6,8 +6,17 @@ type OpenTsdbQuery struct { Queries []map[string]any `json:"queries"` } +type OpenTsdbCommon struct { + Metric string `json:"metric"` + Tags map[string]string `json:"tags"` +} + type OpenTsdbResponse struct { - Metric string `json:"metric"` - Tags map[string]string `json:"tags"` - DataPoints [][]float64 `json:"dps"` + OpenTsdbCommon + DataPoints map[string]float64 `json:"dps"` +} + +type OpenTsdbResponse24 struct { + OpenTsdbCommon + DataPoints [][]float64 `json:"dps"` } diff --git a/public/app/plugins/datasource/opentsdb/components/OpenTsdbDetails.tsx b/public/app/plugins/datasource/opentsdb/components/OpenTsdbDetails.tsx index ba45bde7b29..2c913d55014 100644 --- a/public/app/plugins/datasource/opentsdb/components/OpenTsdbDetails.tsx +++ b/public/app/plugins/datasource/opentsdb/components/OpenTsdbDetails.tsx @@ -9,6 +9,7 @@ const tsdbVersions = [ { label: '<=2.1', value: 1 }, { label: '==2.2', value: 2 }, { label: '==2.3', value: 3 }, + { label: '==2.4', value: 4 }, ]; const tsdbResolutions = [ From c5250311fce1c3c62c5f993b4e9e8ddf7e1eafe8 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Tue, 25 Feb 2025 15:06:48 +0000 Subject: [PATCH 10/51] Logs: Re-run Loki queries in Explore when direction and sort order are changed (#99994) --- public/app/features/explore/Logs/Logs.test.tsx | 9 ++++++--- public/app/features/explore/Logs/Logs.tsx | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index 1749d92d48d..958bad54ad4 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -48,17 +48,18 @@ jest.mock('../state/explorePane', () => ({ changePanelState: (exploreId: string, panel: 'logs', panelState: {} | ExploreLogsPanelState) => { return fakeChangePanelState(exploreId, panel, panelState); }, - changeQueries: (args: { queries: DataQuery[]; exploreId: string | undefined }) => { - return fakeChangeQueries(args); - }, })); const fakeChangeQueries = jest.fn().mockReturnValue({ type: 'fakeChangeQueries' }); +const fakeRunQueries = jest.fn().mockReturnValue({ type: 'fakeRunQueries' }); jest.mock('../state/query', () => ({ ...jest.requireActual('../state/query'), changeQueries: (args: { queries: DataQuery[]; exploreId: string | undefined }) => { return fakeChangeQueries(args); }, + runQueries: (args: { queries: DataQuery[]; exploreId: string | undefined }) => { + return fakeRunQueries(args); + }, })); describe('Logs', () => { @@ -388,6 +389,7 @@ describe('Logs', () => { expect(logRows.length).toBe(3); expect(logRows[0].textContent).toContain('log message 1'); expect(logRows[2].textContent).toContain('log message 3'); + expect(fakeRunQueries).not.toHaveBeenCalled(); }); it('should sync the query direction when changing the order of loki queries', async () => { @@ -399,6 +401,7 @@ describe('Logs', () => { exploreId: 'left', queries: [{ ...query, direction: LokiQueryDirection.Forward }], }); + expect(fakeRunQueries).toHaveBeenCalledWith({ exploreId: 'left' }); }); it('should not change the query direction when changing the order of non-loki queries', async () => { diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index ae6c8e1f7d0..feefbb1bee7 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -75,7 +75,7 @@ import { import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext'; import { getUrlStateFromPaneState } from '../hooks/useStateSync'; import { changePanelState } from '../state/explorePane'; -import { changeQueries } from '../state/query'; +import { changeQueries, runQueries } from '../state/query'; import { LogsFeedback } from './LogsFeedback'; import { LogsMetaRow } from './LogsMetaRow'; @@ -478,12 +478,11 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { if (query.datasource?.type !== 'loki' || !isLokiQuery(query)) { return query; } - hasLokiQueries = true; - if (query.direction === LokiQueryDirection.Scan) { // Don't override Scan. When the direction is Scan it means that the user specifically assigned this direction to the query. return query; } + hasLokiQueries = true; const newDirection = newSortOrder === LogsSortOrder.Ascending ? LokiQueryDirection.Forward : LokiQueryDirection.Backward; if (newDirection !== query.direction) { @@ -494,6 +493,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { if (hasLokiQueries) { dispatch(changeQueries({ exploreId, queries: newQueries })); + dispatch(runQueries({ exploreId })); } } From 53e91fd5e8703b20c360c2a42f87dfb10b4b55f1 Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Tue, 25 Feb 2025 17:28:31 +0100 Subject: [PATCH 11/51] unistore: close event stream on context cancelation (#101293) * add tests for broacaster * fix sql notifier not closing the stream * fix sql notifier not closing the stream * close sub * fix broadcaster test * fix broadcaster test * suggestion --- .../unified/resource/broadcaster_test.go | 38 +++++++++++++++++++ pkg/storage/unified/resource/server.go | 5 +-- pkg/storage/unified/sql/notifier_sql.go | 2 + pkg/storage/unified/sql/notifier_sql_test.go | 37 ++++++++++++++++++ .../unified/testing/storage_backend.go | 10 ++++- 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/pkg/storage/unified/resource/broadcaster_test.go b/pkg/storage/unified/resource/broadcaster_test.go index 8eedfa01ce5..3ae056a8a77 100644 --- a/pkg/storage/unified/resource/broadcaster_test.go +++ b/pkg/storage/unified/resource/broadcaster_test.go @@ -104,3 +104,41 @@ func TestCache(t *testing.T) { // slice should return all values require.Equal(t, []int{4, 5, 6, 7, 8, 9, 10, 11, 12, 13}, c.Slice()) } + +func TestBroadcaster(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + ch := make(chan int) + input := []int{1, 2, 3} + go func() { + for _, v := range input { + ch <- v + } + }() + t.Cleanup(func() { + close(ch) + }) + + b, err := NewBroadcaster(ctx, func(out chan<- int) error { + go func() { + for v := range ch { + out <- v + } + }() + return nil + }) + require.NoError(t, err) + + sub, err := b.Subscribe(ctx) + require.NoError(t, err) + + for _, expected := range input { + v, ok := <-sub + require.True(t, ok) + require.Equal(t, expected, v) + } + + // cancel the context should close the stream + cancel() + _, ok := <-sub + require.False(t, ok) +} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 7853f9125db..148de06595a 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -919,10 +919,7 @@ func (s *server) initWatcher() error { return err } go func() { - for { - // pipe all events - v := <-events - + for v := range events { if v == nil { s.log.Error("received nil event") continue diff --git a/pkg/storage/unified/sql/notifier_sql.go b/pkg/storage/unified/sql/notifier_sql.go index a4d0a008fcc..9940f7c882f 100644 --- a/pkg/storage/unified/sql/notifier_sql.go +++ b/pkg/storage/unified/sql/notifier_sql.go @@ -120,6 +120,8 @@ func (p *pollingNotifier) poller(ctx context.Context, since groupResourceRV, str for { select { + case <-ctx.Done(): + return case <-p.done: return case <-t.C: diff --git a/pkg/storage/unified/sql/notifier_sql_test.go b/pkg/storage/unified/sql/notifier_sql_test.go index 693d63ea485..07fcfac3365 100644 --- a/pkg/storage/unified/sql/notifier_sql_test.go +++ b/pkg/storage/unified/sql/notifier_sql_test.go @@ -357,4 +357,41 @@ func TestPollingNotifier(t *testing.T) { t.Fatal("timeout waiting for events channel to close") } }) + + t.Run("stops polling when context is cancelled", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + + cfg := &pollingNotifierConfig{ + dialect: sqltemplate.SQLite, + pollingInterval: 10 * time.Millisecond, + watchBufferSize: 10, + log: log.NewNopLogger(), + tracer: noop.NewTracerProvider().Tracer("test"), + batchLock: &batchLock{}, + listLatestRVs: func(ctx context.Context) (groupResourceRV, error) { return nil, nil }, + historyPoll: func(ctx context.Context, grp string, res string, since int64) ([]*historyPollResponse, error) { + return nil, nil + }, + done: make(chan struct{}), + } + + notifier, err := newPollingNotifier(cfg) + require.NoError(t, err) + require.NotNil(t, notifier) + + events, err := notifier.notify(ctx) + require.NoError(t, err) + require.NotNil(t, events) + + cancel() + + select { + case _, ok := <-events: + require.False(t, ok, "events channel should be closed") + case <-time.After(time.Second): + t.Fatal("timeout waiting for events channel to close") + } + }) } diff --git a/pkg/storage/unified/testing/storage_backend.go b/pkg/storage/unified/testing/storage_backend.go index d34cf3c0c71..d19f9d2b05a 100644 --- a/pkg/storage/unified/testing/storage_backend.go +++ b/pkg/storage/unified/testing/storage_backend.go @@ -51,7 +51,7 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp fn func(*testing.T, resource.StorageBackend) }{ {TestHappyPath, runTestIntegrationBackendHappyPath}, - {TestWatchWriteEvents, runTestIntegrationBackendWatchWriteEventsFromLastest}, + {TestWatchWriteEvents, runTestIntegrationBackendWatchWriteEvents}, {TestList, runTestIntegrationBackendList}, {TestBlobSupport, runTestIntegrationBlobSupport}, {TestGetResourceStats, runTestIntegrationBackendGetResourceStats}, @@ -272,7 +272,7 @@ func runTestIntegrationBackendGetResourceStats(t *testing.T, backend resource.St }) } -func runTestIntegrationBackendWatchWriteEventsFromLastest(t *testing.T, backend resource.StorageBackend) { +func runTestIntegrationBackendWatchWriteEvents(t *testing.T, backend resource.StorageBackend) { ctx := testutil.NewTestContext(t, time.Now().Add(5*time.Second)) // Create a few resources before initing the watch @@ -287,6 +287,12 @@ func runTestIntegrationBackendWatchWriteEventsFromLastest(t *testing.T, backend _, err = writeEvent(ctx, backend, "item2", resource.WatchEvent_ADDED) require.NoError(t, err) require.Equal(t, "item2", (<-stream).Key.Name) + + // Should close the stream + ctx.Cancel() + + _, ok := <-stream + require.False(t, ok) } func runTestIntegrationBackendList(t *testing.T, backend resource.StorageBackend) { From bc00462875375ec26d6b0c6a81625df2f413418e Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:52:06 -0600 Subject: [PATCH 12/51] Table: Enable actions option (#101069) --- public/app/plugins/panel/table/module.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index f930e45de19..0d6466426da 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -6,6 +6,7 @@ import { ReducerID, standardEditorsRegistry, identityOverrideProcessor, + FieldConfigProperty, } from '@grafana/data'; import { TableCellOptions, TableCellDisplayMode, defaultTableFieldOptions, TableCellHeight } from '@grafana/schema'; @@ -23,6 +24,11 @@ export const plugin = new PanelPlugin(TablePanel) .setPanelChangeHandler(tablePanelChangedHandler) .setMigrationHandler(tableMigrationHandler) .useFieldConfig({ + standardOptions: { + [FieldConfigProperty.Actions]: { + hideFromDefaults: false, + }, + }, useCustomConfig: (builder) => { builder .addNumberInput({ From 142a100915fd0362ecd98469b81b143fd3491b3e Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Tue, 25 Feb 2025 17:07:17 +0000 Subject: [PATCH 13/51] fix(timeseries): allow annotations without color/isRegion/timeEnd (#101301) --- .../panel/timeseries/plugins/AnnotationsPlugin2.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx index 7a4c2b2db58..f01152e2f88 100644 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx @@ -142,7 +142,7 @@ export const AnnotationsPlugin2 = ({ let yKey = config.scales[1].props.scaleKey; for (let i = 0; i < frame.length; i++) { - let color = getColorByName(vals.color[i] || DEFAULT_ANNOTATION_COLOR_HEX8); + let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8); let x0 = u.valToPos(vals.xMin[i], xKey, true); let x1 = u.valToPos(vals.xMax[i], xKey, true); @@ -173,12 +173,12 @@ export const AnnotationsPlugin2 = ({ ctx.setLineDash([5, 5]); for (let i = 0; i < vals.time.length; i++) { - let color = getColorByName(vals.color[i] || DEFAULT_ANNOTATION_COLOR_HEX8); + let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR_HEX8); let x0 = u.valToPos(vals.time[i], 'x', true); renderLine(ctx, y0, y1, x0, color); - if (vals.isRegion[i]) { + if (vals.isRegion?.[i]) { let x1 = u.valToPos(vals.timeEnd[i], 'x', true); renderLine(ctx, y0, y1, x1, color); @@ -216,14 +216,14 @@ export const AnnotationsPlugin2 = ({ let markers: React.ReactNode[] = []; for (let i = 0; i < vals.time.length; i++) { - let color = getColorByName(vals.color[i] || DEFAULT_ANNOTATION_COLOR); + let color = getColorByName(vals.color?.[i] || DEFAULT_ANNOTATION_COLOR); let left = Math.round(plot.valToPos(vals.time[i], 'x')) || 0; // handles -0 let style: React.CSSProperties | null = null; let className = ''; let isVisible = true; - if (vals.isRegion[i]) { - let right = Math.round(plot.valToPos(vals.timeEnd[i], 'x')) || 0; // handles -0 + if (vals.isRegion?.[i]) { + let right = Math.round(plot.valToPos(vals.timeEnd?.[i], 'x')) || 0; // handles -0 isVisible = left < plot.rect.width && right > 0; From 4538c8cad96ed6e2817152de0edf97c789ab8788 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 25 Feb 2025 17:30:58 +0000 Subject: [PATCH 14/51] DS proxy: Remove ft `datasourceProxyDisableRBAC` and logic (#101239) delete ft datasourceproxy --- .../grafana-data/src/types/featureToggles.gen.ts | 1 - pkg/api/pluginproxy/ds_proxy.go | 12 ++---------- pkg/services/featuremgmt/registry.go | 8 -------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 1 + 6 files changed, 3 insertions(+), 24 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b64ba1ed238..85ba3f238c5 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -172,7 +172,6 @@ export interface FeatureToggles { newDashboardSharingComponent?: boolean; alertingListViewV2?: boolean; dashboardRestore?: boolean; - datasourceProxyDisableRBAC?: boolean; alertingDisableSendAlertsExternal?: boolean; preserveDashboardStateWhenNavigating?: boolean; alertingCentralAlertHistory?: boolean; diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index c8f158138bf..ce8bdf2770f 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -306,16 +306,8 @@ func (proxy *DataSourceProxy) validateRequest() error { continue } - if proxy.features.IsEnabled(proxy.ctx.Req.Context(), featuremgmt.FlagDatasourceProxyDisableRBAC) { - // TODO(aarongodin): following logic can be removed with FlagDatasourceProxyDisableRBAC as it is covered by - // proxy.hasAccessToRoute(..) - if route.ReqRole.IsValid() && !proxy.ctx.HasUserRole(route.ReqRole) { - return errors.New("plugin proxy route access denied") - } - } else { - if !proxy.hasAccessToRoute(route) { - return errors.New("plugin proxy route access denied") - } + if !proxy.hasAccessToRoute(route) { + return errors.New("plugin proxy route access denied") } proxy.matchedRoute = route diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 937cd429810..a3a8a86191d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1170,14 +1170,6 @@ var ( HideFromAdminPage: true, Expression: "false", // enabled by default }, - { - Name: "datasourceProxyDisableRBAC", - Description: "Disables applying a plugin route's ReqAction field to authorization", - Stage: FeatureStageGeneralAvailability, - Owner: identityAccessTeam, - HideFromDocs: true, - Expression: "false", - }, { Name: "alertingDisableSendAlertsExternal", Description: "Disables the ability to send alerts to an external Alertmanager datasource.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index f6c75931392..065323e011e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -153,7 +153,6 @@ logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,fa newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true dashboardRestore,experimental,@grafana/search-and-storage,false,false,false -datasourceProxyDisableRBAC,GA,@grafana/identity-access-team,false,false,false alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index eb23fb7815b..b2d704e7a10 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -623,10 +623,6 @@ const ( // Enables deleted dashboard restore feature FlagDashboardRestore = "dashboardRestore" - // FlagDatasourceProxyDisableRBAC - // Disables applying a plugin route's ReqAction field to authorization - FlagDatasourceProxyDisableRBAC = "datasourceProxyDisableRBAC" - // FlagAlertingDisableSendAlertsExternal // Disables the ability to send alerts to an external Alertmanager datasource. FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index fcf2d517304..0d6059d5520 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1277,6 +1277,7 @@ "name": "datasourceProxyDisableRBAC", "resourceVersion": "1720021873452", "creationTimestamp": "2024-05-21T13:05:16Z", + "deletionTimestamp": "2025-02-24T17:23:43Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } From cd7a1d515c27d4ac11148102023f7d91e4f9704f Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:54:07 -0600 Subject: [PATCH 15/51] Canvas: Fix oneClick migration (#101311) --- public/app/plugins/panel/canvas/migrations.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/canvas/migrations.ts b/public/app/plugins/panel/canvas/migrations.ts index 479a540bff7..22923598029 100644 --- a/public/app/plugins/panel/canvas/migrations.ts +++ b/public/app/plugins/panel/canvas/migrations.ts @@ -64,9 +64,9 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial => { const root = panel.options?.root; if (root?.elements) { for (const element of root.elements) { - if (element.oneClickMode === OneClickMode.Link || element.oneClickLinks) { + if ((element.oneClickMode === OneClickMode.Link || element.oneClickLinks) && element.links?.length) { element.links[0].oneClick = true; - } else if (element.oneClickMode === OneClickMode.Action) { + } else if (element.oneClickMode === OneClickMode.Action && element.actions?.length) { element.actions[0].oneClick = true; } From 2681a93b478f6783f975803372374da1af49a7d7 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 25 Feb 2025 15:05:29 -0500 Subject: [PATCH 16/51] Fix permissions for Update Alerting Module action (#101223) * add id-token permission * use alerting-team app --- .github/workflows/alerting-update-module.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/alerting-update-module.yml b/.github/workflows/alerting-update-module.yml index eece934525e..1d919f3dc4b 100644 --- a/.github/workflows/alerting-update-module.yml +++ b/.github/workflows/alerting-update-module.yml @@ -13,6 +13,7 @@ jobs: permissions: contents: write pull-requests: write + id-token: write steps: - name: Checkout repository @@ -93,8 +94,8 @@ jobs: uses: grafana/shared-workflows/actions/get-vault-secrets@28361cdb22223e5f1e34358c86c20908e7248760 # 1.1.0 with: repo_secrets: | - GITHUB_APP_ID=github-app:app-id - GITHUB_APP_PRIVATE_KEY=github-app:private-key + GITHUB_APP_ID=alerting-team:app-id + GITHUB_APP_PRIVATE_KEY=alerting-team:private-key - name: "Generate token" id: generate_token @@ -127,4 +128,4 @@ jobs: if: steps.create-pr.outputs.pull-request-url != '' run: | echo "## Pull Request Created" >> $GITHUB_STEP_SUMMARY - echo "🔗 [View Pull Request](${{ steps.create-pr.outputs.pull-request-url }})" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "🔗 [View Pull Request](${{ steps.create-pr.outputs.pull-request-url }})" >> $GITHUB_STEP_SUMMARY From a7ecb19c3149b3c43bf9cf08a10f4842dbb59975 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:23:28 -0700 Subject: [PATCH 17/51] Chore: Update base alpine docker image (#101320) * baldm0mma/ update base image arg * baldm0mma/ update alpine image * baldm0mma/ skip failing test * baldm0mma/ specifiy patch * baldm0mma/ flaky test? --- .drone.yml | 82 ++++++++++++++++----------------- Dockerfile | 2 +- scripts/drone/utils/images.star | 2 +- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.drone.yml b/.drone.yml index 6883dcac6b6..491c607a83b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -18,7 +18,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -69,7 +69,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - go install github.com/bazelbuild/buildtools/buildifier@latest @@ -112,7 +112,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -170,7 +170,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -309,7 +309,7 @@ steps: path: /github-app - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -427,7 +427,7 @@ steps: path: /github-app - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -517,7 +517,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -642,7 +642,7 @@ steps: path: /github-app - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - mkdir -p bin @@ -713,7 +713,7 @@ steps: -a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.5 --yarn-cache=$$YARN_CACHE_FOLDER - --build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.6 + --build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.21.3 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD > packages.txt @@ -770,7 +770,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.20.6 + image: alpine:3.21.3 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -1114,7 +1114,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1280,7 +1280,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1637,7 +1637,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1708,7 +1708,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1766,7 +1766,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1842,7 +1842,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1922,7 +1922,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -1988,7 +1988,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2063,7 +2063,7 @@ steps: path: /github-app - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - mkdir -p bin @@ -2133,7 +2133,7 @@ steps: -a targz:grafana:linux/arm/v7 -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --go-version=1.23.5 --yarn-cache=$$YARN_CACHE_FOLDER - --build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.6 + --build-id=$$DRONE_BUILD_NUMBER --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.21.3 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' --verify='false' --grafana-dir=$$PWD > packages.txt @@ -2194,7 +2194,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.20.6 + image: alpine:3.21.3 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -2611,7 +2611,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2856,7 +2856,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2912,7 +2912,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2986,7 +2986,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3064,7 +3064,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -3169,7 +3169,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3372,7 +3372,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - mkdir -p bin @@ -3504,7 +3504,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - mkdir -p bin @@ -3994,7 +3994,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.6 + ALPINE_BASE: alpine:3.21.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4069,7 +4069,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.6 + ALPINE_BASE: alpine:3.21.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4186,7 +4186,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.6 + ALPINE_BASE: alpine:3.21.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4288,7 +4288,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -4342,7 +4342,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4423,7 +4423,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.6 + ALPINE_BASE: alpine:3.21.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4567,7 +4567,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.6 + ALPINE_BASE: alpine:3.21.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4695,7 +4695,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.20.6 + ALPINE_BASE: alpine:3.21.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4838,7 +4838,7 @@ steps: name: grabpl - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.20.6 + image: alpine:3.21.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -5262,7 +5262,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:22-bookworm - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.20.6 + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.21.3 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM ubuntu:22.04 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM byrnedo/alpine-curl:0.1.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack @@ -5300,7 +5300,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL node:22-bookworm - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.20.6 + - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.21.3 - trivy --exit-code 1 --severity HIGH,CRITICAL ubuntu:22.04 - trivy --exit-code 1 --severity HIGH,CRITICAL byrnedo/alpine-curl:0.1.8 - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack @@ -5564,6 +5564,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 558d477c002eb799c23f6631aafc7df933518e445e59f34ceb989e73f4dc60bc +hmac: f16a4715c7a4e6a4ffb1fe041b42fb966310fd5da455239614e9a239493aff82 ... diff --git a/Dockerfile b/Dockerfile index 9914a48dce0..09cba46da26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # to maintain formatting of multiline commands in vscode, add the following to settings.json: # "docker.languageserver.formatter.ignoreMultilineInstructions": true -ARG BASE_IMAGE=alpine:3.20 +ARG BASE_IMAGE=alpine:3.21 ARG JS_IMAGE=node:22-alpine ARG JS_PLATFORM=linux/amd64 ARG GO_IMAGE=golang:1.23.5-alpine diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index af160dacd5a..d51f9905df0 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -16,7 +16,7 @@ images = { "node_deb": "node:{}-bookworm".format(nodejs_version[:2]), "cloudsdk": "google/cloud-sdk:431.0.0", "publish": "grafana/grafana-ci-deploy:1.3.3", - "alpine": "alpine:3.20.6", + "alpine": "alpine:3.21.3", "ubuntu": "ubuntu:22.04", "curl": "byrnedo/alpine-curl:0.1.8", "plugins_slack": "plugins/slack", From fa74d1c36dfd504f84ff61c7a2c90c65d8dd6140 Mon Sep 17 00:00:00 2001 From: Karl Persson <23356117+kalleep@users.noreply.github.com> Date: Wed, 26 Feb 2025 09:22:09 +0100 Subject: [PATCH 18/51] Authn: Sync authlib and update how we construct authn client interceptor (#101124) * Sync authlib and update how we construct authn client interceptor * Remove namespace from checker --- go.mod | 4 +- go.sum | 8 ++-- pkg/apimachinery/go.mod | 4 +- pkg/apimachinery/go.sum | 8 ++-- pkg/apiserver/go.mod | 2 +- pkg/apiserver/go.sum | 4 +- pkg/registry/apis/iam/common/common.go | 6 +-- pkg/services/accesscontrol/authorizer.go | 2 +- pkg/services/apiserver/options/storage.go | 21 ++++----- .../authn/grpcutils/inproc_exchanger.go | 28 ++++-------- pkg/services/authz/token_auth.go | 4 +- pkg/services/authz/zanzana/client/client.go | 4 +- pkg/storage/unified/apistore/go.mod | 4 +- pkg/storage/unified/apistore/go.sum | 8 ++-- pkg/storage/unified/client.go | 25 +++++------ pkg/storage/unified/resource/access.go | 2 +- pkg/storage/unified/resource/access_test.go | 5 ++- pkg/storage/unified/resource/batch.go | 2 +- pkg/storage/unified/resource/client.go | 45 +++++++++++-------- pkg/storage/unified/resource/go.mod | 4 +- pkg/storage/unified/resource/go.sum | 8 ++-- pkg/storage/unified/resource/server.go | 4 +- pkg/storage/unified/search/bleve.go | 2 +- pkg/storage/unified/search/bleve_test.go | 2 +- .../unified/sql/test/integration_test.go | 6 ++- 25 files changed, 105 insertions(+), 107 deletions(-) diff --git a/go.mod b/go.mod index ae2caa2aaef..8b91c5cb880 100644 --- a/go.mod +++ b/go.mod @@ -72,8 +72,8 @@ require ( github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team - github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 // @grafana/identity-access-team + github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 // @grafana/identity-access-team github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // @grafana/grafana-backend-group diff --git a/go.sum b/go.sum index edbbd7eb159..7604fb6d2bb 100644 --- a/go.sum +++ b/go.sum @@ -1513,10 +1513,10 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 h1:iQ0h/h+QoguSZDF+ZpPxcM/C+m1kjh+aXjMpxywowPA= github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= -github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= -github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= +github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 h1:FTuDRy/Shw8yOdG+v1DnkeuaCAl8fvwgcfaG9Wccuhg= +github.com/grafana/authlib v0.0.0-20250225105729-99e678595501/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 36b8d4b827e..033f48738ec 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -3,8 +3,8 @@ module github.com/grafana/grafana/pkg/apimachinery go 1.23.1 require ( - github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team - github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 // @grafana/identity-access-team + github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 // @grafana/identity-access-team github.com/stretchr/testify v1.10.0 k8s.io/apimachinery v0.32.1 k8s.io/apiserver v0.32.1 diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index 5b3d03a464a..bd82bcbb03f 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -32,10 +32,10 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.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/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= -github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= +github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 h1:FTuDRy/Shw8yOdG+v1DnkeuaCAl8fvwgcfaG9Wccuhg= +github.com/grafana/authlib v0.0.0-20250225105729-99e678595501/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 83a53b3151a..0c45aa74e58 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.6 require ( github.com/google/go-cmp v0.6.0 - github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 + github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/grafana-app-sdk/logging v0.30.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 github.com/prometheus/client_golang v1.20.5 diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index fddf55e1b93..b3d15d3aee2 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -79,8 +79,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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/grafana-app-sdk/logging v0.30.0 h1:K/P/bm7Cp7Di4tqIJ3EQz2+842JozQGRaz62r95ApME= github.com/grafana/grafana-app-sdk/logging v0.30.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 h1:ItDcDxUjVLPKja+hogpqgW/kj8LxUL2qscelXIsN1Bs= diff --git a/pkg/registry/apis/iam/common/common.go b/pkg/registry/apis/iam/common/common.go index a0896d0fb25..a93c90a7d36 100644 --- a/pkg/registry/apis/iam/common/common.go +++ b/pkg/registry/apis/iam/common/common.go @@ -61,7 +61,7 @@ func List[T Resource]( return nil, err } - check := func(_, _, _ string) bool { return true } + check := func(_, _ string) bool { return true } if ac != nil { var err error check, err = ac.Compile(ctx, ident, authlib.ListRequest{ @@ -82,7 +82,7 @@ func List[T Resource]( } for _, item := range first.Items { - if !check(ns.Value, item.AuthID(), "") { + if !check(item.AuthID(), "") { continue } res.Items = append(res.Items, item) @@ -105,7 +105,7 @@ outer: break outer } - if !check(ns.Value, item.AuthID(), "") { + if !check(item.AuthID(), "") { continue } diff --git a/pkg/services/accesscontrol/authorizer.go b/pkg/services/accesscontrol/authorizer.go index fb0bbf93125..f4c8d27ddce 100644 --- a/pkg/services/accesscontrol/authorizer.go +++ b/pkg/services/accesscontrol/authorizer.go @@ -158,7 +158,7 @@ func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, re } check := Checker(ident, action) - return func(_, name, _ string) bool { + return func(name, _ string) bool { return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name)) }, nil } diff --git a/pkg/services/apiserver/options/storage.go b/pkg/services/apiserver/options/storage.go index 4d2b7dd1b3f..b24d3780b29 100644 --- a/pkg/services/apiserver/options/storage.go +++ b/pkg/services/apiserver/options/storage.go @@ -11,7 +11,6 @@ import ( genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/options" - "github.com/grafana/authlib/authn" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" @@ -123,17 +122,15 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi if err != nil { return err } - authCfg := authn.GrpcClientConfig{ - TokenClientConfig: &authn.TokenExchangeConfig{ - Token: o.GrpcClientAuthenticationToken, - TokenExchangeURL: o.GrpcClientAuthenticationTokenExchangeURL, - }, - TokenRequest: &authn.TokenExchangeRequest{ - Audiences: []string{"resourceStore"}, - Namespace: o.GrpcClientAuthenticationTokenNamespace, - }, - } - unified, err := resource.NewRemoteResourceClient(tracer, conn, authCfg, o.GrpcClientAuthenticationAllowInsecure) + + const resourceStoreAudience = "resourceStore" + + unified, err := resource.NewRemoteResourceClient(tracer, conn, resource.RemoteResourceClientConfig{ + Token: o.GrpcClientAuthenticationToken, + TokenExchangeURL: o.GrpcClientAuthenticationTokenExchangeURL, + Namespace: o.GrpcClientAuthenticationTokenNamespace, + Audiences: []string{resourceStoreAudience}, + }) if err != nil { return err } diff --git a/pkg/services/authn/grpcutils/inproc_exchanger.go b/pkg/services/authn/grpcutils/inproc_exchanger.go index af252ca2fb1..6a219abc1e8 100644 --- a/pkg/services/authn/grpcutils/inproc_exchanger.go +++ b/pkg/services/authn/grpcutils/inproc_exchanger.go @@ -1,10 +1,8 @@ package grpcutils import ( - "context" "encoding/base64" "encoding/json" - "fmt" "github.com/go-jose/go-jose/v3/jwt" "github.com/grafana/authlib/authn" @@ -12,29 +10,21 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" ) -type inProcExchanger struct { - tokenResponse *authn.TokenExchangeResponse -} - -func ProvideInProcExchanger() *inProcExchanger { - tokenResponse, err := createInProcToken() +func ProvideInProcExchanger() authn.StaticTokenExchanger { + token, err := createInProcToken() if err != nil { panic(err) } - return &inProcExchanger{tokenResponse} + return authn.NewStaticTokenExchanger(token) } -func (e *inProcExchanger) Exchange(ctx context.Context, r authn.TokenExchangeRequest) (*authn.TokenExchangeResponse, error) { - return e.tokenResponse, nil -} - -func createInProcToken() (*authn.TokenExchangeResponse, error) { +func createInProcToken() (string, error) { claims := authn.Claims[authn.AccessTokenClaims]{ Claims: jwt.Claims{ - Audience: []string{"resourceStore"}, Issuer: "grafana", Subject: types.NewTypeID(types.TypeAccessPolicy, "grafana"), + Audience: []string{"resourceStore"}, }, Rest: authn.AccessTokenClaims{ Namespace: "*", @@ -48,15 +38,13 @@ func createInProcToken() (*authn.TokenExchangeResponse, error) { "typ": authn.TokenTypeAccess, }) if err != nil { - return nil, err + return "", err } payload, err := json.Marshal(claims) if err != nil { - return nil, err + return "", err } - return &authn.TokenExchangeResponse{ - Token: fmt.Sprintf("%s.%s.", base64.RawURLEncoding.EncodeToString(header), base64.RawURLEncoding.EncodeToString(payload)), - }, nil + return base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(payload) + ".", nil } diff --git a/pkg/services/authz/token_auth.go b/pkg/services/authz/token_auth.go index 2f6549a4d8b..1546379741e 100644 --- a/pkg/services/authz/token_auth.go +++ b/pkg/services/authz/token_auth.go @@ -25,7 +25,9 @@ func (t *tokenAuth) GetRequestMetadata(ctx context.Context, _ ...string) (map[st return nil, err } - return map[string]string{authn.DefaultAccessTokenMetadataKey: token.Token}, nil + const metadataKey = "X-Access-Token" + + return map[string]string{metadataKey: token.Token}, nil } func (t *tokenAuth) RequireTransportSecurity() bool { return false } diff --git a/pkg/services/authz/zanzana/client/client.go b/pkg/services/authz/zanzana/client/client.go index d430be493a9..6860f6831be 100644 --- a/pkg/services/authz/zanzana/client/client.go +++ b/pkg/services/authz/zanzana/client/client.go @@ -79,7 +79,7 @@ func (c *Client) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.L func newItemChecker(res *authzv1.ListResponse) authlib.ItemChecker { // if we can see all resource of this type we can just return a function that always return true if res.GetAll() { - return func(_, _, _ string) bool { return true } + return func(_, _ string) bool { return true } } folders := make(map[string]struct{}, len(res.Folders)) @@ -92,7 +92,7 @@ func newItemChecker(res *authzv1.ListResponse) authlib.ItemChecker { items[i] = struct{}{} } - return func(_, name, folder string) bool { + return func(name, folder string) bool { if _, ok := items[name]; ok { return true } diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 16a36800ba0..748b5e929ff 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -14,7 +14,7 @@ exclude k8s.io/client-go v12.0.0+incompatible require ( github.com/bwmarrin/snowflake v0.3.0 github.com/google/uuid v1.6.0 - github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 + github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 @@ -193,7 +193,7 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 // indirect - github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // indirect + github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 27c6a2641e9..35a9215a782 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -568,10 +568,10 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 h1:iQ0h/h+QoguSZDF+ZpPxcM/C+m1kjh+aXjMpxywowPA= github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= -github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= -github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= +github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 h1:FTuDRy/Shw8yOdG+v1DnkeuaCAl8fvwgcfaG9Wccuhg= +github.com/grafana/authlib v0.0.0-20250225105729-99e678595501/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA= github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= diff --git a/pkg/storage/unified/client.go b/pkg/storage/unified/client.go index 396ce141c06..a0ddcce31a0 100644 --- a/pkg/storage/unified/client.go +++ b/pkg/storage/unified/client.go @@ -15,7 +15,6 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - authnlib "github.com/grafana/authlib/authn" "github.com/grafana/authlib/types" "github.com/grafana/dskit/flagext" "github.com/grafana/dskit/grpcclient" @@ -143,24 +142,20 @@ func newClient(opts options.StorageOptions, } } -func clientCfgMapping(clientCfg *grpcutils.GrpcClientConfig) authnlib.GrpcClientConfig { - return authnlib.GrpcClientConfig{ - TokenClientConfig: &authnlib.TokenExchangeConfig{ - Token: clientCfg.Token, - TokenExchangeURL: clientCfg.TokenExchangeURL, - }, - TokenRequest: &authnlib.TokenExchangeRequest{ - Namespace: clientCfg.TokenNamespace, - Audiences: []string{resourceStoreAudience}, - }, - } -} - func newResourceClient(conn *grpc.ClientConn, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) (resource.ResourceClient, error) { if !features.IsEnabledGlobally(featuremgmt.FlagAppPlatformGrpcClientAuth) { return resource.NewLegacyResourceClient(conn), nil } - return resource.NewRemoteResourceClient(tracer, conn, clientCfgMapping(grpcutils.ReadGrpcClientConfig(cfg)), cfg.Env == setting.Dev) + + clientCfg := grpcutils.ReadGrpcClientConfig(cfg) + + return resource.NewRemoteResourceClient(tracer, conn, resource.RemoteResourceClientConfig{ + Token: clientCfg.Token, + TokenExchangeURL: clientCfg.TokenExchangeURL, + Audiences: []string{resourceStoreAudience}, + Namespace: clientCfg.TokenNamespace, + AllowInsecure: cfg.Env == setting.Dev, + }) } // GrpcConn creates a new gRPC connection to the provided address. diff --git a/pkg/storage/unified/resource/access.go b/pkg/storage/unified/resource/access.go index a47ce0e6d1e..16dc0d9d170 100644 --- a/pkg/storage/unified/resource/access.go +++ b/pkg/storage/unified/resource/access.go @@ -153,7 +153,7 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req )) defer span.End() if fallbackUsed || !c.IsCompatibleWithRBAC(req.Group, req.Resource) { - return func(namespace string, name, folder string) bool { + return func(name, folder string) bool { return true }, nil } diff --git a/pkg/storage/unified/resource/access_test.go b/pkg/storage/unified/resource/access_test.go index 47e17987076..a056099ec36 100644 --- a/pkg/storage/unified/resource/access_test.go +++ b/pkg/storage/unified/resource/access_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" authlib "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/utils" ) func TestAuthzLimitedClient_Check(t *testing.T) { @@ -27,6 +28,7 @@ func TestAuthzLimitedClient_Check(t *testing.T) { req := authlib.CheckRequest{ Group: test.group, Resource: test.resource, + Verb: utils.VerbGet, } resp, err := client.Check(context.Background(), nil, req) assert.NoError(t, err) @@ -52,12 +54,13 @@ func TestAuthzLimitedClient_Compile(t *testing.T) { req := authlib.ListRequest{ Group: test.group, Resource: test.resource, + Verb: utils.VerbGet, } checker, err := client.Compile(context.Background(), nil, req) assert.NoError(t, err) assert.NotNil(t, checker) - result := checker("namespace", "name", "folder") + result := checker("name", "folder") assert.Equal(t, test.expected, result) } } diff --git a/pkg/storage/unified/resource/batch.go b/pkg/storage/unified/resource/batch.go index 4997706a5e1..6f4f449da1d 100644 --- a/pkg/storage/unified/resource/batch.go +++ b/pkg/storage/unified/resource/batch.go @@ -270,7 +270,7 @@ func (b *batchRunner) Next() bool { if !ok { b.err = fmt.Errorf("missing access control for: %s", k) b.rollback = true - } else if !checker(key.Namespace, key.Name, b.request.Folder) { + } else if !checker(key.Name, b.request.Folder) { b.err = fmt.Errorf("not allowed to create resource") b.rollback = true } diff --git a/pkg/storage/unified/resource/client.go b/pkg/storage/unified/resource/client.go index 0fae55ec6b8..e963655b019 100644 --- a/pkg/storage/unified/resource/client.go +++ b/pkg/storage/unified/resource/client.go @@ -75,10 +75,9 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient { ) } - clientInt, _ := authnlib.NewGrpcClientInterceptor( - &authnlib.GrpcClientConfig{TokenRequest: &authnlib.TokenExchangeRequest{}}, - authnlib.WithTokenClientOption(grpcutils.ProvideInProcExchanger()), - authnlib.WithIDTokenExtractorOption(idTokenExtractor), + clientInt := authnlib.NewGrpcClientInterceptor( + grpcutils.ProvideInProcExchanger(), + authnlib.WithClientInterceptorIDTokenExtractor(idTokenExtractor), ) cc := grpchan.InterceptClientConn(channel, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor) @@ -92,20 +91,36 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient { } } -func NewRemoteResourceClient(tracer tracing.Tracer, conn *grpc.ClientConn, cfg authnlib.GrpcClientConfig, allowInsecure bool) (ResourceClient, error) { - opts := []authnlib.GrpcClientInterceptorOption{ - authnlib.WithIDTokenExtractorOption(idTokenExtractor), - authnlib.WithTracerOption(tracer), +type RemoteResourceClientConfig struct { + Token string + TokenExchangeURL string + Audiences []string + Namespace string + AllowInsecure bool +} + +func NewRemoteResourceClient(tracer tracing.Tracer, conn *grpc.ClientConn, cfg RemoteResourceClientConfig) (ResourceClient, error) { + exchangeOpts := []authnlib.ExchangeClientOpts{} + + if cfg.AllowInsecure { + exchangeOpts = append(exchangeOpts, authnlib.WithHTTPClient(&http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}})) } - if allowInsecure { - opts = allowInsecureTransportOpt(&cfg, opts) - } + tc, err := authnlib.NewTokenExchangeClient(authnlib.TokenExchangeConfig{ + Token: cfg.Token, + TokenExchangeURL: cfg.TokenExchangeURL, + }, exchangeOpts...) - clientInt, err := authnlib.NewGrpcClientInterceptor(&cfg, opts...) if err != nil { return nil, err } + clientInt := authnlib.NewGrpcClientInterceptor( + tc, + authnlib.WithClientInterceptorTracer(tracer), + authnlib.WithClientInterceptorNamespace(cfg.Namespace), + authnlib.WithClientInterceptorAudience(cfg.Audiences), + authnlib.WithClientInterceptorIDTokenExtractor(idTokenExtractor), + ) cc := grpchan.InterceptClientConn(conn, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor) return &resourceClient{ @@ -144,9 +159,3 @@ func idTokenExtractor(ctx context.Context) (string, error) { return "", nil } - -func allowInsecureTransportOpt(grpcClientConfig *authnlib.GrpcClientConfig, opts []authnlib.GrpcClientInterceptorOption) []authnlib.GrpcClientInterceptorOption { - client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} - tokenClient, _ := authnlib.NewTokenExchangeClient(*grpcClientConfig.TokenClientConfig, authnlib.WithHTTPClient(client)) - return append(opts, authnlib.WithTokenClientOption(tokenClient)) -} diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 76a89ac0d4f..42bb07c9e45 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -11,8 +11,8 @@ replace ( require ( github.com/fullstorydev/grpchan v1.1.1 github.com/google/uuid v1.6.0 - github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 - github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 + github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 + github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible github.com/grafana/grafana-plugin-sdk-go v0.266.0 diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index a912c869d28..2bc9e773911 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -399,10 +399,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642 h1:iQ0h/h+QoguSZDF+ZpPxcM/C+m1kjh+aXjMpxywowPA= github.com/grafana/alerting v0.0.0-20250224133628-2acbeef29642/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM= -github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4= -github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA= -github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= +github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 h1:FTuDRy/Shw8yOdG+v1DnkeuaCAl8fvwgcfaG9Wccuhg= +github.com/grafana/authlib v0.0.0-20250225105729-99e678595501/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8= +github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s= github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g= diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 148de06595a..3422aa1514d 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -752,7 +752,7 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err Value: iter.Value(), } - if !checker(iter.Namespace(), iter.Name(), iter.Folder()) { + if !checker(iter.Name(), iter.Folder()) { continue } @@ -1035,7 +1035,7 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { } s.log.Debug("Server Broadcasting", "type", event.Type, "rv", event.ResourceVersion, "previousRV", event.PreviousRV, "group", event.Key.Group, "namespace", event.Key.Namespace, "resource", event.Key.Resource, "name", event.Key.Name) if event.ResourceVersion > since && matchesQueryKey(req.Options.Key, event.Key) { - if !checker(event.Key.Namespace, event.Key.Name, event.Folder) { + if !checker(event.Key.Name, event.Folder) { continue } diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index 3e5264d4d91..d67b145da4d 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -965,7 +965,7 @@ func (q *permissionScopedQuery) Searcher(ctx context.Context, i index.IndexReade q.log.Debug("No resource checker found", "resource", resource) return false } - allowed := q.checkers[resource](ns, name, folder) + allowed := q.checkers[resource](name, folder) if !allowed { q.log.Debug("Denying access", "ns", ns, "name", name, "folder", folder) } diff --git a/pkg/storage/unified/search/bleve_test.go b/pkg/storage/unified/search/bleve_test.go index 66acc8d9a25..b2d90f25c0f 100644 --- a/pkg/storage/unified/search/bleve_test.go +++ b/pkg/storage/unified/search/bleve_test.go @@ -582,7 +582,7 @@ func (nc *StubAccessClient) Check(ctx context.Context, id authlib.AuthInfo, req } func (nc *StubAccessClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, error) { - return func(namespace string, name, folder string) bool { + return func(name, folder string) bool { return nc.resourceResponses[req.Resource] }, nil } diff --git a/pkg/storage/unified/sql/test/integration_test.go b/pkg/storage/unified/sql/test/integration_test.go index 07c9bbb6de7..62224e10a1e 100644 --- a/pkg/storage/unified/sql/test/integration_test.go +++ b/pkg/storage/unified/sql/test/integration_test.go @@ -99,7 +99,11 @@ func TestClientServer(t *testing.T) { t.Run("Create a client", func(t *testing.T) { conn, err := unified.GrpcConn(svc.GetAddress(), prometheus.NewPedanticRegistry()) require.NoError(t, err) - client, err = resource.NewRemoteResourceClient(tracing.NewNoopTracerService(), conn, authn.GrpcClientConfig{}, true) + client, err = resource.NewRemoteResourceClient(tracing.NewNoopTracerService(), conn, resource.RemoteResourceClientConfig{ + Token: "some-token", + TokenExchangeURL: "http://some-change-url", + AllowInsecure: true, + }) require.NoError(t, err) }) From 03de7cbbf155bde7f205d34e2ed4ed50ed45267a Mon Sep 17 00:00:00 2001 From: Bruno Abrantes Date: Wed, 26 Feb 2025 10:00:07 +0100 Subject: [PATCH 19/51] Moves remaining labels from old backend platform project to new projects (#100435) --- .github/commands.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/commands.json b/.github/commands.json index fb10220fb21..f2896d61551 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -640,7 +640,7 @@ "name": "area/configuration", "action": "addToProject", "addToProject": { - "url": "https://github.com/orgs/grafana/projects/96" + "url": "https://github.com/orgs/grafana/projects/665" } }, { @@ -744,7 +744,7 @@ "name": "area/backend/db/postgres", "action": "addToProject", "addToProject": { - "url": "https://github.com/orgs/grafana/projects/96" + "url": "https://github.com/orgs/grafana/projects/835" } }, { From 9406830a948861d6599413db3ed8d84dfa7565a8 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 26 Feb 2025 11:07:06 +0100 Subject: [PATCH 20/51] Alerting: Fix passing time range to query components (#101041) Add range prop to QueryEditorRow, pass range from QueryWrapper --- .../unified/components/rule-editor/QueryWrapper.tsx | 5 ++++- .../features/query/components/QueryEditorRow.test.tsx | 1 + .../app/features/query/components/QueryEditorRow.tsx | 11 +++++------ .../app/features/query/components/QueryEditorRows.tsx | 2 ++ 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx index 0989fada424..2d790354a68 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx @@ -13,6 +13,8 @@ import { PanelData, RelativeTimeRange, ThresholdsConfig, + getDefaultRelativeTimeRange, + rangeUtil, } from '@grafana/data'; import { config } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; @@ -187,12 +189,12 @@ export const QueryWrapper = ({ // ⚠️ the query editors want the entire array of queries passed as "DataQuery" NOT "AlertQuery" // TypeScript isn't complaining here because the interfaces just happen to be compatible const editorQueries = cloneDeep(queries.map((query) => query.model)); + const range = rangeUtil.relativeToTimeRange(query.relativeTimeRange ?? getDefaultRelativeTimeRange()); return (
- alerting hideRefId={!isAdvancedMode} hideActionButtons={!isAdvancedMode} collapsable={false} @@ -209,6 +211,7 @@ export const QueryWrapper = ({ onAddQuery={() => onDuplicateQuery(cloneDeep(query))} onRunQuery={onRunQueries} queries={editorQueries} + range={range} renderHeaderExtras={() => ( )} diff --git a/public/app/features/query/components/QueryEditorRow.test.tsx b/public/app/features/query/components/QueryEditorRow.test.tsx index ae1bf9acc33..350d98bf11d 100644 --- a/public/app/features/query/components/QueryEditorRow.test.tsx +++ b/public/app/features/query/components/QueryEditorRow.test.tsx @@ -354,6 +354,7 @@ describe('QueryEditorRow', () => { onChange: jest.fn(), onRemoveQuery: jest.fn(), index: 0, + range: { from: dateTime(), to: dateTime(), raw: { from: 'now-1d', to: 'now' } }, }); it('should display error message in corresponding panel', async () => { const data = { diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 36a3d5818ee..46093ff35ba 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -63,9 +63,9 @@ export interface Props { visualization?: ReactNode; hideHideQueryButton?: boolean; app?: CoreApp; + range: TimeRange; history?: Array>; eventBus?: EventBusExtended; - alerting?: boolean; hideActionButtons?: boolean; onQueryCopied?: () => void; onQueryRemoved?: () => void; @@ -273,7 +273,7 @@ export class QueryEditorRow extends PureComponent { - const { query, onChange, queries, onRunQuery, onAddQuery, app = CoreApp.PanelEditor, history } = this.props; + const { query, onChange, queries, onRunQuery, onAddQuery, range, app = CoreApp.PanelEditor, history } = this.props; const { datasource, data } = this.state; if (this.isWaitingForDatasourceToLoad()) { @@ -298,7 +298,7 @@ export class QueryEditorRow extends PureComponent extends PureComponent { - const { alerting, query, dataSource, onChangeDataSource, onChange, queries, renderHeaderExtras, hideRefId } = - this.props; + const { app, query, dataSource, onChangeDataSource, onChange, queries, renderHeaderExtras, hideRefId } = this.props; return ( extends PureComponent ); diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 187ea5df7a5..3d87a646ab6 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -11,6 +11,7 @@ import { getDataSourceRef, } from '@grafana/data'; import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { QueryEditorRow } from './QueryEditorRow'; @@ -175,6 +176,7 @@ export class QueryEditorRows extends PureComponent { onQueryToggled={onQueryToggled} queries={queries} app={app} + range={getTimeSrv().timeRange()} history={history} eventBus={eventBus} /> From 6aa353cde61220902b4b861014fd622f998a292f Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 26 Feb 2025 11:11:49 +0100 Subject: [PATCH 21/51] Alerting: Fix notification templates layout (#101232) --- .../features/alerting/unified/Templates.tsx | 21 +- .../DuplicateMessageTemplate.tsx | 39 ++- .../contact-points/EditMessageTemplate.tsx | 23 +- .../contact-points/NewMessageTemplate.tsx | 32 +- .../components/receivers/TemplateForm.tsx | 297 +++++++++--------- public/locales/en-US/grafana.json | 17 + public/locales/pseudo-LOCALE/grafana.json | 17 + 7 files changed, 264 insertions(+), 182 deletions(-) diff --git a/public/app/features/alerting/unified/Templates.tsx b/public/app/features/alerting/unified/Templates.tsx index f3b8b011f36..74b23cfcc1f 100644 --- a/public/app/features/alerting/unified/Templates.tsx +++ b/public/app/features/alerting/unified/Templates.tsx @@ -1,6 +1,5 @@ import { Route, Routes } from 'react-router-dom-v5-compat'; -import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper'; import DuplicateMessageTemplate from './components/contact-points/DuplicateMessageTemplate'; import EditMessageTemplate from './components/contact-points/EditMessageTemplate'; import NewMessageTemplate from './components/contact-points/NewMessageTemplate'; @@ -8,21 +7,11 @@ import { withPageErrorBoundary } from './withPageErrorBoundary'; function NotificationTemplates() { return ( - - - } /> - } /> - } /> - - + + } /> + } /> + } /> + ); } diff --git a/public/app/features/alerting/unified/components/contact-points/DuplicateMessageTemplate.tsx b/public/app/features/alerting/unified/components/contact-points/DuplicateMessageTemplate.tsx index bc89effea98..f94bdb31fe5 100644 --- a/public/app/features/alerting/unified/components/contact-points/DuplicateMessageTemplate.tsx +++ b/public/app/features/alerting/unified/components/contact-points/DuplicateMessageTemplate.tsx @@ -2,16 +2,19 @@ import { useParams } from 'react-router-dom-v5-compat'; import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; +import { t } from 'app/core/internationalization'; import { isNotFoundError } from '../../api/util'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { generateCopiedName } from '../../utils/duplicate'; import { stringifyErrorLike } from '../../utils/misc'; import { updateDefinesWithUniqueValue } from '../../utils/templates'; +import { createRelativeUrl } from '../../utils/url'; import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { TemplateForm } from '../receivers/TemplateForm'; +import { ActiveTab } from './ContactPoints'; import { useGetNotificationTemplate, useNotificationTemplates } from './useNotificationTemplates'; const notFoundComponent = ; @@ -23,16 +26,19 @@ const DuplicateMessageTemplateComponent = () => { const { currentData: template, - isLoading, - error, + isLoading: isLoadingTemplate, + error: templateFetchError, } = useGetNotificationTemplate({ alertmanager: selectedAlertmanager ?? '', uid: templateUid ?? '' }); const { currentData: templates, isLoading: templatesLoading, - error: templatesError, + error: templatesFetchError, } = useNotificationTemplates({ alertmanager: selectedAlertmanager ?? '' }); + const isLoading = isLoadingTemplate || templatesLoading; + const error = templateFetchError || templatesFetchError; + if (!selectedAlertmanager) { return ; } @@ -41,11 +47,11 @@ const DuplicateMessageTemplateComponent = () => { return ; } - if (isLoading || templatesLoading) { + if (isLoading) { return ; } - if (error || templatesError || !template || !templates) { + if (error) { return isNotFoundError(error) ? ( notFoundComponent ) : ( @@ -55,6 +61,10 @@ const DuplicateMessageTemplateComponent = () => { ); } + if (!template) { + return notFoundComponent; + } + const duplicatedName = generateCopiedName(template.title, templates?.map((t) => t.title) ?? []); return ( @@ -67,7 +77,24 @@ const DuplicateMessageTemplateComponent = () => { function DuplicateMessageTemplate() { return ( - + ); diff --git a/public/app/features/alerting/unified/components/contact-points/EditMessageTemplate.tsx b/public/app/features/alerting/unified/components/contact-points/EditMessageTemplate.tsx index 926e1c5404b..d762d727c66 100644 --- a/public/app/features/alerting/unified/components/contact-points/EditMessageTemplate.tsx +++ b/public/app/features/alerting/unified/components/contact-points/EditMessageTemplate.tsx @@ -2,14 +2,17 @@ import { useParams } from 'react-router-dom-v5-compat'; import { Alert, LoadingPlaceholder } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; +import { t } from 'app/core/internationalization'; import { isNotFoundError } from '../../api/util'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { stringifyErrorLike } from '../../utils/misc'; +import { createRelativeUrl } from '../../utils/url'; import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { TemplateForm } from '../receivers/TemplateForm'; +import { ActiveTab } from './ContactPoints'; import { useGetNotificationTemplate } from './useNotificationTemplates'; const notFoundComponent = ; @@ -19,7 +22,7 @@ const EditMessageTemplateComponent = () => { const templateUid = name ? decodeURIComponent(name) : undefined; const { selectedAlertmanager } = useAlertmanager(); - const { currentData, isLoading, error } = useGetNotificationTemplate({ + const { currentData, isLoading, error, isUninitialized } = useGetNotificationTemplate({ alertmanager: selectedAlertmanager ?? '', uid: templateUid ?? '', }); @@ -28,7 +31,7 @@ const EditMessageTemplateComponent = () => { return ; } - if (isLoading) { + if (isLoading || isUninitialized) { return ; } @@ -51,7 +54,21 @@ const EditMessageTemplateComponent = () => { function EditMessageTemplate() { return ( - + ); diff --git a/public/app/features/alerting/unified/components/contact-points/NewMessageTemplate.tsx b/public/app/features/alerting/unified/components/contact-points/NewMessageTemplate.tsx index 7cdd2286be1..43f1246f05c 100644 --- a/public/app/features/alerting/unified/components/contact-points/NewMessageTemplate.tsx +++ b/public/app/features/alerting/unified/components/contact-points/NewMessageTemplate.tsx @@ -1,16 +1,38 @@ +import { t } from 'app/core/internationalization'; + import { useAlertmanager } from '../../state/AlertmanagerContext'; +import { createRelativeUrl } from '../../utils/url'; import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { TemplateForm } from '../receivers/TemplateForm'; -function NewMessageTemplate() { - const { selectedAlertmanager } = useAlertmanager(); +import { ActiveTab } from './ContactPoints'; +function NewMessageTemplatePage() { return ( - - + + ); } -export default withPageErrorBoundary(NewMessageTemplate); +function NewMessageTemplate() { + const { selectedAlertmanager } = useAlertmanager(); + return ; +} + +export default withPageErrorBoundary(NewMessageTemplatePage); diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 96ca2dedd46..29186ded954 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -25,17 +25,14 @@ import { useStyles2, } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { useCleanup } from 'app/core/hooks/useCleanup'; import { Trans, t } from 'app/core/internationalization'; import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints'; import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types'; -import { AppChromeUpdate } from '../../../../../core/components/AppChrome/AppChromeUpdate'; -import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { makeAMLink, stringifyErrorLike } from '../../utils/misc'; -import { initialAsyncRequestState } from '../../utils/redux'; import { ProvisionedResource, ProvisioningAlert } from '../Provisioning'; +import { Spacer } from '../Spacer'; import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader'; import { NotificationTemplate, @@ -95,15 +92,14 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) const appNotification = useAppNotification(); - const [createNewTemplate] = useCreateNotificationTemplate({ alertmanager }); - const [updateTemplate] = useUpdateNotificationTemplate({ alertmanager }); + const [createNewTemplate, { error: createTemplateError }] = useCreateNotificationTemplate({ alertmanager }); + const [updateTemplate, { error: updateTemplateError }] = useUpdateNotificationTemplate({ alertmanager }); const { titleIsUnique } = useValidateNotificationTemplate({ alertmanager, originalTemplate }); - useCleanup((state) => (state.unifiedAlerting.saveAMConfig = initialAsyncRequestState)); const formRef = useRef(null); const isGrafanaAlertManager = alertmanager === GRAFANA_RULES_SOURCE_NAME; - const { error } = useUnifiedAlertingSelector((state) => state.saveAMConfig); + const error = updateTemplateError ?? createTemplateError; const [cheatsheetOpened, toggleCheatsheetOpened] = useToggle(false); @@ -168,28 +164,9 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) setValue('content', newValue); }; - const actionButtons = ( - - - - Cancel - - - ); - return ( <> -
{/* error message */} {error && ( @@ -206,136 +183,155 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) {/* name field for the template */}
- - - + + {/* name and save buttons */} + + + + + + + + + Cancel + + + - {/* editor layout */} -
-
- {/* template content and payload editor column – full height and half-width */} -
- {/* template editor */} -
- {/* primaryProps will set "minHeight: min-content;" so we have to make sure to apply minHeight to the child */} -
-
- - {/* examples dropdown – only available for Grafana Alertmanager */} - {isGrafanaAlertManager && ( - - {GlobalTemplateDataExamples.map((item, index) => ( + {/* editor layout */} +
+
+ {/* template content and payload editor column – full height and half-width */} +
+ {/* template editor */} +
+ {/* primaryProps will set "minHeight: min-content;" so we have to make sure to apply minHeight to the child */} +
+
+ + {/* examples dropdown – only available for Grafana Alertmanager */} + {isGrafanaAlertManager && ( + + {GlobalTemplateDataExamples.map((item, index) => ( + appendExample(item.example)} + /> + ))} + appendExample(item.example)} + label={'Examples documentation'} + url="https://grafana.com/docs/grafana/latest/alerting/configure-notifications/template-notifications/examples/" + target="_blank" + icon="external-link-alt" /> - ))} - - - - } + + } + > + + + )} + - - )} - - - } - /> -
- - - {({ width, height }) => ( - setValue('content', value)} - containerStyles={styles.editorContainer} - width={width} - height={height} - /> - )} - - -
-
- {/* payload editor – only available for Grafana Alertmanager */} - {isGrafanaAlertManager && ( - <> -
-
-
- Help + + + } />
+ + + {({ width, height }) => ( + setValue('content', value)} + containerStyles={styles.editorContainer} + width={width} + height={height} + /> + )} + +
- - )} -
-
- {/* preview column – full height and half-width */} - {isGrafanaAlertManager && ( - <> -
-
- +
+ {/* payload editor – only available for Grafana Alertmanager */} + {isGrafanaAlertManager && ( + <> +
+
+
+ +
+
+ + )}
- - )} -
+
+ {/* preview column – full height and half-width */} + {isGrafanaAlertManager && ( + <> +
+
+ +
+ + )} +
+
@@ -439,9 +435,6 @@ export const getStyles = (theme: GrafanaTheme2) => { label: css({ margin: 0, }), - nameField: css({ - marginBottom: theme.spacing(1), - }), contentContainer: css({ flex: 1, display: 'flex', diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 8849f1b3a69..c2e79b5c104 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -295,6 +295,9 @@ "export-all": "Export all", "loading": "Loading...", "search-by-matchers": "Search by matchers", + "titles": { + "notification-templates": "Notification Templates" + }, "view": "View" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "Alert instance routing preview", "uninitialized": "When you have your folder selected and your query and labels are configured, click \"Preview routing\" to see the results here." }, + "notification-templates": { + "duplicate": { + "subTitle": "Duplicate a group of notification templates", + "title": "Duplicate notification template group" + }, + "edit": { + "subTitle": "Edit a group of notification templates", + "title": "Edit notification template group" + }, + "new": { + "subTitle": "Create a new group of notification templates", + "title": "New notification template group" + } + }, "policies": { "default-policy": { "description": "All alert instances will be handled by the default policy if no other matching policies are found.", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index fd11d47872d..41644d694cb 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -295,6 +295,9 @@ "export-all": "Ēχpőřŧ äľľ", "loading": "Ŀőäđįʼnģ...", "search-by-matchers": "Ŝęäřčĥ þy mäŧčĥęřş", + "titles": { + "notification-templates": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧęş" + }, "view": "Vįęŵ" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "Åľęřŧ įʼnşŧäʼnčę řőūŧįʼnģ přęvįęŵ", "uninitialized": "Ŵĥęʼn yőū ĥävę yőūř ƒőľđęř şęľęčŧęđ äʼnđ yőūř qūęřy äʼnđ ľäþęľş äřę čőʼnƒįģūřęđ, čľįčĸ \"Přęvįęŵ řőūŧįʼnģ\" ŧő şęę ŧĥę řęşūľŧş ĥęřę." }, + "notification-templates": { + "duplicate": { + "subTitle": "Đūpľįčäŧę ä ģřőūp őƒ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş", + "title": "Đūpľįčäŧę ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧę ģřőūp" + }, + "edit": { + "subTitle": "Ēđįŧ ä ģřőūp őƒ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş", + "title": "Ēđįŧ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧę ģřőūp" + }, + "new": { + "subTitle": "Cřęäŧę ä ʼnęŵ ģřőūp őƒ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş", + "title": "Ńęŵ ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧę ģřőūp" + } + }, "policies": { "default-policy": { "description": "Åľľ äľęřŧ įʼnşŧäʼnčęş ŵįľľ þę ĥäʼnđľęđ þy ŧĥę đęƒäūľŧ pőľįčy įƒ ʼnő őŧĥęř mäŧčĥįʼnģ pőľįčįęş äřę ƒőūʼnđ.", From 4e118bc6ad41cf9044621887dc7ff3229671ca77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Wed, 26 Feb 2025 11:38:24 +0100 Subject: [PATCH 22/51] Imports required for Grafana Enterprise are only included when building enterprise version. (#101341) --- .gitignore | 1 + pkg/extensions/enterprise_imports.go | 39 ++++++++++++++++++++++++++++ pkg/extensions/main.go | 35 +------------------------ 3 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 pkg/extensions/enterprise_imports.go diff --git a/.gitignore b/.gitignore index 193de6f539e..c043094a016 100644 --- a/.gitignore +++ b/.gitignore @@ -113,6 +113,7 @@ profile.cov /pkg/extensions/* !/pkg/extensions/.keep !/pkg/extensions/main.go +!/pkg/extensions/enterprise_imports.go /public/app/extensions !/public/app/extensions/.keep diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go new file mode 100644 index 00000000000..ea63f09b2a8 --- /dev/null +++ b/pkg/extensions/enterprise_imports.go @@ -0,0 +1,39 @@ +//go:build enterprise +// +build enterprise + +package extensions + +import ( + _ "cloud.google.com/go/kms/apiv1" + _ "cloud.google.com/go/kms/apiv1/kmspb" + _ "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + _ "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" + _ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + _ "github.com/Azure/go-autorest/autorest" + _ "github.com/Azure/go-autorest/autorest/adal" + _ "github.com/beevik/etree" + _ "github.com/blugelabs/bluge" + _ "github.com/blugelabs/bluge_segment_api" + _ "github.com/crewjam/saml" + _ "github.com/go-jose/go-jose/v3" + _ "github.com/gobwas/glob" + _ "github.com/googleapis/gax-go/v2" + _ "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" + _ "github.com/grpc-ecosystem/go-grpc-middleware/v2" + _ "github.com/hashicorp/go-multierror" + _ "github.com/hashicorp/golang-lru/v2" + _ "github.com/m3db/prometheus_remote_client_golang/promremote" + _ "github.com/phpdave11/gofpdi" + _ "github.com/robfig/cron/v3" + _ "github.com/russellhaering/goxmldsig" + _ "github.com/spf13/cobra" // used by the standalone apiserver cli + _ "github.com/stretchr/testify/require" + _ "golang.org/x/time/rate" + _ "xorm.io/builder" + + _ "github.com/grafana/dskit/backoff" + _ "github.com/grafana/dskit/flagext" + _ "github.com/grafana/e2e" + _ "github.com/grafana/gofpdf" + _ "github.com/grafana/gomemcache/memcache" +) diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index b4aace2cfad..4c86ed05ccd 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -1,38 +1,5 @@ package extensions -import ( - _ "cloud.google.com/go/kms/apiv1" - _ "cloud.google.com/go/kms/apiv1/kmspb" - _ "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - _ "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" - _ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" - _ "github.com/Azure/go-autorest/autorest" - _ "github.com/Azure/go-autorest/autorest/adal" - _ "github.com/beevik/etree" - _ "github.com/blugelabs/bluge" - _ "github.com/blugelabs/bluge_segment_api" - _ "github.com/crewjam/saml" - _ "github.com/go-jose/go-jose/v3" - _ "github.com/gobwas/glob" - _ "github.com/googleapis/gax-go/v2" - _ "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" - _ "github.com/grpc-ecosystem/go-grpc-middleware/v2" - _ "github.com/hashicorp/go-multierror" - _ "github.com/hashicorp/golang-lru/v2" - _ "github.com/m3db/prometheus_remote_client_golang/promremote" - _ "github.com/phpdave11/gofpdi" - _ "github.com/robfig/cron/v3" - _ "github.com/russellhaering/goxmldsig" - _ "github.com/spf13/cobra" // used by the standalone apiserver cli - _ "github.com/stretchr/testify/require" - _ "golang.org/x/time/rate" - _ "xorm.io/builder" - - _ "github.com/grafana/dskit/backoff" - _ "github.com/grafana/dskit/flagext" - _ "github.com/grafana/e2e" - _ "github.com/grafana/gofpdf" - _ "github.com/grafana/gomemcache/memcache" -) +// Imports used by Grafana enterprise are in enterprise_imports.go (behind a build tag). var IsEnterprise bool = false From af7fafd03ac5899c7177fd237a4584b32aea1dc0 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 26 Feb 2025 11:52:21 +0100 Subject: [PATCH 23/51] Alerting: Add rule group name to the rule title when converting Prometheus rules (#101310) Alerting: Add alert rule name to the title when converting Prometheus rules --- pkg/services/ngalert/prom/convert.go | 7 +++++++ pkg/services/ngalert/prom/convert_test.go | 12 ++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index e2f46b74341..31bc9db1187 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -203,6 +203,13 @@ func (p *Converter) convertRule(orgID int64, namespaceUID, group string, rule Pr title = rule.Alert } + // Temporary workaround for avoiding the uniqueness check for the rule title. + // In Grafana alert rule titles must be unique within the same org and folder, + // but Prometheus allows multiple rules with the same name. By adding the group name + // to the title we ensure that the title is unique within the group. + // TODO: Remove this workaround when we have a proper solution for handling rule title uniqueness. + title = fmt.Sprintf("[%s] %s", group, title) + labels := make(map[string]string, len(rule.Labels)+1) for k, v := range rule.Labels { labels[k] = v diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index 332cb356804..4fcf9f42e4d 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -132,12 +132,12 @@ func TestPrometheusRulesToGrafana(t *testing.T) { grafanaRule := grafanaGroup.Rules[j] if promRule.Record != "" { - require.Equal(t, promRule.Record, grafanaRule.Title) + require.Equal(t, fmt.Sprintf("[%s] %s", tc.promGroup.Name, promRule.Record), grafanaRule.Title) require.NotNil(t, grafanaRule.Record) require.Equal(t, grafanaRule.Record.From, queryRefID) require.Equal(t, promRule.Record, grafanaRule.Record.Metric) } else { - require.Equal(t, promRule.Alert, grafanaRule.Title) + require.Equal(t, fmt.Sprintf("[%s] %s", tc.promGroup.Name, promRule.Alert), grafanaRule.Title) } var expectedFor time.Duration @@ -205,10 +205,10 @@ func TestPrometheusRulesToGrafanaWithDuplicateRuleNames(t *testing.T) { require.Equal(t, "test-group-1", group.Title) require.Len(t, group.Rules, 4) - require.Equal(t, "alert", group.Rules[0].Title) - require.Equal(t, "alert (2)", group.Rules[1].Title) - require.Equal(t, "another alert", group.Rules[2].Title) - require.Equal(t, "alert (3)", group.Rules[3].Title) + require.Equal(t, "[test-group-1] alert", group.Rules[0].Title) + require.Equal(t, "[test-group-1] alert (2)", group.Rules[1].Title) + require.Equal(t, "[test-group-1] another alert", group.Rules[2].Title) + require.Equal(t, "[test-group-1] alert (3)", group.Rules[3].Title) } func TestCreateMathNode(t *testing.T) { From fe2beead15032b2ce9825f94cb2cd96c5165d62a Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 26 Feb 2025 12:23:54 +0100 Subject: [PATCH 24/51] Alerting: Fix alert rule loading states v2 (#100747) --- .betterer.results | 3 --- .../alerting/unified/RuleViewer.test.tsx | 16 ++++++++++++++ .../features/alerting/unified/RuleViewer.tsx | 22 ++++++++++++++----- .../alerting/unified/hooks/useCombinedRule.ts | 2 +- .../rule-editor/ExistingRuleEditor.tsx | 2 +- public/locales/en-US/grafana.json | 1 + public/locales/pseudo-LOCALE/grafana.json | 1 + 7 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 public/app/features/alerting/unified/RuleViewer.test.tsx diff --git a/.betterer.results b/.betterer.results index eedd45faab7..b56e99b02a0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1575,9 +1575,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] ], - "public/app/features/alerting/unified/RuleViewer.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], "public/app/features/alerting/unified/Settings.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], diff --git a/public/app/features/alerting/unified/RuleViewer.test.tsx b/public/app/features/alerting/unified/RuleViewer.test.tsx new file mode 100644 index 00000000000..c895c32a136 --- /dev/null +++ b/public/app/features/alerting/unified/RuleViewer.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from 'test/test-utils'; + +import RuleViewer from './RuleViewer'; +import { stringifyErrorLike } from './utils/misc'; + +describe('Rule Viewer page', () => { + it('should throw an error if rule ID cannot be decoded', () => { + // check console errors + jest.spyOn(console, 'error').mockImplementation((error) => { + expect(stringifyErrorLike(error)).toContain('Error: Rule ID is required'); + }); + + render(); + expect(screen.getByText(/Error: Rule ID is required/i)).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 9df84a41b18..9beaea7325a 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -5,6 +5,7 @@ import { NavModelItem } from '@grafana/data'; import { isFetchError } from '@grafana/runtime'; import { Alert } from '@grafana/ui'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; +import { t } from 'app/core/internationalization'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { AlertRuleProvider } from './components/rule-viewer/RuleContext'; @@ -63,11 +64,16 @@ const RuleViewer = (): JSX.Element => { } // if we get here assume we can't find the rule - return ( - - - - ); + if (!rule && !loading) { + return ( + + + + ); + } + + // we should never get to this state + return <>; }; export const defaultPageNav: NavModelItem = { @@ -84,7 +90,11 @@ function ErrorMessage({ error }: ErrorMessageProps) { return ; } - return {stringifyErrorLike(error)}; + return ( + + {stringifyErrorLike(error)} + + ); } export default withPageErrorBoundary(RuleViewer); diff --git a/public/app/features/alerting/unified/hooks/useCombinedRule.ts b/public/app/features/alerting/unified/hooks/useCombinedRule.ts index 0841b794fb5..9d2e37f272e 100644 --- a/public/app/features/alerting/unified/hooks/useCombinedRule.ts +++ b/public/app/features/alerting/unified/hooks/useCombinedRule.ts @@ -158,7 +158,7 @@ export function useCombinedRule({ ruleIdentifier, limitAlerts }: Props): Request }, [ruleIdentifier, ruleSourceName, promRuleNs, rulerRuleGroup, ruleSource, ruleLocation, namespaceName]); return { - loading: isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup, + loading: isLoadingRuleLocation || isLoadingDsFeatures || isLoadingPromRules || isLoadingRulerGroup, error: ruleLocationError ?? promRuleNsError ?? rulerRuleGroupError, result: rule, }; diff --git a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx index 7d1b9d92fcf..fe33ee855ec 100644 --- a/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx +++ b/public/app/features/alerting/unified/rule-editor/ExistingRuleEditor.tsx @@ -37,7 +37,7 @@ export function ExistingRuleEditor({ identifier }: ExistingRuleEditorProps) { ); } - if (!ruleWithLocation) { + if (!ruleWithLocation && !loading) { return Sorry! This rule does not exist.; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c2e79b5c104..6e545aedd6b 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -609,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "Something went wrong loading the rule", "prometheus-consistency-check": { "alert-message": "Alert rule has been updated. Changes may take up to a minute to appear on the Alert rules list view.", "alert-title": "Update in progress" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 41644d694cb..e8fb5da2873 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -609,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "Ŝőmęŧĥįʼnģ ŵęʼnŧ ŵřőʼnģ ľőäđįʼnģ ŧĥę řūľę", "prometheus-consistency-check": { "alert-message": "Åľęřŧ řūľę ĥäş þęęʼn ūpđäŧęđ. Cĥäʼnģęş mäy ŧäĸę ūp ŧő ä mįʼnūŧę ŧő äppęäř őʼn ŧĥę Åľęřŧ řūľęş ľįşŧ vįęŵ.", "alert-title": "Ůpđäŧę įʼn přőģřęşş" From 4391fac13581bf1593a53d95b0b3c94cd054ea96 Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Wed, 26 Feb 2025 12:34:50 +0100 Subject: [PATCH 25/51] unistore: add spanner to go.mod (#101143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add spanner dependency * Update go.mod, go.sum. * Assign owner to spanner dependency, clean up directives. * Rerun go mod tidy. --------- Co-authored-by: Peter Štibraný --- go.mod | 9 +++++++++ go.sum | 18 ++++++++++++++++++ pkg/extensions/enterprise_imports.go | 1 + 3 files changed, 28 insertions(+) diff --git a/go.mod b/go.mod index 8b91c5cb880..d32c2843663 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( buf.build/gen/go/parca-dev/parca/connectrpc/go v1.17.0-20240902100956-02fd72488966.1 // @grafana/observability-traces-and-profiling buf.build/gen/go/parca-dev/parca/protocolbuffers/go v1.34.2-20240902100956-02fd72488966.2 // @grafana/observability-traces-and-profiling cloud.google.com/go/kms v1.20.0 // @grafana/grafana-backend-group + cloud.google.com/go/spanner v1.70.0 // @grafana/grafana-search-and-storage cloud.google.com/go/storage v1.43.0 // @grafana/grafana-backend-group connectrpc.com/connect v1.17.0 // @grafana/observability-traces-and-profiling cuelang.org/go v0.11.1 // @grafana/grafana-as-code @@ -226,6 +227,7 @@ require ( cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/iam v1.2.1 // indirect cloud.google.com/go/longrunning v0.6.1 // indirect + cloud.google.com/go/monitoring v1.21.1 // indirect cuelabs.dev/go/oci/ociregistry v0.0.0-20240906074133-82eb438dd565 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/azure-pipeline-go v0.2.3 // indirect @@ -241,6 +243,8 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect github.com/FZambia/eagle v0.1.0 // indirect + github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -304,12 +308,14 @@ require ( github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 // indirect github.com/caio/go-tdigest v3.1.0+incompatible // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/centrifugal/protocol v0.13.4 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cheekybits/genny v1.0.0 // indirect github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect github.com/cloudflare/circl v1.3.7 // indirect + github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // 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 @@ -329,6 +335,7 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/envoyproxy/go-control-plane v0.13.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -512,9 +519,11 @@ require ( go.mongodb.org/mongo-driver v1.16.1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 7604fb6d2bb..96e57fa263f 100644 --- a/go.sum +++ b/go.sum @@ -187,6 +187,7 @@ cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvj cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= @@ -390,6 +391,8 @@ cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhI cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc= +cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= @@ -536,6 +539,8 @@ cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+ cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/spanner v1.70.0 h1:nj6p/GJTgMDiSQ1gQ034ItsKuJgHiMOjtOlONOg8PSo= +cloud.google.com/go/spanner v1.70.0/go.mod h1:X5T0XftydYp0K1adeJQDJtdWpbrOeJ7wHecM4tK6FiE= cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= @@ -706,6 +711,10 @@ github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/eagle v0.1.0 h1:9gyX6x+xjoIfglgyPTcYm7dvY7FJ93us1QY5De4CyXA= github.com/FZambia/eagle v0.1.0/go.mod h1:YjGSPVkQTNcVLfzEUQJNgW9ScPR0K4u/Ky0yeFa4oDA= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 h1:oVLqHXhnYtUwM89y9T1fXGaK9wTkXHgNp8/ZNMQzUxE= +github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= @@ -954,6 +963,7 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/centrifugal/centrifuge v0.33.3 h1:uyqBc27oM+qnC3NX5imvZxuk9+u2ze6QGWHDICZeoSc= github.com/centrifugal/centrifuge v0.33.3/go.mod h1:GaOF4CiREY5x6lW7zYUz46Qrc6aUZ6J/AMV/kROc2+U= @@ -992,6 +1002,7 @@ 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-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= @@ -1103,12 +1114,14 @@ 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.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE= github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -2316,6 +2329,7 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -2448,6 +2462,8 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/collector/pdata v1.22.0 h1:3yhjL46NLdTMoP8rkkcE9B0pzjf2973crn0KKhX5UrI= go.opentelemetry.io/collector/pdata v1.22.0/go.mod h1:nLLf6uDg8Kn5g3WNZwGyu8+kf77SwOqQvMTb5AXEbEY= +go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8= +go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= @@ -3314,9 +3330,11 @@ google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= 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.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index ea63f09b2a8..f0b4c12f7ec 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -6,6 +6,7 @@ package extensions import ( _ "cloud.google.com/go/kms/apiv1" _ "cloud.google.com/go/kms/apiv1/kmspb" + _ "cloud.google.com/go/spanner" _ "github.com/Azure/azure-sdk-for-go/sdk/azidentity" _ "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys" _ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" From d7a081e3a1ebabcddc43a1f25c05b246037f8c01 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 26 Feb 2025 11:38:13 +0000 Subject: [PATCH 26/51] Theme: Add `ThemePreview` component (#101287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ThemeDrawer: Change theme from anywhere and preview them * Update * extract ThemePreview into it's own component * undo changes everywhere else --------- Co-authored-by: Torkel Ödegaard --- .../core/components/Theme/ThemePreview.tsx | 145 ++++++++++++++++++ public/locales/en-US/grafana.json | 10 ++ public/locales/pseudo-LOCALE/grafana.json | 10 ++ 3 files changed, 165 insertions(+) create mode 100644 public/app/core/components/Theme/ThemePreview.tsx diff --git a/public/app/core/components/Theme/ThemePreview.tsx b/public/app/core/components/Theme/ThemePreview.tsx new file mode 100644 index 00000000000..4d60222dcc6 --- /dev/null +++ b/public/app/core/components/Theme/ThemePreview.tsx @@ -0,0 +1,145 @@ +import { css, cx } from '@emotion/css'; + +import { GrafanaTheme2, ThemeContext } from '@grafana/data'; +import { Box, Divider, Icon, Stack, useStyles2 } from '@grafana/ui'; + +import { Trans } from '../../internationalization'; +import { Branding } from '../Branding/Branding'; + +interface ThemePreviewProps { + theme: GrafanaTheme2; +} + +export function ThemePreview({ theme }: ThemePreviewProps) { + return ( + + + + ); +} + +function ThemePreviewWithContext() { + const styles = useStyles2(getStyles); + + return ( + + + + + +
+ Home + + Dashboards +
+
+ +
+ + + + + +
+
+ Panel +
+ +
+ Form label +
+
+ + +
+
+
+ +
+ + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + breadcrumbs: css({ + alignItems: 'center', + color: theme.colors.text.primary, + display: 'flex', + fontSize: Math.round(theme.typography.fontSize / 3), + gap: theme.spacing(0.25), + lineHeight: Math.round(theme.typography.body.lineHeight / 3), + paddingLeft: theme.spacing(0.5), + }), + breadcrumbSeparator: css({ + height: theme.spacing(0.75), + width: theme.spacing(0.75), + }), + img: css({ + height: theme.spacing(1), + width: theme.spacing(1), + }), + panel: css({ + background: theme.components.panel.background, + border: `1px solid ${theme.components.panel.borderColor}`, + borderRadius: theme.shape.radius.default, + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + }), + panelHeader: css({ + alignItems: 'center', + color: theme.colors.text.primary, + display: 'flex', + fontSize: Math.round(theme.typography.fontSize / 3), + height: theme.spacing(2), + lineHeight: Math.round(theme.typography.body.lineHeight / 3), + padding: theme.spacing(0.5), + }), + formLabel: css({ + color: theme.colors.text.primary, + fontSize: Math.round(theme.typography.fontSize / 3), + lineHeight: Math.round(theme.typography.body.lineHeight / 3), + }), + formInput: css({ + background: theme.components.input.background, + border: `1px solid ${theme.colors.border.medium}`, + borderRadius: theme.shape.radius.default, + height: theme.spacing(1), + width: theme.spacing(6), + }), + action: css({ + borderRadius: theme.shape.radius.default, + height: theme.spacing(1), + width: theme.spacing(2.5), + }), + actionSecondary: css({ + background: theme.colors.secondary.main, + }), + actionDanger: css({ + background: theme.colors.error.main, + }), + actionPrimary: css({ + background: theme.colors.primary.main, + }), + }; +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6e545aedd6b..a96d829d80d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3655,6 +3655,16 @@ "title": "You haven't created any teams yet" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "Dashboards", + "home": "Home" + }, + "panel": { + "form-label": "Form label", + "title": "Panel" + } + }, "time-picker": { "absolute": { "recent-title": "Recently used absolute ranges", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index e8fb5da2873..27833122c61 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -3655,6 +3655,16 @@ "title": "Ÿőū ĥävęʼn'ŧ čřęäŧęđ äʼny ŧęämş yęŧ" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "Đäşĥþőäřđş", + "home": "Ħőmę" + }, + "panel": { + "form-label": "Főřm ľäþęľ", + "title": "Päʼnęľ" + } + }, "time-picker": { "absolute": { "recent-title": "Ŗęčęʼnŧľy ūşęđ äþşőľūŧę řäʼnģęş", From fcdbb5887d1cb68f0e160dd3071790443515521c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Wed, 26 Feb 2025 12:41:27 +0100 Subject: [PATCH 27/51] Internationalization: mark up some `grafana-ui` components for translation (#101303) --- .betterer.results | 94 +------------------ .../grafana-ui/src/components/Alert/Alert.tsx | 7 +- .../grafana-ui/src/components/Card/Card.tsx | 4 +- .../src/components/Cascader/Cascader.tsx | 3 +- .../ColorPicker/NamedColorsPalette.tsx | 5 +- .../ColorPicker/SeriesColorPickerPopover.tsx | 7 +- .../src/components/Combobox/ValuePill.tsx | 4 +- .../ConfirmModal/ConfirmContent.tsx | 7 +- .../components/DataLinks/DataLinkEditor.tsx | 12 ++- .../DataSourceSettings/AlertingSettings.tsx | 9 +- .../DataSourceSettings/BasicAuthSettings.tsx | 5 +- .../CustomHeadersSettings.tsx | 13 +-- .../DataSourceHttpSettings.tsx | 60 ++++++++---- .../DataSourceSettings/HttpProxySettings.tsx | 27 ++++-- .../SecureSocksProxySettings.tsx | 9 +- .../DataSourceSettings/TLSAuthSettings.tsx | 37 ++++++-- .../DateTimePicker/DateTimePicker.tsx | 17 +++- .../RelativeTimeRangePicker.tsx | 17 +++- public/locales/en-US/grafana.json | 84 ++++++++++++++++- public/locales/pseudo-LOCALE/grafana.json | 84 ++++++++++++++++- 20 files changed, 332 insertions(+), 173 deletions(-) diff --git a/.betterer.results b/.betterer.results index b56e99b02a0..b593c39ca68 100644 --- a/.betterer.results +++ b/.betterer.results @@ -518,120 +518,30 @@ exports[`better eslint`] = { "packages/grafana-sql/src/components/visual-query-builder/AwesomeQueryBuilder.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-ui/src/components/Alert/Alert.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], - "packages/grafana-ui/src/components/Card/Card.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "packages/grafana-ui/src/components/Cascader/Cascader.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], "packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], - "packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], "packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "packages/grafana-ui/src/components/Combobox/ValuePill.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], "packages/grafana-ui/src/components/Combobox/useOptions.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "packages/grafana-ui/src/components/ConfirmModal/ConfirmContent.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] - ], - "packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] - ], "packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], - "packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], - "packages/grafana-ui/src/components/DataSourceSettings/BasicAuthSettings.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], "packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"] - ], - "packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "8"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "9"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "10"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "11"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "12"] - ], - "packages/grafana-ui/src/components/DataSourceSettings/HttpProxySettings.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"] - ], - "packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], - "packages/grafana-ui/src/components/DataSourceSettings/TLSAuthSettings.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "6"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "7"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "packages/grafana-ui/src/components/DataSourceSettings/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"] - ], - "packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"] - ], "packages/grafana-ui/src/components/FileDropzone/FileDropzone.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"] ], diff --git a/packages/grafana-ui/src/components/Alert/Alert.tsx b/packages/grafana-ui/src/components/Alert/Alert.tsx index 2789879c8f2..f1f0efe7329 100644 --- a/packages/grafana-ui/src/components/Alert/Alert.tsx +++ b/packages/grafana-ui/src/components/Alert/Alert.tsx @@ -7,6 +7,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { useTheme2 } from '../../themes'; import { IconName } from '../../types/icon'; +import { t } from '../../utils/i18n'; import { Button } from '../Button/Button'; import { Icon } from '../Icon/Icon'; import { Box } from '../Layout/Box/Box'; @@ -53,6 +54,8 @@ export const Alert = React.forwardRef( const role = restProps['role'] || rolesBySeverity[severity]; const ariaLabel = restProps['aria-label'] || title; + const closeLabel = t('grafana-ui.alert.close-button', 'Close alert'); + return (
( {onRemove && !buttonContent && (
diff --git a/packages/grafana-ui/src/components/Card/Card.tsx b/packages/grafana-ui/src/components/Card/Card.tsx index 7171b78b08a..49637c5a695 100644 --- a/packages/grafana-ui/src/components/Card/Card.tsx +++ b/packages/grafana-ui/src/components/Card/Card.tsx @@ -6,6 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; import { getFocusStyles } from '../../themes/mixins'; +import { t } from '../../utils/i18n'; import { CardContainer, CardContainerProps, getCardContainerStyles } from './CardContainer'; @@ -102,6 +103,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps & onClick: undefined, isSelected: undefined, }; + const optionLabel = t('grafana-ui.card.option', 'option'); return (

@@ -117,7 +119,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps & <>{children} )} {/* Input must be readonly because we are providing a value for the checked prop with no onChange handler */} - {isSelected !== undefined && } + {isSelected !== undefined && }

); }; diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.tsx index f3b2114b613..b142aaa2fad 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.tsx @@ -8,6 +8,7 @@ import { SelectableValue } from '@grafana/data'; import { withTheme2 } from '../../themes'; import { Themeable2 } from '../../types'; +import { t } from '../../utils/i18n'; import { Icon } from '../Icon/Icon'; import { IconButton } from '../IconButton/IconButton'; import { Input } from '../Input/Input'; @@ -286,7 +287,7 @@ class UnthemedCascader extends PureComponent { {isClearable && activeLabel !== '' && ( { e.preventDefault(); e.stopPropagation(); diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx index 521500db77d..d4cfbe381c5 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx @@ -3,6 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; +import { t } from '../../utils/i18n'; import { ColorSwatch } from './ColorSwatch'; import NamedColorsGroup from './NamedColorsGroup'; @@ -28,13 +29,13 @@ export const NamedColorsPalette = ({ color, onChange }: NamedColorsPaletteProps) onChange('transparent')} /> onChange('text')} />
diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 542d8a67d26..f7258cd3ed9 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -1,4 +1,5 @@ import { withTheme2 } from '../../themes'; +import { t } from '../../utils/i18n'; import { InlineField } from '../Forms/InlineField'; import { InlineSwitch } from '../Switch/Switch'; import { PopoverContentProps } from '../Tooltip'; @@ -12,15 +13,15 @@ export interface SeriesColorPickerPopoverProps extends ColorPickerProps, Popover export const SeriesColorPickerPopover = (props: SeriesColorPickerPopoverProps) => { const { yaxis, onToggleAxis, color, ...colorPickerProps } = props; - + const yAxisLabel = t('grafana-ui.series-color-picker-popover.y-axis-usage', 'Use right y-axis'); const customPickers = onToggleAxis ? { yaxis: { name: 'Y-Axis', tabComponent() { return ( - - + + ); }, diff --git a/packages/grafana-ui/src/components/Combobox/ValuePill.tsx b/packages/grafana-ui/src/components/Combobox/ValuePill.tsx index caeb3578df4..5205767a1db 100644 --- a/packages/grafana-ui/src/components/Combobox/ValuePill.tsx +++ b/packages/grafana-ui/src/components/Combobox/ValuePill.tsx @@ -4,6 +4,7 @@ import { forwardRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; +import { t } from '../../utils/i18n'; import { IconButton } from '../IconButton/IconButton'; interface ValuePillProps { @@ -15,6 +16,7 @@ interface ValuePillProps { export const ValuePill = forwardRef( ({ children, onRemove, disabled, ...rest }, ref) => { const styles = useStyles2(getValuePillStyles, disabled); + const removeButtonLabel = t('grafana-ui.value-pill.remove-button', 'Remove {{children}}', { children }); return ( {children} @@ -24,7 +26,7 @@ export const ValuePill = forwardRef( { e.stopPropagation(); onRemove(); diff --git a/packages/grafana-ui/src/components/ConfirmModal/ConfirmContent.tsx b/packages/grafana-ui/src/components/ConfirmModal/ConfirmContent.tsx index 27b94d395c0..48065ea44be 100644 --- a/packages/grafana-ui/src/components/ConfirmModal/ConfirmContent.tsx +++ b/packages/grafana-ui/src/components/ConfirmModal/ConfirmContent.tsx @@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../themes'; +import { t } from '../../utils/i18n'; import { Button, ButtonVariant } from '../Button'; import { Field } from '../Forms/Field'; import { Input } from '../Input/Input'; @@ -90,7 +91,9 @@ export const ConfirmContent = ({ }; const { handleSubmit } = useForm(); - + const placeholder = t('grafana-ui.confirm-content.placeholder', 'Type "{{confirmPromptText}}" to confirm', { + confirmPromptText, + }); return (
@@ -100,7 +103,7 @@ export const ConfirmContent = ({
- +
diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index 8ff7535790c..159161af151 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -53,15 +53,19 @@ export const DataLinkEditor = memo( return (
- - + + - + - + diff --git a/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx index 328ebd7c7fd..7a36fe63e49 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.tsx @@ -1,7 +1,7 @@ import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { InlineSwitch } from '../../components/Switch/Switch'; -import { Trans } from '../../utils/i18n'; +import { t, Trans } from '../../utils/i18n'; import { InlineField } from '../Forms/InlineField'; export interface Props @@ -22,9 +22,12 @@ export function AlertingSettings({ options, onOptionsC
onChange({ ...dataSourceConfig, basicAuthUser: event.currentTarget.value })} /> diff --git a/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx index 953e14c88e7..c61cc97621a 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.tsx @@ -5,7 +5,7 @@ import { PureComponent } from 'react'; import { DataSourceSettings } from '@grafana/data'; import { useStyles2 } from '../../themes'; -import { Trans } from '../../utils/i18n'; +import { t, Trans } from '../../utils/i18n'; import { Button } from '../Button'; import { FormField } from '../FormField/FormField'; import { Icon } from '../Icon/Icon'; @@ -59,8 +59,9 @@ const CustomHeaderRow = ({ header, onBlur, onChange, onRemove, onReset }: Custom return (
onReset(header.id)} onChange={(e) => onChange({ ...header, value: e.target.value })} onBlur={onBlur} />
- + event.stopPropagation()} onBlur={() => setFrom({ ...from, validation: isRangeValid(from.value) })} @@ -187,7 +191,11 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) { value={from.value} /> - + event.stopPropagation()} onBlur={() => setTo({ ...to, validation: isRangeValid(to.value) })} @@ -195,7 +203,10 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) { value={to.value} /> -
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a96d829d80d..366b60a9da0 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1706,10 +1706,19 @@ "one-click-description": "Only one link {{ action }} can have one click enabled at a time" } }, + "alert": { + "close-button": "Close alert" + }, "auto-save-field": { "saved": "Saved!", "saving": "Saving <1>" }, + "card": { + "option": "option" + }, + "cascader": { + "clear-button": "Clear selection" + }, "color-picker-popover": { "palette-tab": "Colors", "spectrum-tab": "Custom" @@ -1717,8 +1726,15 @@ "confirm-button": { "cancel": "Cancel" }, + "confirm-content": { + "placeholder": "Type \"{{confirmPromptText}}\" to confirm" + }, "data-link-editor": { - "info": "With data links you can reference data variables like series name, labels and values. Type CMD+Space, CTRL+Space, or $ to open variable suggestions." + "info": "With data links you can reference data variables like series name, labels and values. Type CMD+Space, CTRL+Space, or $ to open variable suggestions.", + "new-tab-label": "Open in new tab", + "title-label": "Title", + "title-placeholder": "Show details", + "url-label": "URL" }, "data-link-editor-modal": { "cancel": "Cancel", @@ -1738,32 +1754,77 @@ "tooltip-remove": "Remove", "url-not-provided": "Data link url not provided" }, + "data-source-basic-auth-settings": { + "user-label": "User", + "user-placeholder": "user" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "Forward OAuth Identity", + "oauth-identity-tooltip": "Forward the user's upstream OAuth identity to the data source (Their access token gets passed along).", + "skip-tls-verify-label": "Skip TLS Verify", + "ts-client-auth-label": "TLS Client Auth", + "with-ca-cert-label": "With CA Cert", + "with-ca-cert-tooltip": "Needed for verifying self-signed TLS Certs" + }, "data-source-http-settings": { "access-help": "Help <1>", "access-help-details": "Access mode controls how requests to the data source will be handled.<1> <1>Server should be the preferred way if nothing else is stated.", + "access-label": "Access", + "access-options-browser": "Browser", + "access-options-proxy": "Server (default)", "allowed-cookies": "Allowed cookies", + "allowed-cookies-tooltip": "Grafana proxy deletes forwarded cookies by default. Specify cookies by name that should be forwarded to the data source.", "auth": "Auth", + "azure-auth-label": "Azure Authentication", + "azure-auth-tooltip": "Use Azure authentication for Azure endpoint.", "basic-auth": "Basic Auth Details", + "basic-auth-label": "Basic auth", "browser-mode-description": "All requests will be made from the browser directly to the data source and may be subject to Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access mode.", "browser-mode-title": "<0>Browser access mode:", + "default-url-access-select": "Access", "default-url-tooltip": "Specify a complete HTTP URL (for example http://your_server:8080)", "direct-url-tooltip": "Your access method is <1>Browser, this means the URL needs to be accessible from the browser.", "heading": "HTTP", "proxy-url-tooltip": "Your access method is <1>Server, this means the URL needs to be accessible from the grafana backend/server.", "server-mode-description": "All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to the data source and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the grafana backend/server if you select this access mode.", - "server-mode-title": "<0>Server access mode (Default):" + "server-mode-title": "<0>Server access mode (Default):", + "timeout-form-label": "Timeout", + "timeout-label": "Timeout in seconds", + "timeout-tooltip": "HTTP request timeout in seconds", + "url-label": "URL", + "with-credential-label": "With Credentials", + "with-credential-tooltip": "Whether credentials such as cookies or auth headers should be sent with cross-site requests." }, "data-source-settings": { "alerting-settings-heading": "Alerting", + "alerting-settings-label": "Manage alert rules in Alerting UI", + "alerting-settings-tooltip": "Manage alert rules for this data source. To manage other alerting resources, add an Alertmanager data source.", "cert-key-reset": "Reset", "custom-headers-add": "Add header", + "custom-headers-header": "Header", + "custom-headers-header-placeholder": "Header Value", + "custom-headers-header-remove": "Remove header", + "custom-headers-header-value": "Value", "custom-headers-title": "Custom HTTP Headers", "secure-socks-heading": "Secure Socks Proxy", - "tls-heading": "TLS/SSL Auth Details" + "secure-socks-label": "Enabled", + "secure-socks-tooltip": "Connect to this datasource via the secure socks proxy.", + "tls-certification-label": "CA Cert", + "tls-certification-placeholder": "Begins with {{certificateBeginsWith}}", + "tls-client-certification-label": "Client Cert", + "tls-client-key-label": "Client Key", + "tls-client-key-placeholder": "Begins with {{privateKeyBeginsWith}}", + "tls-heading": "TLS/SSL Auth Details", + "tls-server-name-label": "ServerName", + "tls-tooltip": "TLS/SSL Certs are encrypted and stored in the Grafana database." }, "date-time-picker": { "apply": "Apply", - "cancel": "Cancel" + "calendar-icon-label": "Time picker", + "cancel": "Cancel", + "next-label": "Next month", + "previous-label": "Previous month", + "select-placeholder": "Select date/time" }, "drawer": { "close": "Close" @@ -1787,6 +1848,10 @@ "modal": { "close-tooltip": "Close" }, + "named-colors-palette": { + "text-color-swatch": "Text color", + "transparent-swatch": "Transparent" + }, "secret-form-field": { "reset": "Reset" }, @@ -1800,6 +1865,9 @@ "no-options-label": "No options found", "placeholder": "Choose" }, + "series-color-picker-popover": { + "y-axis-usage": "Use right y-axis" + }, "spinner": { "aria-label": "Loading" }, @@ -1819,6 +1887,9 @@ "user-icon": { "active-text": "Active last 15m" }, + "value-pill": { + "remove-button": "Remove {{children}}" + }, "viz-legend": { "right-axis-indicator": "(right y-axis)" }, @@ -3721,10 +3792,13 @@ "example": "Example: to select a time range from 10 minutes ago to now", "example-details": "From: now-10m To: now", "example-title": "Example time ranges", + "from-label": "From", "from-to": "{{timeOptionFrom}} to {{timeOptionTo}}", "more-info": "For more information see <2>docs <1>.", "specify": "Specify time range <1>", - "supported-formats": "Supported formats: <1>now-[digit]s/m/h/d/w" + "submit-button-label": "TimePicker submit button", + "supported-formats": "Supported formats: <1>now-[digit]s/m/h/d/w", + "to-label": "To" }, "zone": { "select-aria-label": "Time zone picker", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 27833122c61..23e1bebb722 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1706,10 +1706,19 @@ "one-click-description": "Øʼnľy őʼnę ľįʼnĸ {{ action }} čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę" } }, + "alert": { + "close-button": "Cľőşę äľęřŧ" + }, "auto-save-field": { "saved": "Ŝävęđ!", "saving": "Ŝävįʼnģ <1>" }, + "card": { + "option": "őpŧįőʼn" + }, + "cascader": { + "clear-button": "Cľęäř şęľęčŧįőʼn" + }, "color-picker-popover": { "palette-tab": "Cőľőřş", "spectrum-tab": "Cūşŧőm" @@ -1717,8 +1726,15 @@ "confirm-button": { "cancel": "Cäʼnčęľ" }, + "confirm-content": { + "placeholder": "Ŧypę \"{{confirmPromptText}}\" ŧő čőʼnƒįřm" + }, "data-link-editor": { - "info": "Ŵįŧĥ đäŧä ľįʼnĸş yőū čäʼn řęƒęřęʼnčę đäŧä väřįäþľęş ľįĸę şęřįęş ʼnämę, ľäþęľş äʼnđ väľūęş. Ŧypę CMĐ+Ŝpäčę, CŦŖĿ+Ŝpäčę, őř $ ŧő őpęʼn väřįäþľę şūģģęşŧįőʼnş." + "info": "Ŵįŧĥ đäŧä ľįʼnĸş yőū čäʼn řęƒęřęʼnčę đäŧä väřįäþľęş ľįĸę şęřįęş ʼnämę, ľäþęľş äʼnđ väľūęş. Ŧypę CMĐ+Ŝpäčę, CŦŖĿ+Ŝpäčę, őř $ ŧő őpęʼn väřįäþľę şūģģęşŧįőʼnş.", + "new-tab-label": "Øpęʼn įʼn ʼnęŵ ŧäþ", + "title-label": "Ŧįŧľę", + "title-placeholder": "Ŝĥőŵ đęŧäįľş", + "url-label": "ŮŖĿ" }, "data-link-editor-modal": { "cancel": "Cäʼnčęľ", @@ -1738,32 +1754,77 @@ "tooltip-remove": "Ŗęmővę", "url-not-provided": "Đäŧä ľįʼnĸ ūřľ ʼnőŧ přővįđęđ" }, + "data-source-basic-auth-settings": { + "user-label": "Ůşęř", + "user-placeholder": "ūşęř" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "Főřŵäřđ ØÅūŧĥ Ĩđęʼnŧįŧy", + "oauth-identity-tooltip": "Főřŵäřđ ŧĥę ūşęř'ş ūpşŧřęäm ØÅūŧĥ įđęʼnŧįŧy ŧő ŧĥę đäŧä şőūřčę (Ŧĥęįř äččęşş ŧőĸęʼn ģęŧş päşşęđ äľőʼnģ).", + "skip-tls-verify-label": "Ŝĸįp ŦĿŜ Vęřįƒy", + "ts-client-auth-label": "ŦĿŜ Cľįęʼnŧ Åūŧĥ", + "with-ca-cert-label": "Ŵįŧĥ CÅ Cęřŧ", + "with-ca-cert-tooltip": "Ńęęđęđ ƒőř vęřįƒyįʼnģ şęľƒ-şįģʼnęđ ŦĿŜ Cęřŧş" + }, "data-source-http-settings": { "access-help": "Ħęľp <1>", "access-help-details": "Åččęşş mőđę čőʼnŧřőľş ĥőŵ řęqūęşŧş ŧő ŧĥę đäŧä şőūřčę ŵįľľ þę ĥäʼnđľęđ.<1> <1>Ŝęřvęř şĥőūľđ þę ŧĥę přęƒęřřęđ ŵäy įƒ ʼnőŧĥįʼnģ ęľşę įş şŧäŧęđ.", + "access-label": "Åččęşş", + "access-options-browser": "ßřőŵşęř", + "access-options-proxy": "Ŝęřvęř (đęƒäūľŧ)", "allowed-cookies": "Åľľőŵęđ čőőĸįęş", + "allowed-cookies-tooltip": "Ğřäƒäʼnä přőχy đęľęŧęş ƒőřŵäřđęđ čőőĸįęş þy đęƒäūľŧ. Ŝpęčįƒy čőőĸįęş þy ʼnämę ŧĥäŧ şĥőūľđ þę ƒőřŵäřđęđ ŧő ŧĥę đäŧä şőūřčę.", "auth": "Åūŧĥ", + "azure-auth-label": "Åžūřę Åūŧĥęʼnŧįčäŧįőʼn", + "azure-auth-tooltip": "Ůşę Åžūřę äūŧĥęʼnŧįčäŧįőʼn ƒőř Åžūřę ęʼnđpőįʼnŧ.", "basic-auth": "ßäşįč Åūŧĥ Đęŧäįľş", + "basic-auth-label": "ßäşįč äūŧĥ", "browser-mode-description": "Åľľ řęqūęşŧş ŵįľľ þę mäđę ƒřőm ŧĥę þřőŵşęř đįřęčŧľy ŧő ŧĥę đäŧä şőūřčę äʼnđ mäy þę şūþĵęčŧ ŧő Cřőşş-Øřįģįʼn Ŗęşőūřčę Ŝĥäřįʼnģ (CØŖŜ) řęqūįřęmęʼnŧş. Ŧĥę ŮŖĿ ʼnęęđş ŧő þę äččęşşįþľę ƒřőm ŧĥę þřőŵşęř įƒ yőū şęľęčŧ ŧĥįş äččęşş mőđę.", "browser-mode-title": "<0>ßřőŵşęř äččęşş mőđę:", + "default-url-access-select": "Åččęşş", "default-url-tooltip": "Ŝpęčįƒy ä čőmpľęŧę ĦŦŦP ŮŖĿ (ƒőř ęχämpľę ĥŧŧp://yőūř_şęřvęř:8080)", "direct-url-tooltip": "Ÿőūř äččęşş męŧĥőđ įş <1>ßřőŵşęř, ŧĥįş męäʼnş ŧĥę ŮŖĿ ʼnęęđş ŧő þę äččęşşįþľę ƒřőm ŧĥę þřőŵşęř.", "heading": "ĦŦŦP", "proxy-url-tooltip": "Ÿőūř äččęşş męŧĥőđ įş <1>Ŝęřvęř, ŧĥįş męäʼnş ŧĥę ŮŖĿ ʼnęęđş ŧő þę äččęşşįþľę ƒřőm ŧĥę ģřäƒäʼnä þäčĸęʼnđ/şęřvęř.", "server-mode-description": "Åľľ řęqūęşŧş ŵįľľ þę mäđę ƒřőm ŧĥę þřőŵşęř ŧő Ğřäƒäʼnä þäčĸęʼnđ/şęřvęř ŵĥįčĥ įʼn ŧūřʼn ŵįľľ ƒőřŵäřđ ŧĥę řęqūęşŧş ŧő ŧĥę đäŧä şőūřčę äʼnđ þy ŧĥäŧ čįřčūmvęʼnŧ pőşşįþľę Cřőşş-Øřįģįʼn Ŗęşőūřčę Ŝĥäřįʼnģ (CØŖŜ) řęqūįřęmęʼnŧş. Ŧĥę ŮŖĿ ʼnęęđş ŧő þę äččęşşįþľę ƒřőm ŧĥę ģřäƒäʼnä þäčĸęʼnđ/şęřvęř įƒ yőū şęľęčŧ ŧĥįş äččęşş mőđę.", - "server-mode-title": "<0>Ŝęřvęř äččęşş mőđę (Đęƒäūľŧ):" + "server-mode-title": "<0>Ŝęřvęř äččęşş mőđę (Đęƒäūľŧ):", + "timeout-form-label": "Ŧįmęőūŧ", + "timeout-label": "Ŧįmęőūŧ įʼn şęčőʼnđş", + "timeout-tooltip": "ĦŦŦP řęqūęşŧ ŧįmęőūŧ įʼn şęčőʼnđş", + "url-label": "ŮŖĿ", + "with-credential-label": "Ŵįŧĥ Cřęđęʼnŧįäľş", + "with-credential-tooltip": "Ŵĥęŧĥęř čřęđęʼnŧįäľş şūčĥ äş čőőĸįęş őř äūŧĥ ĥęäđęřş şĥőūľđ þę şęʼnŧ ŵįŧĥ čřőşş-şįŧę řęqūęşŧş." }, "data-source-settings": { "alerting-settings-heading": "Åľęřŧįʼnģ", + "alerting-settings-label": "Mäʼnäģę äľęřŧ řūľęş įʼn Åľęřŧįʼnģ ŮĨ", + "alerting-settings-tooltip": "Mäʼnäģę äľęřŧ řūľęş ƒőř ŧĥįş đäŧä şőūřčę. Ŧő mäʼnäģę őŧĥęř äľęřŧįʼnģ řęşőūřčęş, äđđ äʼn Åľęřŧmäʼnäģęř đäŧä şőūřčę.", "cert-key-reset": "Ŗęşęŧ", "custom-headers-add": "Åđđ ĥęäđęř", + "custom-headers-header": "Ħęäđęř", + "custom-headers-header-placeholder": "Ħęäđęř Väľūę", + "custom-headers-header-remove": "Ŗęmővę ĥęäđęř", + "custom-headers-header-value": "Väľūę", "custom-headers-title": "Cūşŧőm ĦŦŦP Ħęäđęřş", "secure-socks-heading": "Ŝęčūřę Ŝőčĸş Přőχy", - "tls-heading": "ŦĿŜ/ŜŜĿ Åūŧĥ Đęŧäįľş" + "secure-socks-label": "Ēʼnäþľęđ", + "secure-socks-tooltip": "Cőʼnʼnęčŧ ŧő ŧĥįş đäŧäşőūřčę vįä ŧĥę şęčūřę şőčĸş přőχy.", + "tls-certification-label": "CÅ Cęřŧ", + "tls-certification-placeholder": "ßęģįʼnş ŵįŧĥ {{certificateBeginsWith}}", + "tls-client-certification-label": "Cľįęʼnŧ Cęřŧ", + "tls-client-key-label": "Cľįęʼnŧ Ķęy", + "tls-client-key-placeholder": "ßęģįʼnş ŵįŧĥ {{privateKeyBeginsWith}}", + "tls-heading": "ŦĿŜ/ŜŜĿ Åūŧĥ Đęŧäįľş", + "tls-server-name-label": "ŜęřvęřŃämę", + "tls-tooltip": "ŦĿŜ/ŜŜĿ Cęřŧş äřę ęʼnčřypŧęđ äʼnđ şŧőřęđ įʼn ŧĥę Ğřäƒäʼnä đäŧäþäşę." }, "date-time-picker": { "apply": "Åppľy", - "cancel": "Cäʼnčęľ" + "calendar-icon-label": "Ŧįmę pįčĸęř", + "cancel": "Cäʼnčęľ", + "next-label": "Ńęχŧ mőʼnŧĥ", + "previous-label": "Přęvįőūş mőʼnŧĥ", + "select-placeholder": "Ŝęľęčŧ đäŧę/ŧįmę" }, "drawer": { "close": "Cľőşę" @@ -1787,6 +1848,10 @@ "modal": { "close-tooltip": "Cľőşę" }, + "named-colors-palette": { + "text-color-swatch": "Ŧęχŧ čőľőř", + "transparent-swatch": "Ŧřäʼnşpäřęʼnŧ" + }, "secret-form-field": { "reset": "Ŗęşęŧ" }, @@ -1800,6 +1865,9 @@ "no-options-label": "Ńő őpŧįőʼnş ƒőūʼnđ", "placeholder": "Cĥőőşę" }, + "series-color-picker-popover": { + "y-axis-usage": "Ůşę řįģĥŧ y-äχįş" + }, "spinner": { "aria-label": "Ŀőäđįʼnģ" }, @@ -1819,6 +1887,9 @@ "user-icon": { "active-text": "Åčŧįvę ľäşŧ 15m" }, + "value-pill": { + "remove-button": "Ŗęmővę {{children}}" + }, "viz-legend": { "right-axis-indicator": "(řįģĥŧ y-äχįş)" }, @@ -3721,10 +3792,13 @@ "example": "Ēχämpľę: ŧő şęľęčŧ ä ŧįmę řäʼnģę ƒřőm 10 mįʼnūŧęş äģő ŧő ʼnőŵ", "example-details": "Fřőm: ʼnőŵ-10m Ŧő: ʼnőŵ", "example-title": "Ēχämpľę ŧįmę řäʼnģęş", + "from-label": "Fřőm", "from-to": "{{timeOptionFrom}} ŧő {{timeOptionTo}}", "more-info": "Főř mőřę įʼnƒőřmäŧįőʼn şęę <2>đőčş <1>.", "specify": "Ŝpęčįƒy ŧįmę řäʼnģę <1>", - "supported-formats": "Ŝūppőřŧęđ ƒőřmäŧş: <1>ʼnőŵ-[đįģįŧ]ş/m/ĥ/đ/ŵ" + "submit-button-label": "ŦįmęPįčĸęř şūþmįŧ þūŧŧőʼn", + "supported-formats": "Ŝūppőřŧęđ ƒőřmäŧş: <1>ʼnőŵ-[đįģįŧ]ş/m/ĥ/đ/ŵ", + "to-label": "Ŧő" }, "zone": { "select-aria-label": "Ŧįmę žőʼnę pįčĸęř", From af0e38862294b19a5deb122beea20f6abff368dd Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Wed, 26 Feb 2025 21:36:46 +0900 Subject: [PATCH 28/51] Jaeger: run metadata requests through the backend (#100337) * run metadata requests throught the backend * fix tests * add tests to backend * fix lint --- pkg/tsdb/jaeger/callresource.go | 69 ++++++++ pkg/tsdb/jaeger/client.go | 28 +++ pkg/tsdb/jaeger/client_test.go | 166 ++++++++++++++++++ pkg/tsdb/jaeger/jaeger.go | 6 + .../jaeger/components/SearchForm.test.tsx | 2 +- .../jaeger/components/SearchForm.tsx | 8 +- .../plugins/datasource/jaeger/datasource.ts | 12 +- 7 files changed, 285 insertions(+), 6 deletions(-) create mode 100644 pkg/tsdb/jaeger/callresource.go create mode 100644 pkg/tsdb/jaeger/client_test.go diff --git a/pkg/tsdb/jaeger/callresource.go b/pkg/tsdb/jaeger/callresource.go new file mode 100644 index 00000000000..0d83be284d8 --- /dev/null +++ b/pkg/tsdb/jaeger/callresource.go @@ -0,0 +1,69 @@ +package jaeger + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" +) + +func (s *Service) registerResourceRoutes() *http.ServeMux { + router := http.NewServeMux() + router.HandleFunc("GET /services", s.withDatasourceHandlerFunc(getServicesHandler)) + router.HandleFunc("GET /services/{service}/operations", s.withDatasourceHandlerFunc(getOperationsHandler)) + return router +} + +func (s *Service) withDatasourceHandlerFunc(getHandler func(d *datasourceInfo) http.HandlerFunc) func(rw http.ResponseWriter, r *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + client, err := s.getDSInfo(r.Context(), backend.PluginConfigFromContext(r.Context())) + if err != nil { + writeResponse(nil, errors.New("error getting data source information from context"), rw, client.JaegerClient.logger) + return + } + h := getHandler(client) + h.ServeHTTP(rw, r) + } +} + +func getServicesHandler(ds *datasourceInfo) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + services, err := ds.JaegerClient.Services() + writeResponse(services, err, rw, ds.JaegerClient.logger) + } +} + +func getOperationsHandler(ds *datasourceInfo) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + service := strings.TrimSpace(r.PathValue("service")) + operations, err := ds.JaegerClient.Operations(service) + writeResponse(operations, err, rw, ds.JaegerClient.logger) + } +} + +func writeResponse(res interface{}, err error, rw http.ResponseWriter, logger log.Logger) { + if err != nil { + // This is used for resource calls, we don't need to add actual error message, but we should log it + logger.Warn("An error occurred while doing a resource call", "error", err) + http.Error(rw, "An error occurred within the plugin", http.StatusInternalServerError) + return + } + // Response should not be string, but just in case, handle it + if str, ok := res.(string); ok { + rw.Header().Set("Content-Type", "text/plain") + _, _ = rw.Write([]byte(str)) + return + } + b, err := json.Marshal(res) + if err != nil { + // This is used for resource calls, we don't need to add actual error message, but we should log it + logger.Warn("An error occurred while processing response from resource call", "error", err) + http.Error(rw, "An error occurred within the plugin", http.StatusInternalServerError) + return + } + rw.Header().Set("Content-Type", "application/json") + _, _ = rw.Write(b) +} diff --git a/pkg/tsdb/jaeger/client.go b/pkg/tsdb/jaeger/client.go index f1b14823e53..51c3ceb9d42 100644 --- a/pkg/tsdb/jaeger/client.go +++ b/pkg/tsdb/jaeger/client.go @@ -60,3 +60,31 @@ func (j *JaegerClient) Services() ([]string, error) { services = response.Data return services, err } + +func (j *JaegerClient) Operations(s string) ([]string, error) { + var response ServicesResponse + operations := []string{} + + u, err := url.JoinPath(j.url, "/api/services/", s, "/operations") + if err != nil { + return operations, backend.DownstreamError(fmt.Errorf("failed to join url: %w", err)) + } + + res, err := j.httpClient.Get(u) + if err != nil { + return operations, err + } + + defer func() { + if err = res.Body.Close(); err != nil { + j.logger.Error("Failed to close response body", "error", err) + } + }() + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return operations, err + } + + operations = response.Data + return operations, err +} diff --git a/pkg/tsdb/jaeger/client_test.go b/pkg/tsdb/jaeger/client_test.go new file mode 100644 index 00000000000..9351cd07dfa --- /dev/null +++ b/pkg/tsdb/jaeger/client_test.go @@ -0,0 +1,166 @@ +package jaeger + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/stretchr/testify/assert" +) + +func TestJaegerClient_Services(t *testing.T) { + tests := []struct { + name string + mockResponse string + mockStatusCode int + mockStatus string + expectedResult []string + expectError bool + expectedError error + }{ + { + name: "Successful response", + mockResponse: `{"data": ["service1", "service2"], "total": 2, "limit": 0, "offset": 0}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{"service1", "service2"}, + expectError: false, + expectedError: nil, + }, + { + name: "Non-200 response", + mockResponse: "", + mockStatusCode: http.StatusInternalServerError, + mockStatus: "Internal Server Error", + expectedResult: []string{}, + expectError: true, + expectedError: errors.New("Internal Server Error"), + }, + { + name: "Invalid JSON response", + mockResponse: `{invalid json`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{}, + expectError: true, + expectedError: &json.SyntaxError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.mockStatusCode) + _, _ = w.Write([]byte(tt.mockResponse)) + })) + defer server.Close() + + client, err := New(server.URL, server.Client(), log.NewNullLogger()) + assert.NoError(t, err) + + services, err := client.Services() + + if tt.expectError { + assert.Error(t, err) + if tt.expectedError != nil { + assert.IsType(t, tt.expectedError, err) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, services) + } + }) + } +} + +func TestJaegerClient_Operations(t *testing.T) { + tests := []struct { + name string + service string + mockResponse string + mockStatusCode int + mockStatus string + expectedResult []string + expectError bool + expectedError error + }{ + { + name: "Successful response", + service: "test-service", + mockResponse: `{"data": ["operation1", "operation2"], "total": 2, "limit": 0, "offset": 0}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{"operation1", "operation2"}, + expectError: false, + expectedError: nil, + }, + { + name: "Non-200 response", + service: "test-service", + mockResponse: "", + mockStatusCode: http.StatusInternalServerError, + mockStatus: "Internal Server Error", + expectedResult: []string{}, + expectError: true, + expectedError: errors.New("Internal Server Error"), + }, + { + name: "Invalid JSON response", + service: "test-service", + mockResponse: `{invalid json`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{}, + expectError: true, + expectedError: &json.SyntaxError{}, + }, + { + name: "Service with special characters", + service: "test/service:1", + mockResponse: `{"data": ["operation1"], "total": 1, "limit": 0, "offset": 0}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{"operation1"}, + expectError: false, + expectedError: nil, + }, + { + name: "Empty service", + service: "", + mockResponse: `{"data": [], "total": 0, "limit": 0, "offset": 0}`, + mockStatusCode: http.StatusOK, + mockStatus: "OK", + expectedResult: []string{}, + expectError: false, + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.mockStatusCode) + _, _ = w.Write([]byte(tt.mockResponse)) + })) + defer server.Close() + + client, err := New(server.URL, server.Client(), log.NewNullLogger()) + assert.NoError(t, err) + + operations, err := client.Operations(tt.service) + + if tt.expectError { + assert.Error(t, err) + if tt.expectedError != nil { + assert.IsType(t, tt.expectedError, err) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, operations) + } + }) + } +} diff --git a/pkg/tsdb/jaeger/jaeger.go b/pkg/tsdb/jaeger/jaeger.go index 24244c69e8e..1128192db1d 100644 --- a/pkg/tsdb/jaeger/jaeger.go +++ b/pkg/tsdb/jaeger/jaeger.go @@ -8,6 +8,7 @@ import ( "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" + "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" "github.com/grafana/grafana/pkg/infra/httpclient" ) @@ -85,3 +86,8 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque Message: "Data source is working", }, nil } + +func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + handler := httpadapter.New(s.registerResourceRoutes()) + return handler.CallResource(ctx, req, sender) +} diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx index 533f5b900eb..719b2ca49d9 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.test.tsx @@ -34,7 +34,7 @@ describe('SearchForm', () => { }; const ds = { async metadataRequest(url) { - if (url === '/api/services') { + if (url === 'services') { return Promise.resolve(['jaeger-query', 'service2', 'service3']); } return undefined; diff --git a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx index f0eee728f38..c5840a60bab 100644 --- a/public/app/plugins/datasource/jaeger/components/SearchForm.tsx +++ b/public/app/plugins/datasource/jaeger/components/SearchForm.tsx @@ -68,7 +68,7 @@ export function SearchForm({ datasource, query, onChange }: Props) { useEffect(() => { const getServices = async () => { - const services = await loadOptions('/api/services', 'services'); + const services = await loadOptions('services', 'services'); if (query.service && getTemplateSrv().containsTemplate(query.service)) { services.push(toOption(query.service)); } @@ -80,7 +80,7 @@ export function SearchForm({ datasource, query, onChange }: Props) { useEffect(() => { const getOperations = async () => { const operations = await loadOptions( - `/api/services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`, + `services/${encodeURIComponent(getTemplateSrv().replace(query.service!))}/operations`, 'operations' ); if (query.operation && getTemplateSrv().containsTemplate(query.operation)) { @@ -101,7 +101,7 @@ export function SearchForm({ datasource, query, onChange }: Props) { {!isEmpty(commonLabels) && ( -
- - Common labels + + + Common labels - + - -
+ + )} {isEmpty(frameSubset) ? ( <> @@ -254,10 +254,6 @@ export const getStyles = (theme: GrafanaTheme2) => ({ color: theme.colors.warning.text, padding: theme.spacing(), }), - commonLabels: css({ - display: 'grid', - gridTemplateColumns: 'max-content auto', - }), // we need !important here to override the list item default styles highlightedLogRecord: css({ background: `${theme.colors.primary.transparent} !important`, From 8c935c8f4a003a6aea18e4c0acbdf2734a261ad7 Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Wed, 26 Feb 2025 16:17:35 +0100 Subject: [PATCH 35/51] [unistore] Add benchmark for write throughput (#101345) * Add generic benchmark * address comments --- pkg/storage/unified/sql/backend.go | 29 ++- .../unified/sql/test/benchmark_test.go | 41 +++++ pkg/storage/unified/testing/benchmark.go | 171 ++++++++++++++++++ 3 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 pkg/storage/unified/sql/test/benchmark_test.go create mode 100644 pkg/storage/unified/testing/benchmark.go diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 024bc5ab7d0..c404e467eed 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -40,6 +40,9 @@ type BackendOptions struct { PollingInterval time.Duration WatchBufferSize int IsHA bool + + // testing + SimulatedNetworkLatency time.Duration // slows down the create transactions by a fixed amount } func NewBackend(opts BackendOptions) (Backend, error) { @@ -58,15 +61,16 @@ func NewBackend(opts BackendOptions) (Backend, error) { opts.WatchBufferSize = defaultWatchBufferSize } return &backend{ - isHA: opts.IsHA, - done: ctx.Done(), - cancel: cancel, - log: log.New("sql-resource-server"), - tracer: opts.Tracer, - dbProvider: opts.DBProvider, - pollingInterval: opts.PollingInterval, - watchBufferSize: opts.WatchBufferSize, - batchLock: &batchLock{running: make(map[string]bool)}, + isHA: opts.IsHA, + done: ctx.Done(), + cancel: cancel, + log: log.New("sql-resource-server"), + tracer: opts.Tracer, + dbProvider: opts.DBProvider, + pollingInterval: opts.PollingInterval, + watchBufferSize: opts.WatchBufferSize, + batchLock: &batchLock{running: make(map[string]bool)}, + simulatedNetworkLatency: opts.SimulatedNetworkLatency, }, nil } @@ -95,6 +99,9 @@ type backend struct { pollingInterval time.Duration watchBufferSize int notifier eventNotifier + + // testing + simulatedNetworkLatency time.Duration } func (b *backend) Init(ctx context.Context) error { @@ -249,7 +256,9 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, return fmt.Errorf("update resource rv: %w", err) } newVersion = rv - + if b.simulatedNetworkLatency > 0 { + time.Sleep(b.simulatedNetworkLatency) + } return nil }) diff --git a/pkg/storage/unified/sql/test/benchmark_test.go b/pkg/storage/unified/sql/test/benchmark_test.go new file mode 100644 index 00000000000..9ac1713db66 --- /dev/null +++ b/pkg/storage/unified/sql/test/benchmark_test.go @@ -0,0 +1,41 @@ +package test + +import ( + "context" + "testing" + "time" + + infraDB "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/sql" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + test "github.com/grafana/grafana/pkg/storage/unified/testing" + "github.com/stretchr/testify/require" +) + +func newTestBackend(b *testing.B) resource.StorageBackend { + dbstore := infraDB.InitTestDB(b) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) + require.NoError(b, err) + require.NotNil(b, eDB) + + backend, err := sql.NewBackend(sql.BackendOptions{ + DBProvider: eDB, + IsHA: true, + SimulatedNetworkLatency: 5 * time.Millisecond, // to simulate some network latency + }) + require.NoError(b, err) + require.NotNil(b, backend) + err = backend.Init(context.Background()) + require.NoError(b, err) + return backend +} + +func BenchmarkSQLStorageBackend(b *testing.B) { + opts := test.DefaultBenchmarkOptions() + if infraDB.IsTestDbSQLite() { + opts.Concurrency = 1 // to avoid SQLite database is locked error + } + test.BenchmarkStorageBackend(b, newTestBackend(b), opts) +} diff --git a/pkg/storage/unified/testing/benchmark.go b/pkg/storage/unified/testing/benchmark.go new file mode 100644 index 00000000000..0f16e5a7d15 --- /dev/null +++ b/pkg/storage/unified/testing/benchmark.go @@ -0,0 +1,171 @@ +package test + +import ( + "context" + "fmt" + "sort" + "strings" + "sync" + "testing" + "time" + + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/stretchr/testify/require" +) + +// BenchmarkOptions configures the benchmark parameters +type BenchmarkOptions struct { + NumResources int // total number of resources to write + Concurrency int // number of concurrent writers + NumNamespaces int // number of different namespaces + NumGroups int // number of different groups + NumResourceTypes int // number of different resource types +} + +// DefaultBenchmarkOptions returns the default benchmark options +func DefaultBenchmarkOptions() *BenchmarkOptions { + return &BenchmarkOptions{ + NumResources: 1000, + Concurrency: 20, + NumNamespaces: 1, + NumGroups: 1, + NumResourceTypes: 1, + } +} + +// BenchmarkResult contains the benchmark metrics +type BenchmarkResult struct { + TotalDuration time.Duration + WriteCount int + Throughput float64 // writes per second + P50Latency time.Duration + P90Latency time.Duration + P99Latency time.Duration +} + +// runStorageBackendBenchmark runs a write throughput benchmark +func runStorageBackendBenchmark(ctx context.Context, backend resource.StorageBackend, opts *BenchmarkOptions) (*BenchmarkResult, error) { + if opts == nil { + opts = DefaultBenchmarkOptions() + } + + // Create channels for workers + jobs := make(chan int, opts.NumResources) + results := make(chan time.Duration, opts.NumResources) + errors := make(chan error, opts.NumResources) + + // Fill the jobs channel + for i := 0; i < opts.NumResources; i++ { + jobs <- i + } + close(jobs) + + var wg sync.WaitGroup + + // Initialize each group and resource type combination in the init namespace + namespace := "ns-init" + for g := 0; g < opts.NumGroups; g++ { + group := fmt.Sprintf("group-%d", g) + for r := 0; r < opts.NumResourceTypes; r++ { + resourceType := fmt.Sprintf("resource-%d", r) + _, err := writeEvent(ctx, backend, "init", resource.WatchEvent_ADDED, + WithNamespace(namespace), + WithGroup(group), + WithResource(resourceType), + WithValue([]byte("init"))) + if err != nil { + return nil, fmt.Errorf("failed to initialize backend: %w", err) + } + } + } + // Start workers + startTime := time.Now() + for workerID := 0; workerID < opts.Concurrency; workerID++ { + wg.Add(1) + go func() { + defer wg.Done() + for jobID := range jobs { + // Calculate a unique ID for this job that's guaranteed to be unique across all workers + uniqueID := jobID + + // Generate deterministic and unique resource details + namespace := fmt.Sprintf("ns-%d", uniqueID%opts.NumNamespaces) + group := fmt.Sprintf("group-%d", uniqueID%opts.NumGroups) + resourceType := fmt.Sprintf("resource-%d", uniqueID%opts.NumResourceTypes) + // Ensure name is unique by using the global uniqueID + name := fmt.Sprintf("item-%d", uniqueID) + + writeStart := time.Now() + _, err := writeEvent(ctx, backend, name, resource.WatchEvent_ADDED, + WithNamespace(namespace), + WithGroup(group), + WithResource(resourceType), + WithValue([]byte(strings.Repeat("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 20)))) // ~1.21 KiB + + if err != nil { + errors <- err + return + } + + results <- time.Since(writeStart) + } + }() + } + + // Wait for all workers to complete + wg.Wait() + close(results) + close(errors) + + // Check for errors + if len(errors) > 0 { + return nil, <-errors // Return the first error encountered + } + + // Collect all latencies + latencies := make([]time.Duration, 0, opts.NumResources) + for latency := range results { + latencies = append(latencies, latency) + } + + // Sort latencies for percentile calculation + sort.Slice(latencies, func(i, j int) bool { + return latencies[i] < latencies[j] + }) + + totalDuration := time.Since(startTime) + throughput := float64(opts.NumResources) / totalDuration.Seconds() + + return &BenchmarkResult{ + TotalDuration: totalDuration, + WriteCount: opts.NumResources, + Throughput: throughput, + P50Latency: latencies[len(latencies)*50/100], + P90Latency: latencies[len(latencies)*90/100], + P99Latency: latencies[len(latencies)*99/100], + }, nil +} + +// BenchmarkStorageBackend runs a benchmark test for a storage backend implementation +func BenchmarkStorageBackend(b *testing.B, backend resource.StorageBackend, opts *BenchmarkOptions) { + ctx := context.Background() + + result, err := runStorageBackendBenchmark(ctx, backend, opts) + require.NoError(b, err) + + b.ReportMetric(result.Throughput, "writes/sec") + b.ReportMetric(float64(result.P50Latency.Milliseconds()), "p50-latency-ms") + b.ReportMetric(float64(result.P90Latency.Milliseconds()), "p90-latency-ms") + b.ReportMetric(float64(result.P99Latency.Milliseconds()), "p99-latency-ms") + + // Also log the results for better visibility + b.Logf("Benchmark Configuration: Workers=%d, Resources=%d, Namespaces=%d, Groups=%d, Resource Types=%d", opts.Concurrency, opts.NumResources, opts.NumNamespaces, opts.NumGroups, opts.NumResourceTypes) + b.Logf("") + b.Logf("Benchmark Results:") + b.Logf("Total Duration: %v", result.TotalDuration) + b.Logf("Write Count: %d", result.WriteCount) + b.Logf("Throughput: %.2f writes/sec", result.Throughput) + b.Logf("P50 Latency: %v", result.P50Latency) + b.Logf("P90 Latency: %v", result.P90Latency) + b.Logf("P99 Latency: %v", result.P99Latency) +} From 6614f5c3b2de7fc6d58e56ef57b660acd0cb32d3 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Wed, 26 Feb 2025 16:08:20 +0000 Subject: [PATCH 36/51] Log Context: Unify pinnedLogs and pinnedRowId props (#101067) --- public/app/features/logs/components/LogRows.tsx | 3 +-- .../logs/components/log-context/LogRowContextModal.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index 206e3b88023..f0e037c5665 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -61,7 +61,6 @@ export interface Props { permalinkedRowId?: string; scrollIntoView?: (element: HTMLElement) => void; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; - pinnedRowId?: string; pinnedLogs?: string[]; /** * If false or undefined, the `contain:strict` css property will be added to the wrapping `` for performance reasons. @@ -305,7 +304,7 @@ export const LogRows = memo( onPinLine={props.onPinLine} onUnpinLine={props.onUnpinLine} pinLineButtonTooltipTitle={props.pinLineButtonTooltipTitle} - pinned={props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)} + pinned={pinnedLogs?.some((logId) => logId === row.rowId || logId === row.uid)} isFilterLabelActive={props.isFilterLabelActive} handleTextSelection={handleSelection} enableLogDetails={enableLogDetails} diff --git a/public/app/features/logs/components/log-context/LogRowContextModal.tsx b/public/app/features/logs/components/log-context/LogRowContextModal.tsx index 798e03ce065..60cb0d6a092 100644 --- a/public/app/features/logs/components/log-context/LogRowContextModal.tsx +++ b/public/app/features/logs/components/log-context/LogRowContextModal.tsx @@ -561,7 +561,7 @@ export const LogRowContextModal: React.FunctionComponent setSticky(false)} onPinLine={() => setSticky(true)} - pinnedRowId={sticky ? row.uid : undefined} + pinnedLogs={sticky ? [row.uid] : undefined} overflowingContent={true} scrollElement={null} /> From c3505f08646f51f7db3ee58ce1e78439d4417a58 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 26 Feb 2025 17:29:32 +0100 Subject: [PATCH 37/51] AuthZ: Make `NewGrpcTokenAuth` public (#101352) * AuthZ: Expose NewGrpcTokenAuth * Lint --- pkg/services/authz/rbac.go | 6 +++--- pkg/services/authz/token_auth.go | 3 ++- pkg/services/authz/zanzana.go | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/services/authz/rbac.go b/pkg/services/authz/rbac.go index fcb398667ab..215bd3dbad6 100644 --- a/pkg/services/authz/rbac.go +++ b/pkg/services/authz/rbac.go @@ -33,8 +33,8 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql" ) -// `authzService` is hardcoded in authz-service -const authzServiceAudience = "authzService" +// AuthzServiceAudience is the audience for the authz service. +const AuthzServiceAudience = "authzService" // ProvideAuthZClient provides an AuthZ client and creates the AuthZ service. func ProvideAuthZClient( @@ -123,7 +123,7 @@ func newRemoteRBACClient(clientCfg *authzClientSettings, tracer tracing.Tracer) clientCfg.remoteAddress, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials( - newGRPCTokenAuth(authzServiceAudience, clientCfg.tokenNamespace, tokenClient), + NewGRPCTokenAuth(AuthzServiceAudience, clientCfg.tokenNamespace, tokenClient), ), ) if err != nil { diff --git a/pkg/services/authz/token_auth.go b/pkg/services/authz/token_auth.go index 1546379741e..6014d445df3 100644 --- a/pkg/services/authz/token_auth.go +++ b/pkg/services/authz/token_auth.go @@ -6,7 +6,8 @@ import ( "github.com/grafana/authlib/authn" ) -func newGRPCTokenAuth(audience, namespace string, tc authn.TokenExchanger) *tokenAuth { +// TODO: move this to authlib +func NewGRPCTokenAuth(audience, namespace string, tc authn.TokenExchanger) *tokenAuth { return &tokenAuth{audience, namespace, tc} } diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 8d644167528..6067a232082 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -57,7 +57,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features // TODO: add TLS support grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials( - newGRPCTokenAuth(authzServiceAudience, fmt.Sprintf("stacks-%s", cfg.StackID), tokenClient), + NewGRPCTokenAuth(AuthzServiceAudience, fmt.Sprintf("stacks-%s", cfg.StackID), tokenClient), ), } @@ -172,7 +172,7 @@ func (z *Zanzana) start(ctx context.Context) error { authenticator := authnlib.NewAccessTokenAuthenticator( authnlib.NewAccessTokenVerifier( - authnlib.VerifierConfig{AllowedAudiences: []string{authzServiceAudience}}, + authnlib.VerifierConfig{AllowedAudiences: []string{AuthzServiceAudience}}, authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{ SigningKeysURL: z.cfg.ZanzanaServer.SigningKeysURL, }), From b5faf5d9a12a68f2fe8063c1ecefe7e0e97ee64c Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Wed, 26 Feb 2025 17:33:05 +0100 Subject: [PATCH 38/51] Drilldown: Require `datasources:explore` RBAC action (#101366) Drilldown: Require `datasources:explore` acton --- pkg/api/accesscontrol.go | 3 --- pkg/api/api.go | 2 +- pkg/services/accesscontrol/models.go | 3 +-- pkg/services/navtree/navtreeimpl/navtree.go | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 0f1c4efe772..0ad8d8eeedc 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -66,9 +66,6 @@ func (hs *HTTPServer) declareFixedRoles() error { { Action: ac.ActionDatasourcesExplore, }, - { - Action: ac.ActionDatasourcesDrilldown, - }, }, }, Grants: []string{string(org.RoleEditor)}, diff --git a/pkg/api/api.go b/pkg/api/api.go index 18e52419ece..61ced28f276 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -183,7 +183,7 @@ func (hs *HTTPServer) registerRoutes() { } r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) - r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesDrilldown)), hs.Index) + r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) r.Get("/playlists/", reqSignedIn, hs.Index) r.Get("/playlists/*", reqSignedIn, hs.Index) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 4c15e3c5e56..dc12171eaa5 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -386,8 +386,7 @@ const ( ActionSettingsWrite = "settings:write" // Datasources actions - ActionDatasourcesExplore = "datasources:explore" - ActionDatasourcesDrilldown = "datasources:drilldown" + ActionDatasourcesExplore = "datasources:explore" // Global Scopes ScopeGlobalUsersAll = "global.users:*" diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 7158c8884dc..d84240556f8 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -130,7 +130,7 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere }) } - if hasAccess(ac.EvalPermission(ac.ActionDatasourcesDrilldown)) { + if hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) { drilldownChildNavLinks := s.buildDrilldownNavLinks(c) treeRoot.AddSection(&navtree.NavLink{ Text: "Drilldown", From 32fde6dba4be14fc90d58b07d97d5b3585b50d86 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 26 Feb 2025 14:39:39 -0500 Subject: [PATCH 39/51] Alerting: Update scheduler to provide full specification to rule update channel (#101375) update scheduler's aler rule to accept regular Evaluation in update channel This makes it accept the full rule definition, which is required in reset state. --- pkg/services/ngalert/schedule/alert_rule.go | 33 +++++++------------ .../ngalert/schedule/alert_rule_test.go | 21 ++++++------ .../ngalert/schedule/recording_rule.go | 2 +- .../ngalert/schedule/recording_rule_test.go | 8 +++-- pkg/services/ngalert/schedule/registry.go | 5 --- pkg/services/ngalert/schedule/schedule.go | 7 ++-- 6 files changed, 32 insertions(+), 44 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 85c254b0a59..771d32a2573 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -37,7 +37,7 @@ type Rule interface { // It has no effect if the rule has not yet been Run, or if the rule is Stopped. Eval(eval *Evaluation) (bool, *Evaluation) // Update sends a singal to change the definition of the rule. - Update(lastVersion RuleVersionAndPauseStatus) bool + Update(eval *Evaluation) bool // Type gives the type of the rule. Type() ngmodels.RuleType // Status indicates the status of the evaluating rule. @@ -59,7 +59,6 @@ func newRuleFactory( sender AlertsSender, stateManager *state.Manager, evalFactory eval.EvaluatorFactory, - ruleProvider ruleProvider, clock clock.Clock, rrCfg setting.RecordingRuleSettings, met *metrics.Scheduler, @@ -95,7 +94,6 @@ func newRuleFactory( sender, stateManager, evalFactory, - ruleProvider, clock, met, logger, @@ -109,15 +107,11 @@ func newRuleFactory( type evalAppliedFunc = func(ngmodels.AlertRuleKey, time.Time) type stopAppliedFunc = func(ngmodels.AlertRuleKey) -type ruleProvider interface { - get(ngmodels.AlertRuleKey) *ngmodels.AlertRule -} - type alertRule struct { key ngmodels.AlertRuleKeyWithGroup evalCh chan *Evaluation - updateCh chan RuleVersionAndPauseStatus + updateCh chan *Evaluation ctx context.Context stopFn util.CancelCauseFunc @@ -129,7 +123,6 @@ type alertRule struct { sender AlertsSender stateManager *state.Manager evalFactory eval.EvaluatorFactory - ruleProvider ruleProvider // Event hooks that are only used in tests. evalAppliedHook evalAppliedFunc @@ -149,7 +142,6 @@ func newAlertRule( sender AlertsSender, stateManager *state.Manager, evalFactory eval.EvaluatorFactory, - ruleProvider ruleProvider, clock clock.Clock, met *metrics.Scheduler, logger log.Logger, @@ -161,7 +153,7 @@ func newAlertRule( return &alertRule{ key: key, evalCh: make(chan *Evaluation), - updateCh: make(chan RuleVersionAndPauseStatus), + updateCh: make(chan *Evaluation), ctx: ctx, stopFn: stop, appURL: appURL, @@ -171,7 +163,6 @@ func newAlertRule( sender: sender, stateManager: stateManager, evalFactory: evalFactory, - ruleProvider: ruleProvider, evalAppliedHook: evalAppliedHook, stopAppliedHook: stopAppliedHook, metrics: met, @@ -220,7 +211,7 @@ func (a *alertRule) Eval(eval *Evaluation) (bool, *Evaluation) { } // update sends an instruction to the rule evaluation routine to update the scheduled rule to the specified version. The specified version must be later than the current version, otherwise no update will happen. -func (a *alertRule) Update(lastVersion RuleVersionAndPauseStatus) bool { +func (a *alertRule) Update(eval *Evaluation) bool { // check if the channel is not empty. select { case <-a.updateCh: @@ -230,7 +221,7 @@ func (a *alertRule) Update(lastVersion RuleVersionAndPauseStatus) bool { } select { - case a.updateCh <- lastVersion: + case a.updateCh <- eval: return true case <-a.ctx.Done(): return false @@ -254,15 +245,16 @@ func (a *alertRule) Run() error { select { // used by external services (API) to notify that rule is updated. case ctx := <-a.updateCh: - if currentFingerprint == ctx.Fingerprint { + fp := ctx.Fingerprint() + if currentFingerprint == fp { a.logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", currentFingerprint) continue } - a.logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.IsPaused, "fingerprint", ctx.Fingerprint) + a.logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.rule.IsPaused, "fingerprint", fp) // clear the state. So the next evaluation will start from the scratch. - a.resetState(grafanaCtx, ctx.IsPaused) - currentFingerprint = ctx.Fingerprint + a.resetState(grafanaCtx, ctx.rule, ctx.rule.IsPaused) + currentFingerprint = fp // evalCh - used by the scheduler to signal that evaluation is needed. case ctx, ok := <-a.evalCh: if !ok { @@ -298,7 +290,7 @@ func (a *alertRule) Run() error { // lingers in DB and won't be cleaned up until next alert rule update. needReset = needReset || (currentFingerprint == 0 && isPaused) if needReset { - a.resetState(grafanaCtx, isPaused) + a.resetState(grafanaCtx, ctx.rule, isPaused) } currentFingerprint = f if isPaused { @@ -494,8 +486,7 @@ func (a *alertRule) expireAndSend(ctx context.Context, states []state.StateTrans } } -func (a *alertRule) resetState(ctx context.Context, isPaused bool) { - rule := a.ruleProvider.get(a.key.AlertRuleKey) +func (a *alertRule) resetState(ctx context.Context, rule *ngmodels.AlertRule, isPaused bool) { reason := ngmodels.StateReasonUpdated if isPaused { reason = ngmodels.StateReasonPaused diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 02ea0dd5735..b9f66875c6b 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -43,7 +43,7 @@ func TestAlertRule(t *testing.T) { r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) resultCh := make(chan bool) go func() { - resultCh <- r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + resultCh <- r.Update(&Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()}) }() select { case <-r.updateCh: @@ -54,8 +54,8 @@ func TestAlertRule(t *testing.T) { }) t.Run("update should drop any concurrent sending to updateCh", func(t *testing.T) { r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) - version1 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} - version2 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} + version1 := &Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()} + version2 := &Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()} wg := sync.WaitGroup{} wg.Add(1) @@ -178,7 +178,7 @@ func TestAlertRule(t *testing.T) { r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) r.Stop(errRuleDeleted) require.ErrorIs(t, r.ctx.Err(), errRuleDeleted) - require.False(t, r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})) + require.False(t, r.Update(&Evaluation{rule: gen.GenerateRef()})) }) t.Run("eval should do nothing", func(t *testing.T) { ruleSpec := gen.GenerateRef() @@ -234,7 +234,7 @@ func TestAlertRule(t *testing.T) { } switch rand.Intn(max) + 1 { case 1: - r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + r.Update(&Evaluation{rule: gen.GenerateRef()}) case 2: r.Eval(&Evaluation{ scheduledAt: time.Now(), @@ -284,7 +284,7 @@ func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *a Log: log.NewNopLogger(), } st := state.NewManager(managerCfg, state.NewNoopPersister()) - return newAlertRule(ctx, key, nil, false, 0, nil, st, nil, nil, nil, nil, log.NewNopLogger(), nil, nil, nil) + return newAlertRule(ctx, key, nil, false, 0, nil, st, nil, nil, nil, log.NewNopLogger(), nil, nil, nil) } func TestRuleRoutine(t *testing.T) { @@ -572,7 +572,6 @@ func TestRuleRoutine(t *testing.T) { t.Run("when a message is sent to update channel", func(t *testing.T) { rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef() folderTitle := "folderName" - ruleFp := ruleWithFolder{rule, folderTitle}.Fingerprint() evalAppliedChan := make(chan time.Time) @@ -628,8 +627,8 @@ func TestRuleRoutine(t *testing.T) { require.Greaterf(t, expectedToBeSent, 0, "State manager was expected to return at least one state that can be expired") t.Run("should do nothing if version in channel is the same", func(t *testing.T) { - ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp, false}) - ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp, false}) // second time just to make sure that previous messages were handled + ruleInfo.Update(&Evaluation{rule: rule, folderTitle: folderTitle}) + ruleInfo.Update(&Evaluation{rule: rule, folderTitle: folderTitle}) // second time just to make sure that previous messages were handled actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) require.Len(t, actualStates, len(states)) @@ -638,7 +637,7 @@ func TestRuleRoutine(t *testing.T) { }) t.Run("should clear the state and expire firing alerts if version in channel is greater", func(t *testing.T) { - ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp + 1, false}) + ruleInfo.Update(&Evaluation{rule: models.CopyRule(rule, gen.WithTitle(util.GenerateShortUID())), folderTitle: folderTitle}) require.Eventually(t, func() bool { return len(sender.Calls()) > 0 @@ -905,7 +904,7 @@ func TestRuleRoutine(t *testing.T) { } func ruleFactoryFromScheduler(sch *schedule) ruleFactory { - return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, &sch.schedulableAlertRules, sch.clock, sch.rrCfg, sch.metrics, sch.log, sch.tracer, sch.recordingWriter, sch.evalAppliedFunc, sch.stopAppliedFunc) + return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, sch.clock, sch.rrCfg, sch.metrics, sch.log, sch.tracer, sch.recordingWriter, sch.evalAppliedFunc, sch.stopAppliedFunc) } func stateForRule(rule *models.AlertRule, ts time.Time, evalState eval.State) *state.State { diff --git a/pkg/services/ngalert/schedule/recording_rule.go b/pkg/services/ngalert/schedule/recording_rule.go index 954b0869a4a..6c3f733a5ca 100644 --- a/pkg/services/ngalert/schedule/recording_rule.go +++ b/pkg/services/ngalert/schedule/recording_rule.go @@ -113,7 +113,7 @@ func (r *recordingRule) Eval(eval *Evaluation) (bool, *Evaluation) { } } -func (r *recordingRule) Update(lastVersion RuleVersionAndPauseStatus) bool { +func (r *recordingRule) Update(_ *Evaluation) bool { return true } diff --git a/pkg/services/ngalert/schedule/recording_rule_test.go b/pkg/services/ngalert/schedule/recording_rule_test.go index 7e72453bb9f..c880b1d417f 100644 --- a/pkg/services/ngalert/schedule/recording_rule_test.go +++ b/pkg/services/ngalert/schedule/recording_rule_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -116,7 +117,10 @@ func TestRecordingRule(t *testing.T) { } switch rand.Intn(max) + 1 { case 1: - r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + r.Update(&Evaluation{ + rule: gen.GenerateRef(), + folderTitle: util.GenerateShortUID(), + }) case 2: r.Eval(&Evaluation{ scheduledAt: time.Now(), @@ -492,7 +496,7 @@ func TestRecordingRule_Integration(t *testing.T) { t.Run("status shows evaluation", func(t *testing.T) { status := process.(*recordingRule).Status() - //TODO: assert "error" to fix test, update to "nodata" in the future + // TODO: assert "error" to fix test, update to "nodata" in the future require.Equal(t, "error", status.Health) }) }) diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index 11f72229f51..8ef0c87ddde 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -87,11 +87,6 @@ func (r *ruleRegistry) keyMap() map[models.AlertRuleKey]struct{} { return definitionsIDs } -type RuleVersionAndPauseStatus struct { - Fingerprint fingerprint - IsPaused bool -} - type Evaluation struct { scheduledAt time.Time rule *models.AlertRule diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 3bc21115ca0..0416fa2fb7e 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -302,7 +302,6 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. sch.alertsSender, sch.stateManager, sch.evaluatorFactory, - &sch.schedulableAlertRules, sch.clock, sch.rrCfg, sch.metrics, @@ -372,9 +371,9 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. // if we do not need to eval the rule, check the whether rule was just updated and if it was, notify evaluation routine about that logger.Debug("Rule has been updated. Notifying evaluation routine") go func(routine Rule, rule *ngmodels.AlertRule) { - routine.Update(RuleVersionAndPauseStatus{ - Fingerprint: ruleWithFolder{rule: rule, folderTitle: folderTitle}.Fingerprint(), - IsPaused: rule.IsPaused, + routine.Update(&Evaluation{ + rule: rule, + folderTitle: folderTitle, }) }(ruleRoutine, item) updatedRules = append(updatedRules, ngmodels.AlertRuleKeyWithVersion{ From 4c021aac7a9a8ff54927841f03d9be9f5dbd0000 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:51:10 -0600 Subject: [PATCH 40/51] Table: Remove actions from context menu (#101318) --- .../DataLinks/DataLinksContextMenu.test.tsx | 41 +------------------ .../DataLinks/DataLinksContextMenu.tsx | 13 ++---- .../src/components/Table/BarGaugeCell.tsx | 7 ++-- .../src/components/Table/DefaultCell.tsx | 8 ++-- .../src/components/Table/ImageCell.tsx | 6 +-- .../src/components/Table/JSONViewCell.tsx | 7 ++-- packages/grafana-ui/src/utils/dataLinks.ts | 13 +----- 7 files changed, 18 insertions(+), 77 deletions(-) diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx index cb7870abfad..64d7049d8d4 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx @@ -6,7 +6,7 @@ import { DataLinksContextMenu } from './DataLinksContextMenu'; const fakeAriaLabel = 'fake aria label'; describe('DataLinksContextMenu', () => { - it('renders context menu when there are more than one data links or actions', () => { + it('renders context menu when there are more than one data links', () => { render( [ @@ -23,7 +23,6 @@ describe('DataLinksContextMenu', () => { origin: {}, }, ]} - actions={[{ title: 'Action1', onClick: () => {} }]} > {() => { return
; @@ -35,43 +34,7 @@ describe('DataLinksContextMenu', () => { expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0); }); - it('renders context menu when there are actions and one data link', () => { - render( - [ - { - href: '/link1', - title: 'Link1', - target: '_blank', - origin: {}, - }, - ]} - actions={[{ title: 'Action1', onClick: () => {} }]} - > - {() => { - return
; - }} - - ); - - expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument(); - expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0); - }); - - it('renders context menu when there are only actions', () => { - render( - []} actions={[{ title: 'Action1', onClick: () => {} }]}> - {() => { - return
; - }} - - ); - - expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument(); - expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0); - }); - - it('renders link when there is a single data link and no actions', () => { + it('renders link when there is a single data link', () => { render( [ diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index 8633ade3318..928811bb173 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -2,11 +2,11 @@ import { css } from '@emotion/css'; import { CSSProperties } from 'react'; import * as React from 'react'; -import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../themes'; -import { actionModelToContextMenuItems, linkModelToContextMenuItems } from '../../utils/dataLinks'; +import { linkModelToContextMenuItems } from '../../utils/dataLinks'; import { WithContextMenu } from '../ContextMenu/WithContextMenu'; import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup'; import { MenuItem } from '../Menu/MenuItem'; @@ -15,7 +15,6 @@ export interface DataLinksContextMenuProps { children: (props: DataLinksContextMenuApi) => JSX.Element; links: () => LinkModel[]; style?: CSSProperties; - actions?: ActionModel[]; } export interface DataLinksContextMenuApi { @@ -23,16 +22,12 @@ export interface DataLinksContextMenuApi { targetClassName?: string; } -export const DataLinksContextMenu = ({ children, links, actions, style }: DataLinksContextMenuProps) => { +export const DataLinksContextMenu = ({ children, links, style }: DataLinksContextMenuProps) => { const styles = useStyles2(getStyles); const itemsGroup: MenuItemsGroup[] = [ { items: linkModelToContextMenuItems(links), label: Boolean(links().length) ? 'Data links' : '' }, ]; - const hasActions = Boolean(actions?.length); - if (hasActions) { - itemsGroup.push({ items: actionModelToContextMenuItems(actions!), label: 'Actions' }); - } const linksCounter = itemsGroup[0].items.length; const renderMenuGroupItems = () => { @@ -59,7 +54,7 @@ export const DataLinksContextMenu = ({ children, links, actions, style }: DataLi cursor: 'context-menu', }); - if (linksCounter > 1 || hasActions) { + if (linksCounter > 1) { return ( {({ openMenu }) => { diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index f22643b3ba8..a427451f0c8 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -24,7 +24,7 @@ const defaultScale: ThresholdsConfig = { }; export const BarGaugeCell = (props: TableCellProps) => { - const { field, innerWidth, tableStyles, cell, cellProps, row, actions } = props; + const { field, innerWidth, tableStyles, cell, cellProps, row } = props; const displayValue = field.display!(cell.value); const cellOptions = getCellOptions(field); @@ -56,7 +56,6 @@ export const BarGaugeCell = (props: TableCellProps) => { }; const hasLinks = Boolean(getLinks().length); - const hasActions = Boolean(actions?.length); const alignmentFactors = getAlignmentFactor(field, displayValue, cell.row.index); const renderComponent = (menuProps: DataLinksContextMenuApi) => { @@ -85,8 +84,8 @@ export const BarGaugeCell = (props: TableCellProps) => { return (
- {hasLinks || hasActions ? ( - + {hasLinks ? ( + {(api) => renderComponent(api)} ) : ( diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 652aae07f0c..2ce27572494 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -17,8 +17,7 @@ import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './typ import { getCellColors, getCellOptions } from './utils'; export const DefaultCell = (props: TableCellProps) => { - const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height, actions } = - props; + const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height } = props; const inspectEnabled = Boolean(field.config.custom?.inspect); const displayValue = field.display!(cell.value); @@ -26,7 +25,6 @@ export const DefaultCell = (props: TableCellProps) => { const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; const cellOptions = getCellOptions(field); const hasLinks = Boolean(getCellLinks(field, row)?.length); - const hasActions = Boolean(actions?.length); const clearButtonStyle = useStyles2(clearLinkButtonStyles); let value: string | ReactElement; @@ -81,8 +79,8 @@ export const DefaultCell = (props: TableCellProps) => { return (
- {hasLinks || hasActions ? ( - getCellLinks(field, row) || []} actions={actions}> + {hasLinks ? ( + getCellLinks(field, row) || []}> {(api) => { if (api.openMenu) { return ( diff --git a/packages/grafana-ui/src/components/Table/ImageCell.tsx b/packages/grafana-ui/src/components/Table/ImageCell.tsx index 65fce6214d8..40f8b03dd29 100644 --- a/packages/grafana-ui/src/components/Table/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/ImageCell.tsx @@ -9,13 +9,12 @@ import { getCellOptions } from './utils'; const DATALINKS_HEIGHT_OFFSET = 10; export const ImageCell = (props: TableCellProps) => { - const { field, cell, tableStyles, row, cellProps, actions } = props; + const { field, cell, tableStyles, row, cellProps } = props; const cellOptions = getCellOptions(field); const { title, alt } = cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { title: undefined, alt: undefined }; const displayValue = field.display!(cell.value); const hasLinks = Boolean(getCellLinks(field, row)?.length); - const hasActions = Boolean(actions?.length); // The image element const img = ( @@ -32,11 +31,10 @@ export const ImageCell = (props: TableCellProps) => {
{/* If there are data links/actions, we render them with image */} {/* Otherwise we simply render the image */} - {hasLinks || hasActions ? ( + {hasLinks ? ( getCellLinks(field, row) || []} - actions={actions} > {(api) => { if (api.openMenu) { diff --git a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx index ea2a3092576..e5f9e5916a8 100644 --- a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx @@ -11,7 +11,7 @@ import { TableCellInspectorMode } from './TableCellInspector'; import { TableCellProps } from './types'; export function JSONViewCell(props: TableCellProps): JSX.Element { - const { cell, tableStyles, cellProps, field, row, actions } = props; + const { cell, tableStyles, cellProps, field, row } = props; const inspectEnabled = Boolean(field.config.custom?.inspect); const txt = css({ cursor: 'pointer', @@ -30,14 +30,13 @@ export function JSONViewCell(props: TableCellProps): JSX.Element { } const hasLinks = Boolean(getCellLinks(field, row)?.length); - const hasActions = Boolean(actions?.length); const clearButtonStyle = useStyles2(clearLinkButtonStyles); return (
- {hasLinks || hasActions ? ( - getCellLinks(field, row) || []} actions={actions}> + {hasLinks ? ( + getCellLinks(field, row) || []}> {(api) => { if (api.openMenu) { return ( diff --git a/packages/grafana-ui/src/utils/dataLinks.ts b/packages/grafana-ui/src/utils/dataLinks.ts index f889cf5a2a0..5d2a6db23fd 100644 --- a/packages/grafana-ui/src/utils/dataLinks.ts +++ b/packages/grafana-ui/src/utils/dataLinks.ts @@ -1,4 +1,4 @@ -import { ActionModel, LinkModel } from '@grafana/data'; +import { LinkModel } from '@grafana/data'; import { MenuItemProps } from '../components/Menu/MenuItem'; @@ -18,14 +18,3 @@ export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItem }; }); }; - -export const actionModelToContextMenuItems: (actions: ActionModel[]) => MenuItemProps[] = (actions) => { - return actions.map((action) => { - return { - label: action.title, - ariaLabel: action.title, - icon: 'record-audio', - onClick: action.onClick, - }; - }); -}; From 2b0029267cfbcc65793a4922919afc4a2748c0a0 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:15:01 -0600 Subject: [PATCH 41/51] Actions: Remove `vizActions` feature toggle (#100309) --- .../configure-grafana/feature-toggles/index.md | 1 - packages/grafana-data/src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 8 -------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 3 ++- public/app/core/components/OptionsUI/registry.tsx | 4 +--- public/app/features/actions/ActionEditor.tsx | 6 +----- .../panel/canvas/editor/element/elementEditor.tsx | 7 +------ public/app/plugins/panel/canvas/editor/options.ts | 4 +--- public/app/plugins/panel/status-history/utils.ts | 5 ----- public/app/plugins/panel/table/TableCellOptionEditor.tsx | 9 +-------- public/app/plugins/panel/table/TablePanel.tsx | 4 ---- public/locales/en-US/grafana.json | 2 +- public/locales/pseudo-LOCALE/grafana.json | 2 +- 15 files changed, 9 insertions(+), 52 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 699ecbe13c5..f40ad6a1766 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -130,7 +130,6 @@ Experimental features might be changed or removed without prior notice. | `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | | `storage` | Configurable storage for dashboards, datasources, and resources | | `canvasPanelNesting` | Allow elements nesting | -| `vizActions` | Allow actions in visualizations | | `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | | `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | | `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 85ba3f238c5..87c5ff7c0d1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -36,7 +36,6 @@ export interface FeatureToggles { autoMigrateStatPanel?: boolean; disableAngular?: boolean; canvasPanelNesting?: boolean; - vizActions?: boolean; disableSecretsCompatibility?: boolean; logRequestsInstrumentedAsUnknown?: boolean; grpcServer?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a3a8a86191d..d84ae066775 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -150,14 +150,6 @@ var ( Owner: grafanaDatavizSquad, HideFromAdminPage: true, }, - { - Name: "vizActions", - Description: "Allow actions in visualizations", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaDatavizSquad, - HideFromAdminPage: true, - }, { Name: "disableSecretsCompatibility", Description: "Disable duplicated secret storage in legacy tables", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 065323e011e..feb0e5d5d4b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -17,7 +17,6 @@ autoMigrateWorldmapPanel,preview,@grafana/dataviz-squad,false,false,true autoMigrateStatPanel,preview,@grafana/dataviz-squad,false,false,true disableAngular,preview,@grafana/dataviz-squad,false,false,true canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true -vizActions,experimental,@grafana/dataviz-squad,false,false,true disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,true,false logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false grpcServer,preview,@grafana/search-and-storage,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b2d704e7a10..56c3a7d53da 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -79,10 +79,6 @@ const ( // Allow elements nesting FlagCanvasPanelNesting = "canvasPanelNesting" - // FlagVizActions - // Allow actions in visualizations - FlagVizActions = "vizActions" - // FlagDisableSecretsCompatibility // Disable duplicated secret storage in legacy tables FlagDisableSecretsCompatibility = "disableSecretsCompatibility" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0d6059d5520..569c2cbfc1d 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -4202,7 +4202,8 @@ "metadata": { "name": "vizActions", "resourceVersion": "1722461779830", - "creationTimestamp": "2024-09-09T14:11:55Z" + "creationTimestamp": "2024-09-09T14:11:55Z", + "deletionTimestamp": "2025-02-07T18:50:26Z" }, "spec": { "description": "Allow actions in visualizations", diff --git a/public/app/core/components/OptionsUI/registry.tsx b/public/app/core/components/OptionsUI/registry.tsx index 519a0f0c338..28fcc7ab9e6 100644 --- a/public/app/core/components/OptionsUI/registry.tsx +++ b/public/app/core/components/OptionsUI/registry.tsx @@ -30,7 +30,6 @@ import { DataLinksFieldConfigSettings, } from '@grafana/data'; import { actionsOverrideProcessor } from '@grafana/data/src/field/overrides/processors'; -import { config } from '@grafana/runtime'; import { FieldConfig } from '@grafana/schema'; import { RadioButtonGroup, TimeZonePicker, Switch } from '@grafana/ui'; import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker'; @@ -349,7 +348,7 @@ export const getAllStandardFieldConfigs = () => { category, }; - const dataLinksCategory = config.featureToggles.vizActions ? 'Data links and actions' : 'Data links'; + const dataLinksCategory = 'Data links and actions'; const links: FieldConfigPropertyItem = { id: 'links', @@ -379,7 +378,6 @@ export const getAllStandardFieldConfigs = () => { shouldApply: () => true, category: [dataLinksCategory], getItemsCount: (value) => (value ? value.length : 0), - showIf: () => config.featureToggles.vizActions, hideFromDefaults: true, }; diff --git a/public/app/features/actions/ActionEditor.tsx b/public/app/features/actions/ActionEditor.tsx index 4194ab2680c..81500b9332a 100644 --- a/public/app/features/actions/ActionEditor.tsx +++ b/public/app/features/actions/ActionEditor.tsx @@ -2,7 +2,6 @@ import { css } from '@emotion/css'; import { memo } from 'react'; import { Action, GrafanaTheme2, httpMethodOptions, HttpRequestMethod, VariableSuggestion } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { Switch } from '@grafana/ui/'; import { Field } from '@grafana/ui/src/components/Forms/Field'; import { InlineField } from '@grafana/ui/src/components/Forms/InlineField'; @@ -108,8 +107,6 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions, showOne value.fetch.method !== HttpRequestMethod.GET && value.fetch.headers?.some(([name, value]) => name === 'Content-Type' && value === 'application/json'); - const action = config.featureToggles.vizActions ? 'or action' : ''; - return (
@@ -147,8 +144,7 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions, showOne label={t('grafana-ui.data-link-inline-editor.one-click', 'One click')} description={t( 'grafana-ui.action-editor.modal.one-click-description', - 'Only one link {{ action }} can have one click enabled at a time', - { action } + 'Only one link or action can have one click enabled at a time' )} > diff --git a/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx b/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx index 78562e83e88..bdad1130a50 100644 --- a/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx @@ -1,7 +1,6 @@ import { get as lodashGet } from 'lodash'; import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders'; -import { config } from '@grafana/runtime'; import { CanvasElementOptions } from 'app/features/canvas/element'; import { canvasElementRegistry, @@ -67,8 +66,6 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions< const current = options?.type ? options.type : DEFAULT_CANVAS_ELEMENT_CONFIG.type; const layerTypes = getElementTypes(opts.scene.shouldShowAdvancedTypes, current).options; - const actionsEnabled = config.featureToggles.vizActions; - const isUnsupported = !opts.scene.shouldShowAdvancedTypes && !defaultElementItems.filter((item) => item.id === options?.type).length; @@ -123,9 +120,7 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions< } optionBuilder.addDataLinks(builder, ctx); - if (actionsEnabled) { - optionBuilder.addActions(builder, ctx); - } + optionBuilder.addActions(builder, ctx); }, }; } diff --git a/public/app/plugins/panel/canvas/editor/options.ts b/public/app/plugins/panel/canvas/editor/options.ts index 954478b88dd..221ab5f7979 100644 --- a/public/app/plugins/panel/canvas/editor/options.ts +++ b/public/app/plugins/panel/canvas/editor/options.ts @@ -2,7 +2,6 @@ import { capitalize } from 'lodash'; import { FieldType } from '@grafana/data'; import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; -import { config } from '@grafana/runtime'; import { ConnectionDirection } from 'app/features/canvas/element'; import { SVGElements } from 'app/features/canvas/runtime/element'; import { ColorDimensionEditor, ResourceDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors'; @@ -213,7 +212,7 @@ export const optionBuilder: OptionSuppliers = { addDataLinks: (builder, context) => { builder.addCustomEditor({ - category: config.featureToggles.vizActions ? ['Data links and actions'] : ['Data links'], + category: ['Data links and actions'], id: 'dataLinks', path: 'links', name: 'Links', @@ -230,7 +229,6 @@ export const optionBuilder: OptionSuppliers = { name: 'Actions', editor: ActionsEditor, settings: context.options, - showIf: () => config.featureToggles.vizActions, }); }, }; diff --git a/public/app/plugins/panel/status-history/utils.ts b/public/app/plugins/panel/status-history/utils.ts index 9651797df10..cd683b7129c 100644 --- a/public/app/plugins/panel/status-history/utils.ts +++ b/public/app/plugins/panel/status-history/utils.ts @@ -1,6 +1,5 @@ import { ActionModel, Field, InterpolateFunction, LinkModel } from '@grafana/data'; import { DataFrame } from '@grafana/data/'; -import { config } from '@grafana/runtime'; import { getActions } from 'app/features/actions/utils'; export const getDataLinks = (field: Field, rowIdx: number) => { @@ -32,10 +31,6 @@ export const getFieldActions = ( replaceVars: InterpolateFunction, rowIndex: number ) => { - if (!config.featureToggles?.vizActions) { - return []; - } - const actions: Array> = []; const actionLookup = new Set(); diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index cff753a5344..39b2059d760 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -3,7 +3,6 @@ import { merge } from 'lodash'; import { useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { TableCellOptions } from '@grafana/schema'; import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui'; @@ -91,15 +90,9 @@ let cellDisplayModeOptions: Array> = [ { value: { type: TableCellDisplayMode.DataLinks }, label: 'Data links' }, { value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' }, { value: { type: TableCellDisplayMode.Image }, label: 'Image' }, + { value: { type: TableCellDisplayMode.Actions }, label: 'Actions' }, ]; -if (config.featureToggles.vizActions) { - cellDisplayModeOptions = [ - ...cellDisplayModeOptions, - { value: { type: TableCellDisplayMode.Actions }, label: 'Actions' }, - ]; -} - const getStyles = (theme: GrafanaTheme2) => ({ fixBottomMargin: css({ marginBottom: theme.spacing(-2), diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 3945829809b..527389c9e90 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -152,10 +152,6 @@ const getCellActions = ( rowIndex: number, replaceVariables: InterpolateFunction | undefined ) => { - if (!config.featureToggles?.vizActions) { - return []; - } - const actions: Array> = []; const actionLookup = new Set(); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 366b60a9da0..2a6465e6357 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1703,7 +1703,7 @@ "action-query-params": "Query parameters", "action-title": "Title", "action-title-placeholder": "Action title", - "one-click-description": "Only one link {{ action }} can have one click enabled at a time" + "one-click-description": "Only one link or action can have one click enabled at a time" } }, "alert": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 23e1bebb722..3eea1ed364a 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1703,7 +1703,7 @@ "action-query-params": "Qūęřy päřämęŧęřş", "action-title": "Ŧįŧľę", "action-title-placeholder": "Åčŧįőʼn ŧįŧľę", - "one-click-description": "Øʼnľy őʼnę ľįʼnĸ {{ action }} čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę" + "one-click-description": "Øʼnľy őʼnę ľįʼnĸ őř äčŧįőʼn čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę" } }, "alert": { From e0a4a69c2eebf5832fd88a3f537849aafd5b5208 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:35:30 -0500 Subject: [PATCH 42/51] Docs: Add actions to visualizations (#100684) --- .../configure-data-links/index.md | 113 ++++++++++++++---- .../visualizations/bar-chart/index.md | 2 +- .../visualizations/bar-gauge/index.md | 2 +- .../visualizations/candlestick/index.md | 2 +- .../visualizations/gauge/index.md | 2 +- .../visualizations/geomap/index.md | 2 +- .../visualizations/heatmap/index.md | 2 +- .../visualizations/histogram/index.md | 2 +- .../visualizations/pie-chart/index.md | 2 +- .../visualizations/stat/index.md | 2 +- .../visualizations/state-timeline/index.md | 2 +- .../visualizations/status-history/index.md | 2 +- .../visualizations/table/index.md | 4 +- .../visualizations/time-series/index.md | 2 +- .../visualizations/trend/index.md | 2 +- .../visualizations/xy-chart/index.md | 2 +- .../visualizations/datalink-options-1.md | 13 +- .../visualizations/datalink-options-2.md | 24 +++- .../visualizations/datalink-options-3.md | 36 ++++++ .../shared/visualizations/datalink-options.md | 11 +- 20 files changed, 183 insertions(+), 46 deletions(-) create mode 100644 docs/sources/shared/visualizations/datalink-options-3.md diff --git a/docs/sources/panels-visualizations/configure-data-links/index.md b/docs/sources/panels-visualizations/configure-data-links/index.md index 0245bc75eb5..2b057694e6c 100644 --- a/docs/sources/panels-visualizations/configure-data-links/index.md +++ b/docs/sources/panels-visualizations/configure-data-links/index.md @@ -18,11 +18,16 @@ labels: - cloud - enterprise - oss -menuTitle: Configure data links -title: Configure data links +menuTitle: Configure data links and actions +title: Configure data links and actions description: Configure data links to create links between dashboards and link to external resources weight: 80 refs: + api-settings: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/visualizations/canvas/#button-api-options + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana/latest/panels-visualizations/visualizations/canvas/#button-api-options global-variables: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/add-template-variables/#__from-and-__to @@ -125,9 +130,14 @@ refs: destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-chart/ --- -# Configure data links +# Configure data links and actions -Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. For example, if your visualization shows four servers, you can add a data link to one or two of them. +_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls. +In both cases, you can carry out these tasks while maintaining the context of the source panel. + +## Data links + +With data links, you can create links that include the series name or even the value under the cursor. For example, if your visualization shows four servers, you can add a data link to one or two of them. The link itself is accessible in different ways depending on the visualization. For the time series visualization you need to click a data point or line: @@ -139,7 +149,7 @@ For visualizations like stat, gauge, or bar gauge you can click anywhere on the If there's only one data link in the visualization, clicking anywhere on the visualization opens the link rather than the context menu. -## Supported visualizations +### Supported visualizations You can configure data links for the following visualizations: @@ -164,11 +174,32 @@ You can configure data links for the following visualizations: {{< /column-list >}} -## Data link variables +## Actions -Variables in data links let you send people to a detailed dashboard with preserved data filters. For example, you could use variables to specify a label, time range, series, or variable selection. +Using actions, you can trigger processes like starting or shutting down a server, directly from a dashboard panel. [API settings](ref:api-settings) are configured in the **Add action** dialog box. You can also pass variables in the API editor. -To see a list of available variables, enter `$` in the data link **URL** field. +### Supported visualizations + +You can configure actions for the following visualizations: + +{{< column-list >}} + +- [Bar chart](ref:bar-chart) +- [Candlestick](ref:candlestick) +- [State timeline](ref:state-timeline) +- [Status history](ref:status-history) +- [Table](ref:table) +- [Time series](ref:time-series) +- [Trend](ref:trend) +- [XY chart](ref:xy-chart) + +{{< /column-list >}} + +## Data link and action variables {#data-link-variables} + +Variables in data links and actions let you send people to a detailed dashboard or trigger an API call with preserved data filters. For example, you could use variables to specify a label, time range, series, or variable selection. + +To see a list of available variables, enter `$` in the data link or action **URL** field. {{% admonition type="note" %}} These variables changed in 6.4 so if you have an older version of Grafana, then use the version picker to select docs for an older version of Grafana. @@ -176,11 +207,11 @@ These variables changed in 6.4 so if you have an older version of Grafana, then Azure Monitor, [CloudWatch](ref:cloudwatch), and [Google Cloud Monitoring](ref:google-cloud-monitoring) have pre-configured data links called _deep links_. -You can also use template variables in your data links URLs. For more information, refer to [Templates and variables](ref:templates-and-variables). +You can also use template variables in your data links or actions URLs. For more information, refer to [Templates and variables](ref:templates-and-variables). ### Time range panel variables -These variables allow you to include the current time range in the data link URL: +These variables allow you to include the current time range in the data link or action URL: | Variable | Description | | ------------------ | ------------------------------------------------------------------------ | @@ -188,7 +219,7 @@ These variables allow you to include the current time range in the data link URL | `__from` | For more information, refer to [Global variables](ref:global-variables). | | `__to` | For more information, refer to [Global variables](ref:global-variables). | -When you create data links using time range variables like `__url_time_range` in the URL, you have to form the query parameter syntax yourself; that is, you must format the URL by appending query parameters using the question mark (`?`) and ampersand (`&`) syntax. These characters aren't automatically generated. +When you create data links and actions using time range variables like `__url_time_range` in the URL, you have to form the query parameter syntax yourself; that is, you must format the URL by appending query parameters using the question mark (`?`) and ampersand (`&`) syntax. These characters aren't automatically generated. ### Series variables @@ -219,9 +250,9 @@ Value-specific variables are available under `__value` namespace: | `__value.text` | Text representation of a value | | `__value.calc` | Calculation name if the value is result of calculation | -Using value-specific variables in data links can show different results depending on the set option of Tooltip mode. +Using value-specific variables in data links and actions can show different results depending on the set option of Tooltip mode. -When you create data links using time range variables like `__value.time` in the URL, you have to form the query parameter syntax yourself; that is, you must add the question mark (`?`) and ampersand (`&`). These characters aren't automatically generated. +When you create data links and actions using time range variables like `__value.time` in the URL, you have to form the query parameter syntax yourself; that is, you must add the question mark (`?`) and ampersand (`&`). These characters aren't automatically generated. ### Data variables @@ -247,20 +278,29 @@ When linking to another dashboard that uses template variables, select variable If you want to add all of the current dashboard's variables to the URL, then use `${__all_variables}`. -## Add a data link +## Add data links or actions {#add-a-data-link} + +The following tasks describe how to configure data links and actions. + +{{< tabs >}} +{{< tab-content name="Add data links" >}} +To add a data link, follow these steps: 1. Navigate to the panel to which you want to add the data link. 1. Hover over any part of the panel to display the menu icon in the upper-right corner. 1. Click the menu icon and select **Edit** to open the panel editor. -1. In the panel edit pane, scroll down to the **Data links** section and expand it. +1. Scroll down to the **Data links and actions** section and expand it. 1. Click **+ Add link**. -1. In the dialog box that opens, enter a **Title**. This is a human-readable label for the link displayed in the UI. This is a required field. -1. Enter the **URL** or variable to which you want to link. This is a required field. +1. In the dialog box that opens, enter a **Title**. - To add a data link variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. + This is a human-readable label for the link displayed in the UI. This is a required field. + +1. Enter the **URL** to which you want to link. + + To add a data link variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. This is a required field. 1. If you want the link to open in a new tab, toggle the **Open in a new tab** switch. -1. If you want the data link to open with a single click, toggle the **One click** switch. +1. If you want the data link to open with a single click on the visualization, toggle the **One click** switch. Only one data link can have **One click** enabled at a time. **One click** is only supported for some visualizations. @@ -268,4 +308,37 @@ If you want to add all of the current dashboard's variables to the URL, then use 1. Click **Save dashboard**. 1. Click **Back to dashboard** and then **Exit edit**. -If you add multiple data links, you can control the order in which they appear in the visualization. To do this, click and drag the data link to the desired position. + {{< /tab-content >}} + {{< tab-content name="Add actions" >}} + + {{< admonition type="note">}} + Actions are not supported for all visualizations. For the list of supported visualizations, refer to [Supported visualizations](#supported-visualizations-1). + {{< /admonition >}} + + To add an action, by follow these steps: + +1. Navigate to the panel to which you want to add the action. +1. Hover over any part of the panel to display the menu icon in the upper-right corner. +1. Click the menu icon and select **Edit** to open the panel editor. +1. Scroll down to the **Data links and actions** section and expand it. +1. Click **+ Add action**. +1. In the dialog box that opens, define the API call settings: + + | Option | Description | + | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | + | Title | A human-readable label for the action that's displayed in the UI. | + | Confirmation message | A descriptive prompt to confirm or cancel the action. | + | Method | Select from **POST**, **PUT**, or **GET**. | + | URL | The request URL.

To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. | + | Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. | + | Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.

Click the **+** icon to add as many key/value pairs as you need. | + | Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. | + | Body | The body of the request. | + +1. Click **Save** to save changes and close the dialog box. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. + {{< /tab-content >}} + {{< /tabs >}} + +If you add multiple data links or actions, you can control the order in which they appear in the visualization. To do this, click and drag the data link or action to the desired position. diff --git a/docs/sources/panels-visualizations/visualizations/bar-chart/index.md b/docs/sources/panels-visualizations/visualizations/bar-chart/index.md index c9aaf77688b..6de1af95513 100644 --- a/docs/sources/panels-visualizations/visualizations/bar-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/bar-chart/index.md @@ -199,7 +199,7 @@ You can set standard min/max options to define hard limits of the Y-axis. For mo {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md b/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md index cc70b2bbb20..3fd9b5c24ea 100644 --- a/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md +++ b/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md @@ -211,7 +211,7 @@ This option only applies when bar size is set to manual. {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/candlestick/index.md b/docs/sources/panels-visualizations/visualizations/candlestick/index.md index c673509a446..bbaccb65254 100644 --- a/docs/sources/panels-visualizations/visualizations/candlestick/index.md +++ b/docs/sources/panels-visualizations/visualizations/candlestick/index.md @@ -136,7 +136,7 @@ The candlestick visualization is based on the time series visualization. It can {{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/gauge/index.md b/docs/sources/panels-visualizations/visualizations/gauge/index.md index fb6b224a12e..fbb1cb922ae 100644 --- a/docs/sources/panels-visualizations/visualizations/gauge/index.md +++ b/docs/sources/panels-visualizations/visualizations/gauge/index.md @@ -171,7 +171,7 @@ Adjust the sizes of the gauge text. {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/geomap/index.md b/docs/sources/panels-visualizations/visualizations/geomap/index.md index b42d1f78a6c..64b20d33dd7 100644 --- a/docs/sources/panels-visualizations/visualizations/geomap/index.md +++ b/docs/sources/panels-visualizations/visualizations/geomap/index.md @@ -711,7 +711,7 @@ Displays debug information in the upper right corner. This can be useful for deb {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/heatmap/index.md b/docs/sources/panels-visualizations/visualizations/heatmap/index.md index 99bd1f53e68..a51479e4e7a 100644 --- a/docs/sources/panels-visualizations/visualizations/heatmap/index.md +++ b/docs/sources/panels-visualizations/visualizations/heatmap/index.md @@ -208,7 +208,7 @@ Choose whether you want to display the heatmap legend on the visualization by to Set the color used to show exemplar data. -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/histogram/index.md b/docs/sources/panels-visualizations/visualizations/histogram/index.md index 70fcc675d02..f005c8ad7cb 100644 --- a/docs/sources/panels-visualizations/visualizations/histogram/index.md +++ b/docs/sources/panels-visualizations/visualizations/histogram/index.md @@ -157,7 +157,7 @@ Choose from the following: {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md index 83efab500f9..679f4e61f3b 100644 --- a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md @@ -174,7 +174,7 @@ Use these settings to define how the legend appears in your visualization. For m {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/stat/index.md b/docs/sources/panels-visualizations/visualizations/stat/index.md index 8eca8f63b29..e1ad7fada4a 100644 --- a/docs/sources/panels-visualizations/visualizations/stat/index.md +++ b/docs/sources/panels-visualizations/visualizations/stat/index.md @@ -172,7 +172,7 @@ Adjust the sizes of the gauge text. {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md index cc861506203..192bcb8fdb1 100644 --- a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md +++ b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md @@ -157,7 +157,7 @@ The **Page size** option lets you paginate the state timeline visualization to l {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/status-history/index.md b/docs/sources/panels-visualizations/visualizations/status-history/index.md index 998ff72e251..0382deffd77 100644 --- a/docs/sources/panels-visualizations/visualizations/status-history/index.md +++ b/docs/sources/panels-visualizations/visualizations/status-history/index.md @@ -143,7 +143,7 @@ Controls the opacity of state regions. {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index c53a0f7adff..96c95f437d2 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -362,9 +362,9 @@ If you want to apply this setting to only some fields instead of all fields, you {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions -{{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} +{{< docs/shared lookup="visualizations/datalink-options-3.md" source="grafana" version="" >}} ### Value mappings diff --git a/docs/sources/panels-visualizations/visualizations/time-series/index.md b/docs/sources/panels-visualizations/visualizations/time-series/index.md index 2031d0db76a..7b3dec5e5bf 100644 --- a/docs/sources/panels-visualizations/visualizations/time-series/index.md +++ b/docs/sources/panels-visualizations/visualizations/time-series/index.md @@ -362,7 +362,7 @@ Set the position of the bar relative to a data point. In the examples below, **S {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/trend/index.md b/docs/sources/panels-visualizations/visualizations/trend/index.md index 49032640568..e8cc7fa8d30 100644 --- a/docs/sources/panels-visualizations/visualizations/trend/index.md +++ b/docs/sources/panels-visualizations/visualizations/trend/index.md @@ -60,7 +60,7 @@ For example, you could represent engine power and torque versus speed where spee {{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/xy-chart/index.md b/docs/sources/panels-visualizations/visualizations/xy-chart/index.md index 8e25e2e2dfa..83becc1b7df 100644 --- a/docs/sources/panels-visualizations/visualizations/xy-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/xy-chart/index.md @@ -308,7 +308,7 @@ You can customize the following standard options: To learn more, refer to [Configure standard options](ref:configure-standard-options). -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/shared/visualizations/datalink-options-1.md b/docs/sources/shared/visualizations/datalink-options-1.md index debfdcccbf9..5cfe706abf6 100644 --- a/docs/sources/shared/visualizations/datalink-options-1.md +++ b/docs/sources/shared/visualizations/datalink-options-1.md @@ -1,12 +1,16 @@ --- -title: Data link options +title: Data links and actions options comments: | - This file is used in the following visualizations: bar gauge, gauge, pie chart, stat, table + This file is used in the following visualizations: bar gauge, gauge, pie chart, stat --- Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. -To learn more, refer to [Configure data links](../../configure-data-links/). +To learn more, refer to [Configure data links and actions](../../configure-data-links/). + +{{< admonition type="note" >}} +Actions are not supported for this visualization. +{{< /admonition >}} For each data link, set the following options: @@ -14,4 +18,5 @@ For each data link, set the following options: - **URL** - **Open in new tab** -Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality. If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links. +Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality. +If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links. diff --git a/docs/sources/shared/visualizations/datalink-options-2.md b/docs/sources/shared/visualizations/datalink-options-2.md index ec8502a1ae5..b74b753b4e6 100644 --- a/docs/sources/shared/visualizations/datalink-options-2.md +++ b/docs/sources/shared/visualizations/datalink-options-2.md @@ -1,10 +1,11 @@ --- -title: Data link options +title: Data links and actions options comments: | This file is used in the following visualizations: bar chart, candlestick, state timeline, status history, time series, trend, xy chart --- -Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. +_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls. +In both cases, you can carry out these tasks while maintaining the context of the source panel. For each data link, set the following options: @@ -13,4 +14,21 @@ For each data link, set the following options: - **Open in new tab** - **One click** - Opens the data link with a single click. Only one data link can have **One click** enabled at a time. -To learn more, refer to [Configure data links](../../configure-data-links/). +For each action, define the following API call settings: + + + +| Option | Description | +| -------------------- | ----------------------------------------------------------------- | +| Title | A human-readable label for the action that's displayed in the UI. | +| Confirmation message | A descriptive prompt to confirm or cancel the action. | +| Method | Select from **POST**, **PUT**, or **GET**. | +| URL | The request URL.

To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. | +| Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. | +| Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.

Click the **+** icon to add as many key/value pairs as you need. | +| Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. | +| Body | The body of the request. | + + + +To learn more, refer to [Configure data links and actions](../../configure-data-links/). diff --git a/docs/sources/shared/visualizations/datalink-options-3.md b/docs/sources/shared/visualizations/datalink-options-3.md new file mode 100644 index 00000000000..738a5898596 --- /dev/null +++ b/docs/sources/shared/visualizations/datalink-options-3.md @@ -0,0 +1,36 @@ +--- +title: Data links and actions options +comments: | + This file is used in the following visualizations: table +--- + +_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls. +In both cases, you can carry out these tasks while maintaining the context of the source panel. + +For each data link, set the following options: + +- **Title** +- **URL** +- **Open in new tab** + +Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality. +If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links. + +For each action, define the following API call settings: + + + +| Option | Description | +| -------------------- | ----------------------------------------------------------------- | +| Title | A human-readable label for the action that's displayed in the UI. | +| Confirmation message | A descriptive prompt to confirm or cancel the action. | +| Method | Select from **POST**, **PUT**, or **GET**. | +| URL | The request URL.

To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. | +| Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. | +| Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.

Click the **+** icon to add as many key/value pairs as you need. | +| Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. | +| Body | The body of the request. | + + + +To learn more, refer to [Configure data links and actions](../../configure-data-links/). diff --git a/docs/sources/shared/visualizations/datalink-options.md b/docs/sources/shared/visualizations/datalink-options.md index 6e567cfd464..131640d86ee 100644 --- a/docs/sources/shared/visualizations/datalink-options.md +++ b/docs/sources/shared/visualizations/datalink-options.md @@ -1,10 +1,15 @@ --- -title: Data link options +title: Data links and actions options comments: | This file is used in the following visualizations: geomap, heatmap, histogram --- -Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. +Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. +You can create links that include the series name or even the value under the cursor. + +{{< admonition type="note" >}} +Actions are not supported for this visualization. +{{< /admonition >}} For each data link, set the following options: @@ -12,4 +17,4 @@ For each data link, set the following options: - **URL** - **Open in new tab** -To learn more, refer to [Configure data links](../../configure-data-links/). +To learn more, refer to [Configure data links and actions](../../configure-data-links/). From c605e4557d022fe01b5dac4ca5680389f6b1a328 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 26 Feb 2025 19:37:51 -0500 Subject: [PATCH 43/51] Docs: add Actions cell type for table (#101380) --- .../visualizations/table/index.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 96c95f437d2..e99622019d8 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -246,6 +246,7 @@ If you want to apply a cell type to only some fields instead of all fields, you | Data links | If you've configured data links, when the cell type is **Auto**, the cell text becomes clickable. If you change the cell type to **Data links**, the cell text reflects the titles of the configured data links. To control the application of data link text more granularly, use a **Cell option > Cell type > Data links** field override. | | [JSON View](#json-view) | Shows values formatted as code. | | [Image](#image) | If the field value is an image URL or a base64 encoded image, the table displays the image. | +| [Actions](#actions) | The cell displays a button that triggers a basic, unauthenticated API call when clicked. | ##### Sparkline @@ -333,6 +334,23 @@ Set the following options: - **Alt text** - Set the alternative text of an image. The text will be available for screen readers and in cases when images can't be loaded. - **Title text** - Set the text that's displayed when the image is hovered over with a cursor. +##### Actions + +The cell displays a button that triggers a basic, unauthenticated API call when clicked. +Configure the API call with the following options: + + +| Option | Description | +| ------- | ------------ | +| Endpoint | Enter the endpoint URL. | +| Method | Choose from **GET**, **POST**, and **PUT**. | +| Content-Type | Select an option in the drop-down list. Choose from: JSON, Text, JavaScript, HTML, XML, and x-www-form-urlencoded. | +| Query parameters | Enter as many **Key**, **Value** pairs as you need. | +| Header parameters | Enter as many **Key**, **Value** pairs as you need. | +| Payload | Enter the body of the API call. | + + + #### Wrap text {{< admonition type="note" >}} From 22a6dc6b52c40bbf0fff60abe3b20585622f473e Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:36:09 -0600 Subject: [PATCH 44/51] Canvas: Fix no series timestamp (#101390) --- public/app/plugins/panel/canvas/components/CanvasTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index f2a5959edc6..6234a4a7693 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -49,7 +49,7 @@ export const CanvasTooltip = ({ scene }: Props) => { } // Retrieve timestamp of the last data point if available - const timeField = scene.data?.series[0].fields?.find((field) => field.type === FieldType.time); + const timeField = scene.data?.series[0]?.fields?.find((field) => field.type === FieldType.time); const lastTimeValue = timeField?.values[timeField.values.length - 1]; const shouldDisplayTimeContentItem = timeField && lastTimeValue && element.data.field && getFieldDisplayName(timeField) !== element.data.field; From e8e79e9c79b5eacd88c2e6cb7461e3a84ce22507 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Thu, 27 Feb 2025 09:16:00 +0100 Subject: [PATCH 45/51] Plugins: Fix version tab breaking for non semantic version (#101225) --- .../components/VersionInstallButton.test.tsx | 40 ++++++++++++ .../admin/components/VersionInstallButton.tsx | 61 +++++++++++-------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx index 38511a2dfea..16ec1d3ca70 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx @@ -177,6 +177,46 @@ describe('VersionInstallButton', () => { ); expect(screen.getByText('Downgrade')).not.toBeVisible(); }); + + it('should show the installation button if invalid semver version is provided', () => { + const version: Version = { + version: '1.0.a', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.1'; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Install')).toBeInTheDocument(); + }); + + it('should show the installation button if invalid semver installed version is provided', () => { + const version: Version = { + version: '1.0.0', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.a'; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Install')).toBeInTheDocument(); + }); }); function renderWithStore(component: JSX.Element) { diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.tsx index 1da8bfe01f7..f0bec011875 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { useEffect, useState } from 'react'; -import { gt } from 'semver'; +import { gt, valid } from 'semver'; import { GrafanaTheme2 } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; @@ -14,6 +14,12 @@ import { Version } from '../types'; const PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME = 'plugins_upgrade_clicked'; const PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME = 'plugins_downgrade_clicked'; +enum InstallState { + INSTALL = 'Install', + UPGRADE = 'Upgrade', + DOWNGRADE = 'Downgrade', +} + interface Props { pluginId: string; version: Version; @@ -38,7 +44,7 @@ export const VersionInstallButton = ({ const [isModalOpen, setIsModalOpen] = useState(false); const styles = useStyles2(getStyles); - const isDowngrade = installedVersion && gt(installedVersion, version.version); + const installState = getInstallState(installedVersion, version.version); useEffect(() => { if (installedVersion === version.version) { @@ -61,7 +67,7 @@ export const VersionInstallButton = ({ schema_version: '1.0.0', }; - if (!installedVersion || gt(version.version, installedVersion)) { + if (installState === InstallState.UPGRADE) { reportInteraction(PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME, trackProps); } else { reportInteraction(PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME, { @@ -76,7 +82,7 @@ export const VersionInstallButton = ({ }; const onInstallClick = () => { - if (isDowngrade) { + if (installState === InstallState.DOWNGRADE) { setIsModalOpen(true); } else { performInstallation(); @@ -91,24 +97,9 @@ export const VersionInstallButton = ({ setIsModalOpen(false); }; - let label = 'Downgrade'; - let hidden = false; const isPreinstalled = isPreinstalledPlugin(pluginId); - if (!installedVersion) { - label = 'Install'; - } else if (gt(version.version, installedVersion)) { - label = 'Upgrade'; - if (isPreinstalled.withVersion) { - // Hide button if the plugin is preinstalled with a specific version - hidden = true; - } - } else { - if (isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate)) { - // Hide the downgrade button if the plugin is preinstalled since it will be auto-updated - hidden = true; - } - } + const hidden = getButtonHiddenState(installState, isPreinstalled); return ( <> @@ -124,7 +115,7 @@ export const VersionInstallButton = ({ tooltip={tooltip} tooltipPlacement="bottom-start" > - {label} {isInstalling ? : getIcon(label)} + {installState} {isInstalling ? : getIcon(installState)} ; } - if (label === 'Upgrade') { + if (installState === InstallState.UPGRADE) { return ; } return ''; } +function getInstallState(installedVersion?: string, version?: string): InstallState { + if (!installedVersion || !version || !valid(installedVersion) || !valid(version)) { + return InstallState.INSTALL; + } + return gt(installedVersion, version) ? InstallState.DOWNGRADE : InstallState.UPGRADE; +} + +function getButtonHiddenState(installState: InstallState, isPreinstalled: { found: boolean; withVersion: boolean }) { + // Default state for initial install + if (installState === InstallState.INSTALL) { + return false; + } + + // Handle downgrade case + if (installState === InstallState.DOWNGRADE) { + return isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate); + } + + // Handle upgrade case + return isPreinstalled.withVersion; +} + const getStyles = (theme: GrafanaTheme2) => ({ spinner: css({ marginLeft: theme.spacing(1), From 9ad01fda649aa508589a588f4acaa5044174e582 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:59:44 +0200 Subject: [PATCH 46/51] I18n: Download translations from Crowdin (#101387) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/de-DE/grafana.json | 112 ++++++++++++++++++++++++++-- public/locales/es-ES/grafana.json | 112 ++++++++++++++++++++++++++-- public/locales/fr-FR/grafana.json | 112 ++++++++++++++++++++++++++-- public/locales/pt-BR/grafana.json | 112 ++++++++++++++++++++++++++-- public/locales/zh-Hans/grafana.json | 112 ++++++++++++++++++++++++++-- 5 files changed, 535 insertions(+), 25 deletions(-) diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 1222cec2022..355610c995d 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Schließen" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Schließen" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Keine Optionen gefunden", "placeholder": "Auswählen" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Kürzlich verwendete absolute Bereiche", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Zeitbereiche-Beispiel", + "from-label": "", "from-to": "", "more-info": "", "specify": "Zeitbereich festlegen <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Zeitzonen-Auswähler", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index e6d9efb5f9b..1fa664da890 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Cerrar" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Cerrar" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "No se ha encontrado ninguna opción", "placeholder": "Elegir" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Intervalos absolutos utilizados recientemente", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Ejemplos de intervalos de tiempo", + "from-label": "", "from-to": "", "more-info": "", "specify": "Especificar el intervalo de tiempo <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Selector de huso horario", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index f6ab1a8be74..4def0fc0e73 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Fermer" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Fermer" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Aucune option trouvée", "placeholder": "Choisir" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Périodes absolues récemment utilisées", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Exemple de plages de temps", + "from-label": "", "from-to": "", "more-info": "", "specify": "Spécifiez la plage de temps <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Outil de sélection du fuseau horaire", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index c10c3c0eaa4..6e0f3ba0687 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -404,6 +407,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -592,6 +609,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1688,10 +1706,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1699,8 +1726,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1720,32 +1754,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "Fechar" @@ -1769,6 +1848,10 @@ "modal": { "close-tooltip": "Fechar" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1782,6 +1865,9 @@ "no-options-label": "Nenhuma opção encontrada", "placeholder": "Escolher" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1801,6 +1887,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3637,6 +3726,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "Intervalos absolutos usados recentemente", @@ -3693,10 +3792,13 @@ "example": "", "example-details": "", "example-title": "Exemplos de intervalos de tempo", + "from-label": "", "from-to": "", "more-info": "", "specify": "Especifique o intervalo de tempo <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "Seletor de fuso horário", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index eb52a53913b..d1f6b954db5 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -295,6 +295,9 @@ "export-all": "", "loading": "", "search-by-matchers": "", + "titles": { + "notification-templates": "" + }, "view": "" }, "contact-points": { @@ -402,6 +405,20 @@ "title": "", "uninitialized": "" }, + "notification-templates": { + "duplicate": { + "subTitle": "", + "title": "" + }, + "edit": { + "subTitle": "", + "title": "" + }, + "new": { + "subTitle": "", + "title": "" + } + }, "policies": { "default-policy": { "description": "", @@ -588,6 +605,7 @@ } }, "rule-viewer": { + "error-loading": "", "prometheus-consistency-check": { "alert-message": "", "alert-title": "" @@ -1679,10 +1697,19 @@ "one-click-description": "" } }, + "alert": { + "close-button": "" + }, "auto-save-field": { "saved": "", "saving": "" }, + "card": { + "option": "" + }, + "cascader": { + "clear-button": "" + }, "color-picker-popover": { "palette-tab": "", "spectrum-tab": "" @@ -1690,8 +1717,15 @@ "confirm-button": { "cancel": "" }, + "confirm-content": { + "placeholder": "" + }, "data-link-editor": { - "info": "" + "info": "", + "new-tab-label": "", + "title-label": "", + "title-placeholder": "", + "url-label": "" }, "data-link-editor-modal": { "cancel": "", @@ -1711,32 +1745,77 @@ "tooltip-remove": "", "url-not-provided": "" }, + "data-source-basic-auth-settings": { + "user-label": "", + "user-placeholder": "" + }, + "data-source-http-proxy-settings": { + "oauth-identity-label": "", + "oauth-identity-tooltip": "", + "skip-tls-verify-label": "", + "ts-client-auth-label": "", + "with-ca-cert-label": "", + "with-ca-cert-tooltip": "" + }, "data-source-http-settings": { "access-help": "", "access-help-details": "", + "access-label": "", + "access-options-browser": "", + "access-options-proxy": "", "allowed-cookies": "", + "allowed-cookies-tooltip": "", "auth": "", + "azure-auth-label": "", + "azure-auth-tooltip": "", "basic-auth": "", + "basic-auth-label": "", "browser-mode-description": "", "browser-mode-title": "", + "default-url-access-select": "", "default-url-tooltip": "", "direct-url-tooltip": "", "heading": "", "proxy-url-tooltip": "", "server-mode-description": "", - "server-mode-title": "" + "server-mode-title": "", + "timeout-form-label": "", + "timeout-label": "", + "timeout-tooltip": "", + "url-label": "", + "with-credential-label": "", + "with-credential-tooltip": "" }, "data-source-settings": { "alerting-settings-heading": "", + "alerting-settings-label": "", + "alerting-settings-tooltip": "", "cert-key-reset": "", "custom-headers-add": "", + "custom-headers-header": "", + "custom-headers-header-placeholder": "", + "custom-headers-header-remove": "", + "custom-headers-header-value": "", "custom-headers-title": "", "secure-socks-heading": "", - "tls-heading": "" + "secure-socks-label": "", + "secure-socks-tooltip": "", + "tls-certification-label": "", + "tls-certification-placeholder": "", + "tls-client-certification-label": "", + "tls-client-key-label": "", + "tls-client-key-placeholder": "", + "tls-heading": "", + "tls-server-name-label": "", + "tls-tooltip": "" }, "date-time-picker": { "apply": "", - "cancel": "" + "calendar-icon-label": "", + "cancel": "", + "next-label": "", + "previous-label": "", + "select-placeholder": "" }, "drawer": { "close": "关闭" @@ -1760,6 +1839,10 @@ "modal": { "close-tooltip": "关闭" }, + "named-colors-palette": { + "text-color-swatch": "", + "transparent-swatch": "" + }, "secret-form-field": { "reset": "" }, @@ -1773,6 +1856,9 @@ "no-options-label": "未找到选项", "placeholder": "选择" }, + "series-color-picker-popover": { + "y-axis-usage": "" + }, "spinner": { "aria-label": "" }, @@ -1792,6 +1878,9 @@ "user-icon": { "active-text": "" }, + "value-pill": { + "remove-button": "" + }, "viz-legend": { "right-axis-indicator": "" }, @@ -3623,6 +3712,16 @@ "title": "" } }, + "theme-preview": { + "breadcrumbs": { + "dashboards": "", + "home": "" + }, + "panel": { + "form-label": "", + "title": "" + } + }, "time-picker": { "absolute": { "recent-title": "最近使用的绝对范围", @@ -3679,10 +3778,13 @@ "example": "", "example-details": "", "example-title": "示例时间范围", + "from-label": "", "from-to": "", "more-info": "", "specify": "指定时间范围 <1>", - "supported-formats": "" + "submit-button-label": "", + "supported-formats": "", + "to-label": "" }, "zone": { "select-aria-label": "时区选择器", From b16904651fbae4eaf413baa44c83c51a1b2edd71 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 27 Feb 2025 11:13:58 +0100 Subject: [PATCH 47/51] Chore: Bump grafana-plugin-sdk-go to v0.267.0 (#101376) * bump grafana-plugin-sdk-go to v0.267.0 * make update-workspace --- apps/alerting/notifications/go.mod | 4 ++-- apps/alerting/notifications/go.sum | 8 ++++---- apps/investigations/go.mod | 4 ++-- apps/investigations/go.sum | 8 ++++---- apps/playlist/go.mod | 4 ++-- apps/playlist/go.sum | 8 ++++---- go.mod | 8 ++++---- go.sum | 15 ++++++++------- go.work.sum | 20 ++++---------------- pkg/aggregator/go.mod | 8 ++++---- pkg/aggregator/go.sum | 15 ++++++++------- pkg/apimachinery/go.mod | 3 ++- pkg/apimachinery/go.sum | 8 ++++---- pkg/apiserver/go.mod | 4 ++-- pkg/apiserver/go.sum | 8 ++++---- pkg/build/go.mod | 4 ++-- pkg/build/go.sum | 8 ++++---- pkg/build/wire/go.mod | 2 +- pkg/build/wire/go.sum | 4 ++-- pkg/codegen/go.mod | 2 +- pkg/codegen/go.sum | 4 ++-- pkg/plugins/codegen/go.mod | 2 +- pkg/plugins/codegen/go.sum | 4 ++-- pkg/promlib/go.mod | 8 ++++---- pkg/promlib/go.sum | 15 ++++++++------- pkg/semconv/go.mod | 1 + pkg/semconv/go.sum | 4 ++-- pkg/storage/unified/apistore/go.mod | 8 ++++---- pkg/storage/unified/apistore/go.sum | 15 ++++++++------- pkg/storage/unified/resource/go.mod | 8 ++++---- pkg/storage/unified/resource/go.sum | 15 ++++++++------- 31 files changed, 112 insertions(+), 117 deletions(-) diff --git a/apps/alerting/notifications/go.mod b/apps/alerting/notifications/go.mod index 74dc1dfe028..cf49c4e862d 100644 --- a/apps/alerting/notifications/go.mod +++ b/apps/alerting/notifications/go.mod @@ -32,7 +32,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -83,7 +83,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/apps/alerting/notifications/go.sum b/apps/alerting/notifications/go.sum index cae5be00879..3b3312aebf7 100644 --- a/apps/alerting/notifications/go.sum +++ b/apps/alerting/notifications/go.sum @@ -60,8 +60,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -269,8 +269,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go. google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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= diff --git a/apps/investigations/go.mod b/apps/investigations/go.mod index 12da166af27..7e9c06424d0 100644 --- a/apps/investigations/go.mod +++ b/apps/investigations/go.mod @@ -27,7 +27,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -73,7 +73,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.1 // indirect diff --git a/apps/investigations/go.sum b/apps/investigations/go.sum index d3caa3d08e3..bc0a2332d93 100644 --- a/apps/investigations/go.sum +++ b/apps/investigations/go.sum @@ -40,8 +40,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -195,8 +195,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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= diff --git a/apps/playlist/go.mod b/apps/playlist/go.mod index cd07f8c199e..474148a6799 100644 --- a/apps/playlist/go.mod +++ b/apps/playlist/go.mod @@ -28,7 +28,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -74,7 +74,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.1 // indirect diff --git a/apps/playlist/go.sum b/apps/playlist/go.sum index d3caa3d08e3..bc0a2332d93 100644 --- a/apps/playlist/go.sum +++ b/apps/playlist/go.sum @@ -40,8 +40,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -195,8 +195,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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= diff --git a/go.mod b/go.mod index d32c2843663..e664a31975a 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/golang/mock v1.7.0-rc.1 // @grafana/alerting-backend github.com/golang/protobuf v1.5.4 // @grafana/grafana-backend-group github.com/golang/snappy v0.0.4 // @grafana/alerting-backend - github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group + github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group github.com/google/go-querystring v1.1.0 // indirect; @grafana/oss-big-tent github.com/google/uuid v1.6.0 // @grafana/grafana-backend-group github.com/google/wire v0.6.0 // @grafana/grafana-backend-group @@ -88,7 +88,7 @@ require ( github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.2.1 // @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.266.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.267.0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling @@ -180,7 +180,7 @@ require ( gonum.org/v1/gonum v0.15.1 // @grafana/oss-big-tent google.golang.org/api v0.216.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.70.0 // @grafana/plugins-platform-backend - google.golang.org/protobuf v1.36.4 // @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.5 // @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 gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend @@ -331,7 +331,7 @@ require ( github.com/dolthub/maphash v0.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.2.0 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/proto v1.13.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect diff --git a/go.sum b/go.sum index 96e57fa263f..29c820b8964 100644 --- a/go.sum +++ b/go.sum @@ -1094,8 +1094,8 @@ github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8E github.com/efficientgo/core v1.0.0-rc.3 h1:X6CdgycYWDcbYiJr1H1+lQGzx13o7bq3EUkbB9DsSPc= github.com/efficientgo/core v1.0.0-rc.3/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -1422,8 +1422,9 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -1558,8 +1559,8 @@ github.com/grafana/grafana-google-sdk-go v0.2.1 h1:XeFdKnkXBjOJjXc1gf4iMx4h5aCHT github.com/grafana/grafana-google-sdk-go v0.2.1/go.mod h1:RiITSHwBhqVTTd3se3HQq5Ncs/wzzhTB9OK5N0J0PEU= 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.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8 h1:mG/6nDlEBVxWlo2GQJVASzucw3ByPIBsec06XcPrjgQ= github.com/grafana/grafana/apps/advisor v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:9I1dKV3Dqr0NPR9Af0WJGxOytp5/6W3JLiNChOz8r+c= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250220163425-b4c4b9abbdc8 h1:w42GlvkmHG4nM/p1kb2nKmROVP+AHtL3qWEYMhnhCVM= @@ -3357,8 +3358,8 @@ google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/go.work.sum b/go.work.sum index 0e79edfd1d2..25fc8254a99 100644 --- a/go.work.sum +++ b/go.work.sum @@ -142,8 +142,6 @@ cloud.google.com/go/memcache v1.11.1 h1:2FGuyd3WY7buNDAkMBdmeIOheNWA3gwaXrttLrEd cloud.google.com/go/memcache v1.11.1/go.mod h1:3zF+dEqmEmElHuO4NtHiShekQY5okQtssjPBv7jpmZ8= cloud.google.com/go/metastore v1.14.1 h1:kGx+IUSSYCVn8LisCT4fpxCC9rauEVonzi7RlygdqWY= cloud.google.com/go/metastore v1.14.1/go.mod h1:WDvsAcbQLl9M4xL+eIpbKogH7aEaPWMhO9aRBcFOnJE= -cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc= -cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c= cloud.google.com/go/networkconnectivity v1.15.1 h1:EizN+cFGHzRAyiFTK8jT1PqTo+cSnbc2IGh6OmllS7Y= cloud.google.com/go/networkconnectivity v1.15.1/go.mod h1:tYAcT4Ahvq+BiePXL/slYipf/8FF0oNJw3MqFhBnSPI= cloud.google.com/go/networkmanagement v1.14.1 h1:0x3hVI6xbp3N/choffKPHMSxbzaPdHSD92cBElebXEk= @@ -204,8 +202,6 @@ cloud.google.com/go/servicemanagement v1.8.0 h1:fopAQI/IAzlxnVeiKn/8WiV6zKndjFkv cloud.google.com/go/serviceusage v1.6.0 h1:rXyq+0+RSIm3HFypctp7WoXxIA563rn206CfMWdqXX4= cloud.google.com/go/shell v1.8.1 h1:etoJal+LB7Pn8+5vE2aAh6QcFbBmerIOh5MxNDoXykw= cloud.google.com/go/shell v1.8.1/go.mod h1:jaU7OHeldDhTwgs3+clM0KYEDYnBAPevUI6wNLf7ycE= -cloud.google.com/go/spanner v1.70.0 h1:nj6p/GJTgMDiSQ1gQ034ItsKuJgHiMOjtOlONOg8PSo= -cloud.google.com/go/spanner v1.70.0/go.mod h1:X5T0XftydYp0K1adeJQDJtdWpbrOeJ7wHecM4tK6FiE= cloud.google.com/go/speech v1.25.1 h1:iGZJS3wrdkje/Vqiacx1+r+zVwUZoXVMdklYIVsvfNw= cloud.google.com/go/speech v1.25.1/go.mod h1:WgQghvghkZ1htG6BhYn98mP7Tg0mti8dBFDLMVXH/vM= cloud.google.com/go/storagetransfer v1.11.1 h1:Hd7H1zXGQGEWyWXxWVXDMuNCGasNQim1y9CIaMZIBX8= @@ -280,10 +276,6 @@ github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW5 github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0 h1:kAtNAWwvTt5+iew6baV0kbOrtjYTXPtWNSyOFlcxkBU= github.com/GoogleCloudPlatform/cloudsql-proxy v1.36.0/go.mod h1:VRKXU8C7Y/aUKjRBTGfw0Ndv4YqNxlB8zAPJJDxbASE= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 h1:oVLqHXhnYtUwM89y9T1fXGaK9wTkXHgNp8/ZNMQzUxE= -github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc= github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns= github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/1cE8= @@ -387,7 +379,6 @@ github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9Mwe github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= @@ -510,6 +501,7 @@ github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJ github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elazarl/goproxy v1.3.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -593,6 +585,7 @@ github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs0 github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= @@ -907,7 +900,6 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93whDM5o= github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/substrait-io/substrait v0.57.1 h1:GW8nnYfSowMseHR8Os82/X6lNtQGIK7p4p+lr6r+auw= github.com/substrait-io/substrait v0.57.1/go.mod h1:q9s+tjo+gK0lsA+SqYB0lhojNuxvdPdfYlGUP0hjbrA= github.com/substrait-io/substrait-go v1.2.0 h1:3ZNRkc8FYD7ifCagKEOZQtUcgMceMQfwo2N1NGaK4Q4= @@ -1055,8 +1047,6 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.53.0 h1:BdkKDtcrHThgjcEia1737O go.opentelemetry.io/contrib/bridges/prometheus v0.53.0/go.mod h1:ZkhVxcJgeXlL/lVyT/vxNHVFiSG5qOaDwYaSgD8IfZo= go.opentelemetry.io/contrib/config v0.7.0 h1:b1rK5tGTuhhPirJiMxOcyQfZs76j2VapY6ODn3b2Dbs= go.opentelemetry.io/contrib/config v0.7.0/go.mod h1:8tdiFd8N5etOi3XzBmAoMxplEzI3TcL8dU5rM5/xcOQ= -go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8= -go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU= go.opentelemetry.io/contrib/exporters/autoexport v0.53.0 h1:13K+tY7E8GJInkrvRiPAhC0gi/7vKjzDNhtmCf+QXG8= go.opentelemetry.io/contrib/exporters/autoexport v0.53.0/go.mod h1:lyQF6xQ4iDnMg4sccNdFs1zf62xd79YI8vZqKjOTwMs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= @@ -1137,12 +1127,9 @@ golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1200,6 +1187,7 @@ google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index edfe54e00b2..cac9067a6a9 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.6 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -39,7 +39,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -59,7 +59,7 @@ require ( github.com/google/cel-go v0.22.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect @@ -150,7 +150,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index eb715898bf0..11bd9b72803 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -49,8 +49,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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -118,8 +118,9 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -134,8 +135,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= @@ -495,8 +496,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 033f48738ec..a3f8b199c92 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -23,6 +23,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -43,7 +44,7 @@ require ( golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // 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 diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index bd82bcbb03f..a2532bad3ff 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -25,8 +25,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -151,8 +151,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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= diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 0c45aa74e58..59a16f25782 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -5,7 +5,7 @@ go 1.23.4 toolchain go1.23.6 require ( - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/grafana-app-sdk/logging v0.30.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 @@ -93,7 +93,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index b3d15d3aee2..f1076d70d55 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -68,8 +68,8 @@ github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvR github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -312,8 +312,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 1d5f29a7bed..fd52527bf94 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -14,7 +14,7 @@ require ( github.com/docker/docker v27.4.1+incompatible // @grafana/grafana-developer-enablement-squad github.com/drone/drone-cli v1.8.0 // @grafana/grafana-developer-enablement-squad github.com/gogo/protobuf v1.3.2 // indirect; @grafana/alerting-backend - github.com/google/go-cmp v0.6.0 // @grafana/grafana-backend-group + github.com/google/go-cmp v0.7.0 // @grafana/grafana-backend-group github.com/google/go-github/v69 v69.2.0 // @grafana/grafana-developer-enablement-squad github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group github.com/googleapis/gax-go/v2 v2.14.1 // indirect; @grafana/grafana-backend-group @@ -35,7 +35,7 @@ require ( golang.org/x/time v0.9.0 // indirect; @grafana/grafana-backend-group google.golang.org/api v0.216.0 // @grafana/grafana-backend-group google.golang.org/grpc v1.70.0 // indirect; @grafana/plugins-platform-backend - google.golang.org/protobuf v1.36.4 // indirect; @grafana/plugins-platform-backend + google.golang.org/protobuf v1.36.5 // indirect; @grafana/plugins-platform-backend gopkg.in/yaml.v3 v3.0.1 // @grafana/alerting-backend ) diff --git a/pkg/build/go.sum b/pkg/build/go.sum index 7c868c04a4c..b1e8e4c3aa9 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -126,8 +126,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -362,8 +362,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/build/wire/go.mod b/pkg/build/wire/go.mod index ee43dc13181..61a2bb2b9db 100644 --- a/pkg/build/wire/go.mod +++ b/pkg/build/wire/go.mod @@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/build/wire go 1.23.1 require ( - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/subcommands v1.2.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 golang.org/x/tools v0.29.0 diff --git a/pkg/build/wire/go.sum b/pkg/build/wire/go.sum index 56cfeb71f60..07103d75876 100644 --- a/pkg/build/wire/go.sum +++ b/pkg/build/wire/go.sum @@ -1,5 +1,5 @@ -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= diff --git a/pkg/codegen/go.mod b/pkg/codegen/go.mod index 3c3b0bfa94b..f6f900fc6b8 100644 --- a/pkg/codegen/go.mod +++ b/pkg/codegen/go.mod @@ -21,7 +21,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/golang/glog v1.2.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/pkg/codegen/go.sum b/pkg/codegen/go.sum index 25214c4b99e..e125a5bdc0f 100644 --- a/pkg/codegen/go.sum +++ b/pkg/codegen/go.sum @@ -25,8 +25,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/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/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= diff --git a/pkg/plugins/codegen/go.mod b/pkg/plugins/codegen/go.mod index 5d415a7f052..8205acbb824 100644 --- a/pkg/plugins/codegen/go.mod +++ b/pkg/plugins/codegen/go.mod @@ -21,7 +21,7 @@ require ( github.com/getkin/kin-openapi v0.129.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/pkg/plugins/codegen/go.sum b/pkg/plugins/codegen/go.sum index a940ced9650..c6205d86509 100644 --- a/pkg/plugins/codegen/go.sum +++ b/pkg/plugins/codegen/go.sum @@ -24,8 +24,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/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/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk= diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index 95380de2939..fb2946bb264 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.6 require ( github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/json-iterator/go v1.1.12 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/common v0.62.0 @@ -33,7 +33,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -48,7 +48,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -121,7 +121,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index 66a0da7e790..f6b5b3b3d42 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -55,8 +55,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/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -100,8 +100,9 @@ github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZat github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -120,8 +121,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g= github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= 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.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -394,8 +395,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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= diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index b79133b4a5b..d015899f50c 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -6,5 +6,6 @@ require go.opentelemetry.io/otel v1.34.0 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect ) diff --git a/pkg/semconv/go.sum b/pkg/semconv/go.sum index 2b997e160c5..2d9cf378678 100644 --- a/pkg/semconv/go.sum +++ b/pkg/semconv/go.sum @@ -1,7 +1,7 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 748b5e929ff..c515663008a 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -134,7 +134,7 @@ require ( github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 // indirect github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect @@ -183,7 +183,7 @@ require ( github.com/google/cel-go v0.22.1 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect @@ -199,7 +199,7 @@ require ( github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect github.com/grafana/grafana-aws-sdk v0.31.5 // indirect github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect - github.com/grafana/grafana-plugin-sdk-go v0.266.0 // indirect + github.com/grafana/grafana-plugin-sdk-go v0.267.0 // indirect github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // indirect github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // indirect @@ -382,7 +382,7 @@ require ( google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect - google.golang.org/protobuf v1.36.4 // indirect + google.golang.org/protobuf v1.36.5 // 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/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 35a9215a782..11a44305649 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -334,8 +334,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -515,8 +515,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -584,8 +585,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= @@ -1552,8 +1553,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 42bb07c9e45..75b69ced369 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -15,7 +15,7 @@ require ( github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible - github.com/grafana/grafana-plugin-sdk-go v0.266.0 + github.com/grafana/grafana-plugin-sdk-go v0.267.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250220154326-6e5de80ef295 github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 @@ -28,7 +28,7 @@ require ( gocloud.dev v0.40.0 golang.org/x/sync v0.11.0 google.golang.org/grpc v1.70.0 - google.golang.org/protobuf v1.36.4 + google.golang.org/protobuf v1.36.5 k8s.io/apimachinery v0.32.1 ) @@ -82,7 +82,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/elazarl/goproxy v1.7.0 // indirect + github.com/elazarl/goproxy v1.7.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -110,7 +110,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/google/wire v0.6.0 // indirect diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index 2bc9e773911..83d766f41aa 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -216,8 +216,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v1.7.0 h1:EXv2nV4EjM60ZtsEVLYJG4oBXhDGutMKperpHsZ/v+0= -github.com/elazarl/goproxy v1.7.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ= +github.com/elazarl/goproxy v1.7.1 h1:1P7LPSxbqtNxusFnXclj6O56pjfq1xOQZ6a0mwwKUlY= +github.com/elazarl/goproxy v1.7.1/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -361,8 +361,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo= @@ -413,8 +414,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 h1:OfCkitCuomzZKW1WYHrG8MxKwtMhALb7jqoj+487eTg= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo= -github.com/grafana/grafana-plugin-sdk-go v0.266.0 h1:YP+iEpXH3HRX9Xo4NHjsrJhN2W7uVTtkLNzMHYbmiLI= -github.com/grafana/grafana-plugin-sdk-go v0.266.0/go.mod h1:bxkXrBQ4QSmOncsWdIOcpgP+M6wajQNMAPXlbWrqAWY= +github.com/grafana/grafana-plugin-sdk-go v0.267.0 h1:4AcuLEE0UeFG0Eo3P8r1FDS89hgZZ73K550DRz1fgjQ= +github.com/grafana/grafana-plugin-sdk-go v0.267.0/go.mod h1:OuwS4c/JYgn0rr/w5zhJBpLo4gKm/vw15RsfpYAvK9Q= 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.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -1021,8 +1022,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= -google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= From 58457d41d3257d712b8fa88c9c88b83793bcdb80 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 27 Feb 2025 13:27:28 +0300 Subject: [PATCH 48/51] K8s/DualWriter: Remove legacy interface (#101395) --- pkg/apiserver/rest/dualwriter.go | 41 ++++----- pkg/apiserver/rest/dualwriter_mode1.go | 4 +- pkg/apiserver/rest/dualwriter_mode1_test.go | 48 +++++----- pkg/apiserver/rest/dualwriter_mode2.go | 10 +- pkg/apiserver/rest/dualwriter_mode2_test.go | 24 ++--- pkg/apiserver/rest/dualwriter_mode3.go | 4 +- pkg/apiserver/rest/dualwriter_mode3_test.go | 28 +++--- pkg/apiserver/rest/dualwriter_syncer.go | 2 +- pkg/apiserver/rest/dualwriter_syncer_test.go | 8 +- pkg/apiserver/rest/dualwriter_test.go | 4 +- pkg/apiserver/rest/storage_mocks_test.go | 91 ------------------- .../notifications/receiver/legacy_storage.go | 4 +- .../routingtree/legacy_storage.go | 4 +- .../templategroup/legacy_storage.go | 5 +- .../timeinterval/legacy_storage.go | 4 +- pkg/registry/apis/dashboard/legacy_storage.go | 2 +- pkg/registry/apis/dashboard/register.go | 5 +- pkg/registry/apps/playlist/register.go | 2 +- pkg/services/apiserver/builder/helper.go | 2 +- .../apiserver/builder/runner/builder.go | 2 +- pkg/storage/legacysql/dualwrite/mock.go | 2 +- pkg/storage/legacysql/dualwrite/runtime.go | 4 +- .../legacysql/dualwrite/runtime_test.go | 12 +-- pkg/storage/legacysql/dualwrite/static.go | 2 +- .../legacysql/dualwrite/storage_mocks_test.go | 91 ------------------- pkg/storage/legacysql/dualwrite/types.go | 2 +- 26 files changed, 108 insertions(+), 299 deletions(-) diff --git a/pkg/apiserver/rest/dualwriter.go b/pkg/apiserver/rest/dualwriter.go index 15be881b250..31ca7e9c97e 100644 --- a/pkg/apiserver/rest/dualwriter.go +++ b/pkg/apiserver/rest/dualwriter.go @@ -26,8 +26,18 @@ var ( _ rest.SingularNameProvider = (DualWriter)(nil) ) +type dualWriteContextKey struct{} + +func IsDualWriteUpdate(ctx context.Context) bool { + return ctx.Value(dualWriteContextKey{}) == true +} + +func WithDualWriteUpdate(ctx context.Context) context.Context { + return context.WithValue(ctx, dualWriteContextKey{}, true) +} + // Function that will create a dual writer -type DualWriteBuilder func(gr schema.GroupResource, legacy LegacyStorage, storage Storage) (Storage, error) +type DualWriteBuilder func(gr schema.GroupResource, legacy Storage, unified Storage) (Storage, error) // Storage is a storage implementation that satisfies the same interfaces as genericregistry.Store. type Storage interface { @@ -36,26 +46,12 @@ type Storage interface { rest.TableConvertor rest.SingularNameProvider rest.Getter - // TODO: when watch is implemented, we can replace all the below with rest.StandardStorage rest.Lister rest.CreaterUpdater rest.GracefulDeleter rest.CollectionDeleter } -// LegacyStorage is a storage implementation that writes to the Grafana SQL database. -type LegacyStorage interface { - rest.Storage - rest.Scoper - rest.SingularNameProvider - rest.CreaterUpdater - rest.Lister - rest.GracefulDeleter - rest.CollectionDeleter - rest.TableConvertor - rest.Getter -} - // DualWriter is a storage implementation that writes first to LegacyStorage and then to Storage. // If writing to LegacyStorage fails, the write to Storage is skipped and the error is returned. // Storage is used for all read operations. This is useful as a migration step from SQL based @@ -79,7 +75,6 @@ type LegacyStorage interface { type DualWriter interface { Storage - LegacyStorage Mode() DualWriterMode } @@ -110,8 +105,8 @@ const ( // NewDualWriter returns a new DualWriter. func NewDualWriter( mode DualWriterMode, - legacy LegacyStorage, - storage Storage, + legacy Storage, + unified Storage, reg prometheus.Registerer, resource string, ) Storage { @@ -122,17 +117,17 @@ func NewDualWriter( return legacy case Mode1: // read and write only from legacy storage - return newDualWriterMode1(legacy, storage, metrics, resource) + return newDualWriterMode1(legacy, unified, metrics, resource) case Mode2: // write to both, read from storage but use legacy as backup - return newDualWriterMode2(legacy, storage, metrics, resource) + return newDualWriterMode2(legacy, unified, metrics, resource) case Mode3: // write to both, read from storage only - return newDualWriterMode3(legacy, storage, metrics, resource) + return newDualWriterMode3(legacy, unified, metrics, resource) case Mode4, Mode5: - return storage + return unified default: - return newDualWriterMode1(legacy, storage, metrics, resource) + return newDualWriterMode1(legacy, unified, metrics, resource) } } diff --git a/pkg/apiserver/rest/dualwriter_mode1.go b/pkg/apiserver/rest/dualwriter_mode1.go index 2ce24e52008..7395fc5d13b 100644 --- a/pkg/apiserver/rest/dualwriter_mode1.go +++ b/pkg/apiserver/rest/dualwriter_mode1.go @@ -15,7 +15,7 @@ import ( ) type DualWriterMode1 struct { - Legacy LegacyStorage + Legacy Storage Storage Storage *dualWriterMetrics resource string @@ -26,7 +26,7 @@ const mode1Str = "1" // NewDualWriterMode1 returns a new DualWriter in mode 1. // Mode 1 represents writing to and reading from LegacyStorage. -func newDualWriterMode1(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 { +func newDualWriterMode1(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 { return &DualWriterMode1{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode1_test.go b/pkg/apiserver/rest/dualwriter_mode1_test.go index e5504e4df4e..398b25a3acc 100644 --- a/pkg/apiserver/rest/dualwriter_mode1_test.go +++ b/pkg/apiserver/rest/dualwriter_mode1_test.go @@ -60,10 +60,10 @@ func TestMode1_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -122,10 +122,10 @@ func TestMode1_CreateOnUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -190,10 +190,10 @@ func TestMode1_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -251,10 +251,10 @@ func TestMode1_GetFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -308,10 +308,10 @@ func TestMode1_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -365,10 +365,10 @@ func TestMode1_ListFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -434,10 +434,10 @@ func TestMode1_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -494,10 +494,10 @@ func TestMode1_DeleteFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -565,10 +565,10 @@ func TestMode1_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -626,10 +626,10 @@ func TestMode1_DeleteCollectionFromUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -695,10 +695,10 @@ func TestMode1_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -761,10 +761,10 @@ func TestMode1_UpdateOnUnifiedStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_mode2.go b/pkg/apiserver/rest/dualwriter_mode2.go index 84e841270e5..17aae84e2fb 100644 --- a/pkg/apiserver/rest/dualwriter_mode2.go +++ b/pkg/apiserver/rest/dualwriter_mode2.go @@ -16,15 +16,9 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" ) -type dualWriteContextKey struct{} - -func IsDualWriteUpdate(ctx context.Context) bool { - return ctx.Value(dualWriteContextKey{}) == true -} - type DualWriterMode2 struct { Storage Storage - Legacy LegacyStorage + Legacy Storage *dualWriterMetrics resource string Log klog.Logger @@ -35,7 +29,7 @@ const mode2Str = "2" // newDualWriterMode2 returns a new DualWriter in mode 2. // Mode 2 represents writing to LegacyStorage first, then to Storage. // When reading, values from LegacyStorage will be returned. -func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 { +func newDualWriterMode2(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 { return &DualWriterMode2{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode2_test.go b/pkg/apiserver/rest/dualwriter_mode2_test.go index dfd7b6db0f5..b0d653053df 100644 --- a/pkg/apiserver/rest/dualwriter_mode2_test.go +++ b/pkg/apiserver/rest/dualwriter_mode2_test.go @@ -54,10 +54,10 @@ func TestMode2_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -138,10 +138,10 @@ func TestMode2_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -212,10 +212,10 @@ func TestMode2_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -313,10 +313,10 @@ func TestMode2_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -382,10 +382,10 @@ func TestMode2_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -451,10 +451,10 @@ func TestMode2_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_mode3.go b/pkg/apiserver/rest/dualwriter_mode3.go index bd28ef3c31c..db61f0ed7f7 100644 --- a/pkg/apiserver/rest/dualwriter_mode3.go +++ b/pkg/apiserver/rest/dualwriter_mode3.go @@ -17,7 +17,7 @@ import ( ) type DualWriterMode3 struct { - Legacy LegacyStorage + Legacy Storage Storage Storage watchImp rest.Watcher // watch is only available in mode 3 and 4 *dualWriterMetrics @@ -27,7 +27,7 @@ type DualWriterMode3 struct { // newDualWriterMode3 returns a new DualWriter in mode 3. // Mode 3 represents writing to LegacyStorage and Storage and reading from Storage. -func newDualWriterMode3(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 { +func newDualWriterMode3(legacy Storage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 { return &DualWriterMode3{ Legacy: legacy, Storage: storage, diff --git a/pkg/apiserver/rest/dualwriter_mode3_test.go b/pkg/apiserver/rest/dualwriter_mode3_test.go index f68287f4ead..832c8957aeb 100644 --- a/pkg/apiserver/rest/dualwriter_mode3_test.go +++ b/pkg/apiserver/rest/dualwriter_mode3_test.go @@ -61,10 +61,10 @@ func TestMode3_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -128,10 +128,10 @@ func TestMode3_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -187,10 +187,10 @@ func TestMode1_GetFromLegacyStorage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -234,10 +234,10 @@ func TestMode3_List(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupStorageFn != nil { @@ -311,10 +311,10 @@ func TestMode3_Delete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -380,10 +380,10 @@ func TestMode3_DeleteCollection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -449,10 +449,10 @@ func TestMode3_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_syncer.go b/pkg/apiserver/rest/dualwriter_syncer.go index 9c36fdd22b7..b15cf5376ac 100644 --- a/pkg/apiserver/rest/dualwriter_syncer.go +++ b/pkg/apiserver/rest/dualwriter_syncer.go @@ -33,7 +33,7 @@ type SyncerConfig struct { RequestInfo *request.RequestInfo Mode DualWriterMode - LegacyStorage LegacyStorage + LegacyStorage Storage Storage Storage ServerLockService ServerLockService diff --git a/pkg/apiserver/rest/dualwriter_syncer_test.go b/pkg/apiserver/rest/dualwriter_syncer_test.go index d9d94cea54c..c1e0a216e9f 100644 --- a/pkg/apiserver/rest/dualwriter_syncer_test.go +++ b/pkg/apiserver/rest/dualwriter_syncer_test.go @@ -181,12 +181,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { // mode 1 for _, tt := range tests { t.Run("Mode-1-"+tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) lm := &mock.Mock{} um := &mock.Mock{} - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} us := storageMock{um, s} if tt.setupLegacyFn != nil { @@ -221,12 +221,12 @@ func TestLegacyToUnifiedStorage_DataSyncer(t *testing.T) { // mode 2 for _, tt := range tests { t.Run("Mode-2-"+tt.name, func(t *testing.T) { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) lm := &mock.Mock{} um := &mock.Mock{} - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} us := storageMock{um, s} if tt.setupLegacyFn != nil { diff --git a/pkg/apiserver/rest/dualwriter_test.go b/pkg/apiserver/rest/dualwriter_test.go index 13fc007dc53..3faf131424e 100644 --- a/pkg/apiserver/rest/dualwriter_test.go +++ b/pkg/apiserver/rest/dualwriter_test.go @@ -64,7 +64,7 @@ func TestSetDualWritingMode(t *testing.T) { } for _, tt := range tests { - l := (LegacyStorage)(nil) + l := (Storage)(nil) s := (Storage)(nil) sm := &mock.Mock{} @@ -75,7 +75,7 @@ func TestSetDualWritingMode(t *testing.T) { lm := &mock.Mock{} lm.On("List", mock.Anything, mock.Anything).Return(exampleList, nil) - ls := legacyStoreMock{lm, l} + ls := storageMock{lm, l} serverLockSvc := &fakeServerLock{ err: tt.serverLockError, diff --git a/pkg/apiserver/rest/storage_mocks_test.go b/pkg/apiserver/rest/storage_mocks_test.go index 582bcdc067c..3baaa14c526 100644 --- a/pkg/apiserver/rest/storage_mocks_test.go +++ b/pkg/apiserver/rest/storage_mocks_test.go @@ -11,102 +11,11 @@ import ( "k8s.io/apiserver/pkg/registry/rest" ) -type legacyStoreMock struct { - *mock.Mock - LegacyStorage -} - type storageMock struct { *mock.Mock Storage } -func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, obj, createValidation, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) NewList() runtime.Object { - return nil -} - -func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, deleteValidation, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - args := m.Called(ctx, deleteValidation, options, listOptions) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - // Unified Store func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { select { diff --git a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go index 6b1377dc1f3..65f5593cf3a 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go @@ -13,7 +13,7 @@ import ( model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -22,7 +22,7 @@ import ( ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type ReceiverService interface { diff --git a/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go b/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go index 00c20880212..07f5138aca8 100644 --- a/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/routingtree/legacy_storage.go @@ -11,14 +11,14 @@ import ( "k8s.io/apiserver/pkg/registry/rest" model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" alerting_models "github.com/grafana/grafana/pkg/services/ngalert/models" ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type RouteService interface { diff --git a/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go b/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go index a492c4b3b49..ada3f463820 100644 --- a/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/templategroup/legacy_storage.go @@ -4,13 +4,14 @@ import ( "context" "fmt" - "github.com/grafana/alerting/templates" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/alerting/templates" + model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -19,7 +20,7 @@ import ( ) var ( - _ grafanarest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type TemplateService interface { diff --git a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go index 7a7ef74165a..bb382ee9525 100644 --- a/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/timeinterval/legacy_storage.go @@ -11,14 +11,14 @@ import ( "k8s.io/apiserver/pkg/registry/rest" model "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1" - grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ) var ( - _ grafanaRest.LegacyStorage = (*legacyStorage)(nil) + _ grafanarest.Storage = (*legacyStorage)(nil) ) type TimeIntervalService interface { diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go index 214c2ad2202..b5e033df390 100644 --- a/pkg/registry/apis/dashboard/legacy_storage.go +++ b/pkg/registry/apis/dashboard/legacy_storage.go @@ -28,7 +28,7 @@ type DashboardStorage struct { Features featuremgmt.FeatureToggles } -func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.LegacyStorage, error) { +func (s *DashboardStorage) NewStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.Storage, error) { server, err := resource.NewResourceServer(resource.ResourceServerOptions{ Backend: s.Access, Reg: reg, diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index c223026576f..a06dfe50a0f 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -17,6 +17,8 @@ import ( "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" + "github.com/prometheus/client_golang/prometheus" + claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard" @@ -42,7 +44,6 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/prometheus/client_golang/prometheus" ) var ( @@ -247,7 +248,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver func (b *DashboardsAPIBuilder) storageForVersion( opts builder.APIGroupOptions, - legacyStore grafanarest.LegacyStorage, + legacyStore grafanarest.Storage, largeObjects apistore.LargeObjectSupport, newDTOFunc func() runtime.Object, ) (map[string]rest.Storage, error) { diff --git a/pkg/registry/apps/playlist/register.go b/pkg/registry/apps/playlist/register.go index 9f79bb22f1d..a4215d880d4 100644 --- a/pkg/registry/apps/playlist/register.go +++ b/pkg/registry/apps/playlist/register.go @@ -48,7 +48,7 @@ func RegisterApp( return provider } -func (p *PlaylistAppProvider) legacyStorageGetter(requested schema.GroupVersionResource) grafanarest.LegacyStorage { +func (p *PlaylistAppProvider) legacyStorageGetter(requested schema.GroupVersionResource) grafanarest.Storage { gvr := schema.GroupVersionResource{ Group: playlistv0alpha1.PlaylistKind().Group(), Version: playlistv0alpha1.PlaylistKind().Version(), diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 33e06c2580b..8f6fe0e3783 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -286,7 +286,7 @@ func InstallAPIs( // nolint:staticcheck if storageOpts.StorageType != options.StorageTypeLegacy { - dualWrite = func(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error) { + dualWrite = func(gr schema.GroupResource, legacy grafanarest.Storage, storage grafanarest.Storage) (grafanarest.Storage, error) { // Dashboards + Folders may be managed (depends on feature toggles and database state) if dualWriteService != nil && dualWriteService.ShouldManage(gr) { return dualWriteService.NewStorage(gr, legacy, storage) // eventually this can replace this whole function diff --git a/pkg/services/apiserver/builder/runner/builder.go b/pkg/services/apiserver/builder/runner/builder.go index 1bc052fb46e..8a7b18a5fe2 100644 --- a/pkg/services/apiserver/builder/runner/builder.go +++ b/pkg/services/apiserver/builder/runner/builder.go @@ -18,7 +18,7 @@ import ( var _ AppBuilder = (*appBuilder)(nil) -type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.LegacyStorage +type LegacyStorageGetter func(schema.GroupVersionResource) grafanarest.Storage type AppBuilderConfig struct { Authorizer authorizer.Authorizer diff --git a/pkg/storage/legacysql/dualwrite/mock.go b/pkg/storage/legacysql/dualwrite/mock.go index 130c5c0feff..3dbf71a126a 100644 --- a/pkg/storage/legacysql/dualwrite/mock.go +++ b/pkg/storage/legacysql/dualwrite/mock.go @@ -26,7 +26,7 @@ type mockService struct { } // NewStorage implements Service. -func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) { +func (m *mockService) NewStorage(gr schema.GroupResource, legacy rest.Storage, storage rest.Storage) (rest.Storage, error) { return nil, fmt.Errorf("not implemented") } diff --git a/pkg/storage/legacysql/dualwrite/runtime.go b/pkg/storage/legacysql/dualwrite/runtime.go index 5fe705c3409..33896e669e2 100644 --- a/pkg/storage/legacysql/dualwrite/runtime.go +++ b/pkg/storage/legacysql/dualwrite/runtime.go @@ -15,7 +15,7 @@ import ( ) func (m *service) NewStorage(gr schema.GroupResource, - legacy grafanarest.LegacyStorage, + legacy grafanarest.Storage, storage grafanarest.Storage, ) (grafanarest.Storage, error) { status, err := m.Status(context.Background(), gr) @@ -53,7 +53,7 @@ func (m *service) NewStorage(gr schema.GroupResource, // When a resource is marked as "migrating", all write requests will be 503 unavailable type runtimeDualWriter struct { service Service - legacy grafanarest.LegacyStorage + legacy grafanarest.Storage unified grafanarest.Storage dualwrite grafanarest.Storage gr schema.GroupResource diff --git a/pkg/storage/legacysql/dualwrite/runtime_test.go b/pkg/storage/legacysql/dualwrite/runtime_test.go index c97139948f2..9c148eaf07f 100644 --- a/pkg/storage/legacysql/dualwrite/runtime_test.go +++ b/pkg/storage/legacysql/dualwrite/runtime_test.go @@ -76,10 +76,10 @@ func TestManagedMode3_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -148,10 +148,10 @@ func TestManagedMode3_Get(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { @@ -249,10 +249,10 @@ func TestManagedMode3_CreateWhileMigrating(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := (rest.LegacyStorage)(nil) + l := (rest.Storage)(nil) s := (rest.Storage)(nil) - ls := legacyStoreMock{&mock.Mock{}, l} + ls := storageMock{&mock.Mock{}, l} us := storageMock{&mock.Mock{}, s} if tt.setupLegacyFn != nil { diff --git a/pkg/storage/legacysql/dualwrite/static.go b/pkg/storage/legacysql/dualwrite/static.go index 64e8c14a85a..d74e2a5b914 100644 --- a/pkg/storage/legacysql/dualwrite/static.go +++ b/pkg/storage/legacysql/dualwrite/static.go @@ -14,7 +14,7 @@ type staticService struct { cfg *setting.Cfg } -func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.LegacyStorage, storage rest.Storage) (rest.Storage, error) { +func (m *staticService) NewStorage(gr schema.GroupResource, legacy rest.Storage, storage rest.Storage) (rest.Storage, error) { return nil, fmt.Errorf("not implemented") } diff --git a/pkg/storage/legacysql/dualwrite/storage_mocks_test.go b/pkg/storage/legacysql/dualwrite/storage_mocks_test.go index 62ce9be2344..b3905d1c3d3 100644 --- a/pkg/storage/legacysql/dualwrite/storage_mocks_test.go +++ b/pkg/storage/legacysql/dualwrite/storage_mocks_test.go @@ -13,102 +13,11 @@ import ( grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" ) -type legacyStoreMock struct { - *mock.Mock - grafanarest.LegacyStorage -} - type storageMock struct { *mock.Mock grafanarest.Storage } -func (m legacyStoreMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, obj, createValidation, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - - args := m.Called(ctx, options) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - -func (m legacyStoreMock) NewList() runtime.Object { - return nil -} - -func (m legacyStoreMock) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - args := m.Called(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - select { - case <-ctx.Done(): - return nil, false, errors.New("context canceled") - default: - } - - args := m.Called(ctx, name, deleteValidation, options) - if err := args.Get(2); err != nil { - return nil, false, err.(error) - } - return args.Get(0).(runtime.Object), args.Bool(1), args.Error(2) -} - -func (m legacyStoreMock) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { - select { - case <-ctx.Done(): - return nil, errors.New("context canceled") - default: - } - args := m.Called(ctx, deleteValidation, options, listOptions) - if err := args.Get(1); err != nil { - return nil, err.(error) - } - return args.Get(0).(runtime.Object), args.Error(1) -} - // Unified Store func (m storageMock) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { select { diff --git a/pkg/storage/legacysql/dualwrite/types.go b/pkg/storage/legacysql/dualwrite/types.go index 4b43f0d39a8..0478aa5438e 100644 --- a/pkg/storage/legacysql/dualwrite/types.go +++ b/pkg/storage/legacysql/dualwrite/types.go @@ -36,7 +36,7 @@ type Service interface { ShouldManage(gr schema.GroupResource) bool // Create a managed k8s storage instance - NewStorage(gr schema.GroupResource, legacy grafanarest.LegacyStorage, storage grafanarest.Storage) (grafanarest.Storage, error) + NewStorage(gr schema.GroupResource, legacy grafanarest.Storage, storage grafanarest.Storage) (grafanarest.Storage, error) // Check if the dual writes is reading from unified storage (mode3++) ReadFromUnified(ctx context.Context, gr schema.GroupResource) (bool, error) From 03dcd25a3213255930b14ce0e0285c593ce14933 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Thu, 27 Feb 2025 10:31:55 +0000 Subject: [PATCH 49/51] New Logs Panel: Displayed fields support (#100643) * LogList: add displayedFields and getFieldLinks props * Render displayed fields * LogLine: rename function * Refactor log dimensions * Generate styles in parent component * Log List: implement tabular unwrapped logs * Rename class * Log line: center fields * Parametrize field gap * Virtualization: update measurement to support displayed fields * Shorten visible level * Do not calculate dimensions when logs are wrapped * Logs Navigation: fix width when flag is enabled * Pass styles to LogLineMessage * Formatting * Fix unwrapped logs when showTime is off * LogLine: update css selectors for fields --- public/app/features/explore/Logs/Logs.tsx | 2 + .../features/explore/Logs/LogsNavigation.tsx | 2 +- .../logs/components/panel/InfiniteScroll.tsx | 28 +++++- .../logs/components/panel/LogLine.tsx | 86 +++++++++++++++--- .../logs/components/panel/LogLineMessage.tsx | 9 +- .../logs/components/panel/LogList.tsx | 56 ++++++++++-- .../logs/components/panel/processing.ts | 89 ++++++++++++++++--- .../logs/components/panel/virtualization.ts | 24 ++++- .../app/plugins/panel/logs-new/LogsPanel.tsx | 1 + 9 files changed, 250 insertions(+), 47 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index feefbb1bee7..24bc46790a5 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1070,8 +1070,10 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { { return { navContainer: css({ maxHeight: navContainerHeight, - width: oldestLogsFirst ? '58px' : 'auto', + width: oldestLogsFirst && !config.featureToggles.newLogsPanel ? '58px' : 'auto', display: 'flex', flexDirection: 'column', justifyContent: config.featureToggles.logsInfiniteScrolling diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index 4f0e7a9584b..3790f5f0b0f 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -4,12 +4,12 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window' import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { Spinner } from '@grafana/ui'; +import { Spinner, useTheme2 } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { canScrollBottom, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll'; -import { LogLine } from './LogLine'; +import { getStyles, LogLine } from './LogLine'; import { LogLineMessage } from './LogLineMessage'; import { LogListModel } from './processing'; @@ -22,6 +22,7 @@ interface ChildrenProps { interface Props { children: (props: ChildrenProps) => ReactNode; + displayedFields: string[]; handleOverflow: (index: number, id: string, height: number) => void; loadMore?: (range: AbsoluteTimeRange) => void; logs: LogListModel[]; @@ -38,6 +39,7 @@ type InfiniteLoaderState = 'idle' | 'out-of-bounds' | 'pre-scroll' | 'loading'; export const InfiniteScroll = ({ children, + displayedFields, handleOverflow, loadMore, logs, @@ -57,6 +59,8 @@ export const InfiniteScroll = ({ const lastEvent = useRef(null); const countRef = useRef(0); const lastLogOfPage = useRef([]); + const theme = useTheme2(); + const styles = getStyles(theme); useEffect(() => { // Logs have not changed, ignore effect @@ -132,24 +136,40 @@ export const InfiniteScroll = ({ ({ index, style }: ListChildComponentProps) => { if (!logs[index] && infiniteLoaderState !== 'idle') { return ( - + {getMessageFromInfiniteLoaderState(infiniteLoaderState, sortOrder)} ); } return ( ); }, - [handleOverflow, infiniteLoaderState, logs, onLoadMore, showTime, sortOrder, wrapLogMessage] + [ + displayedFields, + handleOverflow, + infiniteLoaderState, + logs, + onLoadMore, + showTime, + sortOrder, + styles, + wrapLogMessage, + ] ); const onItemsRendered = useCallback( diff --git a/public/app/features/logs/components/panel/LogLine.tsx b/public/app/features/logs/components/panel/LogLine.tsx index bba06ff1ab0..d484675d424 100644 --- a/public/app/features/logs/components/panel/LogLine.tsx +++ b/public/app/features/logs/components/panel/LogLine.tsx @@ -2,24 +2,35 @@ import { css } from '@emotion/css'; import { CSSProperties, useEffect, useRef } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; -import { LogListModel } from './processing'; -import { hasUnderOrOverflow } from './virtualization'; +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; + +import { LogFieldDimension, LogListModel } from './processing'; +import { FIELD_GAP_MULTIPLIER, hasUnderOrOverflow } from './virtualization'; interface Props { + displayedFields: string[]; index: number; log: LogListModel; showTime: boolean; style: CSSProperties; + styles: LogLineStyles; onOverflow?: (index: number, id: string, height: number) => void; variant?: 'infinite-scroll'; wrapLogMessage: boolean; } -export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrapLogMessage }: Props) => { - const theme = useTheme2(); - const styles = getStyles(theme); +export const LogLine = ({ + displayedFields, + index, + log, + style, + styles, + onOverflow, + showTime, + variant, + wrapLogMessage, +}: Props) => { const logLineRef = useRef(null); useEffect(() => { @@ -34,16 +45,59 @@ export const LogLine = ({ index, log, style, onOverflow, showTime, variant, wrap }, [index, log.uid, onOverflow, style.height]); return ( -

-
- {showTime && {log.timestamp}} - {log.logLevel && {log.logLevel}} - {log.body} +
+
+
); }; +interface LogProps { + displayedFields: string[]; + log: LogListModel; + showTime: boolean; + styles: ReturnType; +} + +const Log = ({ displayedFields, log, showTime, styles }: LogProps) => { + return ( + <> + {showTime && {log.timestamp}} + {log.displayLevel} + {displayedFields.length > 0 ? ( + displayedFields.map((field) => ( + + {getDisplayedFieldValue(field, log)} + + )) + ) : ( + {log.body} + )} + + ); +}; + +export function getDisplayedFieldValue(fieldName: string, log: LogListModel): string { + if (fieldName === LOG_LINE_BODY_FIELD_NAME) { + return log.body; + } + if (log.labels[fieldName] != null) { + return log.labels[fieldName]; + } + const field = log.fields.find((field) => { + return field.keys[0] === fieldName; + }); + + return field ? field.values.toString() : ''; +} + +export function getGridTemplateColumns(dimensions: LogFieldDimension[]) { + const columns = dimensions.map((dimension) => dimension.width).join('px '); + return `${columns}px 1fr`; +} + +export type LogLineStyles = ReturnType; export const getStyles = (theme: GrafanaTheme2) => { const colors = { critical: '#B877D9', @@ -82,7 +136,6 @@ export const getStyles = (theme: GrafanaTheme2) => { timestamp: css({ color: theme.colors.text.secondary, display: 'inline-block', - marginRight: theme.spacing(1), '&.level-critical': { color: colors.critical, }, @@ -103,7 +156,6 @@ export const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.text.secondary, fontWeight: theme.typography.fontWeightBold, display: 'inline-block', - marginRight: theme.spacing(1), '&.level-critical': { color: colors.critical, }, @@ -129,12 +181,20 @@ export const getStyles = (theme: GrafanaTheme2) => { outline: 'solid 1px red', }), unwrappedLogLine: css({ + display: 'grid', + gridColumnGap: theme.spacing(FIELD_GAP_MULTIPLIER), whiteSpace: 'pre', paddingBottom: theme.spacing(0.75), }), wrappedLogLine: css({ whiteSpace: 'pre-wrap', paddingBottom: theme.spacing(0.75), + '& .field': { + marginRight: theme.spacing(FIELD_GAP_MULTIPLIER), + }, + '& .field:last-child': { + marginRight: 0, + }, }), }; }; diff --git a/public/app/features/logs/components/panel/LogLineMessage.tsx b/public/app/features/logs/components/panel/LogLineMessage.tsx index 2bdff1c03f5..9d2bfba0eed 100644 --- a/public/app/features/logs/components/panel/LogLineMessage.tsx +++ b/public/app/features/logs/components/panel/LogLineMessage.tsx @@ -1,18 +1,15 @@ import { CSSProperties, ReactNode } from 'react'; -import { useTheme2 } from '@grafana/ui'; - -import { getStyles } from './LogLine'; +import { LogLineStyles } from './LogLine'; interface Props { children: ReactNode; onClick?: () => void; style: CSSProperties; + styles: LogLineStyles; } -export const LogLineMessage = ({ children, onClick, style }: Props) => { - const theme = useTheme2(); - const styles = getStyles(theme); +export const LogLineMessage = ({ children, onClick, style, styles }: Props) => { return (
{onClick ? ( diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index 9e42cb73214..abc48cbe9ce 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -1,12 +1,24 @@ +import { css } from '@emotion/css'; import { debounce } from 'lodash'; -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { VariableSizeList } from 'react-window'; -import { AbsoluteTimeRange, CoreApp, EventBus, LogRowModel, LogsSortOrder, TimeRange } from '@grafana/data'; +import { + AbsoluteTimeRange, + CoreApp, + DataFrame, + EventBus, + Field, + LinkModel, + LogRowModel, + LogsSortOrder, + TimeRange, +} from '@grafana/data'; import { useTheme2 } from '@grafana/ui'; import { InfiniteScroll } from './InfiniteScroll'; -import { preProcessLogs, LogListModel } from './processing'; +import { getGridTemplateColumns } from './LogLine'; +import { preProcessLogs, LogListModel, calculateFieldDimensions, LogFieldDimension } from './processing'; import { getLogLineSize, init as initVirtualization, @@ -15,14 +27,18 @@ import { storeLogLineSize, } from './virtualization'; +export type GetFieldLinksFn = (field: Field, rowIndex: number, dataFrame: DataFrame) => Array>; + interface Props { app: CoreApp; - logs: LogRowModel[]; containerElement: HTMLDivElement; + displayedFields: string[]; eventBus: EventBus; forceEscape?: boolean; + getFieldLinks?: GetFieldLinksFn; initialScrollPosition?: 'top' | 'bottom'; loadMore?: (range: AbsoluteTimeRange) => void; + logs: LogRowModel[]; showTime: boolean; sortOrder: LogsSortOrder; timeRange: TimeRange; @@ -33,8 +49,10 @@ interface Props { export const LogList = ({ app, containerElement, + displayedFields = [], eventBus, forceEscape = false, + getFieldLinks, initialScrollPosition = 'top', loadMore, logs, @@ -52,6 +70,11 @@ export const LogList = ({ const listRef = useRef(null); const widthRef = useRef(containerElement.clientWidth); const scrollRef = useRef(null); + const dimensions = useMemo( + () => (wrapLogMessage ? [] : calculateFieldDimensions(processedLogs, displayedFields)), + [displayedFields, processedLogs, wrapLogMessage] + ); + const styles = getStyles(dimensions, { showTime }); useEffect(() => { initVirtualization(theme); @@ -65,9 +88,11 @@ export const LogList = ({ }, [eventBus, logs.length]); useEffect(() => { - setProcessedLogs(preProcessLogs(logs, { wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone })); + setProcessedLogs( + preProcessLogs(logs, { getFieldLinks, wrap: wrapLogMessage, escape: forceEscape, order: sortOrder, timeZone }) + ); listRef.current?.resetAfterIndex(0); - }, [forceEscape, logs, sortOrder, timeZone, wrapLogMessage]); + }, [forceEscape, getFieldLinks, logs, sortOrder, timeZone, wrapLogMessage]); useEffect(() => { const handleResize = debounce(() => { @@ -110,6 +135,7 @@ export const LogList = ({ return ( {({ getItemKey, itemCount, onItemsRendered, Renderer }) => ( index > 0); + return { + logList: css({ + '& .unwrapped-log-line': { + display: 'grid', + gridTemplateColumns: getGridTemplateColumns(columns), + }, + }), + }; +} + function handleScrollToEvent(event: ScrollToLogsEvent, logsCount: number, list: VariableSizeList | null) { if (event.payload.scrollTo === 'top') { list?.scrollTo(0); diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 77f525614b5..2d62db31370 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -1,22 +1,28 @@ -import { dateTimeFormat, LogRowModel, LogsSortOrder } from '@grafana/data'; +import { dateTimeFormat, LogLevel, LogRowModel, LogsSortOrder } from '@grafana/data'; import { escapeUnescapedString, sortLogRows } from '../../utils'; +import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { FieldDef, getAllFields } from '../logParser'; +import { getDisplayedFieldValue } from './LogLine'; +import { GetFieldLinksFn } from './LogList'; import { measureTextWidth } from './virtualization'; export interface LogListModel extends LogRowModel { body: string; + displayLevel: string; + fields: FieldDef[]; timestamp: string; - dimensions: LogDimensions; } -export interface LogDimensions { - timestampWidth: number; - levelWidth: number; +export interface LogFieldDimension { + field: string; + width: number; } interface PreProcessOptions { escape: boolean; + getFieldLinks?: GetFieldLinksFn; order: LogsSortOrder; timeZone: string; wrap: boolean; @@ -24,19 +30,23 @@ interface PreProcessOptions { export const preProcessLogs = ( logs: LogRowModel[], - { escape, order, timeZone, wrap }: PreProcessOptions + { escape, getFieldLinks, order, timeZone, wrap }: PreProcessOptions ): LogListModel[] => { const orderedLogs = sortLogRows(logs, order); - return orderedLogs.map((log) => preProcessLog(log, { wrap, escape, timeZone, expanded: false })); + return orderedLogs.map((log) => preProcessLog(log, { escape, expanded: false, getFieldLinks, timeZone, wrap })); }; interface PreProcessLogOptions { escape: boolean; expanded: boolean; // Not yet implemented + getFieldLinks?: GetFieldLinksFn; timeZone: string; wrap: boolean; } -const preProcessLog = (log: LogRowModel, { escape, expanded, timeZone, wrap }: PreProcessLogOptions): LogListModel => { +const preProcessLog = ( + log: LogRowModel, + { escape, expanded, getFieldLinks, timeZone, wrap }: PreProcessLogOptions +): LogListModel => { let body = log.entry; const timestamp = dateTimeFormat(log.timeEpochMs, { timeZone, @@ -54,10 +64,65 @@ const preProcessLog = (log: LogRowModel, { escape, expanded, timeZone, wrap }: P return { ...log, body, + displayLevel: logLevelToDisplayLevel(log.logLevel), + fields: getAllFields(log, getFieldLinks), timestamp, - dimensions: { - timestampWidth: measureTextWidth(timestamp), - levelWidth: measureTextWidth(log.logLevel), - }, }; }; + +function logLevelToDisplayLevel(level = '') { + switch (level) { + case LogLevel.critical: + return 'crit'; + case LogLevel.warning: + return 'warn'; + case LogLevel.unknown: + return ''; + default: + return level; + } +} + +export const calculateFieldDimensions = (logs: LogListModel[], displayedFields: string[] = []) => { + if (!logs.length) { + return []; + } + let timestampWidth = 0; + let levelWidth = 0; + const fieldWidths: Record = {}; + for (let i = 0; i < logs.length; i++) { + let width = measureTextWidth(logs[i].timestamp); + if (width > timestampWidth) { + timestampWidth = Math.round(width); + } + width = measureTextWidth(logs[i].displayLevel); + if (width > levelWidth) { + levelWidth = Math.round(width); + } + for (const field of displayedFields) { + width = measureTextWidth(getDisplayedFieldValue(field, logs[i])); + fieldWidths[field] = !fieldWidths[field] || width > fieldWidths[field] ? Math.round(width) : fieldWidths[field]; + } + } + const dimensions: LogFieldDimension[] = [ + { + field: 'timestamp', + width: timestampWidth, + }, + { + field: 'level', + width: levelWidth, + }, + ]; + for (const field in fieldWidths) { + // Skip the log line when it's a displayed field + if (field === LOG_LINE_BODY_FIELD_NAME) { + continue; + } + dimensions.push({ + field, + width: fieldWidths[field], + }); + } + return dimensions; +}; diff --git a/public/app/features/logs/components/panel/virtualization.ts b/public/app/features/logs/components/panel/virtualization.ts index 201cc4c82ed..a367b41c050 100644 --- a/public/app/features/logs/components/panel/virtualization.ts +++ b/public/app/features/logs/components/panel/virtualization.ts @@ -1,5 +1,6 @@ import { BusEventWithPayload, GrafanaTheme2 } from '@grafana/data'; +import { getDisplayedFieldValue } from './LogLine'; import { LogListModel } from './processing'; let ctx: CanvasRenderingContext2D | null = null; @@ -8,6 +9,9 @@ let paddingBottom = gridSize * 0.75; let lineHeight = 22; let measurementMode: 'canvas' | 'dom' = 'canvas'; +// Controls the space between fields in the log line, timestamp, level, displayed fields, and log line body +export const FIELD_GAP_MULTIPLIER = 1.5; + export function init(theme: GrafanaTheme2) { const font = `${theme.typography.fontSize}px ${theme.typography.fontFamilyMonospace}`; const letterSpacing = theme.typography.body.letterSpacing; @@ -146,6 +150,7 @@ interface DisplayOptions { export function getLogLineSize( logs: LogListModel[], container: HTMLDivElement | null, + displayedFields: string[], { wrap, showTime }: DisplayOptions, index: number ) { @@ -160,15 +165,26 @@ export function getLogLineSize( if (storedSize) { return storedSize; } - const gap = gridSize; + + let textToMeasure = ''; + const gap = gridSize * FIELD_GAP_MULTIPLIER; let optionsWidth = 0; if (showTime) { - optionsWidth += logs[index].dimensions.timestampWidth + gap; + optionsWidth += gap; + textToMeasure += logs[index].timestamp; } if (logs[index].logLevel) { - optionsWidth += logs[index].dimensions.levelWidth + gap; + optionsWidth += gap; + textToMeasure += logs[index].logLevel; } - const { height } = measureTextHeight(logs[index].body, getLogContainerWidth(container), optionsWidth); + for (const field of displayedFields) { + textToMeasure = getDisplayedFieldValue(field, logs[index]) + textToMeasure; + } + if (!displayedFields.length) { + textToMeasure += logs[index].body; + } + + const { height } = measureTextHeight(textToMeasure, getLogContainerWidth(container), optionsWidth); return height; } diff --git a/public/app/plugins/panel/logs-new/LogsPanel.tsx b/public/app/plugins/panel/logs-new/LogsPanel.tsx index 44dea0e5f54..9b0b26b4c23 100644 --- a/public/app/plugins/panel/logs-new/LogsPanel.tsx +++ b/public/app/plugins/panel/logs-new/LogsPanel.tsx @@ -102,6 +102,7 @@ export const LogsPanel = ({ Date: Thu, 27 Feb 2025 10:47:39 +0000 Subject: [PATCH 50/51] Add GitHub Actions workflow for feature toggle tests (#101270) ci: Add GitHub Actions workflow for feature toggle tests Signed-off-by: Jack Baldry --- .github/CODEOWNERS | 1 + .github/workflows/feature-toggles-ci.yml | 25 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/feature-toggles-ci.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 92581c315aa..4e3ca236ebf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -776,6 +776,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/doc-validator.yml @grafana/docs-tooling /.github/workflows/deploy-pr-preview.yml @grafana/docs-tooling /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina +/.github/workflows/feature-toggles-ci.yml @grafana/docs-tooling /.github/workflows/github-release.yml @grafana/grafana-developer-enablement-squad /.github/workflows/issue-opened.yml @grafana/grafana-community-support /.github/workflows/metrics-collector.yml @torkelo diff --git a/.github/workflows/feature-toggles-ci.yml b/.github/workflows/feature-toggles-ci.yml new file mode 100644 index 00000000000..a6c9f5c52dc --- /dev/null +++ b/.github/workflows/feature-toggles-ci.yml @@ -0,0 +1,25 @@ +name: Feature toggles CI + +on: + pull_request: + paths: + - 'pkg/services/featuremgmt/toggles_gen_test.go' + - 'pkg/services/featuremgmt/registry.go' + - 'docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache: true + + - name: Run feature toggle tests + run: go test -v -run TestFeatureToggleFiles ./pkg/services/featuremgmt/ From 8f465f12492461c7f32e7d4b7babd018fd77f9f7 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 27 Feb 2025 10:56:25 +0000 Subject: [PATCH 51/51] Plugins: Add confirmation modal for uninstalling updateable plugin (#101297) * add confirmation modal for uninstalling updateable plugin * shush betterer * refactor with master Levi * update betterer * update name --- .../InstallControls/InstallControlsButton.tsx | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index 6af5c5904ad..9e0bdf9f811 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -126,24 +126,28 @@ export function InstallControlsButton({ uninstallTitle = 'Preinstalled plugin. Remove from Grafana config before uninstalling.'; } + const uninstallControls = ( + <> + + + + ); + if (pluginStatus === PluginStatus.UNINSTALL) { return ( - <> - - - - - + + {uninstallControls} + ); } @@ -162,9 +166,7 @@ export function InstallControlsButton({ {isInstalling ? 'Updating' : 'Update'} )} - + {uninstallControls} ); }